Cover JSON subcategory generation matrix

This commit is contained in:
2026-06-27 15:03:47 +02:00
parent a8d69083cd
commit 5ae2f31a20
2 changed files with 123 additions and 1 deletions
+3 -1
View File
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import re
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -45,7 +46,8 @@ def is_pose_content_category(category: dict[str, Any], subcategory: dict[str, An
subcategory.get("item_label", ""), subcategory.get("item_label", ""),
) )
).lower() ).lower()
return "pose" in haystack or "sex" in haystack tokens = set(re.findall(r"[a-z0-9]+", haystack))
return bool(tokens.intersection({"pose", "poses", "sex", "sexual"}))
def cast_count_adjustment( def cast_count_adjustment(
+120
View File
@@ -284,6 +284,43 @@ def _character_cast_subjects(subjects: list[str] | tuple[str, ...]) -> str:
return cast return cast
def _exact_subcategory_selector(category: dict[str, Any], subcategory: dict[str, Any]) -> str:
return f"{category.get('name')} / {subcategory.get('name')}"
def _matrix_cast_for_route(category: dict[str, Any], subcategory: dict[str, Any]) -> tuple[int, int, str]:
subject_type = str(subcategory.get("subject_type") or category.get("subject_type") or "woman")
if subject_type == "woman":
return 1, 0, ""
if subject_type == "man":
return 0, 1, ""
if subject_type == "couple":
return 1, 1, ""
min_people = int(subcategory.get("min_people") or 0)
women_count = int(subcategory.get("min_women") or 0)
men_count = int(subcategory.get("min_men") or 0)
if min_people <= 1 and not women_count and not men_count:
women_count = 1
elif min_people >= 4:
women_count = max(women_count, 2)
men_count = max(men_count, min_people - women_count)
elif min_people >= 3:
women_count = max(women_count, 1)
men_count = max(men_count, min_people - women_count)
elif min_people >= 2:
women_count = max(women_count, 1)
men_count = max(men_count, min_people - women_count)
required_total = max(1, min_people)
if women_count + men_count < required_total:
men_count += required_total - (women_count + men_count)
if women_count == 0 and men_count == 0:
women_count = 1
cast = _character_cast_subjects(["woman"] * women_count + ["man"] * men_count)
return women_count, men_count, cast
def _action_filter(focus: str, hardcore_position_config: str | dict[str, Any] | None = "") -> str: def _action_filter(focus: str, hardcore_position_config: str | dict[str, Any] | None = "") -> str:
kwargs = { kwargs = {
"allow_toys": False, "allow_toys": False,
@@ -304,6 +341,22 @@ def _action_filter(focus: str, hardcore_position_config: str | dict[str, Any] |
) )
def _broad_hardcore_filter() -> str:
return pb.build_hardcore_action_filter_json(
focus="keep_pool",
allow_toys=True,
allow_double=True,
allow_penetration=True,
allow_foreplay=True,
allow_interaction=True,
allow_manual=True,
allow_oral=True,
allow_outercourse=True,
allow_anal=True,
allow_climax=True,
)
def _position_filter(focus: str, family: str, positions: list[str] | tuple[str, ...] | str) -> str: def _position_filter(focus: str, family: str, positions: list[str] | tuple[str, ...] | str) -> str:
position_config = pb.build_hardcore_position_pool_json( position_config = pb.build_hardcore_position_pool_json(
combine_mode="replace", combine_mode="replace",
@@ -1691,6 +1744,20 @@ def smoke_row_category_route_policy() -> None:
_expect(casual_route["content_axis"] == "content", "Non-pose category should use content seed axis") _expect(casual_route["content_axis"] == "content", "Non-pose category should use content seed axis")
_expect(casual_route["is_pose_category"] is False, "Non-pose category should not be marked as pose content") _expect(casual_route["is_pose_category"] is False, "Non-pose category should not be marked as pose content")
exposed_route = row_category_route.select_category_item_route(
category_choice="custom_random",
subcategory_choice="Provocative erotic clothes / Sheer exposed",
seed_config=seed_cfg,
seed=2302,
row_number=1,
women_count=1,
men_count=0,
hardcore_position_config={},
)
_expect(exposed_route["subcategory"]["slug"] == "sheer_exposed", "Row category route selected wrong exposed category")
_expect(exposed_route["content_axis"] == "content", "Exposed clothing slug should not be treated as pose content")
_expect(exposed_route["is_pose_category"] is False, "Exposed clothing slug should not be marked as pose content")
def smoke_row_generation_policy() -> None: def smoke_row_generation_policy() -> None:
_expect(pb._ratio_or_none(-1) is None, "Prompt builder ratio helper should treat negative as unset") _expect(pb._ratio_or_none(-1) is None, "Prompt builder ratio helper should treat negative as unset")
@@ -4311,6 +4378,58 @@ def smoke_category_library_route() -> None:
) )
def smoke_category_subcategory_matrix() -> None:
categories = category_library.load_category_library()
cases: list[tuple[dict[str, Any], dict[str, Any]]] = []
for category in categories:
for subcategory in category.get("subcategories") or []:
if subcategory.get("item_templates") or subcategory.get("items"):
cases.append((category, subcategory))
_expect(len(cases) >= 30, "category matrix should cover all configured JSON subcategories")
hardcore_filter = _broad_hardcore_filter()
for index, (category, subcategory) in enumerate(cases, start=6101):
category_slug = str(category.get("slug") or "")
subcategory_slug = str(subcategory.get("slug") or "")
name = f"category_matrix.{category_slug}.{subcategory_slug}"
women_count, men_count, cast = _matrix_cast_for_route(category, subcategory)
row = _prompt_row(
name=name,
category=str(category.get("name") or ""),
subcategory=_exact_subcategory_selector(category, subcategory),
seed=index,
character_cast=cast,
women_count=women_count,
men_count=men_count,
hardcore_position_config=hardcore_filter if category_slug == "hardcore_sexual_poses" else "",
)
_expect(row.get("source") == "json_category", f"{name}.source should be json_category")
_expect(row.get("category_slug") == category_slug, f"{name}.category_slug drifted to {row.get('category_slug')}")
_expect(
row.get("subcategory_slug") == subcategory_slug,
f"{name}.subcategory_slug drifted to {row.get('subcategory_slug')}",
)
_expect_text(f"{name}.item", row.get("item"), 8)
_expect_text(f"{name}.scene_text", row.get("scene_text"), 8)
_expect_text(f"{name}.composition", row.get("composition"), 8)
_expect(isinstance(row.get("item_axis_values"), dict), f"{name}.item_axis_values missing")
_expect(isinstance(row.get("formatter_hints"), dict), f"{name}.formatter_hints missing")
if category_slug == "hardcore_sexual_poses":
_expect(row.get("content_seed_axis") == "pose", f"{name}.content_seed_axis should be pose")
_expect_text(f"{name}.source_role_graph", row.get("source_role_graph") or row.get("role_graph"), 20)
_expect_text(f"{name}.action_family", row.get("action_family"), 3)
_expect_text(f"{name}.position_family", row.get("position_family"), 3)
_expect(isinstance(row.get("position_keys"), list), f"{name}.position_keys missing")
_expect(isinstance(row.get("item_template_metadata"), dict), f"{name}.item_template_metadata missing")
_expect(row.get("item_template_metadata"), f"{name}.item_template_metadata should not be empty")
else:
_expect(row.get("content_seed_axis") == "content", f"{name}.content_seed_axis should be content")
_expect_formatter_outputs(row, name, target="single")
def smoke_hardcore_category_routes() -> None: def smoke_hardcore_category_routes() -> None:
cast = _character_cast() cast = _character_cast()
cases = [ cases = [
@@ -7219,6 +7338,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("hardcore_position_config_policy", smoke_hardcore_position_config_policy), ("hardcore_position_config_policy", smoke_hardcore_position_config_policy),
("row_route_metadata_policy", smoke_row_route_metadata_policy), ("row_route_metadata_policy", smoke_row_route_metadata_policy),
("category_library_route", smoke_category_library_route), ("category_library_route", smoke_category_library_route),
("category_subcategory_matrix", smoke_category_subcategory_matrix),
("hardcore_category_routes", smoke_hardcore_category_routes), ("hardcore_category_routes", smoke_hardcore_category_routes),
("krea_close_foreplay_route", smoke_krea_close_foreplay_route), ("krea_close_foreplay_route", smoke_krea_close_foreplay_route),
("pair_options_policy", smoke_pair_options_policy), ("pair_options_policy", smoke_pair_options_policy),