Route clothing choices through clothing seed

This commit is contained in:
2026-07-01 16:43:43 +02:00
parent 885f136cf3
commit 8c3f61ea6d
8 changed files with 331 additions and 22 deletions
+3 -1
View File
@@ -146,13 +146,14 @@ def build_prompt_result(request: PromptBuildRequest, deps: PromptBuildDependenci
parsed_location_config = deps.parse_location_config(request.location_config)
parsed_composition_config = deps.parse_composition_config(request.composition_config)
content_rng = deps.axis_rng(parsed_seed_config, "content", seed, row_number)
clothing_rng = deps.axis_rng(parsed_seed_config, "clothing", seed, row_number)
pose_axis_rng = deps.axis_rng(parsed_seed_config, "pose", seed, row_number)
person_rng = deps.axis_rng(parsed_seed_config, "person", seed, row_number)
expression_rng = deps.axis_rng(parsed_seed_config, "expression", seed, row_number)
clothing = request.clothing if request.clothing in ("full", "minimal", "random") else "full"
poses = request.poses if request.poses in ("standard", "evocative", "random") else "standard"
figure = request.figure if request.figure in ("curvy", "balanced", "bombshell", "random") else "curvy"
clothing = deps.pick_clothing_mode(content_rng, clothing, minimal_ratio)
clothing = deps.pick_clothing_mode(clothing_rng, clothing, minimal_ratio)
poses = deps.pick_pose_mode(pose_axis_rng, poses, pose_ratio)
figure = deps.pick_figure_bias(person_rng, figure)
minimal_ratio = None
@@ -185,6 +186,7 @@ def build_prompt_result(request: PromptBuildRequest, deps: PromptBuildDependenci
minimal_ratio,
pose_ratio,
seed,
seed_config=parsed_seed_config,
)
elif category in ("woman", "man", "couple", "group_or_layout") and not exact_custom_subcategory:
branch = "built_in"
+8 -7
View File
@@ -3059,16 +3059,17 @@ def row_base(index: int, batch: int, subject: str, age: str, body: str, scene_sl
}
def make_single(index: int, batch: int, rng: random.Random, gender: str, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", poses: str = "standard", backside_bias: float = 0.0, figure: str = "curvy", no_plus: bool = False, no_black: bool = False) -> dict:
def make_single(index: int, batch: int, rng: random.Random, gender: str, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", poses: str = "standard", backside_bias: float = 0.0, figure: str = "curvy", no_plus: bool = False, no_black: bool = False, clothing_rng: random.Random | None = None) -> dict:
minimal = clothing == "minimal"
wardrobe_rng = clothing_rng or rng
if gender == "woman":
subject, age, body, skin, hair, eyes = choose_woman(rng, ethnicity, no_plus, no_black)
clothes = choose(rng, WOMEN_CLOTHES_MINIMAL if minimal else WOMEN_CLOTHES)
clothes = choose(wardrobe_rng, WOMEN_CLOTHES_MINIMAL if minimal else WOMEN_CLOTHES)
figure_note = choose(rng, figure_pool(figure))
else:
men_pool = by_ethnicity(MEN, ethnicity)
subject, age, body, skin, hair, eyes = choose(rng, men_pool)
clothes = choose(rng, MEN_CLOTHES_MINIMAL if minimal else MEN_CLOTHES)
clothes = choose(wardrobe_rng, MEN_CLOTHES_MINIMAL if minimal else MEN_CLOTHES)
figure_note = ""
body_phrase = make_body_phrase(body, figure_note)
@@ -3119,7 +3120,7 @@ def make_single(index: int, batch: int, rng: random.Random, gender: str, expr_de
return row
def make_couple(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False) -> dict:
def make_couple(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False, clothing_rng: random.Random | None = None) -> dict:
primary_subject, subject_phrase, pose = choose(rng, COUPLE_TYPES)
if ethnicity == "asian":
subject_phrase = {
@@ -3140,7 +3141,7 @@ def make_couple(index: int, batch: int, rng: random.Random, expr_deck: Expressio
if no_plus:
body_options = [b for b in body_options if "plus" not in b and "fat" not in b]
body = choose(rng, body_options)
outfits = choose(rng, COUPLE_OUTFITS_MINIMAL if clothing == "minimal" else COUPLE_OUTFITS)
outfits = choose(clothing_rng or rng, COUPLE_OUTFITS_MINIMAL if clothing == "minimal" else COUPLE_OUTFITS)
expr_a, expr_b = expr_deck.draw_two()
row = row_base(index, batch, primary_subject, ages, body, scene_slug, composition)
@@ -3164,7 +3165,7 @@ def make_couple(index: int, batch: int, rng: random.Random, expr_deck: Expressio
return row
def make_group_or_layout(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False) -> dict:
def make_group_or_layout(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False, clothing_rng: random.Random | None = None) -> dict:
minimal = clothing == "minimal"
group_outfits = "minimal beachwear and lingerie-inspired outfits" if minimal else "stylish revealing party outfits"
if ethnicity == "asian":
@@ -3203,7 +3204,7 @@ def make_group_or_layout(index: int, batch: int, rng: random.Random, expr_deck:
)
return row
layout_slug, layout_desc = choose(rng, LAYOUTS_MINIMAL if minimal else LAYOUTS_FULL)
layout_slug, layout_desc = choose(clothing_rng or rng, LAYOUTS_MINIMAL if minimal else LAYOUTS_FULL)
if ethnicity == "asian":
layout_desc = layout_desc.replace("adult", "Asian adult")
elif ethnicity == "white_asian":
+5 -5
View File
@@ -166,15 +166,15 @@ def softcore_partner_styling(
choose: Choose,
slot_softcore_outfit: SlotSoftcoreOutfit,
) -> dict[str, Any]:
content_rng = axis_rng(seed_config, "content", seed, row_number + 421)
clothing_rng = axis_rng(seed_config, "clothing", seed, row_number + 421)
pose_rng = axis_rng(seed_config, "pose", seed, row_number + 421)
pov_set = set(pov_labels or [])
outfits: list[str] = []
for index in range(max(0, women_count - 1)):
label = chr(ord("B") + index)
full_label = f"Woman {label}"
outfit = slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or choose(
content_rng,
outfit = slot_softcore_outfit((label_map or {}).get(full_label), clothing_rng) or choose(
clothing_rng,
pair_options.INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS,
)
sentence = pair_clothing.softcore_outfit_sentence(full_label, outfit)
@@ -185,8 +185,8 @@ def softcore_partner_styling(
full_label = f"Man {label}"
if full_label in pov_set:
continue
outfit = slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or choose(
content_rng,
outfit = slot_softcore_outfit((label_map or {}).get(full_label), clothing_rng) or choose(
clothing_rng,
pair_options.INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS,
)
sentence = pair_clothing.softcore_outfit_sentence(full_label, outfit)
+3 -2
View File
@@ -74,6 +74,7 @@ def build_insta_pair_rows_result(
soft_seed_config = parsed_softcore_seed_config or parsed_seed_config
hard_seed_config = parsed_hardcore_seed_config or parsed_seed_config
soft_content_rng = axis_rng(soft_seed_config, "content", seed, row_number + 311)
soft_clothing_rng = axis_rng(soft_seed_config, "clothing", seed, row_number + 311)
soft_pose_rng = axis_rng(soft_seed_config, "pose", seed, row_number + 313)
hard_content_rng = axis_rng(hard_seed_config, "content", seed, row_number + 317)
soft_person_rng = axis_rng(soft_seed_config, "person", seed, row_number)
@@ -147,8 +148,8 @@ def build_insta_pair_rows_result(
if not soft_expression_enabled:
soft_row = disable_row_expression(soft_row, soft_expression_intensity_source)
primary_softcore_outfit = slot_softcore_outfit(primary_slot, soft_content_rng)
soft_row["item"] = primary_softcore_outfit or softcore_outfit(soft_content_rng, softcore_level_key)
primary_softcore_outfit = slot_softcore_outfit(primary_slot, soft_clothing_rng)
soft_row["item"] = primary_softcore_outfit or softcore_outfit(soft_clothing_rng, softcore_level_key)
soft_row["pose"] = softcore_pose(soft_pose_rng, softcore_level_key)
soft_row["item_label"] = (
"Insta/OF softcore body exposure"
+2
View File
@@ -1441,6 +1441,7 @@ def _build_direct_builtin_row(
minimal_clothing_ratio: float | None,
standard_pose_ratio: float | None,
seed: int,
seed_config: dict[str, int] | None = None,
) -> dict[str, Any]:
return row_generation_policy.build_direct_builtin_row(
category,
@@ -1456,6 +1457,7 @@ def _build_direct_builtin_row(
minimal_clothing_ratio,
standard_pose_ratio,
seed,
seed_config=seed_config,
)
+28 -1
View File
@@ -50,6 +50,32 @@ def is_pose_content_category(category: dict[str, Any], subcategory: dict[str, An
return bool(tokens.intersection({"pose", "poses", "sex", "sexual"}))
def is_clothing_content_category(category: dict[str, Any], subcategory: dict[str, Any]) -> bool:
haystack = " ".join(
str(value)
for value in (
category.get("name", ""),
category.get("slug", ""),
category.get("item_label", ""),
subcategory.get("name", ""),
subcategory.get("slug", ""),
subcategory.get("item_label", ""),
)
).lower()
tokens = set(re.findall(r"[a-z0-9]+", haystack))
if tokens.intersection({"clothes", "clothing", "outfit", "outfits", "streetwear", "menswear", "daywear", "activewear", "lingerie", "wardrobe"}):
return True
return any(
phrase in haystack
for phrase in (
"casual clothes",
"men casual clothes",
"couple casual clothes",
"provocative erotic clothes",
)
)
def cast_count_adjustment(
requested_women_count: int,
requested_men_count: int,
@@ -266,7 +292,8 @@ def select_category_item_route_result(
)
is_pose_category = is_pose_content_category(category, subcategory)
content_axis = "pose" if is_pose_category else "content"
is_clothing_category = is_clothing_content_category(category, subcategory)
content_axis = "pose" if is_pose_category else ("clothing" if is_clothing_category else "content")
content_rng = seed_policy.axis_rng(seed_config, content_axis, seed, row_number)
item = row_item_policy.weighted_choice(content_rng, _list_from(subcategory.get("items", [subcategory["name"]])))
item_text, item_name, item_axis_values, item_template_metadata = row_item_policy.compose_item(
+21 -4
View File
@@ -116,15 +116,19 @@ def build_direct_builtin_row(
minimal_clothing_ratio: float | None,
standard_pose_ratio: float | None,
seed: int,
seed_config: dict[str, int] | None = None,
) -> dict[str, Any]:
rng = random.Random(seed_policy.row_seed(seed, row_number))
clothing_rng = None
if seed_config is not None:
clothing_rng = seed_policy.axis_rng(seed_config, "clothing", seed, row_number)
expr_deck = g.ExpressionDeck(
g.EXPRESSIONS,
random.Random(seed_policy.row_seed(g.EXPRESSION_SEED + seed, row_number)),
)
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
index = start_index + row_number - 1
row_clothing = pick_clothing_mode(rng, clothing, minimal_clothing_ratio)
row_clothing = pick_clothing_mode(clothing_rng or rng, clothing, minimal_clothing_ratio)
row_poses = pick_pose_mode(rng, poses, standard_pose_ratio)
if category == "woman":
@@ -141,13 +145,26 @@ def build_direct_builtin_row(
figure,
no_plus_women,
no_black,
clothing_rng=clothing_rng,
)
elif category == "man":
row = g.make_single(index, batch, rng, "man", expr_deck, row_clothing, ethnicity, row_poses, backside_bias, figure)
row = g.make_single(
index,
batch,
rng,
"man",
expr_deck,
row_clothing,
ethnicity,
row_poses,
backside_bias,
figure,
clothing_rng=clothing_rng,
)
elif category == "couple":
row = g.make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women)
row = g.make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women, clothing_rng=clothing_rng)
elif category == "group_or_layout":
row = g.make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women)
row = g.make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women, clothing_rng=clothing_rng)
else:
raise ValueError(f"Unknown built-in category: {category}")
+261 -2
View File
@@ -2493,7 +2493,7 @@ def smoke_row_category_route_policy() -> None:
hardcore_position_config={},
)
_expect(casual_route["category"]["slug"] == "casual_clothes", "Row category route selected wrong casual category")
_expect(casual_route["content_axis"] == "content", "Non-pose category should use content seed axis")
_expect(casual_route["content_axis"] == "clothing", "Casual clothes category should use clothing 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(
@@ -2507,9 +2507,35 @@ def smoke_row_category_route_policy() -> None:
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["content_axis"] == "clothing", "Exposed clothing slug should use clothing seed axis")
_expect(exposed_route["is_pose_category"] is False, "Exposed clothing slug should not be marked as pose content")
generic_categories = [
{
"name": "Lighting mood",
"slug": "lighting_mood",
"subcategories": [
{
"name": "Window light",
"slug": "window_light",
"items": ["soft window glow", "warm rim light"],
}
],
}
]
generic_route = row_category_route.select_category_item_route(
category_choice="Lighting mood",
subcategory_choice="Window light",
seed_config=seed_cfg,
seed=2303,
row_number=1,
women_count=1,
men_count=0,
hardcore_position_config={},
categories=generic_categories,
)
_expect(generic_route["content_axis"] == "content", "Generic non-clothing category should keep content seed axis")
def smoke_row_generation_policy() -> None:
_expect(pb._ratio_or_none(-1) is None, "Prompt builder ratio helper should treat negative as unset")
@@ -2557,6 +2583,36 @@ def smoke_row_generation_policy() -> None:
pb._build_direct_builtin_row(**direct_args) == row_generation.build_direct_builtin_row(**direct_args),
"Prompt builder direct built-in row should delegate to row_generation",
)
direct_random_args = {**direct_args, "clothing": "random", "minimal_clothing_ratio": 0.5}
direct_locked = row_generation.build_direct_builtin_row(
**direct_random_args,
seed_config=seed_config.parse_seed_config(pb.build_seed_lock_config_json(base_seed=5050)),
)
direct_clothing_reroll = row_generation.build_direct_builtin_row(
**direct_random_args,
seed_config=seed_config.parse_seed_config(
pb.build_seed_lock_config_json(base_seed=5050, reroll_axis="clothing", reroll_seed=5052)
),
)
direct_stable_keys = ("primary_subject", "age_band", "body_type", "scene", "composition", "pose_mode")
_expect(
tuple(direct_locked.get(key) for key in direct_stable_keys)
== tuple(direct_clothing_reroll.get(key) for key in direct_stable_keys),
"Direct built-in clothing reroll should keep non-clothing row fields stable",
)
_expect(
(
direct_locked.get("clothing_mode"),
direct_locked.get("prompt"),
direct_locked.get("caption"),
)
!= (
direct_clothing_reroll.get("clothing_mode"),
direct_clothing_reroll.get("prompt"),
direct_clothing_reroll.get("caption"),
),
"Direct built-in clothing reroll should change clothing mode or clothing text",
)
auto_args = dict(
row_number=2,
start_index=41,
@@ -14336,6 +14392,209 @@ def smoke_seed_config_policy() -> None:
break
_expect(pose_changed, "pose reroll should change pose/action metadata while cast and scene stay locked")
clothing_axis_seed = 42001
def clothing_category_row(seed_config_value: str | dict[str, Any], *, name: str = "seed_config_policy_clothing_axis") -> dict[str, Any]:
return pb.build_prompt(
category="Casual clothes",
subcategory="Casual clothes / Streetwear",
row_number=1,
start_index=1,
seed=clothing_axis_seed,
clothing="random",
ethnicity="any",
poses="standard",
backside_bias=0.0,
figure="curvy",
no_plus_women=False,
no_black=False,
minimal_clothing_ratio=0.5,
standard_pose_ratio=-1,
trigger=Trigger,
prepend_trigger_to_prompt=True,
extra_positive="",
extra_negative="",
seed_config=seed_config_value,
women_count=1,
men_count=0,
expression_enabled=True,
expression_intensity=0.6,
)
def clothing_trace(row: dict[str, Any]) -> str:
trace = row.get("generation_trace") if isinstance(row.get("generation_trace"), dict) else {}
return str(trace.get("clothing") or "")
clothing_locked_row = clothing_category_row(pb.build_seed_lock_config_json(base_seed=clothing_axis_seed))
content_pose_row = clothing_category_row(
pb.build_seed_lock_config_json(
base_seed=clothing_axis_seed,
reroll_axis="content_pose",
reroll_seed=clothing_axis_seed + 2,
),
name="seed_config_policy_content_pose_locked_clothing",
)
clothing_stable_fields = ("item", "scene_text", "subject_phrase")
_expect(
tuple(clothing_locked_row.get(key) for key in clothing_stable_fields)
== tuple(content_pose_row.get(key) for key in clothing_stable_fields),
"content_pose reroll should keep clothing-category item, scene, and subject stable when clothing is locked",
)
_expect(
clothing_trace(clothing_locked_row) == clothing_trace(content_pose_row),
"content_pose reroll should not change prompt clothing mode when clothing seed is locked",
)
clothing_reroll_row = clothing_category_row(
pb.build_seed_lock_config_json(
base_seed=clothing_axis_seed,
reroll_axis="clothing",
reroll_seed=clothing_axis_seed + 2,
),
name="seed_config_policy_clothing_reroll",
)
_expect(
tuple(clothing_locked_row.get(key) for key in ("scene_text", "pose", "subject_phrase"))
== tuple(clothing_reroll_row.get(key) for key in ("scene_text", "pose", "subject_phrase")),
"clothing reroll should keep scene, pose, and subject stable",
)
_expect(
(
clothing_trace(clothing_locked_row),
clothing_locked_row.get("item"),
)
!= (
clothing_trace(clothing_reroll_row),
clothing_reroll_row.get("item"),
),
"clothing reroll should change clothing mode or outfit text",
)
content_clothing_row = clothing_category_row(
pb.build_seed_lock_config_json(
base_seed=clothing_axis_seed,
reroll_axis="content_clothing",
reroll_seed=clothing_axis_seed + 2,
),
name="seed_config_policy_content_clothing_reroll",
)
_expect(
clothing_trace(clothing_locked_row) != clothing_trace(content_clothing_row),
"content_clothing reroll should change prompt clothing mode for clothing categories",
)
_expect(
clothing_locked_row.get("item") != content_clothing_row.get("item"),
"content_clothing reroll should change outfit text for clothing categories",
)
outfit_alias_config = seed_config.parse_seed_config(pb.build_seed_lock_config_json(base_seed=clothing_axis_seed))
outfit_alias_config.pop("clothing_seed", None)
outfit_alias_config["content_seed"] = clothing_axis_seed
outfit_alias_config["outfit_seed"] = clothing_axis_seed + 2
outfit_alias_row = clothing_category_row(outfit_alias_config, name="seed_config_policy_outfit_alias")
_expect(
clothing_trace(outfit_alias_row) == clothing_trace(clothing_reroll_row),
"outfit_seed alias should drive prompt clothing mode ahead of content_seed",
)
_expect(
outfit_alias_row.get("item") == clothing_reroll_row.get("item"),
"outfit_seed alias should drive clothing-category outfit text ahead of content_seed",
)
def pair_row_outfit(seed_config_value: dict[str, int]) -> str:
route = pair_rows.build_insta_pair_rows_result(
row_number=1,
start_index=1,
seed=clothing_axis_seed,
active_trigger=Trigger,
parsed_seed_config=seed_config_value,
options={
"softcore_cast": "solo",
"softcore_expression_enabled": False,
"softcore_expression_intensity": 0.5,
"hardcore_expression_enabled": False,
"hardcore_expression_intensity": 0.5,
"hardcore_detail_density": "standard",
},
ethnicity="any",
figure="curvy",
no_plus_women=False,
no_black=False,
character_profile="",
character_cast="",
character_slot_map={},
pov_character_labels=[],
hard_women_count=1,
hard_men_count=1,
soft_category="Casual clothes",
soft_subcategory="Casual clothes / Streetwear",
softcore_level_key="suggestive",
hardcore_random_subcategory="Penetrative sex",
hardcore_position_config="",
location_config="",
composition_config="",
build_prompt=lambda **kwargs: {"prompt": "", "negative_prompt": "", "caption": ""},
axis_rng=seed_config.axis_rng,
cast_expression_intensity_override=lambda *_args: (None, "disabled"),
context_from_character_slot=lambda *_args: {},
apply_character_context_to_row=lambda row, _context: row,
disable_row_expression=lambda row, _source: row,
slot_softcore_outfit=lambda _slot, _rng: "",
softcore_outfit=lambda rng, _level: f"outfit-{rng.randint(1, 1000000)}",
softcore_pose=lambda rng, _level: f"pose-{rng.randint(1, 1000000)}",
softcore_item_prompt_label=lambda _level: "outfit",
pov_prompt_directive=lambda _labels: "",
pov_composition_prompt=lambda composition, _labels: str(composition),
)
return str(route.soft_row.get("item") or "")
pair_base_config = seed_config.parse_seed_config(pb.build_seed_lock_config_json(base_seed=clothing_axis_seed))
pair_content_reroll_config = seed_config.parse_seed_config(
pb.build_seed_lock_config_json(
base_seed=clothing_axis_seed,
reroll_axis="content",
reroll_seed=clothing_axis_seed + 4,
)
)
pair_clothing_reroll_config = seed_config.parse_seed_config(
pb.build_seed_lock_config_json(
base_seed=clothing_axis_seed,
reroll_axis="clothing",
reroll_seed=clothing_axis_seed + 4,
)
)
_expect(
pair_row_outfit(pair_base_config) == pair_row_outfit(pair_content_reroll_config),
"Pair softcore primary outfit should stay stable across content reroll when clothing is locked",
)
_expect(
pair_row_outfit(pair_base_config) != pair_row_outfit(pair_clothing_reroll_config),
"Pair softcore primary outfit should follow clothing reroll",
)
def partner_outfits(seed_config_value: dict[str, int]) -> list[str]:
return pair_cast.softcore_partner_styling(
seed_config=seed_config_value,
seed=clothing_axis_seed,
row_number=1,
women_count=2,
men_count=1,
pov_labels=[],
label_map={},
axis_rng=seed_config.axis_rng,
choose=lambda rng, values: values[rng.randrange(len(values))],
slot_softcore_outfit=lambda _slot, _rng: "",
)["outfits"]
_expect(
partner_outfits(pair_base_config) == partner_outfits(pair_content_reroll_config),
"Pair partner outfits should stay stable across content reroll when clothing is locked",
)
_expect(
partner_outfits(pair_base_config) != partner_outfits(pair_clothing_reroll_config),
"Pair partner outfits should follow clothing reroll",
)
def smoke_prompt_route_simulation_policy() -> None:
report = prompt_route_simulation.run_simulation(seed=3901, include_prompts=False)