Centralize exact subcategory selectors
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user