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_CLIMAX = "climax"
ACTION_ANAL = "anal"
ACTION_FOREPLAY = "foreplay" ACTION_FOREPLAY = "foreplay"
ACTION_MANUAL = "manual" ACTION_MANUAL = "manual"
ACTION_OUTERCOURSE = "outercourse" ACTION_OUTERCOURSE = "outercourse"
@@ -36,6 +37,7 @@ ACTION_DEFAULT = "default"
HARDCORE_ACTION_FAMILY_CHOICES = { HARDCORE_ACTION_FAMILY_CHOICES = {
ACTION_CLIMAX, ACTION_CLIMAX,
ACTION_ANAL,
ACTION_FOREPLAY, ACTION_FOREPLAY,
ACTION_MANUAL, ACTION_MANUAL,
ACTION_OUTERCOURSE, ACTION_OUTERCOURSE,
@@ -59,6 +61,9 @@ def normalize_hardcore_action_family(value: Any, default: str = "") -> str:
"toy_double_penetration": ACTION_TOY_DOUBLE, "toy_double_penetration": ACTION_TOY_DOUBLE,
"toy_assisted_double": ACTION_TOY_DOUBLE, "toy_assisted_double": ACTION_TOY_DOUBLE,
"toy_assisted_double_penetration": 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, "outer_course": ACTION_OUTERCOURSE,
"outercourse_sex": ACTION_OUTERCOURSE, "outercourse_sex": ACTION_OUTERCOURSE,
"manual": ACTION_MANUAL, "manual": ACTION_MANUAL,
@@ -132,6 +137,7 @@ def source_hardcore_action_family(
}.get(family, family) }.get(family, family)
source_mapping = { source_mapping = {
"penetrative": ACTION_PENETRATION, "penetrative": ACTION_PENETRATION,
"anal": ACTION_ANAL,
"foreplay": ACTION_FOREPLAY, "foreplay": ACTION_FOREPLAY,
"interaction": ACTION_FOREPLAY, "interaction": ACTION_FOREPLAY,
"manual": ACTION_MANUAL, "manual": ACTION_MANUAL,
@@ -139,6 +145,4 @@ def source_hardcore_action_family(
"outercourse": ACTION_OUTERCOURSE, "outercourse": ACTION_OUTERCOURSE,
"climax": ACTION_CLIMAX, "climax": ACTION_CLIMAX,
} }
if family == "anal":
return ACTION_DEFAULT
return source_mapping.get(family, inferred) 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: def is_toy_assisted_double_text(*parts: Any) -> bool:
text = " ".join(_clean(part).lower() for part in parts if _clean(part)) text = " ".join(_clean(part).lower() for part in parts if _clean(part))
if "toy" not in text:
return False
return any( return any(
token in text token in text
for token in ( for token in (
"double penetration", "double penetration",
"double-penetration", "double-penetration",
"front-and-back double",
"vaginal and anal penetration", "vaginal and anal penetration",
"pussy and ass filled",
"one penis in pussy and one penis in ass",
"second penetration point", "second penetration point",
"second point of contact", "second point of contact",
"second contact", "second contact",
+3 -1
View File
@@ -12,6 +12,7 @@ try:
normalize_hardcore_detail_density, normalize_hardcore_detail_density,
) )
from .hardcore_action_metadata import ( from .hardcore_action_metadata import (
ACTION_ANAL,
ACTION_CLIMAX, ACTION_CLIMAX,
ACTION_FOREPLAY, ACTION_FOREPLAY,
ACTION_MANUAL, ACTION_MANUAL,
@@ -42,6 +43,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
normalize_hardcore_detail_density, normalize_hardcore_detail_density,
) )
from hardcore_action_metadata import ( from hardcore_action_metadata import (
ACTION_ANAL,
ACTION_CLIMAX, ACTION_CLIMAX,
ACTION_FOREPLAY, ACTION_FOREPLAY,
ACTION_MANUAL, ACTION_MANUAL,
@@ -156,7 +158,7 @@ def action_detail_for_family(
if family == ACTION_ORAL and role_graph: if family == ACTION_ORAL and role_graph:
detail = dedupe_oral_detail(detail, role_graph, hard_item, axis_values) detail = dedupe_oral_detail(detail, role_graph, hard_item, axis_values)
return "", limit_detail_for_density(detail, detail_density, False) 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) detail = dedupe_penetration_detail(detail, role_graph, hard_item, axis_values)
return "", limit_detail_for_density(detail, detail_density, False) 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 "footjob outercourse pose"
return "non-penetrative outercourse pose" return "non-penetrative outercourse pose"
if is_toy_assisted_double_text(role_graph, hard_item, composition, axis_values_text(axis_values)): 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: 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: 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: 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: 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: 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: 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: if "kneeling anal" in text or "kneeling" in text:
return "toy-assisted kneeling 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 "toy-assisted 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 "double penetration" in text or "vaginal and anal penetration" in text or "front-and-back" in text:
if "face-down ass-up" in text: if "face-down ass-up" in text:
return "face-down rear-entry double-penetration pose" return "face-down rear-entry double-penetration pose"
+1
View File
@@ -47,6 +47,7 @@ SDXL_DEFAULT_NEGATIVE = (
) )
SDXL_ACTION_FAMILY_TAGS = { SDXL_ACTION_FAMILY_TAGS = {
"anal": ("anal sex",),
"foreplay": ("foreplay", "body contact"), "foreplay": ("foreplay", "body contact"),
"manual": ("manual stimulation",), "manual": ("manual stimulation",),
"outercourse": ("outercourse", "non-penetrative sex"), "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() PROMPT_FIELD_LABELS = input_policy.prompt_field_labels()
INCOMPATIBLE_ROUTE_TAGS = { INCOMPATIBLE_ROUTE_TAGS = {
"action:anal": ("oral sex", "outercourse", "manual stimulation"),
"action:penetration": ("oral sex", "outercourse", "anal sex", "manual stimulation"), "action:penetration": ("oral sex", "outercourse", "anal sex", "manual stimulation"),
"action:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"), "action:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"),
"action:outercourse": ("penetrative sex", "penetration", "oral sex", "anal sex", "manual stimulation"), "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", "subcategory": "Anal and double penetration",
"focus": "anal_only", "focus": "anal_only",
"family": "anal", "family": "anal",
"expected_route": {"position_family": "anal"}, "expected_route": {"action_family": "anal", "position_family": "anal"},
"expected_terms": { "expected_terms": {
"krea": ("anal",), "krea": ("anal",),
"sdxl": ("anal sex",), "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", category_template_metadata.template_action_family({"action_family": "manual stimulation"}) == "manual",
"Template action-family normalizer should accept subcategory-style aliases", "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( _expect(
category_template_metadata.template_position_family({"position_family": "penetration"}) == "penetrative", category_template_metadata.template_position_family({"position_family": "penetration"}) == "penetrative",
"Template position-family normalizer should accept action-style aliases", "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_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_oral", "Oral sex", "oral_only", "oral", {"oral"}, "oral sex", "oral action"),
("hardcore_manual", "Manual stimulation", "manual_only", "manual", {"manual"}, "manual stimulation", "manual 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_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_foreplay", "Foreplay and teasing", "foreplay_only", "foreplay", {"foreplay"}, "foreplay", "foreplay action"),
("hardcore_aftercare", "Aftercare and cleanup", "interaction_only", "interaction", {"foreplay"}, "interaction", "interaction beat"), ("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) _expect_pair(pair, name)
hard_row = pair.get("hardcore_row") or {} 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("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}") _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() role_graph = _expect_text(f"{name}.source_role_graph", hard_row.get("source_role_graph"), 40).lower()
for term in role_terms: for term in role_terms:
@@ -6752,6 +6758,7 @@ def smoke_double_front_back_route() -> None:
_expect_pair(pair, "double_front_back_route") _expect_pair(pair, "double_front_back_route")
hard_row = pair.get("hardcore_row") or {} 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("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") _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() 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}") _expect("second penetration point from the front" in role_graph, f"double route role graph lost front/back placement: {role_graph}")