Promote multi-person hardcore action routing
This commit is contained in:
@@ -55,6 +55,8 @@ ACTION_FAMILY_CAPTION_LABELS = {
|
||||
"outercourse": "non-penetrative action",
|
||||
"oral": "oral action",
|
||||
"penetration": "penetrative action",
|
||||
"threesome": "three-person action",
|
||||
"group": "group action",
|
||||
"toy_double": "toy-assisted double-contact action",
|
||||
"climax": "climax action",
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ ACTION_MANUAL = "manual"
|
||||
ACTION_OUTERCOURSE = "outercourse"
|
||||
ACTION_ORAL = "oral"
|
||||
ACTION_PENETRATION = "penetration"
|
||||
ACTION_THREESOME = "threesome"
|
||||
ACTION_GROUP = "group"
|
||||
ACTION_TOY_DOUBLE = "toy_double"
|
||||
ACTION_DEFAULT = "default"
|
||||
|
||||
@@ -43,6 +45,8 @@ HARDCORE_ACTION_FAMILY_CHOICES = {
|
||||
ACTION_OUTERCOURSE,
|
||||
ACTION_ORAL,
|
||||
ACTION_PENETRATION,
|
||||
ACTION_THREESOME,
|
||||
ACTION_GROUP,
|
||||
ACTION_TOY_DOUBLE,
|
||||
ACTION_DEFAULT,
|
||||
}
|
||||
@@ -66,6 +70,16 @@ def normalize_hardcore_action_family(value: Any, default: str = "") -> str:
|
||||
"anal_penetration": ACTION_ANAL,
|
||||
"outer_course": ACTION_OUTERCOURSE,
|
||||
"outercourse_sex": ACTION_OUTERCOURSE,
|
||||
"three_person": ACTION_THREESOME,
|
||||
"three_person_action": ACTION_THREESOME,
|
||||
"threesome": ACTION_THREESOME,
|
||||
"threesomes": ACTION_THREESOME,
|
||||
"threeway": ACTION_THREESOME,
|
||||
"three_way": ACTION_THREESOME,
|
||||
"group": ACTION_GROUP,
|
||||
"group_sex": ACTION_GROUP,
|
||||
"group_sex_orgy": ACTION_GROUP,
|
||||
"orgy": ACTION_GROUP,
|
||||
"manual": ACTION_MANUAL,
|
||||
"manual_stimulation": ACTION_MANUAL,
|
||||
"interaction": ACTION_FOREPLAY,
|
||||
@@ -143,6 +157,8 @@ def source_hardcore_action_family(
|
||||
"manual": ACTION_MANUAL,
|
||||
"oral": ACTION_ORAL,
|
||||
"outercourse": ACTION_OUTERCOURSE,
|
||||
"threesome": ACTION_THREESOME,
|
||||
"group": ACTION_GROUP,
|
||||
"climax": ACTION_CLIMAX,
|
||||
}
|
||||
return source_mapping.get(family, inferred)
|
||||
|
||||
@@ -525,7 +525,8 @@ def hardcore_position_template_required(config: dict[str, Any]) -> bool:
|
||||
|
||||
def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
||||
family = normalize_hardcore_position_family(config.get("family"))
|
||||
allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
|
||||
base_allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
|
||||
allowed = set(base_allowed)
|
||||
if not config.get("allow_penetration", True):
|
||||
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
||||
if not config.get("allow_foreplay", True):
|
||||
@@ -554,7 +555,11 @@ def hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
||||
allowed.discard("cumshot_climax")
|
||||
if not config.get("allow_double", True) and family == "anal":
|
||||
allowed.add("anal_double_penetration")
|
||||
return allowed or set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
|
||||
if allowed:
|
||||
return allowed
|
||||
if family != "any":
|
||||
return base_allowed
|
||||
return set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
|
||||
|
||||
|
||||
def is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
|
||||
|
||||
@@ -53,6 +53,8 @@ SDXL_ACTION_FAMILY_TAGS = {
|
||||
"outercourse": ("outercourse", "non-penetrative sex"),
|
||||
"oral": ("oral sex",),
|
||||
"penetration": ("penetrative sex", "penetration"),
|
||||
"threesome": ("threesome",),
|
||||
"group": ("group sex",),
|
||||
"toy_double": ("double penetration", "toy-assisted sex"),
|
||||
"climax": ("climax", "semen"),
|
||||
}
|
||||
|
||||
@@ -113,6 +113,29 @@ def _character_cast(*, pov_man: bool = False) -> str:
|
||||
)["character_cast"]
|
||||
|
||||
|
||||
def _character_cast_subjects(subjects: list[str] | tuple[str, ...]) -> str:
|
||||
cast = ""
|
||||
counts = {"woman": 0, "man": 0}
|
||||
for subject in subjects:
|
||||
subject = str(subject)
|
||||
counts[subject] += 1
|
||||
label = chr(ord("A") + counts[subject] - 1)
|
||||
cast = pb.build_character_slot_json(
|
||||
subject_type=subject,
|
||||
label=label,
|
||||
age="25-year-old adult" if subject == "woman" else "40-year-old adult",
|
||||
ethnicity="western_european",
|
||||
figure="balanced",
|
||||
body="slim busty" if subject == "woman" else "average",
|
||||
descriptor_detail="compact",
|
||||
expression_intensity=0.55,
|
||||
softcore_expression_intensity=0.35,
|
||||
hardcore_expression_intensity=0.75,
|
||||
character_cast=cast,
|
||||
)["character_cast"]
|
||||
return cast
|
||||
|
||||
|
||||
def _random_character_cast() -> str:
|
||||
cast = pb.build_character_slot_json(
|
||||
subject_type="woman",
|
||||
@@ -302,6 +325,30 @@ HARDCORE_ROUTE_CASES = (
|
||||
"caption": ("anal action",),
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "hardcore.single.threesome",
|
||||
"subcategory": "Threesomes",
|
||||
"focus": "threesome_only",
|
||||
"family": "threesome",
|
||||
"expected_route": {"action_family": "threesome", "position_family": "threesome"},
|
||||
"expected_terms": {
|
||||
"krea": ("three",),
|
||||
"sdxl": ("threesome",),
|
||||
"caption": ("three-person action",),
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "hardcore.single.group",
|
||||
"subcategory": "Group sex and orgy",
|
||||
"focus": "group_only",
|
||||
"family": "group",
|
||||
"expected_route": {"action_family": "group", "position_family": "group"},
|
||||
"expected_terms": {
|
||||
"krea": ("group",),
|
||||
"sdxl": ("group sex",),
|
||||
"caption": ("group action",),
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "hardcore.single.climax",
|
||||
"subcategory": "Cumshot and climax",
|
||||
@@ -309,7 +356,7 @@ HARDCORE_ROUTE_CASES = (
|
||||
"family": "climax",
|
||||
"expected_route": {"action_family": "climax", "position_family": "climax"},
|
||||
"expected_terms": {
|
||||
"krea": ("ejaculation",),
|
||||
"krea": ("semen",),
|
||||
"sdxl": ("climax", "semen"),
|
||||
"caption": ("climax action",),
|
||||
},
|
||||
@@ -734,6 +781,10 @@ def _regular_single_case(seed: int) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _hardcore_single_case(seed: int, subcategory: str, focus: str, family: str) -> dict[str, Any]:
|
||||
women_count, men_count, character_cast = {
|
||||
"threesome": (1, 2, _character_cast_subjects(("woman", "man", "man"))),
|
||||
"group": (2, 2, _character_cast_subjects(("woman", "woman", "man", "man"))),
|
||||
}.get(family, (1, 1, _character_cast()))
|
||||
return pb.build_prompt(
|
||||
category="Hardcore sexual poses",
|
||||
subcategory=subcategory,
|
||||
@@ -754,9 +805,9 @@ def _hardcore_single_case(seed: int, subcategory: str, focus: str, family: str)
|
||||
extra_positive="",
|
||||
extra_negative="",
|
||||
seed_config=pb.build_seed_lock_config_json(base_seed=seed),
|
||||
women_count=1,
|
||||
men_count=1,
|
||||
character_cast=_character_cast(),
|
||||
women_count=women_count,
|
||||
men_count=men_count,
|
||||
character_cast=character_cast,
|
||||
hardcore_position_config=_position_filter(focus, family, []),
|
||||
location_config=_coworking_location_config(),
|
||||
camera_config=_orbit_camera(horizontal_angle=35, vertical_angle=0, zoom=6.5),
|
||||
|
||||
+68
-1
@@ -4015,6 +4015,10 @@ def smoke_caption_policy() -> None:
|
||||
)
|
||||
row = {"action_family": "oral", "position_family": ""}
|
||||
_expect(caption_policy.metadata_action_label(row) == "oral action", "Caption action-family label changed")
|
||||
row = {"action_family": "threesome", "position_family": ""}
|
||||
_expect(caption_policy.metadata_action_label(row) == "three-person action", "Caption threesome action-family label changed")
|
||||
row = {"action_family": "group", "position_family": ""}
|
||||
_expect(caption_policy.metadata_action_label(row) == "group action", "Caption group action-family label changed")
|
||||
row = {"action_family": "oral", "position_family": "Anal"}
|
||||
_expect(caption_naturalizer._metadata_action_label(row) == "anal action", "Caption position-family label priority changed")
|
||||
browsing_caption, browsing_method = caption_naturalizer.naturalize_caption(
|
||||
@@ -4913,6 +4917,14 @@ def smoke_hardcore_position_config_policy() -> None:
|
||||
category_template_metadata.template_action_family({"action_family": "anal sex"}) == "anal",
|
||||
"Template action-family normalizer should accept anal aliases",
|
||||
)
|
||||
_expect(
|
||||
category_template_metadata.template_action_family({"action_family": "three way"}) == "threesome",
|
||||
"Template action-family normalizer should accept threesome aliases",
|
||||
)
|
||||
_expect(
|
||||
category_template_metadata.template_action_family({"action_family": "group sex"}) == "group",
|
||||
"Template action-family normalizer should accept group aliases",
|
||||
)
|
||||
_expect(
|
||||
category_template_metadata.template_position_family({"position_family": "penetration"}) == "penetrative",
|
||||
"Template position-family normalizer should accept action-style aliases",
|
||||
@@ -4965,6 +4977,26 @@ def smoke_hardcore_position_config_policy() -> None:
|
||||
_expect(filtered.get("allow_penetration") is False, "Hardcore outercourse focus should disable penetration")
|
||||
_expect("outercourse_sex" in hardcore_position_config.hardcore_allowed_subcategory_slugs(filtered), "Allowed subcategories lost outercourse")
|
||||
_expect("oral_sex" not in hardcore_position_config.hardcore_allowed_subcategory_slugs(filtered), "Allowed subcategories should exclude oral")
|
||||
strict_threesome = json.loads(
|
||||
pb.build_hardcore_action_filter_json(
|
||||
hardcore_position_config=pb.build_hardcore_position_pool_json(family="threesome"),
|
||||
focus="threesome_only",
|
||||
allow_toys=False,
|
||||
allow_double=False,
|
||||
allow_penetration=False,
|
||||
allow_foreplay=False,
|
||||
allow_interaction=False,
|
||||
allow_manual=False,
|
||||
allow_oral=False,
|
||||
allow_outercourse=False,
|
||||
allow_anal=False,
|
||||
allow_climax=False,
|
||||
)
|
||||
)
|
||||
_expect(
|
||||
hardcore_position_config.hardcore_allowed_subcategory_slugs(strict_threesome) == {"threesomes"},
|
||||
"Specific hardcore family filter should not widen to the full pool when boolean filters empty it",
|
||||
)
|
||||
action_only = json.loads(
|
||||
hardcore_position_config.build_hardcore_action_filter_json(
|
||||
focus="outercourse_only",
|
||||
@@ -5062,6 +5094,14 @@ def smoke_hardcore_position_config_policy() -> None:
|
||||
"generic contact",
|
||||
)
|
||||
_expect(source_action_family == "outercourse", "Source action-family fallback should accept hyphenated source aliases")
|
||||
_expect(
|
||||
hardcore_action_metadata.source_hardcore_action_family("threesome", "", "three-body contact") == "threesome",
|
||||
"Source action-family fallback should accept threesome source family",
|
||||
)
|
||||
_expect(
|
||||
hardcore_action_metadata.source_hardcore_action_family("group", "", "group sex contact") == "group",
|
||||
"Source action-family fallback should accept group source family",
|
||||
)
|
||||
default_action_route = row_route_metadata.resolve_action_position_route(
|
||||
is_pose_category=True,
|
||||
subcategory={"slug": "anal_double_penetration"},
|
||||
@@ -5550,6 +5590,31 @@ def smoke_hardcore_category_routes() -> None:
|
||||
_expect(sdxl_tag in (sdxl.get("sdxl_prompt") or "").lower(), f"{name} SDXL prompt did not include family tag {sdxl_tag!r}")
|
||||
caption, _method = caption_naturalizer.naturalize_caption("", metadata_json=_json(row), trigger=Trigger, include_trigger=True)
|
||||
_expect(caption_label in caption.lower(), f"{name} caption did not include family label {caption_label!r}")
|
||||
multi_cases = [
|
||||
("hardcore_threesome", "Threesomes", "threesome_only", "threesome", {"threesome", "toy_double"}, "threesome", "three-person action", 1, 2),
|
||||
("hardcore_group", "Group sex and orgy", "group_only", "group", {"group", "toy_double"}, "group sex", "group action", 2, 2),
|
||||
]
|
||||
for index, (name, subcategory, focus, position_family, action_families, sdxl_tag, caption_label, women_count, men_count) in enumerate(multi_cases, start=1151):
|
||||
subjects = ["woman"] * women_count + ["man"] * men_count
|
||||
row = _prompt_row(
|
||||
name=name,
|
||||
category="Hardcore sexual poses",
|
||||
subcategory=subcategory,
|
||||
seed=index,
|
||||
character_cast=_character_cast_subjects(subjects),
|
||||
women_count=women_count,
|
||||
men_count=men_count,
|
||||
hardcore_position_config=_action_filter(focus),
|
||||
)
|
||||
_expect_custom_row(row, name)
|
||||
_expect(row.get("subject_type") == "configured_cast", f"{name} should use configured cast")
|
||||
_expect(row.get("position_family") == position_family, f"{name} position_family mismatch: {row.get('position_family')}")
|
||||
_expect(row.get("action_family") in action_families, f"{name} action_family mismatch: {row.get('action_family')}")
|
||||
_expect_formatter_outputs(row, name, target="single")
|
||||
sdxl = sdxl_formatter.format_sdxl_prompt("", metadata_json=_json(row), target="single", trigger=SdxlTrigger, prepend_trigger=True)
|
||||
_expect(sdxl_tag in (sdxl.get("sdxl_prompt") or "").lower(), f"{name} SDXL prompt did not include family tag {sdxl_tag!r}")
|
||||
caption, _method = caption_naturalizer.naturalize_caption("", metadata_json=_json(row), trigger=Trigger, include_trigger=True)
|
||||
_expect(caption_label in caption.lower(), f"{name} caption did not include family label {caption_label!r}")
|
||||
annotated_row = None
|
||||
for seed in range(1801, 1841):
|
||||
row = _prompt_row(
|
||||
@@ -7071,6 +7136,8 @@ def smoke_fallback_role_graph_routes() -> None:
|
||||
)
|
||||
_expect_custom_row(row, name)
|
||||
_expect(row.get("position_family") == family, f"{name} position_family mismatch: {row.get('position_family')}")
|
||||
if family == "threesome":
|
||||
_expect(row.get("action_family") == "threesome", f"{name} action_family should be threesome")
|
||||
_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"), 30).lower()
|
||||
for term in role_terms:
|
||||
@@ -7844,7 +7911,7 @@ def smoke_seed_config_policy() -> None:
|
||||
def smoke_prompt_route_simulation_policy() -> None:
|
||||
report = prompt_route_simulation.run_simulation(seed=3901, include_prompts=False)
|
||||
summary = report.get("summary") or {}
|
||||
_expect(summary.get("cases") == 11, "Prompt route simulation case count changed unexpectedly")
|
||||
_expect(summary.get("cases") == 13, "Prompt route simulation case count changed unexpectedly")
|
||||
_expect(summary.get("axis_checks") == 6, "Prompt route simulation lost axis check coverage")
|
||||
_expect(summary.get("issues") == 0, f"Prompt route simulation reported issues: {report.get('issues')}")
|
||||
cases = {case.get("name"): case for case in report.get("cases") or []}
|
||||
|
||||
Reference in New Issue
Block a user