From 5ae2f31a20614cb0ed83d33a2a6cbb302f5534c9 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 15:03:47 +0200 Subject: [PATCH] Cover JSON subcategory generation matrix --- row_category_route.py | 4 +- tools/prompt_smoke.py | 120 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/row_category_route.py b/row_category_route.py index 15e4b65..5016ad8 100644 --- a/row_category_route.py +++ b/row_category_route.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from dataclasses import dataclass 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", ""), ) ).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( diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 187ad71..c672441 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -284,6 +284,43 @@ def _character_cast_subjects(subjects: list[str] | tuple[str, ...]) -> str: 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: kwargs = { "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: position_config = pb.build_hardcore_position_pool_json( 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["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: _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: cast = _character_cast() cases = [ @@ -7219,6 +7338,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("hardcore_position_config_policy", smoke_hardcore_position_config_policy), ("row_route_metadata_policy", smoke_row_route_metadata_policy), ("category_library_route", smoke_category_library_route), + ("category_subcategory_matrix", smoke_category_subcategory_matrix), ("hardcore_category_routes", smoke_hardcore_category_routes), ("krea_close_foreplay_route", smoke_krea_close_foreplay_route), ("pair_options_policy", smoke_pair_options_policy),