diff --git a/hardcore_action_metadata.py b/hardcore_action_metadata.py index 5f0521b..1ad798b 100644 --- a/hardcore_action_metadata.py +++ b/hardcore_action_metadata.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from typing import Any try: @@ -44,9 +45,32 @@ HARDCORE_ACTION_FAMILY_CHOICES = { def normalize_hardcore_action_family(value: Any, default: str = "") -> str: - text = str(value or "").strip().lower() - if text == "penetrative": - text = ACTION_PENETRATION + text = re.sub(r"[^a-z0-9]+", "_", str(value or "").strip().lower()).strip("_") + aliases = { + "penetrative": ACTION_PENETRATION, + "penetrative_sex": ACTION_PENETRATION, + "penetration_sex": ACTION_PENETRATION, + "vaginal": ACTION_PENETRATION, + "vaginal_penetration": ACTION_PENETRATION, + "double": ACTION_TOY_DOUBLE, + "double_penetration": ACTION_TOY_DOUBLE, + "toy_double_penetration": ACTION_TOY_DOUBLE, + "toy_assisted_double": ACTION_TOY_DOUBLE, + "toy_assisted_double_penetration": ACTION_TOY_DOUBLE, + "outer_course": ACTION_OUTERCOURSE, + "outercourse_sex": ACTION_OUTERCOURSE, + "manual": ACTION_FOREPLAY, + "manual_stimulation": ACTION_FOREPLAY, + "interaction": ACTION_FOREPLAY, + "body_worship": ACTION_FOREPLAY, + "body_worship_touching": ACTION_FOREPLAY, + "foreplay_teasing": ACTION_FOREPLAY, + "cumshot": ACTION_CLIMAX, + "cumshot_climax": ACTION_CLIMAX, + "orgasm_aftermath": ACTION_CLIMAX, + "oral_sex": ACTION_ORAL, + } + text = aliases.get(text, text) return text if text in HARDCORE_ACTION_FAMILY_CHOICES else default @@ -86,7 +110,24 @@ def source_hardcore_action_family( inferred = infer_hardcore_action_family(role_graph, hard_item, composition, axis_values) if inferred in (ACTION_CLIMAX, ACTION_TOY_DOUBLE): return inferred - family = str(source_family or "").strip().lower() + family = re.sub(r"[^a-z0-9]+", "_", str(source_family or "").strip().lower()).strip("_") + family = { + "penetration": "penetrative", + "penetrative_sex": "penetrative", + "outer_course": "outercourse", + "outercourse_sex": "outercourse", + "manual_stimulation": "manual", + "foreplay_teasing": "foreplay", + "body_worship": "interaction", + "body_worship_touching": "interaction", + "clothing_position_transitions": "interaction", + "dominant_guidance": "interaction", + "camera_performance": "interaction", + "group_coordination": "interaction", + "cumshot": "climax", + "cumshot_climax": "climax", + "oral_sex": "oral", + }.get(family, family) source_mapping = { "penetrative": ACTION_PENETRATION, "foreplay": ACTION_FOREPLAY, diff --git a/hardcore_position_config.py b/hardcore_position_config.py index 50b9fdd..49a31b8 100644 --- a/hardcore_position_config.py +++ b/hardcore_position_config.py @@ -276,7 +276,36 @@ def hardcore_position_key_choices() -> list[str]: def normalize_hardcore_position_family(value: Any, default: str = "any") -> str: - text = str(value or default).strip() + text = re.sub(r"[^a-z0-9]+", "_", str(value or default).strip().lower()).strip("_") + aliases = { + "penetration": "penetrative", + "penetrative_sex": "penetrative", + "penetration_sex": "penetrative", + "vaginal": "penetrative", + "vaginal_penetration": "penetrative", + "foreplay_teasing": "foreplay", + "body_worship": "interaction", + "body_worship_touching": "interaction", + "clothing_position_transitions": "interaction", + "dominant_guidance": "interaction", + "camera_performance": "interaction", + "group_coordination": "interaction", + "aftercare_cleanup": "interaction", + "manual_stimulation": "manual", + "oral_sex": "oral", + "outer_course": "outercourse", + "outercourse_sex": "outercourse", + "anal_double_penetration": "anal", + "three_some": "threesome", + "threesomes": "threesome", + "group_sex": "group", + "group_sex_orgy": "group", + "orgy": "group", + "cumshot": "climax", + "cumshot_climax": "climax", + "orgasm_aftermath": "climax", + } + text = aliases.get(text, text) return text if text in HARDCORE_POSITION_FAMILY_CHOICES else default diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 5b541b4..16e728b 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -44,6 +44,7 @@ import filter_config # noqa: E402 import formatter_detail # noqa: E402 import formatter_input # noqa: E402 import formatter_target # noqa: E402 +import hardcore_action_metadata # noqa: E402 import hardcore_position_config # noqa: E402 import __init__ as sxcp_nodes # noqa: E402 import generation_profile_config # noqa: E402 @@ -3741,6 +3742,22 @@ def smoke_hardcore_position_config_policy() -> None: ) _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") + _expect( + category_template_metadata.template_action_family({"action_family": "toy double"}) == "toy_double", + "Template action-family normalizer should accept spaced aliases", + ) + _expect( + category_template_metadata.template_action_family({"action_family": "manual stimulation"}) == "foreplay", + "Template action-family normalizer should accept subcategory-style aliases", + ) + _expect( + category_template_metadata.template_position_family({"position_family": "penetration"}) == "penetrative", + "Template position-family normalizer should accept action-style aliases", + ) + _expect( + category_template_metadata.template_position_family({"position_family": "outer-course"}) == "outercourse", + "Template position-family normalizer should accept hyphenated aliases", + ) base = json.loads( pb.build_hardcore_position_pool_json( @@ -3876,6 +3893,12 @@ def smoke_hardcore_position_config_policy() -> None: _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") + source_action_family = hardcore_action_metadata.source_hardcore_action_family( + "outer-course", + "", + "generic contact", + ) + _expect(source_action_family == "outercourse", "Source action-family fallback should accept hyphenated source aliases") item_text, item_name, axis_values, template_metadata = pb._compose_item( random.Random(42), {},