From 08627be954fb450fcd572285a4c5045aebb0a309 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 19:06:32 +0200 Subject: [PATCH] Split manual hardcore action routing --- categories/sexual_poses.json | 2 +- hardcore_action_metadata.py | 8 +++++--- krea_action_dispatch.py | 4 +++- sdxl_presets.py | 1 + sdxl_tag_policy.py | 1 + tools/prompt_route_simulation.py | 2 +- tools/prompt_smoke.py | 20 ++++++++++++-------- 7 files changed, 24 insertions(+), 14 deletions(-) diff --git a/categories/sexual_poses.json b/categories/sexual_poses.json index 93db4e3..39ff9db 100644 --- a/categories/sexual_poses.json +++ b/categories/sexual_poses.json @@ -216,7 +216,7 @@ "inherit_compositions": false, "weight": 0.85, "item_template_metadata": { - "action_family": "foreplay", + "action_family": "manual", "position_family": "manual" }, "item_label": "Manual action", diff --git a/hardcore_action_metadata.py b/hardcore_action_metadata.py index 1ad798b..efa20cc 100644 --- a/hardcore_action_metadata.py +++ b/hardcore_action_metadata.py @@ -27,6 +27,7 @@ except ImportError: # Allows local smoke tests with `python -c`. ACTION_CLIMAX = "climax" ACTION_FOREPLAY = "foreplay" +ACTION_MANUAL = "manual" ACTION_OUTERCOURSE = "outercourse" ACTION_ORAL = "oral" ACTION_PENETRATION = "penetration" @@ -36,6 +37,7 @@ ACTION_DEFAULT = "default" HARDCORE_ACTION_FAMILY_CHOICES = { ACTION_CLIMAX, ACTION_FOREPLAY, + ACTION_MANUAL, ACTION_OUTERCOURSE, ACTION_ORAL, ACTION_PENETRATION, @@ -59,8 +61,8 @@ def normalize_hardcore_action_family(value: Any, default: str = "") -> str: "toy_assisted_double_penetration": ACTION_TOY_DOUBLE, "outer_course": ACTION_OUTERCOURSE, "outercourse_sex": ACTION_OUTERCOURSE, - "manual": ACTION_FOREPLAY, - "manual_stimulation": ACTION_FOREPLAY, + "manual": ACTION_MANUAL, + "manual_stimulation": ACTION_MANUAL, "interaction": ACTION_FOREPLAY, "body_worship": ACTION_FOREPLAY, "body_worship_touching": ACTION_FOREPLAY, @@ -132,7 +134,7 @@ def source_hardcore_action_family( "penetrative": ACTION_PENETRATION, "foreplay": ACTION_FOREPLAY, "interaction": ACTION_FOREPLAY, - "manual": ACTION_FOREPLAY, + "manual": ACTION_MANUAL, "oral": ACTION_ORAL, "outercourse": ACTION_OUTERCOURSE, "climax": ACTION_CLIMAX, diff --git a/krea_action_dispatch.py b/krea_action_dispatch.py index 3ea5ab3..6f0041e 100644 --- a/krea_action_dispatch.py +++ b/krea_action_dispatch.py @@ -14,6 +14,7 @@ try: from .hardcore_action_metadata import ( ACTION_CLIMAX, ACTION_FOREPLAY, + ACTION_MANUAL, ACTION_ORAL, ACTION_OUTERCOURSE, ACTION_PENETRATION, @@ -43,6 +44,7 @@ except ImportError: # Allows local smoke tests with `python -c`. from hardcore_action_metadata import ( ACTION_CLIMAX, ACTION_FOREPLAY, + ACTION_MANUAL, ACTION_ORAL, ACTION_OUTERCOURSE, ACTION_PENETRATION, @@ -145,7 +147,7 @@ def action_detail_for_family( ) -> tuple[str, str]: if family == ACTION_CLIMAX: return "", dedupe_climax_detail(detail, role_graph, detail_density) - if family == ACTION_FOREPLAY: + if family in (ACTION_FOREPLAY, ACTION_MANUAL): detail = sanitize_foreplay_detail(detail, role_graph, composition) return "", limit_detail_for_density(detail, detail_density, False) if family == ACTION_OUTERCOURSE: diff --git a/sdxl_presets.py b/sdxl_presets.py index a4b5699..75bacd7 100644 --- a/sdxl_presets.py +++ b/sdxl_presets.py @@ -48,6 +48,7 @@ SDXL_DEFAULT_NEGATIVE = ( SDXL_ACTION_FAMILY_TAGS = { "foreplay": ("foreplay", "body contact"), + "manual": ("manual stimulation",), "outercourse": ("outercourse", "non-penetrative sex"), "oral": ("oral sex",), "penetration": ("penetrative sex", "penetration"), diff --git a/sdxl_tag_policy.py b/sdxl_tag_policy.py index edd692d..a1977c1 100644 --- a/sdxl_tag_policy.py +++ b/sdxl_tag_policy.py @@ -25,6 +25,7 @@ INCOMPATIBLE_ROUTE_TAGS = { "action:penetration": ("oral sex", "outercourse", "anal sex", "manual stimulation"), "action:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"), "action:outercourse": ("penetrative sex", "penetration", "oral sex", "anal sex", "manual stimulation"), + "action:manual": ("penetrative sex", "penetration", "oral sex", "anal sex", "outercourse"), "position:penetrative": ("oral sex", "outercourse", "anal sex", "manual stimulation"), "position:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"), "position:outercourse": ("penetrative sex", "penetration", "oral sex", "anal sex", "manual stimulation"), diff --git a/tools/prompt_route_simulation.py b/tools/prompt_route_simulation.py index 4146cc1..3bdf1cd 100644 --- a/tools/prompt_route_simulation.py +++ b/tools/prompt_route_simulation.py @@ -259,7 +259,7 @@ HARDCORE_ROUTE_CASES = ( "subcategory": "Manual stimulation", "focus": "manual_only", "family": "manual", - "expected_route": {"position_family": "manual"}, + "expected_route": {"action_family": "manual", "position_family": "manual"}, "expected_terms": { "krea": ("hand",), "sdxl": ("manual stimulation",), diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 7c2f564..d5718ad 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -4906,7 +4906,7 @@ def smoke_hardcore_position_config_policy() -> None: "Template action-family normalizer should accept spaced aliases", ) _expect( - category_template_metadata.template_action_family({"action_family": "manual stimulation"}) == "foreplay", + category_template_metadata.template_action_family({"action_family": "manual stimulation"}) == "manual", "Template action-family normalizer should accept subcategory-style aliases", ) _expect( @@ -5115,7 +5115,7 @@ def smoke_hardcore_position_config_policy() -> None: { "name": "Inherited metadata route", "item_template_metadata": { - "action_family": "foreplay", + "action_family": "manual", "position_family": "manual", "position_keys": ["kneeling"], "formatter_hint": {"caption": "inherited caption cue"}, @@ -5132,7 +5132,7 @@ def smoke_hardcore_position_config_policy() -> None: ) _expect(inherited_text == "hand stimulation in kneeling manual position", "Inherited template metadata changed item text") _expect(inherited_axis_values == {"act": "hand stimulation", "position": "kneeling manual position"}, "Inherited template metadata lost axis values") - _expect(inherited_metadata.get("action_family") == "foreplay", "String template did not inherit action family") + _expect(inherited_metadata.get("action_family") == "manual", "String template did not inherit action family") _expect(inherited_metadata.get("position_family") == "manual", "String template did not inherit position family") _expect(pb._template_position_keys(inherited_metadata) == ["kneeling"], "String template did not inherit position keys") _expect( @@ -5145,7 +5145,7 @@ def smoke_hardcore_position_config_policy() -> None: { "name": "Override metadata route", "item_template_metadata": { - "action_family": "foreplay", + "action_family": "manual", "position_family": "manual", "formatter_hint": {"all": "inherited shared cue"}, }, @@ -5343,7 +5343,7 @@ def smoke_row_route_metadata_policy() -> None: item_axis_values={"position": "kneeling manual position"}, ) _expect(fallback["position_family"] == "manual", "Route policy lost source position-family fallback") - _expect(fallback["action_family"] == "foreplay", "Route policy lost source action-family fallback") + _expect(fallback["action_family"] == "manual", "Route policy lost source action-family fallback") _expect("kneeling" in fallback["position_keys"], "Route policy lost inferred position key") empty = row_route_metadata.resolve_action_position_route( @@ -5519,7 +5519,7 @@ def smoke_hardcore_category_routes() -> None: cases = [ ("hardcore_penetration", "Penetrative sex", "penetration_only", "penetrative", {"penetration", "default"}, "penetrative sex", "penetrative action"), ("hardcore_oral", "Oral sex", "oral_only", "oral", {"oral"}, "oral sex", "oral action"), - ("hardcore_manual", "Manual stimulation", "manual_only", "manual", {"foreplay", "outercourse"}, "manual stimulation", "manual action"), + ("hardcore_manual", "Manual stimulation", "manual_only", "manual", {"manual"}, "manual stimulation", "manual action"), ("hardcore_outercourse", "Outercourse and genital teasing", "outercourse_only", "outercourse", {"outercourse"}, "outercourse", "non-penetrative action"), ("hardcore_foreplay", "Foreplay and teasing", "foreplay_only", "foreplay", {"foreplay"}, "foreplay", "foreplay action"), ("hardcore_aftercare", "Aftercare and cleanup", "interaction_only", "interaction", {"foreplay"}, "interaction", "interaction beat"), @@ -6938,7 +6938,11 @@ def smoke_interaction_role_graph_routes() -> None: hardcore_position_config=_position_filter(focus, family, [position_key]), ) _expect_custom_row(row, name) - _expect(row.get("action_family") == "foreplay", f"{name} action_family should stay formatter foreplay") + expected_action_family = "manual" if family == "manual" else "foreplay" + _expect( + row.get("action_family") == expected_action_family, + f"{name} action_family mismatch: {row.get('action_family')} != {expected_action_family}", + ) _expect(row.get("position_family") == family, f"{name} position_family mismatch: {row.get('position_family')}") _expect(position_key in (row.get("position_keys") or []), f"{name} lost position key {position_key!r}") role_graph = _expect_text(f"{name}.source_role_graph", row.get("source_role_graph"), 40).lower() @@ -7131,7 +7135,7 @@ def smoke_formatter_metadata_fixtures() -> None: "Woman A reclines with thighs open while Man A's hand is between her legs, " "fingers visibly stimulating her pussy." ), - action_family="foreplay", + action_family="manual", position_family="manual", position_key="fingering", position_keys=["fingering", "open_thighs"],