Cover JSON subcategory generation matrix
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user