From 658743d8761d6e2b7b3c48a81e6a2116ac7a4bd5 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 19:11:44 +0200 Subject: [PATCH] Promote anal hardcore action routing --- hardcore_action_metadata.py | 8 ++++++-- krea_action_context.py | 5 +++-- krea_action_dispatch.py | 4 +++- krea_action_positions.py | 18 ++++++++++-------- sdxl_presets.py | 1 + sdxl_tag_policy.py | 1 + tools/prompt_route_simulation.py | 2 +- tools/prompt_smoke.py | 7 +++++++ 8 files changed, 32 insertions(+), 14 deletions(-) diff --git a/hardcore_action_metadata.py b/hardcore_action_metadata.py index efa20cc..778ad39 100644 --- a/hardcore_action_metadata.py +++ b/hardcore_action_metadata.py @@ -26,6 +26,7 @@ except ImportError: # Allows local smoke tests with `python -c`. ACTION_CLIMAX = "climax" +ACTION_ANAL = "anal" ACTION_FOREPLAY = "foreplay" ACTION_MANUAL = "manual" ACTION_OUTERCOURSE = "outercourse" @@ -36,6 +37,7 @@ ACTION_DEFAULT = "default" HARDCORE_ACTION_FAMILY_CHOICES = { ACTION_CLIMAX, + ACTION_ANAL, ACTION_FOREPLAY, ACTION_MANUAL, ACTION_OUTERCOURSE, @@ -59,6 +61,9 @@ def normalize_hardcore_action_family(value: Any, default: str = "") -> str: "toy_double_penetration": ACTION_TOY_DOUBLE, "toy_assisted_double": ACTION_TOY_DOUBLE, "toy_assisted_double_penetration": ACTION_TOY_DOUBLE, + "anal": ACTION_ANAL, + "anal_sex": ACTION_ANAL, + "anal_penetration": ACTION_ANAL, "outer_course": ACTION_OUTERCOURSE, "outercourse_sex": ACTION_OUTERCOURSE, "manual": ACTION_MANUAL, @@ -132,6 +137,7 @@ def source_hardcore_action_family( }.get(family, family) source_mapping = { "penetrative": ACTION_PENETRATION, + "anal": ACTION_ANAL, "foreplay": ACTION_FOREPLAY, "interaction": ACTION_FOREPLAY, "manual": ACTION_MANUAL, @@ -139,6 +145,4 @@ def source_hardcore_action_family( "outercourse": ACTION_OUTERCOURSE, "climax": ACTION_CLIMAX, } - if family == "anal": - return ACTION_DEFAULT return source_mapping.get(family, inferred) diff --git a/krea_action_context.py b/krea_action_context.py index ddb2eb1..5aa955b 100644 --- a/krea_action_context.py +++ b/krea_action_context.py @@ -248,14 +248,15 @@ def is_vaginal_penetration_text(*parts: Any) -> bool: def is_toy_assisted_double_text(*parts: Any) -> bool: text = " ".join(_clean(part).lower() for part in parts if _clean(part)) - if "toy" not in text: - return False return any( token in text for token in ( "double penetration", "double-penetration", + "front-and-back double", "vaginal and anal penetration", + "pussy and ass filled", + "one penis in pussy and one penis in ass", "second penetration point", "second point of contact", "second contact", diff --git a/krea_action_dispatch.py b/krea_action_dispatch.py index 6f0041e..8d7ce85 100644 --- a/krea_action_dispatch.py +++ b/krea_action_dispatch.py @@ -12,6 +12,7 @@ try: normalize_hardcore_detail_density, ) from .hardcore_action_metadata import ( + ACTION_ANAL, ACTION_CLIMAX, ACTION_FOREPLAY, ACTION_MANUAL, @@ -42,6 +43,7 @@ except ImportError: # Allows local smoke tests with `python -c`. normalize_hardcore_detail_density, ) from hardcore_action_metadata import ( + ACTION_ANAL, ACTION_CLIMAX, ACTION_FOREPLAY, ACTION_MANUAL, @@ -156,7 +158,7 @@ def action_detail_for_family( if family == ACTION_ORAL and role_graph: detail = dedupe_oral_detail(detail, role_graph, hard_item, axis_values) return "", limit_detail_for_density(detail, detail_density, False) - if family == ACTION_PENETRATION and role_graph: + if family in (ACTION_ANAL, ACTION_PENETRATION) and role_graph: detail = dedupe_penetration_detail(detail, role_graph, hard_item, axis_values) return "", limit_detail_for_density(detail, detail_density, False) diff --git a/krea_action_positions.py b/krea_action_positions.py index af31a4b..3127b1c 100644 --- a/krea_action_positions.py +++ b/krea_action_positions.py @@ -68,21 +68,23 @@ def hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "", return "footjob outercourse pose" return "non-penetrative outercourse pose" if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_values_text(axis_values)): + prefix = "toy-assisted " if ("toy" in text or "strap-on" in text) else "" + front_back = "front-and-back" in text or "from the front" in text or "pussy and ass" in text if "face-down ass-up" in text or "face-down" in text: - return "toy-assisted face-down rear-entry double-penetration pose" + return f"{prefix}face-down rear-entry double-penetration pose" if "doggy style" in text or "doggy-style" in text or "all fours" in text or "rear-entry" in text: - return "toy-assisted rear-entry double-penetration pose" + return f"{prefix}rear-entry double-penetration pose" if "bent-over" in text or "bent forward" in text: - return "toy-assisted bent-over double-penetration pose" + return f"{prefix}bent-over double-penetration pose" if "spooning anal" in text or "side-lying anal" in text or "side-lying" in text: - return "toy-assisted side-lying double-penetration pose" + return f"{prefix}side-lying double-penetration pose" if "edge-supported" in text or "bed-edge" in text or "edge-of-bed" in text: - return "toy-assisted edge-supported double-penetration pose" + return f"{prefix}edge-supported front-and-back double-penetration pose" if front_back else f"{prefix}edge-supported double-penetration pose" if "standing anal" in text or "standing supported" in text or "standing" in text: - return "toy-assisted standing double-penetration pose" + return f"{prefix}standing front-and-back double-penetration pose" if front_back else f"{prefix}standing double-penetration pose" if "kneeling anal" in text or "kneeling" in text: - return "toy-assisted kneeling rear-entry double-penetration pose" - return "toy-assisted rear-entry double-penetration pose" + return f"{prefix}kneeling front-and-back double-penetration pose" if front_back else f"{prefix}kneeling rear-entry double-penetration pose" + return f"{prefix}front-and-back double-penetration pose" if front_back else f"{prefix}rear-entry double-penetration pose" if "double penetration" in text or "vaginal and anal penetration" in text or "front-and-back" in text: if "face-down ass-up" in text: return "face-down rear-entry double-penetration pose" diff --git a/sdxl_presets.py b/sdxl_presets.py index 75bacd7..56c1ce5 100644 --- a/sdxl_presets.py +++ b/sdxl_presets.py @@ -47,6 +47,7 @@ SDXL_DEFAULT_NEGATIVE = ( ) SDXL_ACTION_FAMILY_TAGS = { + "anal": ("anal sex",), "foreplay": ("foreplay", "body contact"), "manual": ("manual stimulation",), "outercourse": ("outercourse", "non-penetrative sex"), diff --git a/sdxl_tag_policy.py b/sdxl_tag_policy.py index a1977c1..3d68bd1 100644 --- a/sdxl_tag_policy.py +++ b/sdxl_tag_policy.py @@ -22,6 +22,7 @@ except ImportError: # Allows local smoke tests with `python -c`. PROMPT_FIELD_LABELS = input_policy.prompt_field_labels() INCOMPATIBLE_ROUTE_TAGS = { + "action:anal": ("oral sex", "outercourse", "manual stimulation"), "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"), diff --git a/tools/prompt_route_simulation.py b/tools/prompt_route_simulation.py index 3bdf1cd..038f7d6 100644 --- a/tools/prompt_route_simulation.py +++ b/tools/prompt_route_simulation.py @@ -295,7 +295,7 @@ HARDCORE_ROUTE_CASES = ( "subcategory": "Anal and double penetration", "focus": "anal_only", "family": "anal", - "expected_route": {"position_family": "anal"}, + "expected_route": {"action_family": "anal", "position_family": "anal"}, "expected_terms": { "krea": ("anal",), "sdxl": ("anal sex",), diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index d5718ad..68fa449 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -4909,6 +4909,10 @@ def smoke_hardcore_position_config_policy() -> None: category_template_metadata.template_action_family({"action_family": "manual stimulation"}) == "manual", "Template action-family normalizer should accept subcategory-style aliases", ) + _expect( + category_template_metadata.template_action_family({"action_family": "anal sex"}) == "anal", + "Template action-family normalizer should accept anal aliases", + ) _expect( category_template_metadata.template_position_family({"position_family": "penetration"}) == "penetrative", "Template position-family normalizer should accept action-style aliases", @@ -5520,6 +5524,7 @@ def smoke_hardcore_category_routes() -> None: ("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", {"manual"}, "manual stimulation", "manual action"), + ("hardcore_anal", "Anal and double penetration", "anal_only", "anal", {"anal", "toy_double"}, "anal sex", "anal 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"), @@ -6708,6 +6713,7 @@ def smoke_pov_anal_position_routes() -> None: _expect_pair(pair, name) hard_row = pair.get("hardcore_row") or {} _expect(hard_row.get("position_family") == "anal", f"{name} position_family should be anal") + _expect(hard_row.get("action_family") == "anal", f"{name} action_family should be anal") _expect(position_key in (hard_row.get("position_keys") or []), f"{name} lost position key {position_key!r}") role_graph = _expect_text(f"{name}.source_role_graph", hard_row.get("source_role_graph"), 40).lower() for term in role_terms: @@ -6752,6 +6758,7 @@ def smoke_double_front_back_route() -> None: _expect_pair(pair, "double_front_back_route") hard_row = pair.get("hardcore_row") or {} _expect(hard_row.get("position_family") == "anal", "double route position_family should be anal") + _expect(hard_row.get("action_family") == "toy_double", "double route action_family should stay toy_double") _expect("front_back" in (hard_row.get("position_keys") or []), "double route lost front_back key") role_graph = _expect_text("double_front_back_route.source_role_graph", hard_row.get("source_role_graph"), 40).lower() _expect("second penetration point from the front" in role_graph, f"double route role graph lost front/back placement: {role_graph}")