Promote anal hardcore action routing

This commit is contained in:
2026-06-27 19:11:44 +02:00
parent 08627be954
commit 658743d876
8 changed files with 32 additions and 14 deletions
+6 -2
View File
@@ -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)
+3 -2
View File
@@ -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",
+3 -1
View File
@@ -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)
+10 -8
View File
@@ -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"
+1
View File
@@ -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"),
+1
View File
@@ -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"),
+1 -1
View File
@@ -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",),
+7
View File
@@ -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}")