Split manual hardcore action routing

This commit is contained in:
2026-06-27 19:06:32 +02:00
parent c6f0fc34af
commit 08627be954
7 changed files with 24 additions and 14 deletions
+1 -1
View File
@@ -216,7 +216,7 @@
"inherit_compositions": false, "inherit_compositions": false,
"weight": 0.85, "weight": 0.85,
"item_template_metadata": { "item_template_metadata": {
"action_family": "foreplay", "action_family": "manual",
"position_family": "manual" "position_family": "manual"
}, },
"item_label": "Manual action", "item_label": "Manual action",
+5 -3
View File
@@ -27,6 +27,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
ACTION_CLIMAX = "climax" ACTION_CLIMAX = "climax"
ACTION_FOREPLAY = "foreplay" ACTION_FOREPLAY = "foreplay"
ACTION_MANUAL = "manual"
ACTION_OUTERCOURSE = "outercourse" ACTION_OUTERCOURSE = "outercourse"
ACTION_ORAL = "oral" ACTION_ORAL = "oral"
ACTION_PENETRATION = "penetration" ACTION_PENETRATION = "penetration"
@@ -36,6 +37,7 @@ ACTION_DEFAULT = "default"
HARDCORE_ACTION_FAMILY_CHOICES = { HARDCORE_ACTION_FAMILY_CHOICES = {
ACTION_CLIMAX, ACTION_CLIMAX,
ACTION_FOREPLAY, ACTION_FOREPLAY,
ACTION_MANUAL,
ACTION_OUTERCOURSE, ACTION_OUTERCOURSE,
ACTION_ORAL, ACTION_ORAL,
ACTION_PENETRATION, ACTION_PENETRATION,
@@ -59,8 +61,8 @@ def normalize_hardcore_action_family(value: Any, default: str = "") -> str:
"toy_assisted_double_penetration": ACTION_TOY_DOUBLE, "toy_assisted_double_penetration": ACTION_TOY_DOUBLE,
"outer_course": ACTION_OUTERCOURSE, "outer_course": ACTION_OUTERCOURSE,
"outercourse_sex": ACTION_OUTERCOURSE, "outercourse_sex": ACTION_OUTERCOURSE,
"manual": ACTION_FOREPLAY, "manual": ACTION_MANUAL,
"manual_stimulation": ACTION_FOREPLAY, "manual_stimulation": ACTION_MANUAL,
"interaction": ACTION_FOREPLAY, "interaction": ACTION_FOREPLAY,
"body_worship": ACTION_FOREPLAY, "body_worship": ACTION_FOREPLAY,
"body_worship_touching": ACTION_FOREPLAY, "body_worship_touching": ACTION_FOREPLAY,
@@ -132,7 +134,7 @@ def source_hardcore_action_family(
"penetrative": ACTION_PENETRATION, "penetrative": ACTION_PENETRATION,
"foreplay": ACTION_FOREPLAY, "foreplay": ACTION_FOREPLAY,
"interaction": ACTION_FOREPLAY, "interaction": ACTION_FOREPLAY,
"manual": ACTION_FOREPLAY, "manual": ACTION_MANUAL,
"oral": ACTION_ORAL, "oral": ACTION_ORAL,
"outercourse": ACTION_OUTERCOURSE, "outercourse": ACTION_OUTERCOURSE,
"climax": ACTION_CLIMAX, "climax": ACTION_CLIMAX,
+3 -1
View File
@@ -14,6 +14,7 @@ try:
from .hardcore_action_metadata import ( from .hardcore_action_metadata import (
ACTION_CLIMAX, ACTION_CLIMAX,
ACTION_FOREPLAY, ACTION_FOREPLAY,
ACTION_MANUAL,
ACTION_ORAL, ACTION_ORAL,
ACTION_OUTERCOURSE, ACTION_OUTERCOURSE,
ACTION_PENETRATION, ACTION_PENETRATION,
@@ -43,6 +44,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
from hardcore_action_metadata import ( from hardcore_action_metadata import (
ACTION_CLIMAX, ACTION_CLIMAX,
ACTION_FOREPLAY, ACTION_FOREPLAY,
ACTION_MANUAL,
ACTION_ORAL, ACTION_ORAL,
ACTION_OUTERCOURSE, ACTION_OUTERCOURSE,
ACTION_PENETRATION, ACTION_PENETRATION,
@@ -145,7 +147,7 @@ def action_detail_for_family(
) -> tuple[str, str]: ) -> tuple[str, str]:
if family == ACTION_CLIMAX: if family == ACTION_CLIMAX:
return "", dedupe_climax_detail(detail, role_graph, detail_density) 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) detail = sanitize_foreplay_detail(detail, role_graph, composition)
return "", limit_detail_for_density(detail, detail_density, False) return "", limit_detail_for_density(detail, detail_density, False)
if family == ACTION_OUTERCOURSE: if family == ACTION_OUTERCOURSE:
+1
View File
@@ -48,6 +48,7 @@ SDXL_DEFAULT_NEGATIVE = (
SDXL_ACTION_FAMILY_TAGS = { SDXL_ACTION_FAMILY_TAGS = {
"foreplay": ("foreplay", "body contact"), "foreplay": ("foreplay", "body contact"),
"manual": ("manual stimulation",),
"outercourse": ("outercourse", "non-penetrative sex"), "outercourse": ("outercourse", "non-penetrative sex"),
"oral": ("oral sex",), "oral": ("oral sex",),
"penetration": ("penetrative sex", "penetration"), "penetration": ("penetrative sex", "penetration"),
+1
View File
@@ -25,6 +25,7 @@ INCOMPATIBLE_ROUTE_TAGS = {
"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"),
"action:manual": ("penetrative sex", "penetration", "oral sex", "anal sex", "outercourse"),
"position:penetrative": ("oral sex", "outercourse", "anal sex", "manual stimulation"), "position:penetrative": ("oral sex", "outercourse", "anal sex", "manual stimulation"),
"position:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"), "position:oral": ("penetrative sex", "penetration", "anal sex", "outercourse"),
"position:outercourse": ("penetrative sex", "penetration", "oral sex", "anal sex", "manual stimulation"), "position:outercourse": ("penetrative sex", "penetration", "oral sex", "anal sex", "manual stimulation"),
+1 -1
View File
@@ -259,7 +259,7 @@ HARDCORE_ROUTE_CASES = (
"subcategory": "Manual stimulation", "subcategory": "Manual stimulation",
"focus": "manual_only", "focus": "manual_only",
"family": "manual", "family": "manual",
"expected_route": {"position_family": "manual"}, "expected_route": {"action_family": "manual", "position_family": "manual"},
"expected_terms": { "expected_terms": {
"krea": ("hand",), "krea": ("hand",),
"sdxl": ("manual stimulation",), "sdxl": ("manual stimulation",),
+12 -8
View File
@@ -4906,7 +4906,7 @@ def smoke_hardcore_position_config_policy() -> None:
"Template action-family normalizer should accept spaced aliases", "Template action-family normalizer should accept spaced aliases",
) )
_expect( _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", "Template action-family normalizer should accept subcategory-style aliases",
) )
_expect( _expect(
@@ -5115,7 +5115,7 @@ def smoke_hardcore_position_config_policy() -> None:
{ {
"name": "Inherited metadata route", "name": "Inherited metadata route",
"item_template_metadata": { "item_template_metadata": {
"action_family": "foreplay", "action_family": "manual",
"position_family": "manual", "position_family": "manual",
"position_keys": ["kneeling"], "position_keys": ["kneeling"],
"formatter_hint": {"caption": "inherited caption cue"}, "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_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_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(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(pb._template_position_keys(inherited_metadata) == ["kneeling"], "String template did not inherit position keys")
_expect( _expect(
@@ -5145,7 +5145,7 @@ def smoke_hardcore_position_config_policy() -> None:
{ {
"name": "Override metadata route", "name": "Override metadata route",
"item_template_metadata": { "item_template_metadata": {
"action_family": "foreplay", "action_family": "manual",
"position_family": "manual", "position_family": "manual",
"formatter_hint": {"all": "inherited shared cue"}, "formatter_hint": {"all": "inherited shared cue"},
}, },
@@ -5343,7 +5343,7 @@ def smoke_row_route_metadata_policy() -> None:
item_axis_values={"position": "kneeling manual position"}, item_axis_values={"position": "kneeling manual position"},
) )
_expect(fallback["position_family"] == "manual", "Route policy lost source position-family fallback") _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") _expect("kneeling" in fallback["position_keys"], "Route policy lost inferred position key")
empty = row_route_metadata.resolve_action_position_route( empty = row_route_metadata.resolve_action_position_route(
@@ -5519,7 +5519,7 @@ def smoke_hardcore_category_routes() -> None:
cases = [ cases = [
("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", {"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_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"),
@@ -6938,7 +6938,11 @@ def smoke_interaction_role_graph_routes() -> None:
hardcore_position_config=_position_filter(focus, family, [position_key]), hardcore_position_config=_position_filter(focus, family, [position_key]),
) )
_expect_custom_row(row, name) _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(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}") _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() 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, " "Woman A reclines with thighs open while Man A's hand is between her legs, "
"fingers visibly stimulating her pussy." "fingers visibly stimulating her pussy."
), ),
action_family="foreplay", action_family="manual",
position_family="manual", position_family="manual",
position_key="fingering", position_key="fingering",
position_keys=["fingering", "open_thighs"], position_keys=["fingering", "open_thighs"],