Centralize exact subcategory selectors

This commit is contained in:
2026-06-27 15:09:36 +02:00
parent 5ae2f31a20
commit 728d3e559c
5 changed files with 131 additions and 8 deletions
+1 -1
View File
@@ -113,5 +113,5 @@ def subcategory_choices() -> list[str]:
choices = [category_policy.RANDOM_SUBCATEGORY]
for category in category_policy.load_category_library():
for subcategory in category["subcategories"]:
choices.append(f"{category['name']} / {subcategory['name']}")
choices.append(category_policy.exact_subcategory_selector(category, subcategory))
return choices
+29 -4
View File
@@ -422,6 +422,30 @@ def find_category(categories: list[dict[str, Any]], name_or_slug: str) -> dict[s
return None
def exact_subcategory_selector(category: dict[str, Any], subcategory: dict[str, Any]) -> str:
return f"{category.get('name')} / {subcategory.get('name')}"
def split_exact_subcategory_choice(
categories: list[dict[str, Any]],
subcategory_choice: str,
) -> tuple[dict[str, Any], str] | None:
choice = str(subcategory_choice or "").strip()
if not choice or " / " not in choice:
return None
candidates: list[tuple[int, dict[str, Any], str]] = []
for category in categories:
for category_label in (category.get("name", ""), category.get("slug", "")):
category_label = str(category_label).strip()
prefix = f"{category_label} / "
if category_label and choice.lower().startswith(prefix.lower()):
candidates.append((len(prefix), category, choice[len(prefix) :].strip()))
if candidates:
_length, category, subcategory_name = max(candidates, key=lambda candidate: candidate[0])
return category, subcategory_name
return None
def _base_cast_counts(women_count: int, men_count: int) -> tuple[int, int]:
women_count = max(0, int(women_count))
men_count = max(0, int(men_count))
@@ -467,10 +491,11 @@ def find_subcategory(
) -> tuple[dict[str, Any], dict[str, Any], int, int]:
women_count, men_count = _base_cast_counts(women_count, men_count)
if subcategory_choice and subcategory_choice != random_subcategory and " / " in subcategory_choice:
category_name, subcategory_name = subcategory_choice.split(" / ", 1)
category = find_category(categories, category_name)
if not category:
exact_choice = split_exact_subcategory_choice(categories, subcategory_choice)
if not exact_choice:
category_name = str(subcategory_choice).split(" / ", 1)[0]
raise ValueError(f"Unknown category in subcategory picker: {category_name}")
category, subcategory_name = exact_choice
wanted = subcategory_name.strip().lower()
for subcategory in category["subcategories"]:
if subcategory["name"].lower() == wanted or subcategory["slug"].lower() == wanted:
@@ -485,7 +510,7 @@ def find_subcategory(
f"women_count={women_count}, men_count={men_count}"
)
return category, subcategory, adjusted_women_count, adjusted_men_count
raise ValueError(f"Unknown subcategory '{subcategory_name}' for category '{category_name}'")
raise ValueError(f"Unknown subcategory '{subcategory_name}' for category '{category['name']}'")
if category_choice == "custom_random":
if not categories:
+7
View File
@@ -233,6 +233,13 @@ There are two category systems.
JSON categories are the scalable system. Add new main categories or subcategories
there unless the behavior needs Python logic.
Exact JSON subcategory selection uses the full selector returned by
`category_library.exact_subcategory_selector`: `Category name / Subcategory
name`. The resolver parses that selector against the loaded category library
instead of blindly splitting the first slash separator, so custom category or
subcategory names may themselves contain `/` without drifting to a sibling
route.
## JSON Category Road
```mermaid
+10 -1
View File
@@ -4,6 +4,11 @@ import json
import re
from typing import Any
try:
from . import category_library as category_policy
except ImportError: # Allows local smoke tests from the repository root.
import category_library as category_policy
INSTA_OF_SOFT_LEVELS = {
"social_tease": "Instagram-style thirst-trap post, suggestive polished social feed energy",
@@ -409,7 +414,11 @@ def softcore_category(level: str) -> tuple[str, str]:
level,
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL["lingerie_tease"],
)
category, _subcategory = subcategory.split(" / ", 1)
exact_choice = category_policy.split_exact_subcategory_choice(
category_policy.load_category_library(),
subcategory,
)
category = exact_choice[0]["name"] if exact_choice else subcategory.split(" / ", 1)[0]
return category, subcategory
+84 -2
View File
@@ -285,7 +285,7 @@ def _character_cast_subjects(subjects: list[str] | tuple[str, ...]) -> str:
def _exact_subcategory_selector(category: dict[str, Any], subcategory: dict[str, Any]) -> str:
return f"{category.get('name')} / {subcategory.get('name')}"
return category_library.exact_subcategory_selector(category, subcategory)
def _matrix_cast_for_route(category: dict[str, Any], subcategory: dict[str, Any]) -> tuple[int, int, str]:
@@ -1878,7 +1878,24 @@ def smoke_category_extensions_policy() -> None:
"compositions": ["centered catalog frame"],
}
},
}
},
"Dev / Test Wear": {
"prompt_template": (
"{subject_phrase}: slash-name test style. {item_label}: {item}. "
"Scene: {scene}. Pose: {pose}. Facial expression: {expression}. "
"Composition: {composition}."
),
"caption_template": "{subject_phrase}, {item}, {scene}, {composition}",
"subcategories": {
"Layered / Office": {
"items": ["slash-safe structured blazer over tailored trousers"],
"scenes": ["slash-safe showroom with glass shelving"],
"poses": ["standing beside a mirrored divider"],
"expressions": ["focused exact-route look"],
"compositions": ["slash-safe centered catalog frame"],
}
},
},
}
},
ensure_ascii=True,
@@ -1890,6 +1907,12 @@ def smoke_category_extensions_policy() -> None:
"Dev Test Wear / Layered Office" in pb.subcategory_choices(),
"User-added JSON subcategory did not reach subcategory choices",
)
slash_selector = "Dev / Test Wear / Layered / Office"
_expect("Dev / Test Wear" in pb.category_choices(), "Slash-bearing user category did not reach category choices")
_expect(
slash_selector in pb.subcategory_choices(),
"Slash-bearing user subcategory did not reach subcategory choices",
)
row = pb.build_prompt(
category="Dev Test Wear",
subcategory="Dev Test Wear / Layered Office",
@@ -1922,6 +1945,36 @@ def smoke_category_extensions_policy() -> None:
_expect(row.get("expression") == "focused calm look", "User-added JSON expression did not generate")
_expect(row.get("composition") == "centered catalog frame", "User-added JSON composition did not generate")
_expect_formatter_outputs(row, "category_extensions_user_added_json", target="single")
slash_row = pb.build_prompt(
category="Dev / Test Wear",
subcategory=slash_selector,
row_number=1,
start_index=1,
seed=4110,
clothing="random",
ethnicity="any",
poses="random",
backside_bias=0.0,
figure="random",
no_plus_women=False,
no_black=False,
minimal_clothing_ratio=0.0,
standard_pose_ratio=1.0,
trigger=Trigger,
prepend_trigger_to_prompt=True,
extra_positive="",
extra_negative="",
women_count=1,
men_count=0,
)
_expect_row_base(slash_row, "category_extensions_slash_named_json")
_expect(slash_row.get("main_category") == "Dev / Test Wear", "Slash-bearing JSON category name drifted")
_expect(slash_row.get("subcategory") == "Layered / Office", "Slash-bearing JSON subcategory name drifted")
_expect(
slash_row.get("item") == "slash-safe structured blazer over tailored trousers",
"Slash-bearing JSON exact subcategory did not generate the intended item",
)
_expect_formatter_outputs(slash_row, "category_extensions_slash_named_json", target="single")
finally:
category_library.CATEGORY_DIR = previous_category_dir
category_extensions._EXTENSIONS_APPLIED = previous_extensions_applied
@@ -4324,6 +4377,35 @@ def smoke_category_library_route() -> None:
_expect(subcategory.get("slug") == "oral_sex", "exact subcategory lookup selected wrong subcategory")
_expect((women_count, men_count) == (1, 1), "exact subcategory lookup changed compatible cast counts")
slash_categories = [
{"name": "Dev", "slug": "dev", "subcategories": [{"name": "Wrong Route", "slug": "wrong_route", "items": ["wrong item"]}]},
{
"name": "Dev / Test Wear",
"slug": "dev_test_wear",
"subcategories": [{"name": "Layered / Office", "slug": "layered_office", "items": ["structured test item"]}],
},
]
slash_selector = category_library.exact_subcategory_selector(
slash_categories[1],
slash_categories[1]["subcategories"][0],
)
slash_choice = category_library.split_exact_subcategory_choice(slash_categories, slash_selector)
_expect(slash_choice is not None, "Exact selector parser did not accept slash-bearing category/subcategory names")
if slash_choice is not None:
_expect(slash_choice[0].get("slug") == "dev_test_wear", "Exact selector parser did not prefer longest category prefix")
_expect(slash_choice[1] == "Layered / Office", "Exact selector parser trimmed slash-bearing subcategory incorrectly")
slash_category, slash_subcategory, _slash_women, _slash_men = category_library.find_subcategory(
slash_categories,
"custom_random",
slash_selector,
random.Random(201),
random.Random(202),
women_count=1,
men_count=0,
)
_expect(slash_category.get("slug") == "dev_test_wear", "Exact subcategory lookup failed slash-bearing category")
_expect(slash_subcategory.get("slug") == "layered_office", "Exact subcategory lookup failed slash-bearing subcategory")
item = category_library.compatible_entries(list(subcategory.get("items") or []), women_count, men_count)[0]
scenes = category_library.configured_pool(
category,