Extract hardcore position config policy
This commit is contained in:
@@ -117,6 +117,10 @@ Already isolated:
|
||||
location/composition entry parsing, merge behavior, and config parsing live
|
||||
in `location_config.py`; `prompt_builder.py` still applies selected configs
|
||||
to rows.
|
||||
- hardcore position/action-filter choices, selected-position normalization,
|
||||
config JSON builders/parsers, focus-policy toggles, subcategory allow-list
|
||||
policy, and position-key detection live in `hardcore_position_config.py`;
|
||||
`prompt_builder.py` still applies the config to category rows.
|
||||
- hardcore configured-cast role graph generation lives in
|
||||
`hardcore_role_graphs.py`; `prompt_builder.py` selects item/axis metadata and
|
||||
then asks that module for the source role graph.
|
||||
|
||||
@@ -74,6 +74,7 @@ Core helper ownership:
|
||||
| `generation_profile_config.py` | Generation profile presets, profile option overrides, trigger policy, expression/pose/clothing config normalization, and profile config parsing. |
|
||||
| `seed_config.py` | Seed axis salts/aliases, seed mode choices, global/axis lock JSON builders, seed config parsing, row seed math, and deterministic axis RNG construction. |
|
||||
| `location_config.py` | Location/composition preset schemas, themed location packs, custom location/composition parsing, pool merge behavior, and location/composition config parsing. |
|
||||
| `hardcore_position_config.py` | Hardcore position/action-filter choices, selected-position normalization, config JSON builders/parsers, focus-policy toggles, subcategory allow-list policy, and position-key detection. |
|
||||
| `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, and hardcore cast count policy. |
|
||||
| `pair_rows.py` | Insta/OF soft/hard row creation, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, and POV row fields. |
|
||||
| `pair_camera.py` | Insta/OF soft/hard camera route resolution, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, and synchronized row/root camera metadata. |
|
||||
@@ -306,8 +307,9 @@ Edit targets:
|
||||
`foreplay_teasing`, `manual_stimulation`, `body_worship_touching`,
|
||||
`clothing_position_transitions`, `dominant_guidance`,
|
||||
`camera_performance`, `group_coordination`, and `aftercare_cleanup`.
|
||||
- Position filtering UI: `build_hardcore_position_pool_json`,
|
||||
`build_hardcore_action_filter_json`, `_apply_hardcore_position_config_to_subcategory`.
|
||||
- Position filtering UI/config: builders and parsers live in
|
||||
`hardcore_position_config.py`; `prompt_builder._apply_hardcore_position_config_to_subcategory`
|
||||
applies the config to category rows.
|
||||
- Krea2 action rewrite orchestration: `krea_formatter.py`.
|
||||
- Krea2 non-POV position anchors/arrangements: `krea_action_positions.py`.
|
||||
- Krea2 non-climax item/detail cleanup: `krea_action_details.py`.
|
||||
|
||||
@@ -0,0 +1,521 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
HARDCORE_POSITION_FAMILY_CHOICES = [
|
||||
"any",
|
||||
"penetrative",
|
||||
"foreplay",
|
||||
"interaction",
|
||||
"manual",
|
||||
"oral",
|
||||
"outercourse",
|
||||
"anal",
|
||||
"climax",
|
||||
"threesome",
|
||||
"group",
|
||||
]
|
||||
HARDCORE_POSITION_FOCUS_CHOICES = [
|
||||
"keep_pool",
|
||||
"penetration_only",
|
||||
"foreplay_only",
|
||||
"interaction_only",
|
||||
"manual_only",
|
||||
"oral_only",
|
||||
"outercourse_only",
|
||||
"anal_only",
|
||||
"climax_only",
|
||||
"threesome_only",
|
||||
"group_only",
|
||||
]
|
||||
HARDCORE_POSITION_KEY_CHOICES = [
|
||||
"missionary",
|
||||
"cowgirl",
|
||||
"reverse_cowgirl",
|
||||
"doggy",
|
||||
"bent_over",
|
||||
"face_down_ass_up",
|
||||
"standing",
|
||||
"side_lying",
|
||||
"edge_supported",
|
||||
"kneeling",
|
||||
"lotus_lap",
|
||||
"face_sitting",
|
||||
"sixty_nine",
|
||||
"reclining_oral",
|
||||
"straddled_oral",
|
||||
"spread_leg_oral",
|
||||
"chair_oral",
|
||||
"kissing",
|
||||
"caressing",
|
||||
"breast_touch",
|
||||
"face_touch",
|
||||
"undressing",
|
||||
"body_worship",
|
||||
"nipple_play",
|
||||
"ass_grab",
|
||||
"thigh_kissing",
|
||||
"hair_holding",
|
||||
"wrist_pinning",
|
||||
"dirty_talk",
|
||||
"position_transition",
|
||||
"guided_positioning",
|
||||
"camera_showing",
|
||||
"watching",
|
||||
"aftercare",
|
||||
"cleanup",
|
||||
"fingering",
|
||||
"clit_rubbing",
|
||||
"mutual_masturbation",
|
||||
"boobjob",
|
||||
"testicle_sucking",
|
||||
"penis_licking",
|
||||
"handjob",
|
||||
"footjob",
|
||||
"open_thighs",
|
||||
"front_back",
|
||||
]
|
||||
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
|
||||
"any": [
|
||||
"penetrative_sex",
|
||||
"foreplay_teasing",
|
||||
"body_worship_touching",
|
||||
"clothing_position_transitions",
|
||||
"dominant_guidance",
|
||||
"camera_performance",
|
||||
"manual_stimulation",
|
||||
"oral_sex",
|
||||
"outercourse_sex",
|
||||
"anal_double_penetration",
|
||||
"threesomes",
|
||||
"group_coordination",
|
||||
"group_sex_orgy",
|
||||
"cumshot_climax",
|
||||
"aftercare_cleanup",
|
||||
],
|
||||
"penetrative": ["penetrative_sex"],
|
||||
"foreplay": ["foreplay_teasing"],
|
||||
"interaction": [
|
||||
"foreplay_teasing",
|
||||
"body_worship_touching",
|
||||
"clothing_position_transitions",
|
||||
"dominant_guidance",
|
||||
"camera_performance",
|
||||
"group_coordination",
|
||||
"aftercare_cleanup",
|
||||
],
|
||||
"manual": ["manual_stimulation"],
|
||||
"oral": ["oral_sex"],
|
||||
"outercourse": ["outercourse_sex", "manual_stimulation"],
|
||||
"anal": ["anal_double_penetration"],
|
||||
"climax": ["cumshot_climax"],
|
||||
"threesome": ["threesomes"],
|
||||
"group": ["group_sex_orgy"],
|
||||
}
|
||||
HARDCORE_POSITION_KEY_MATCHES = {
|
||||
"missionary": ("missionary", "above her", "under her"),
|
||||
"cowgirl": ("cowgirl", "straddling", "straddles", "on top", "squatting on top"),
|
||||
"reverse_cowgirl": ("reverse cowgirl", "facing away"),
|
||||
"doggy": ("doggy", "all fours", "rear-entry", "from behind"),
|
||||
"bent_over": ("bent-over", "bent over", "hips raised"),
|
||||
"face_down_ass_up": ("face-down", "ass-up"),
|
||||
"standing": ("standing", "stands", "braced standing"),
|
||||
"side_lying": ("side-lying", "side lying", "spooning", "on the side", "on her side"),
|
||||
"edge_supported": ("edge-of-bed", "edge of bed", "bed edge", "raised edge", "edge-supported"),
|
||||
"kneeling": ("kneeling", "kneels", "kneeling center"),
|
||||
"lotus_lap": ("lotus", "lap", "seated in a partner's lap"),
|
||||
"face_sitting": ("face-sitting", "face sitting"),
|
||||
"sixty_nine": ("sixty-nine", "69"),
|
||||
"reclining_oral": ("reclining cunnilingus",),
|
||||
"straddled_oral": ("straddled oral",),
|
||||
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
|
||||
"chair_oral": ("chair oral",),
|
||||
"kissing": ("kiss", "kissing", "mouth-to-mouth", "mouth to mouth", "lips pressed"),
|
||||
"caressing": ("caress", "caressing", "hands roaming", "stroking skin", "hands sliding"),
|
||||
"breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
|
||||
"face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
|
||||
"undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
|
||||
"body_worship": ("body worship", "worship", "kissing down", "mouth on skin", "kissing the body"),
|
||||
"nipple_play": ("nipple", "nipples", "licking nipples", "sucking nipples", "nipple play"),
|
||||
"ass_grab": ("ass grab", "ass-grab", "ass grabbing", "hand on the ass", "squeezing the ass"),
|
||||
"thigh_kissing": ("thigh kiss", "thigh kissing", "kissing thighs", "mouth on inner thighs"),
|
||||
"hair_holding": ("hair holding", "hair held", "holding hair", "hair pulled back"),
|
||||
"wrist_pinning": ("wrist", "wrists", "pinning wrists", "wrists pinned", "hands pinned"),
|
||||
"dirty_talk": ("dirty talk", "whispering", "mouth near the ear", "telling", "verbal teasing"),
|
||||
"position_transition": ("transition", "turning around", "pulling onto the bed", "moving into position", "position change"),
|
||||
"guided_positioning": ("guiding", "guided", "guides", "lifting legs", "spreading thighs", "pulling hips", "turning the body"),
|
||||
"camera_showing": ("camera", "showing to camera", "presenting to camera", "spread open for camera", "creator-shot"),
|
||||
"watching": ("watching", "voyeur", "waiting turn", "partner watches", "onlooker"),
|
||||
"aftercare": ("aftercare", "cuddling", "kissing after", "holding close", "post-sex"),
|
||||
"cleanup": ("cleanup", "wiping", "cleaning", "towel", "wet cloth"),
|
||||
"fingering": ("fingering", "fingers inside", "fingers in pussy", "finger stimulation"),
|
||||
"clit_rubbing": ("clit", "clitoris", "clit rubbing", "rubbing the clit", "fingers on clit"),
|
||||
"mutual_masturbation": ("mutual masturbation", "both touching themselves", "masturbating together", "hands on their own bodies"),
|
||||
"boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
|
||||
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
|
||||
"penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
|
||||
"handjob": ("handjob", "hand job", "stroking the penis", "hand stroking", "manual stimulation"),
|
||||
"footjob": ("footjob", "soles", "toes curled", "feet stroking"),
|
||||
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
|
||||
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
|
||||
}
|
||||
HARDCORE_POSITION_AXIS_KEYS = {
|
||||
"position",
|
||||
"body_position",
|
||||
"body_arrangement",
|
||||
"arrangement",
|
||||
"tease_act",
|
||||
"touch_detail",
|
||||
"manual_act",
|
||||
"manual_detail",
|
||||
"worship_act",
|
||||
"transition_act",
|
||||
"control_act",
|
||||
"performance_act",
|
||||
"coordination_act",
|
||||
"aftercare_act",
|
||||
"cleanup_detail",
|
||||
}
|
||||
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = {
|
||||
"penetrative_sex": "penetrative",
|
||||
"foreplay_teasing": "foreplay",
|
||||
"body_worship_touching": "interaction",
|
||||
"clothing_position_transitions": "interaction",
|
||||
"dominant_guidance": "interaction",
|
||||
"camera_performance": "interaction",
|
||||
"manual_stimulation": "manual",
|
||||
"oral_sex": "oral",
|
||||
"outercourse_sex": "outercourse",
|
||||
"anal_double_penetration": "anal",
|
||||
"threesomes": "threesome",
|
||||
"group_coordination": "interaction",
|
||||
"group_sex_orgy": "group",
|
||||
"cumshot_climax": "climax",
|
||||
"aftercare_cleanup": "interaction",
|
||||
}
|
||||
FOCUS_FAMILY_BY_KEY = {
|
||||
"penetration_only": "penetrative",
|
||||
"foreplay_only": "foreplay",
|
||||
"interaction_only": "interaction",
|
||||
"manual_only": "manual",
|
||||
"oral_only": "oral",
|
||||
"outercourse_only": "outercourse",
|
||||
"anal_only": "anal",
|
||||
"climax_only": "climax",
|
||||
"threesome_only": "threesome",
|
||||
"group_only": "group",
|
||||
}
|
||||
|
||||
|
||||
def _is_false(value: Any) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value is False
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ("false", "0", "no", "off")
|
||||
return False
|
||||
|
||||
|
||||
def _list_from(value: Any) -> list[Any]:
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
|
||||
def hardcore_position_family_choices() -> list[str]:
|
||||
return list(HARDCORE_POSITION_FAMILY_CHOICES)
|
||||
|
||||
|
||||
def hardcore_position_focus_choices() -> list[str]:
|
||||
return list(HARDCORE_POSITION_FOCUS_CHOICES)
|
||||
|
||||
|
||||
def hardcore_position_key_choices() -> list[str]:
|
||||
return list(HARDCORE_POSITION_KEY_CHOICES)
|
||||
|
||||
|
||||
def normalize_hardcore_position_family(value: Any, default: str = "any") -> str:
|
||||
text = str(value or default).strip()
|
||||
return text if text in HARDCORE_POSITION_FAMILY_CHOICES else default
|
||||
|
||||
|
||||
def normalize_hardcore_position_values(values: Any) -> list[str]:
|
||||
raw_values = _list_from(values)
|
||||
selected: list[str] = []
|
||||
for value in raw_values:
|
||||
text = str(value or "").strip()
|
||||
if not text or text == "any":
|
||||
continue
|
||||
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
|
||||
if normalized in HARDCORE_POSITION_KEY_CHOICES and normalized not in selected:
|
||||
selected.append(normalized)
|
||||
return selected
|
||||
|
||||
|
||||
def empty_hardcore_position_config() -> dict[str, Any]:
|
||||
return {
|
||||
"config_type": "hardcore_position",
|
||||
"enabled": False,
|
||||
"family": "any",
|
||||
"positions": [],
|
||||
"require_position": False,
|
||||
"allow_toys": True,
|
||||
"allow_double": True,
|
||||
"allow_penetration": True,
|
||||
"allow_foreplay": True,
|
||||
"allow_interaction": True,
|
||||
"allow_manual": True,
|
||||
"allow_oral": True,
|
||||
"allow_outercourse": True,
|
||||
"allow_anal": True,
|
||||
"allow_climax": True,
|
||||
}
|
||||
|
||||
|
||||
def parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not value:
|
||||
return empty_hardcore_position_config()
|
||||
if isinstance(value, dict):
|
||||
raw = value
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(value))
|
||||
except json.JSONDecodeError:
|
||||
return empty_hardcore_position_config()
|
||||
if not isinstance(raw, dict):
|
||||
return empty_hardcore_position_config()
|
||||
parsed = {**empty_hardcore_position_config(), **raw}
|
||||
parsed["enabled"] = bool(parsed.get("enabled", True))
|
||||
parsed["family"] = normalize_hardcore_position_family(parsed.get("family"))
|
||||
parsed["positions"] = normalize_hardcore_position_values(parsed.get("positions"))
|
||||
parsed["require_position"] = not _is_false(parsed.get("require_position", False))
|
||||
for key in (
|
||||
"allow_toys",
|
||||
"allow_double",
|
||||
"allow_penetration",
|
||||
"allow_foreplay",
|
||||
"allow_interaction",
|
||||
"allow_manual",
|
||||
"allow_oral",
|
||||
"allow_outercourse",
|
||||
"allow_anal",
|
||||
"allow_climax",
|
||||
):
|
||||
parsed[key] = not _is_false(parsed.get(key, True))
|
||||
return parsed
|
||||
|
||||
|
||||
def hardcore_position_summary(config: dict[str, Any]) -> str:
|
||||
if not config.get("enabled"):
|
||||
return "hardcore position unrestricted"
|
||||
parts = [f"family={config.get('family', 'any')}"]
|
||||
positions = config.get("positions") or []
|
||||
if positions:
|
||||
parts.append("positions=" + ",".join(positions))
|
||||
elif config.get("require_position"):
|
||||
parts.append("position_templates=required")
|
||||
disabled = [
|
||||
label
|
||||
for key, label in (
|
||||
("allow_toys", "toys"),
|
||||
("allow_double", "double"),
|
||||
("allow_penetration", "penetration"),
|
||||
("allow_foreplay", "foreplay"),
|
||||
("allow_interaction", "interaction"),
|
||||
("allow_manual", "manual"),
|
||||
("allow_oral", "oral"),
|
||||
("allow_outercourse", "outercourse"),
|
||||
("allow_anal", "anal"),
|
||||
("allow_climax", "climax"),
|
||||
)
|
||||
if not config.get(key, True)
|
||||
]
|
||||
if disabled:
|
||||
parts.append("blocked=" + ",".join(disabled))
|
||||
return "; ".join(parts)
|
||||
|
||||
|
||||
def build_hardcore_position_pool_json(
|
||||
hardcore_position_config: str | dict[str, Any] | None = "",
|
||||
combine_mode: str = "replace",
|
||||
family: str = "any",
|
||||
selected_positions: list[str] | tuple[str, ...] | str | None = None,
|
||||
) -> str:
|
||||
base = parse_hardcore_position_config(hardcore_position_config)
|
||||
if combine_mode == "replace":
|
||||
base = {**empty_hardcore_position_config(), "enabled": True}
|
||||
else:
|
||||
base["enabled"] = True
|
||||
base["family"] = normalize_hardcore_position_family(family, base.get("family", "any"))
|
||||
selected = normalize_hardcore_position_values(selected_positions)
|
||||
if combine_mode == "add":
|
||||
existing = list(base.get("positions") or [])
|
||||
for value in selected:
|
||||
if value not in existing:
|
||||
existing.append(value)
|
||||
base["positions"] = existing
|
||||
else:
|
||||
base["positions"] = selected
|
||||
base["require_position"] = bool(base.get("require_position")) or bool(base["positions"]) or base["family"] != "any"
|
||||
base["summary"] = hardcore_position_summary(base)
|
||||
return json.dumps(base, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def build_hardcore_action_filter_json(
|
||||
hardcore_position_config: str | dict[str, Any] | None = "",
|
||||
focus: str = "keep_pool",
|
||||
allow_toys: bool = False,
|
||||
allow_double: bool = False,
|
||||
allow_penetration: bool = True,
|
||||
allow_foreplay: bool = True,
|
||||
allow_interaction: bool = True,
|
||||
allow_manual: bool = True,
|
||||
allow_oral: bool = True,
|
||||
allow_outercourse: bool = True,
|
||||
allow_anal: bool = True,
|
||||
allow_climax: bool = True,
|
||||
) -> str:
|
||||
config = parse_hardcore_position_config(hardcore_position_config)
|
||||
config["enabled"] = True
|
||||
focus = str(focus or "keep_pool").strip()
|
||||
focus_family = FOCUS_FAMILY_BY_KEY.get(focus)
|
||||
if focus_family:
|
||||
config["family"] = focus_family
|
||||
config["allow_toys"] = bool(allow_toys)
|
||||
config["allow_double"] = bool(allow_double)
|
||||
config["allow_penetration"] = bool(allow_penetration)
|
||||
config["allow_foreplay"] = bool(allow_foreplay)
|
||||
config["allow_interaction"] = bool(allow_interaction)
|
||||
config["allow_manual"] = bool(allow_manual)
|
||||
config["allow_oral"] = bool(allow_oral)
|
||||
config["allow_outercourse"] = bool(allow_outercourse)
|
||||
config["allow_anal"] = bool(allow_anal)
|
||||
config["allow_climax"] = bool(allow_climax)
|
||||
|
||||
if not focus_family and config["family"] != "any":
|
||||
enabled_action_families = {
|
||||
family
|
||||
for enabled, family in (
|
||||
(config["allow_penetration"], "penetrative"),
|
||||
(config["allow_foreplay"], "foreplay"),
|
||||
(config["allow_interaction"], "interaction"),
|
||||
(config["allow_manual"], "manual"),
|
||||
(config["allow_oral"], "oral"),
|
||||
(config["allow_outercourse"], "outercourse"),
|
||||
(config["allow_anal"], "anal"),
|
||||
(config["allow_climax"], "climax"),
|
||||
)
|
||||
if enabled
|
||||
}
|
||||
if config["family"] in enabled_action_families and len(enabled_action_families) > 1:
|
||||
config["family"] = "any"
|
||||
|
||||
if focus == "foreplay_only":
|
||||
config["allow_foreplay"] = True
|
||||
config["allow_interaction"] = True
|
||||
elif focus == "interaction_only":
|
||||
config["allow_interaction"] = True
|
||||
config["allow_foreplay"] = True
|
||||
elif focus == "manual_only":
|
||||
config["allow_manual"] = True
|
||||
elif focus == "oral_only":
|
||||
config["allow_oral"] = True
|
||||
config["allow_penetration"] = False
|
||||
elif focus == "outercourse_only":
|
||||
config["allow_outercourse"] = True
|
||||
config["allow_oral"] = False
|
||||
config["allow_penetration"] = False
|
||||
elif focus == "anal_only":
|
||||
config["allow_anal"] = True
|
||||
config["allow_penetration"] = True
|
||||
elif focus == "climax_only":
|
||||
config["allow_climax"] = True
|
||||
config["summary"] = hardcore_position_summary(config)
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def hardcore_position_config_active(config: dict[str, Any]) -> bool:
|
||||
return bool(config.get("enabled"))
|
||||
|
||||
|
||||
def hardcore_position_template_required(config: dict[str, Any]) -> bool:
|
||||
if not hardcore_position_config_active(config):
|
||||
return False
|
||||
return (
|
||||
bool(config.get("require_position"))
|
||||
or bool(config.get("positions"))
|
||||
or normalize_hardcore_position_family(config.get("family")) != "any"
|
||||
)
|
||||
|
||||
|
||||
def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
||||
family = normalize_hardcore_position_family(config.get("family"))
|
||||
allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
|
||||
if not config.get("allow_penetration", True):
|
||||
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
||||
if not config.get("allow_foreplay", True):
|
||||
allowed.discard("foreplay_teasing")
|
||||
if not config.get("allow_interaction", True):
|
||||
allowed.difference_update(
|
||||
{
|
||||
"foreplay_teasing",
|
||||
"body_worship_touching",
|
||||
"clothing_position_transitions",
|
||||
"dominant_guidance",
|
||||
"camera_performance",
|
||||
"group_coordination",
|
||||
"aftercare_cleanup",
|
||||
}
|
||||
)
|
||||
if not config.get("allow_manual", True):
|
||||
allowed.discard("manual_stimulation")
|
||||
if not config.get("allow_oral", True):
|
||||
allowed.discard("oral_sex")
|
||||
if not config.get("allow_outercourse", True):
|
||||
allowed.discard("outercourse_sex")
|
||||
if not config.get("allow_anal", True):
|
||||
allowed.discard("anal_double_penetration")
|
||||
if not config.get("allow_climax", True):
|
||||
allowed.discard("cumshot_climax")
|
||||
if not config.get("allow_double", True) and family == "anal":
|
||||
allowed.add("anal_double_penetration")
|
||||
return allowed or set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
|
||||
|
||||
|
||||
def hardcore_source_position_family(subcategory: dict[str, Any], config: dict[str, Any] | None = None) -> str:
|
||||
slug = str(subcategory.get("slug") or subcategory.get("name") or "").strip().lower()
|
||||
family = HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY.get(slug, "")
|
||||
if family:
|
||||
return family
|
||||
config_family = normalize_hardcore_position_family((config or {}).get("family"), "")
|
||||
return "" if config_family == "any" else config_family
|
||||
|
||||
|
||||
def hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
|
||||
text_parts = [str(part or "") for part in parts if str(part or "").strip()]
|
||||
if isinstance(axis_values, dict):
|
||||
text_parts.extend(str(value or "") for value in axis_values.values() if str(value or "").strip())
|
||||
text = " ".join(text_parts).lower()
|
||||
if not text:
|
||||
return []
|
||||
keys: list[str] = []
|
||||
for key, tokens in HARDCORE_POSITION_KEY_MATCHES.items():
|
||||
if any(token in text for token in tokens):
|
||||
keys.append(key)
|
||||
return keys
|
||||
|
||||
|
||||
_normalize_hardcore_position_family = normalize_hardcore_position_family
|
||||
_normalize_hardcore_position_values = normalize_hardcore_position_values
|
||||
_empty_hardcore_position_config = empty_hardcore_position_config
|
||||
_parse_hardcore_position_config = parse_hardcore_position_config
|
||||
_hardcore_position_summary = hardcore_position_summary
|
||||
_hardcore_position_config_active = hardcore_position_config_active
|
||||
_hardcore_position_template_required = hardcore_position_template_required
|
||||
_hardcore_allowed_subcategory_slugs = hardcore_allowed_subcategory_slugs
|
||||
_hardcore_source_position_family = hardcore_source_position_family
|
||||
_hardcore_position_keys = hardcore_position_keys
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
|
||||
try:
|
||||
from .prompt_builder import (
|
||||
from .hardcore_position_config import (
|
||||
build_hardcore_action_filter_json,
|
||||
build_hardcore_position_pool_json,
|
||||
hardcore_position_family_choices,
|
||||
@@ -11,7 +11,7 @@ try:
|
||||
hardcore_position_key_choices,
|
||||
)
|
||||
except ImportError: # Allows local smoke tests from the repository root.
|
||||
from prompt_builder import (
|
||||
from hardcore_position_config import (
|
||||
build_hardcore_action_filter_json,
|
||||
build_hardcore_position_pool_json,
|
||||
hardcore_position_family_choices,
|
||||
|
||||
+41
-418
@@ -28,6 +28,7 @@ try:
|
||||
from . import filter_config as filter_policy
|
||||
from . import generate_prompt_batches as g
|
||||
from . import generation_profile_config as generation_profile_policy
|
||||
from . import hardcore_position_config as hardcore_position_policy
|
||||
from . import location_config as location_policy
|
||||
from . import pair_clothing
|
||||
from . import pair_camera
|
||||
@@ -69,6 +70,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
||||
import filter_config as filter_policy
|
||||
import generate_prompt_batches as g
|
||||
import generation_profile_config as generation_profile_policy
|
||||
import hardcore_position_config as hardcore_position_policy
|
||||
import location_config as location_policy
|
||||
import pair_clothing
|
||||
import pair_camera
|
||||
@@ -292,221 +294,21 @@ CHARACTER_EYE_COLOR_CHOICES = [
|
||||
|
||||
CAMERA_DETAIL_CHOICES = camera_policy.CAMERA_DETAIL_CHOICES
|
||||
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
|
||||
HARDCORE_POSITION_FAMILY_CHOICES = [
|
||||
"any",
|
||||
"penetrative",
|
||||
"foreplay",
|
||||
"interaction",
|
||||
"manual",
|
||||
"oral",
|
||||
"outercourse",
|
||||
"anal",
|
||||
"climax",
|
||||
"threesome",
|
||||
"group",
|
||||
]
|
||||
HARDCORE_POSITION_FOCUS_CHOICES = [
|
||||
"keep_pool",
|
||||
"penetration_only",
|
||||
"foreplay_only",
|
||||
"interaction_only",
|
||||
"manual_only",
|
||||
"oral_only",
|
||||
"outercourse_only",
|
||||
"anal_only",
|
||||
"climax_only",
|
||||
"threesome_only",
|
||||
"group_only",
|
||||
]
|
||||
HARDCORE_POSITION_KEY_CHOICES = [
|
||||
"missionary",
|
||||
"cowgirl",
|
||||
"reverse_cowgirl",
|
||||
"doggy",
|
||||
"bent_over",
|
||||
"face_down_ass_up",
|
||||
"standing",
|
||||
"side_lying",
|
||||
"edge_supported",
|
||||
"kneeling",
|
||||
"lotus_lap",
|
||||
"face_sitting",
|
||||
"sixty_nine",
|
||||
"reclining_oral",
|
||||
"straddled_oral",
|
||||
"spread_leg_oral",
|
||||
"chair_oral",
|
||||
"kissing",
|
||||
"caressing",
|
||||
"breast_touch",
|
||||
"face_touch",
|
||||
"undressing",
|
||||
"body_worship",
|
||||
"nipple_play",
|
||||
"ass_grab",
|
||||
"thigh_kissing",
|
||||
"hair_holding",
|
||||
"wrist_pinning",
|
||||
"dirty_talk",
|
||||
"position_transition",
|
||||
"guided_positioning",
|
||||
"camera_showing",
|
||||
"watching",
|
||||
"aftercare",
|
||||
"cleanup",
|
||||
"fingering",
|
||||
"clit_rubbing",
|
||||
"mutual_masturbation",
|
||||
"boobjob",
|
||||
"testicle_sucking",
|
||||
"penis_licking",
|
||||
"handjob",
|
||||
"footjob",
|
||||
"open_thighs",
|
||||
"front_back",
|
||||
]
|
||||
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
|
||||
"any": [
|
||||
"penetrative_sex",
|
||||
"foreplay_teasing",
|
||||
"body_worship_touching",
|
||||
"clothing_position_transitions",
|
||||
"dominant_guidance",
|
||||
"camera_performance",
|
||||
"manual_stimulation",
|
||||
"oral_sex",
|
||||
"outercourse_sex",
|
||||
"anal_double_penetration",
|
||||
"threesomes",
|
||||
"group_coordination",
|
||||
"group_sex_orgy",
|
||||
"cumshot_climax",
|
||||
"aftercare_cleanup",
|
||||
],
|
||||
"penetrative": ["penetrative_sex"],
|
||||
"foreplay": ["foreplay_teasing"],
|
||||
"interaction": [
|
||||
"foreplay_teasing",
|
||||
"body_worship_touching",
|
||||
"clothing_position_transitions",
|
||||
"dominant_guidance",
|
||||
"camera_performance",
|
||||
"group_coordination",
|
||||
"aftercare_cleanup",
|
||||
],
|
||||
"manual": ["manual_stimulation"],
|
||||
"oral": ["oral_sex"],
|
||||
"outercourse": ["outercourse_sex", "manual_stimulation"],
|
||||
"anal": ["anal_double_penetration"],
|
||||
"climax": ["cumshot_climax"],
|
||||
"threesome": ["threesomes"],
|
||||
"group": ["group_sex_orgy"],
|
||||
}
|
||||
HARDCORE_POSITION_KEY_MATCHES = {
|
||||
"missionary": ("missionary", "above her", "under her"),
|
||||
"cowgirl": ("cowgirl", "straddling", "straddles", "on top", "squatting on top"),
|
||||
"reverse_cowgirl": ("reverse cowgirl", "facing away"),
|
||||
"doggy": ("doggy", "all fours", "rear-entry", "from behind"),
|
||||
"bent_over": ("bent-over", "bent over", "hips raised"),
|
||||
"face_down_ass_up": ("face-down", "ass-up"),
|
||||
"standing": ("standing", "stands", "braced standing"),
|
||||
"side_lying": ("side-lying", "side lying", "spooning", "on the side", "on her side"),
|
||||
"edge_supported": ("edge-of-bed", "edge of bed", "bed edge", "raised edge", "edge-supported"),
|
||||
"kneeling": ("kneeling", "kneels", "kneeling center"),
|
||||
"lotus_lap": ("lotus", "lap", "seated in a partner's lap"),
|
||||
"face_sitting": ("face-sitting", "face sitting"),
|
||||
"sixty_nine": ("sixty-nine", "69"),
|
||||
"reclining_oral": ("reclining cunnilingus",),
|
||||
"straddled_oral": ("straddled oral",),
|
||||
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
|
||||
"chair_oral": ("chair oral",),
|
||||
"kissing": ("kiss", "kissing", "mouth-to-mouth", "mouth to mouth", "lips pressed"),
|
||||
"caressing": ("caress", "caressing", "hands roaming", "stroking skin", "hands sliding"),
|
||||
"breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
|
||||
"face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
|
||||
"undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
|
||||
"body_worship": ("body worship", "worship", "kissing down", "mouth on skin", "kissing the body"),
|
||||
"nipple_play": ("nipple", "nipples", "licking nipples", "sucking nipples", "nipple play"),
|
||||
"ass_grab": ("ass grab", "ass-grab", "ass grabbing", "hand on the ass", "squeezing the ass"),
|
||||
"thigh_kissing": ("thigh kiss", "thigh kissing", "kissing thighs", "mouth on inner thighs"),
|
||||
"hair_holding": ("hair holding", "hair held", "holding hair", "hair pulled back"),
|
||||
"wrist_pinning": ("wrist", "wrists", "pinning wrists", "wrists pinned", "hands pinned"),
|
||||
"dirty_talk": ("dirty talk", "whispering", "mouth near the ear", "telling", "verbal teasing"),
|
||||
"position_transition": ("transition", "turning around", "pulling onto the bed", "moving into position", "position change"),
|
||||
"guided_positioning": ("guiding", "guided", "guides", "lifting legs", "spreading thighs", "pulling hips", "turning the body"),
|
||||
"camera_showing": ("camera", "showing to camera", "presenting to camera", "spread open for camera", "creator-shot"),
|
||||
"watching": ("watching", "voyeur", "waiting turn", "partner watches", "onlooker"),
|
||||
"aftercare": ("aftercare", "cuddling", "kissing after", "holding close", "post-sex"),
|
||||
"cleanup": ("cleanup", "wiping", "cleaning", "towel", "wet cloth"),
|
||||
"fingering": ("fingering", "fingers inside", "fingers in pussy", "finger stimulation"),
|
||||
"clit_rubbing": ("clit", "clitoris", "clit rubbing", "rubbing the clit", "fingers on clit"),
|
||||
"mutual_masturbation": ("mutual masturbation", "both touching themselves", "masturbating together", "hands on their own bodies"),
|
||||
"boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
|
||||
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
|
||||
"penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
|
||||
"handjob": ("handjob", "hand job", "stroking the penis", "hand stroking", "manual stimulation"),
|
||||
"footjob": ("footjob", "soles", "toes curled", "feet stroking"),
|
||||
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
|
||||
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
|
||||
}
|
||||
HARDCORE_POSITION_AXIS_KEYS = {
|
||||
"position",
|
||||
"body_position",
|
||||
"body_arrangement",
|
||||
"arrangement",
|
||||
"tease_act",
|
||||
"touch_detail",
|
||||
"manual_act",
|
||||
"manual_detail",
|
||||
"worship_act",
|
||||
"transition_act",
|
||||
"control_act",
|
||||
"performance_act",
|
||||
"coordination_act",
|
||||
"aftercare_act",
|
||||
"cleanup_detail",
|
||||
}
|
||||
|
||||
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = {
|
||||
"penetrative_sex": "penetrative",
|
||||
"foreplay_teasing": "foreplay",
|
||||
"body_worship_touching": "interaction",
|
||||
"clothing_position_transitions": "interaction",
|
||||
"dominant_guidance": "interaction",
|
||||
"camera_performance": "interaction",
|
||||
"manual_stimulation": "manual",
|
||||
"oral_sex": "oral",
|
||||
"outercourse_sex": "outercourse",
|
||||
"anal_double_penetration": "anal",
|
||||
"threesomes": "threesome",
|
||||
"group_coordination": "interaction",
|
||||
"group_sex_orgy": "group",
|
||||
"cumshot_climax": "climax",
|
||||
"aftercare_cleanup": "interaction",
|
||||
}
|
||||
HARDCORE_POSITION_FAMILY_CHOICES = hardcore_position_policy.HARDCORE_POSITION_FAMILY_CHOICES
|
||||
HARDCORE_POSITION_FOCUS_CHOICES = hardcore_position_policy.HARDCORE_POSITION_FOCUS_CHOICES
|
||||
HARDCORE_POSITION_KEY_CHOICES = hardcore_position_policy.HARDCORE_POSITION_KEY_CHOICES
|
||||
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = hardcore_position_policy.HARDCORE_POSITION_FAMILY_SUBCATEGORIES
|
||||
HARDCORE_POSITION_KEY_MATCHES = hardcore_position_policy.HARDCORE_POSITION_KEY_MATCHES
|
||||
HARDCORE_POSITION_AXIS_KEYS = hardcore_position_policy.HARDCORE_POSITION_AXIS_KEYS
|
||||
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = hardcore_position_policy.HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY
|
||||
|
||||
|
||||
def _hardcore_source_position_family(subcategory: dict[str, Any], config: dict[str, Any] | None = None) -> str:
|
||||
slug = str(subcategory.get("slug") or subcategory.get("name") or "").strip().lower()
|
||||
family = HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY.get(slug, "")
|
||||
if family:
|
||||
return family
|
||||
config_family = _normalize_hardcore_position_family((config or {}).get("family"), "")
|
||||
return "" if config_family == "any" else config_family
|
||||
return hardcore_position_policy.hardcore_source_position_family(subcategory, config)
|
||||
|
||||
|
||||
def _hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
|
||||
text_parts = [str(part or "") for part in parts if str(part or "").strip()]
|
||||
if isinstance(axis_values, dict):
|
||||
text_parts.extend(str(value or "") for value in axis_values.values() if str(value or "").strip())
|
||||
text = " ".join(text_parts).lower()
|
||||
if not text:
|
||||
return []
|
||||
keys: list[str] = []
|
||||
for key, tokens in HARDCORE_POSITION_KEY_MATCHES.items():
|
||||
if any(token in text for token in tokens):
|
||||
keys.append(key)
|
||||
return keys
|
||||
return hardcore_position_policy.hardcore_position_keys(*parts, axis_values=axis_values)
|
||||
|
||||
|
||||
CAMERA_ORBIT_FRAMING_CHOICES = camera_policy.CAMERA_ORBIT_FRAMING_CHOICES
|
||||
@@ -1266,104 +1068,23 @@ def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str
|
||||
|
||||
|
||||
def _normalize_hardcore_position_family(value: Any, default: str = "any") -> str:
|
||||
text = str(value or default).strip()
|
||||
return text if text in HARDCORE_POSITION_FAMILY_CHOICES else default
|
||||
return hardcore_position_policy.normalize_hardcore_position_family(value, default)
|
||||
|
||||
|
||||
def _normalize_hardcore_position_values(values: Any) -> list[str]:
|
||||
raw_values = _list_from(values)
|
||||
selected: list[str] = []
|
||||
for value in raw_values:
|
||||
text = str(value or "").strip()
|
||||
if not text or text == "any":
|
||||
continue
|
||||
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
|
||||
if normalized in HARDCORE_POSITION_KEY_CHOICES and normalized not in selected:
|
||||
selected.append(normalized)
|
||||
return selected
|
||||
return hardcore_position_policy.normalize_hardcore_position_values(values)
|
||||
|
||||
|
||||
def _empty_hardcore_position_config() -> dict[str, Any]:
|
||||
return {
|
||||
"config_type": "hardcore_position",
|
||||
"enabled": False,
|
||||
"family": "any",
|
||||
"positions": [],
|
||||
"require_position": False,
|
||||
"allow_toys": True,
|
||||
"allow_double": True,
|
||||
"allow_penetration": True,
|
||||
"allow_foreplay": True,
|
||||
"allow_interaction": True,
|
||||
"allow_manual": True,
|
||||
"allow_oral": True,
|
||||
"allow_outercourse": True,
|
||||
"allow_anal": True,
|
||||
"allow_climax": True,
|
||||
}
|
||||
return hardcore_position_policy.empty_hardcore_position_config()
|
||||
|
||||
|
||||
def _parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not value:
|
||||
return _empty_hardcore_position_config()
|
||||
if isinstance(value, dict):
|
||||
raw = value
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(value))
|
||||
except json.JSONDecodeError:
|
||||
return _empty_hardcore_position_config()
|
||||
if not isinstance(raw, dict):
|
||||
return _empty_hardcore_position_config()
|
||||
parsed = {**_empty_hardcore_position_config(), **raw}
|
||||
parsed["enabled"] = bool(parsed.get("enabled", True))
|
||||
parsed["family"] = _normalize_hardcore_position_family(parsed.get("family"))
|
||||
parsed["positions"] = _normalize_hardcore_position_values(parsed.get("positions"))
|
||||
parsed["require_position"] = not _is_false(parsed.get("require_position", False))
|
||||
for key in (
|
||||
"allow_toys",
|
||||
"allow_double",
|
||||
"allow_penetration",
|
||||
"allow_foreplay",
|
||||
"allow_interaction",
|
||||
"allow_manual",
|
||||
"allow_oral",
|
||||
"allow_outercourse",
|
||||
"allow_anal",
|
||||
"allow_climax",
|
||||
):
|
||||
parsed[key] = not _is_false(parsed.get(key, True))
|
||||
return parsed
|
||||
return hardcore_position_policy.parse_hardcore_position_config(value)
|
||||
|
||||
|
||||
def _hardcore_position_summary(config: dict[str, Any]) -> str:
|
||||
if not config.get("enabled"):
|
||||
return "hardcore position unrestricted"
|
||||
parts = [f"family={config.get('family', 'any')}"]
|
||||
positions = config.get("positions") or []
|
||||
if positions:
|
||||
parts.append("positions=" + ",".join(positions))
|
||||
elif config.get("require_position"):
|
||||
parts.append("position_templates=required")
|
||||
disabled = [
|
||||
label
|
||||
for key, label in (
|
||||
("allow_toys", "toys"),
|
||||
("allow_double", "double"),
|
||||
("allow_penetration", "penetration"),
|
||||
("allow_foreplay", "foreplay"),
|
||||
("allow_interaction", "interaction"),
|
||||
("allow_manual", "manual"),
|
||||
("allow_oral", "oral"),
|
||||
("allow_outercourse", "outercourse"),
|
||||
("allow_anal", "anal"),
|
||||
("allow_climax", "climax"),
|
||||
)
|
||||
if not config.get(key, True)
|
||||
]
|
||||
if disabled:
|
||||
parts.append("blocked=" + ",".join(disabled))
|
||||
return "; ".join(parts)
|
||||
return hardcore_position_policy.hardcore_position_summary(config)
|
||||
|
||||
|
||||
def build_hardcore_position_pool_json(
|
||||
@@ -1372,24 +1093,12 @@ def build_hardcore_position_pool_json(
|
||||
family: str = "any",
|
||||
selected_positions: list[str] | tuple[str, ...] | str | None = None,
|
||||
) -> str:
|
||||
base = _parse_hardcore_position_config(hardcore_position_config)
|
||||
if combine_mode == "replace":
|
||||
base = {**_empty_hardcore_position_config(), "enabled": True}
|
||||
else:
|
||||
base["enabled"] = True
|
||||
base["family"] = _normalize_hardcore_position_family(family, base.get("family", "any"))
|
||||
selected = _normalize_hardcore_position_values(selected_positions)
|
||||
if combine_mode == "add":
|
||||
existing = list(base.get("positions") or [])
|
||||
for value in selected:
|
||||
if value not in existing:
|
||||
existing.append(value)
|
||||
base["positions"] = existing
|
||||
else:
|
||||
base["positions"] = selected
|
||||
base["require_position"] = bool(base.get("require_position")) or bool(base["positions"]) or base["family"] != "any"
|
||||
base["summary"] = _hardcore_position_summary(base)
|
||||
return json.dumps(base, ensure_ascii=True, sort_keys=True)
|
||||
return hardcore_position_policy.build_hardcore_position_pool_json(
|
||||
hardcore_position_config=hardcore_position_config,
|
||||
combine_mode=combine_mode,
|
||||
family=family,
|
||||
selected_positions=selected_positions,
|
||||
)
|
||||
|
||||
|
||||
def build_hardcore_action_filter_json(
|
||||
@@ -1406,84 +1115,28 @@ def build_hardcore_action_filter_json(
|
||||
allow_anal: bool = True,
|
||||
allow_climax: bool = True,
|
||||
) -> str:
|
||||
config = _parse_hardcore_position_config(hardcore_position_config)
|
||||
config["enabled"] = True
|
||||
focus = str(focus or "keep_pool").strip()
|
||||
focus_family = {
|
||||
"penetration_only": "penetrative",
|
||||
"foreplay_only": "foreplay",
|
||||
"interaction_only": "interaction",
|
||||
"manual_only": "manual",
|
||||
"oral_only": "oral",
|
||||
"outercourse_only": "outercourse",
|
||||
"anal_only": "anal",
|
||||
"climax_only": "climax",
|
||||
"threesome_only": "threesome",
|
||||
"group_only": "group",
|
||||
}.get(focus)
|
||||
if focus_family:
|
||||
config["family"] = focus_family
|
||||
config["allow_toys"] = bool(allow_toys)
|
||||
config["allow_double"] = bool(allow_double)
|
||||
config["allow_penetration"] = bool(allow_penetration)
|
||||
config["allow_foreplay"] = bool(allow_foreplay)
|
||||
config["allow_interaction"] = bool(allow_interaction)
|
||||
config["allow_manual"] = bool(allow_manual)
|
||||
config["allow_oral"] = bool(allow_oral)
|
||||
config["allow_outercourse"] = bool(allow_outercourse)
|
||||
config["allow_anal"] = bool(allow_anal)
|
||||
config["allow_climax"] = bool(allow_climax)
|
||||
|
||||
if not focus_family and config["family"] != "any":
|
||||
enabled_action_families = {
|
||||
family
|
||||
for enabled, family in (
|
||||
(config["allow_penetration"], "penetrative"),
|
||||
(config["allow_foreplay"], "foreplay"),
|
||||
(config["allow_interaction"], "interaction"),
|
||||
(config["allow_manual"], "manual"),
|
||||
(config["allow_oral"], "oral"),
|
||||
(config["allow_outercourse"], "outercourse"),
|
||||
(config["allow_anal"], "anal"),
|
||||
(config["allow_climax"], "climax"),
|
||||
return hardcore_position_policy.build_hardcore_action_filter_json(
|
||||
hardcore_position_config=hardcore_position_config,
|
||||
focus=focus,
|
||||
allow_toys=allow_toys,
|
||||
allow_double=allow_double,
|
||||
allow_penetration=allow_penetration,
|
||||
allow_foreplay=allow_foreplay,
|
||||
allow_interaction=allow_interaction,
|
||||
allow_manual=allow_manual,
|
||||
allow_oral=allow_oral,
|
||||
allow_outercourse=allow_outercourse,
|
||||
allow_anal=allow_anal,
|
||||
allow_climax=allow_climax,
|
||||
)
|
||||
if enabled
|
||||
}
|
||||
if config["family"] in enabled_action_families and len(enabled_action_families) > 1:
|
||||
config["family"] = "any"
|
||||
|
||||
if focus == "foreplay_only":
|
||||
config["allow_foreplay"] = True
|
||||
config["allow_interaction"] = True
|
||||
elif focus == "interaction_only":
|
||||
config["allow_interaction"] = True
|
||||
config["allow_foreplay"] = True
|
||||
elif focus == "manual_only":
|
||||
config["allow_manual"] = True
|
||||
elif focus == "oral_only":
|
||||
config["allow_oral"] = True
|
||||
config["allow_penetration"] = False
|
||||
elif focus == "outercourse_only":
|
||||
config["allow_outercourse"] = True
|
||||
config["allow_oral"] = False
|
||||
config["allow_penetration"] = False
|
||||
elif focus == "anal_only":
|
||||
config["allow_anal"] = True
|
||||
config["allow_penetration"] = True
|
||||
elif focus == "climax_only":
|
||||
config["allow_climax"] = True
|
||||
config["summary"] = _hardcore_position_summary(config)
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def _hardcore_position_config_active(config: dict[str, Any]) -> bool:
|
||||
return bool(config.get("enabled"))
|
||||
return hardcore_position_policy.hardcore_position_config_active(config)
|
||||
|
||||
|
||||
def _hardcore_position_template_required(config: dict[str, Any]) -> bool:
|
||||
if not _hardcore_position_config_active(config):
|
||||
return False
|
||||
return bool(config.get("require_position")) or bool(config.get("positions")) or _normalize_hardcore_position_family(config.get("family")) != "any"
|
||||
return hardcore_position_policy.hardcore_position_template_required(config)
|
||||
|
||||
|
||||
def _is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
|
||||
@@ -1491,37 +1144,7 @@ def _is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
|
||||
|
||||
|
||||
def _hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
||||
family = _normalize_hardcore_position_family(config.get("family"))
|
||||
allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
|
||||
if not config.get("allow_penetration", True):
|
||||
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
||||
if not config.get("allow_foreplay", True):
|
||||
allowed.discard("foreplay_teasing")
|
||||
if not config.get("allow_interaction", True):
|
||||
allowed.difference_update(
|
||||
{
|
||||
"foreplay_teasing",
|
||||
"body_worship_touching",
|
||||
"clothing_position_transitions",
|
||||
"dominant_guidance",
|
||||
"camera_performance",
|
||||
"group_coordination",
|
||||
"aftercare_cleanup",
|
||||
}
|
||||
)
|
||||
if not config.get("allow_manual", True):
|
||||
allowed.discard("manual_stimulation")
|
||||
if not config.get("allow_oral", True):
|
||||
allowed.discard("oral_sex")
|
||||
if not config.get("allow_outercourse", True):
|
||||
allowed.discard("outercourse_sex")
|
||||
if not config.get("allow_anal", True):
|
||||
allowed.discard("anal_double_penetration")
|
||||
if not config.get("allow_climax", True):
|
||||
allowed.discard("cumshot_climax")
|
||||
if not config.get("allow_double", True) and family == "anal":
|
||||
allowed.add("anal_double_penetration")
|
||||
return allowed or set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
|
||||
return hardcore_position_policy.hardcore_allowed_subcategory_slugs(config)
|
||||
|
||||
|
||||
def _filter_hardcore_categories_for_position(
|
||||
@@ -1993,15 +1616,15 @@ def hardcore_detail_density_choices() -> list[str]:
|
||||
|
||||
|
||||
def hardcore_position_family_choices() -> list[str]:
|
||||
return list(HARDCORE_POSITION_FAMILY_CHOICES)
|
||||
return hardcore_position_policy.hardcore_position_family_choices()
|
||||
|
||||
|
||||
def hardcore_position_focus_choices() -> list[str]:
|
||||
return list(HARDCORE_POSITION_FOCUS_CHOICES)
|
||||
return hardcore_position_policy.hardcore_position_focus_choices()
|
||||
|
||||
|
||||
def hardcore_position_key_choices() -> list[str]:
|
||||
return list(HARDCORE_POSITION_KEY_CHOICES)
|
||||
return hardcore_position_policy.hardcore_position_key_choices()
|
||||
|
||||
|
||||
def character_softcore_outfit_source_choices() -> list[str]:
|
||||
|
||||
@@ -27,6 +27,7 @@ import caption_naturalizer # noqa: E402
|
||||
import category_cast_config # noqa: E402
|
||||
import category_library # noqa: E402
|
||||
import filter_config # noqa: E402
|
||||
import hardcore_position_config # noqa: E402
|
||||
import __init__ as sxcp_nodes # noqa: E402
|
||||
import generation_profile_config # noqa: E402
|
||||
import krea_formatter # noqa: E402
|
||||
@@ -661,6 +662,64 @@ def smoke_filter_config_policy() -> None:
|
||||
_expect(pb.normalize_ethnicity_filter("random", "any", allow_random=False) == "any", "Ethnicity default normalization changed")
|
||||
|
||||
|
||||
def smoke_hardcore_position_config_policy() -> None:
|
||||
_expect(
|
||||
pb.HARDCORE_POSITION_FAMILY_CHOICES is hardcore_position_config.HARDCORE_POSITION_FAMILY_CHOICES,
|
||||
"Prompt builder hardcore position family choices are not delegated",
|
||||
)
|
||||
_expect("outercourse_only" in hardcore_position_config.hardcore_position_focus_choices(), "Hardcore focus choices lost outercourse_only")
|
||||
_expect("boobjob" in hardcore_position_config.hardcore_position_key_choices(), "Hardcore position keys lost boobjob")
|
||||
|
||||
base = json.loads(
|
||||
pb.build_hardcore_position_pool_json(
|
||||
combine_mode="replace",
|
||||
family="oral",
|
||||
selected_positions=["standing", "bad value", "standing"],
|
||||
)
|
||||
)
|
||||
_expect(base.get("enabled") is True, "Hardcore position pool should enable config")
|
||||
_expect(base.get("family") == "oral", "Hardcore position pool lost family")
|
||||
_expect(base.get("positions") == ["standing"], "Hardcore position normalization changed")
|
||||
_expect(base.get("require_position") is True, "Hardcore position pool should require selected position")
|
||||
|
||||
added = json.loads(
|
||||
hardcore_position_config.build_hardcore_position_pool_json(
|
||||
hardcore_position_config=base,
|
||||
combine_mode="add",
|
||||
family="any",
|
||||
selected_positions=["kneeling", "standing"],
|
||||
)
|
||||
)
|
||||
_expect(added.get("positions") == ["standing", "kneeling"], "Hardcore position add merge changed")
|
||||
|
||||
filtered = json.loads(
|
||||
pb.build_hardcore_action_filter_json(
|
||||
hardcore_position_config=added,
|
||||
focus="outercourse_only",
|
||||
allow_toys=False,
|
||||
allow_double=False,
|
||||
allow_penetration=True,
|
||||
allow_foreplay=True,
|
||||
allow_interaction=True,
|
||||
allow_manual=True,
|
||||
allow_oral=True,
|
||||
allow_outercourse=True,
|
||||
allow_anal=True,
|
||||
allow_climax=True,
|
||||
)
|
||||
)
|
||||
_expect(filtered.get("family") == "outercourse", "Hardcore action focus did not set outercourse family")
|
||||
_expect(filtered.get("allow_oral") is False, "Hardcore outercourse focus should disable oral")
|
||||
_expect(filtered.get("allow_penetration") is False, "Hardcore outercourse focus should disable penetration")
|
||||
_expect("outercourse_sex" in hardcore_position_config.hardcore_allowed_subcategory_slugs(filtered), "Allowed subcategories lost outercourse")
|
||||
_expect("oral_sex" not in hardcore_position_config.hardcore_allowed_subcategory_slugs(filtered), "Allowed subcategories should exclude oral")
|
||||
|
||||
keys = pb._hardcore_position_keys("woman on all fours from behind", axis_values={"position": "doggy"})
|
||||
_expect(keys == ["doggy"], "Hardcore position key detection changed")
|
||||
source_family = hardcore_position_config.hardcore_source_position_family({"slug": "manual_stimulation"}, filtered)
|
||||
_expect(source_family == "manual", "Hardcore source family lookup changed")
|
||||
|
||||
|
||||
def smoke_category_library_route() -> None:
|
||||
categories = category_library.load_category_library()
|
||||
_expect(len(categories) >= 3, "category library should load JSON categories")
|
||||
@@ -2571,6 +2630,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
||||
("category_cast_config_policy", smoke_category_cast_config_policy),
|
||||
("generation_profile_config_policy", smoke_generation_profile_config_policy),
|
||||
("filter_config_policy", smoke_filter_config_policy),
|
||||
("hardcore_position_config_policy", smoke_hardcore_position_config_policy),
|
||||
("category_library_route", smoke_category_library_route),
|
||||
("hardcore_category_routes", smoke_hardcore_category_routes),
|
||||
("krea_close_foreplay_route", smoke_krea_close_foreplay_route),
|
||||
|
||||
Reference in New Issue
Block a user