Compare commits

...

8 Commits

14 changed files with 674 additions and 89 deletions
+4 -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"
@@ -202,6 +204,7 @@ def build_prompt_result(request: PromptBuildRequest, deps: PromptBuildDependenci
minimal_ratio,
pose_ratio,
seed,
seed_config=parsed_seed_config,
)
else:
row = deps.build_custom_row(
+15 -13
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":
@@ -3231,8 +3232,9 @@ def make_group_or_layout(index: int, batch: int, rng: random.Random, expr_deck:
return row
def build_rows(total: int, start_index: int, 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, minimal_clothing_ratio: float | None = None, standard_pose_ratio: float | None = None, seed: int = DEFAULT_RNG_SEED, expression_seed: int = EXPRESSION_SEED) -> list[dict]:
def build_rows(total: int, start_index: int, 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, minimal_clothing_ratio: float | None = None, standard_pose_ratio: float | None = None, seed: int = DEFAULT_RNG_SEED, expression_seed: int = EXPRESSION_SEED, clothing_rng: random.Random | None = None) -> list[dict]:
rng = random.Random(seed)
wardrobe_rng = clothing_rng or rng
expr_deck = ExpressionDeck(EXPRESSIONS, random.Random(expression_seed))
rows: list[dict] = []
batch_quotas = batch_category_quotas()
@@ -3244,21 +3246,21 @@ def build_rows(total: int, start_index: int, clothing: str = "full", ethnicity:
index = start_index
for batch in range(1, batch_count + 1):
batch_rows: list[dict] = []
clothing_modes = batch_clothing_modes(rng, clothing, minimal_clothing_ratio)
clothing_modes = batch_clothing_modes(wardrobe_rng, clothing, minimal_clothing_ratio)
single_pose_modes = batch_single_pose_modes(rng, poses, standard_pose_ratio, single_subject_count)
for category, count in batch_quotas:
for _ in range(count):
row_clothing = clothing_modes.pop()
if category == "woman":
row_pose = single_pose_modes.pop()
row = make_single(index, batch, rng, "woman", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black)
row = make_single(index, batch, rng, "woman", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black, clothing_rng=wardrobe_rng if clothing_rng else None)
elif category == "man":
row_pose = single_pose_modes.pop()
row = make_single(index, batch, rng, "man", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black)
row = make_single(index, batch, rng, "man", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black, clothing_rng=wardrobe_rng if clothing_rng else None)
elif category == "couple":
row = make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus)
row = make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus, clothing_rng=wardrobe_rng if clothing_rng else None)
else:
row = make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus)
row = make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus, clothing_rng=wardrobe_rng if clothing_rng else None)
batch_rows.append(row)
index += 1
rng.shuffle(batch_rows)
+5 -2
View File
@@ -215,7 +215,7 @@ BRANCH_TARGET_CHOICES = ["both", "softcore", "hardcore"]
SCENE_LAYER_SEED_AXES = {
"cast": ("category",),
"character": ("person",),
"wardrobe": ("content",),
"wardrobe": ("clothing",),
"location": ("scene",),
"set_dressing": ("scene",),
"blocking": ("pose",),
@@ -224,7 +224,7 @@ SCENE_LAYER_SEED_AXES = {
"camera": ("composition",),
"composition": ("composition",),
"lighting": ("composition",),
"softcore_branch": ("content", "pose", "role"),
"softcore_branch": ("clothing", "pose", "role"),
"hardcore_branch": ("pose", "role"),
}
SCENE_REROLL_GROUPS = {
@@ -232,6 +232,7 @@ SCENE_REROLL_GROUPS = {
"category": ("category",),
"subcategory": ("subcategory",),
"content": ("content",),
"clothing": ("clothing",),
"person": ("person",),
"scene": ("scene",),
"pose": ("pose", "role"),
@@ -239,6 +240,8 @@ SCENE_REROLL_GROUPS = {
"expression": ("expression",),
"composition": ("composition",),
"content_pose": ("content", "pose", "role"),
"content_clothing": ("content", "clothing"),
"clothing_pose": ("clothing", "pose", "role"),
"scene_pose": ("scene", "pose", "role"),
}
SCENE_OPTION_TEXT_KEYS = {
+5
View File
@@ -71,6 +71,7 @@ class SxCPSeedControl:
"role",
"expression",
"composition",
"clothing",
)
@classmethod
@@ -129,11 +130,14 @@ class SxCPSeedControl:
expression_seed,
composition_seed_mode,
composition_seed,
clothing_seed_mode="auto",
clothing_seed=-1,
):
config = build_seed_config_json(
category_seed=category_seed,
subcategory_seed=subcategory_seed,
content_seed=content_seed,
clothing_seed=clothing_seed,
person_seed=person_seed,
scene_seed=scene_seed,
pose_seed=pose_seed,
@@ -143,6 +147,7 @@ class SxCPSeedControl:
category_seed_mode=category_seed_mode,
subcategory_seed_mode=subcategory_seed_mode,
content_seed_mode=content_seed_mode,
clothing_seed_mode=clothing_seed_mode,
person_seed_mode=person_seed_mode,
scene_seed_mode=scene_seed_mode,
pose_seed_mode=pose_seed_mode,
+3 -1
View File
@@ -257,6 +257,8 @@ NODE_INPUT_TOOLTIPS = {
"category_seed_mode": "auto/follow_main follows the main seed; fixed uses category_seed; random rerolls this axis at queue time while the field value stays unchanged.",
"subcategory_seed_mode": "Controls which subcategory is selected. Change this to switch oral vs penetration when both are allowed.",
"content_seed_mode": "Controls item/outfit content for non-pose categories.",
"clothing_seed_mode": "Controls clothing/outfit selection separately from content item selection.",
"clothing_seed": "Seed used when clothing_seed_mode is fixed or auto with a non-negative value.",
"person_seed_mode": "Controls generated character appearance unless a slot seed overrides it.",
"scene_seed_mode": "Controls location/scene selection.",
"pose_seed_mode": "Controls pose/item selection for pose categories, including hardcore positions.",
@@ -266,7 +268,7 @@ NODE_INPUT_TOOLTIPS = {
},
"SxCPSeedLocker": {
"base_seed": "Master seed for the locked result. Use the same value as the generator seed for simplest reproduction.",
"reroll_axis": "Choose the one axis to change while the rest stays locked. Use pose for sexual pose, scene for location, person for appearance.",
"reroll_axis": "Choose the one axis to change while the rest stays locked. Use clothing for outfit-only rerolls, pose for sexual pose, scene for location, person for appearance.",
"reroll_seed": "Seed for the selected axis only. Leave -1 to derive a stable reroll from base_seed.",
},
"SxCPCastBias": {
+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"
+8
View File
@@ -890,11 +890,14 @@ def build_seed_config_json(
role_seed_mode: str = "auto",
expression_seed_mode: str = "auto",
composition_seed_mode: str = "auto",
clothing_seed: int = -1,
clothing_seed_mode: str = "auto",
) -> str:
return seed_policy.build_seed_config_json(
category_seed=category_seed,
subcategory_seed=subcategory_seed,
content_seed=content_seed,
clothing_seed=clothing_seed,
person_seed=person_seed,
scene_seed=scene_seed,
pose_seed=pose_seed,
@@ -904,6 +907,7 @@ def build_seed_config_json(
category_seed_mode=category_seed_mode,
subcategory_seed_mode=subcategory_seed_mode,
content_seed_mode=content_seed_mode,
clothing_seed_mode=clothing_seed_mode,
person_seed_mode=person_seed_mode,
scene_seed_mode=scene_seed_mode,
pose_seed_mode=pose_seed_mode,
@@ -1406,6 +1410,7 @@ def _build_auto_weighted_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_auto_weighted_row(
row_number,
@@ -1420,6 +1425,7 @@ def _build_auto_weighted_row(
minimal_clothing_ratio,
standard_pose_ratio,
seed,
seed_config=seed_config,
)
@@ -1437,6 +1443,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,
@@ -1452,6 +1459,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(
+26 -4
View File
@@ -78,8 +78,12 @@ def build_auto_weighted_row(
minimal_clothing_ratio: float | None,
standard_pose_ratio: float | None,
seed: int,
seed_config: dict[str, int] | None = None,
) -> dict[str, Any]:
batch_number = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
clothing_rng = None
if seed_config is not None:
clothing_rng = seed_policy.axis_rng(seed_config, "clothing", seed, row_number)
rows = g.build_rows(
batch_number * g.BATCH_SIZE,
start_index,
@@ -94,6 +98,7 @@ def build_auto_weighted_row(
standard_pose_ratio,
seed,
g.EXPRESSION_SEED + seed,
clothing_rng=clothing_rng,
)
row = rows[row_number - 1]
row["main_category"] = "auto_weighted"
@@ -116,15 +121,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 +150,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}")
+12 -1
View File
@@ -9,6 +9,7 @@ SEED_AXIS_SALTS = {
"category": 31,
"subcategory": 37,
"content": 41,
"clothing": 41,
"person": 43,
"scene": 47,
"pose": 53,
@@ -20,7 +21,8 @@ SEED_AXIS_SALTS = {
SEED_AXIS_ALIASES = {
"category": ("category_seed", "category"),
"subcategory": ("subcategory_seed", "subcategory"),
"content": ("content_seed", "item_seed", "outfit_seed", "sexual_pose_seed", "content"),
"content": ("content_seed", "item_seed", "sexual_pose_seed", "content"),
"clothing": ("clothing_seed", "outfit_seed", "wardrobe_seed", "content_seed", "content"),
"person": ("person_seed", "appearance_seed", "cast_seed", "person"),
"scene": ("scene_seed", "scene"),
"pose": ("pose_seed", "sexual_pose_seed", "pose"),
@@ -33,6 +35,7 @@ SEED_LOCK_AXES = (
"category",
"subcategory",
"content",
"clothing",
"person",
"scene",
"pose",
@@ -46,6 +49,7 @@ SEED_REROLL_GROUPS = {
"category": ("category",),
"subcategory": ("subcategory",),
"content": ("content",),
"clothing": ("clothing",),
"person": ("person",),
"scene": ("scene",),
"pose": ("pose", "role"),
@@ -53,6 +57,8 @@ SEED_REROLL_GROUPS = {
"expression": ("expression",),
"composition": ("composition",),
"content_pose": ("content", "pose", "role"),
"content_clothing": ("content", "clothing"),
"clothing_pose": ("clothing", "pose", "role"),
"scene_pose": ("scene", "pose", "role"),
}
SEED_REROLL_AXIS_CHOICES = list(SEED_REROLL_GROUPS.keys())
@@ -87,6 +93,8 @@ def normalize_reroll_axis(value: Any) -> str:
normalized = _normal_key(value)
aliases = {
"contentpose": "content_pose",
"contentclothing": "content_clothing",
"clothingpose": "clothing_pose",
"scenepose": "scene_pose",
}
normalized = aliases.get(normalized, normalized)
@@ -116,6 +124,8 @@ def build_seed_config_json(
role_seed_mode: str = "auto",
expression_seed_mode: str = "auto",
composition_seed_mode: str = "auto",
clothing_seed: int = -1,
clothing_seed_mode: str = "auto",
) -> str:
rng = random.SystemRandom()
@@ -134,6 +144,7 @@ def build_seed_config_json(
"category_seed": axis_seed(category_seed, category_seed_mode),
"subcategory_seed": axis_seed(subcategory_seed, subcategory_seed_mode),
"content_seed": axis_seed(content_seed, content_seed_mode),
"clothing_seed": axis_seed(clothing_seed, clothing_seed_mode),
"person_seed": axis_seed(person_seed, person_seed_mode),
"scene_seed": axis_seed(scene_seed, scene_seed_mode),
"pose_seed": axis_seed(pose_seed, pose_seed_mode),
+4 -4
View File
@@ -1273,11 +1273,11 @@ def _pair_seed_pose_reroll_check(seed: int) -> dict[str, Any]:
)
def _pair_seed_content_reroll_check(seed: int) -> dict[str, Any]:
def _pair_seed_clothing_reroll_check(seed: int) -> dict[str, Any]:
return _pair_seed_reroll_check(
seed,
name="pair_seed.content_reroll",
reroll_axis="content",
name="pair_seed.clothing_reroll",
reroll_axis="clothing",
changed_fields=("soft_item",),
stable_fields=(
"shared_cast_descriptors",
@@ -1432,7 +1432,7 @@ def _pair_seed_checks(seed: int) -> list[dict[str, Any]]:
_pair_seed_determinism_check(seed),
_pair_seed_person_reroll_check(seed),
_pair_seed_scene_reroll_check(seed),
_pair_seed_content_reroll_check(seed),
_pair_seed_clothing_reroll_check(seed),
_pair_seed_pose_reroll_check(seed),
_pair_seed_expression_reroll_check(seed),
_pair_seed_composition_reroll_check(seed),
+555 -55
View File
@@ -479,6 +479,10 @@ def _prompt_row(
subcategory: str,
seed: int,
seed_config: str | dict[str, Any] | None = None,
clothing: str = "random",
poses: str = "random",
minimal_clothing_ratio: float = 0.5,
standard_pose_ratio: float = 0.5,
character_cast: str = "",
women_count: int = 1,
men_count: int = 1,
@@ -494,15 +498,15 @@ def _prompt_row(
row_number=1,
start_index=1,
seed=seed,
clothing="random",
clothing=clothing,
ethnicity="any",
poses="random",
poses=poses,
backside_bias=0.35,
figure="random",
no_plus_women=False,
no_black=False,
minimal_clothing_ratio=0.5,
standard_pose_ratio=0.5,
minimal_clothing_ratio=minimal_clothing_ratio,
standard_pose_ratio=standard_pose_ratio,
trigger=Trigger,
prepend_trigger_to_prompt=True,
extra_positive="",
@@ -645,10 +649,17 @@ def smoke_builtin_single() -> None:
_expect(hair in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost hair")
_expect(eyes in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost eyes")
item_anchor = " ".join(re.findall(r"[a-z0-9]+", item.lower())[:3])
pose_anchor = " ".join(re.findall(r"[a-z0-9]+", pose.lower())[:4])
pose_anchor_tokens = [
token
for token in re.findall(r"[a-z0-9]+", pose.lower())
if token not in {"a", "an", "and", "both", "by", "in", "of", "on", "the", "to", "with"}
][:4]
sdxl_metadata_prompt = str(sdxl_metadata.get("sdxl_prompt", "")).lower()
_expect(item_anchor in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost explicit item")
_expect(pose_anchor in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost explicit pose")
_expect(
bool(pose_anchor_tokens) and all(token in sdxl_metadata_prompt for token in pose_anchor_tokens),
"SDXL metadata-only built-in route lost explicit pose",
)
for body_tag in sdxl_tag_policy.split_tag_text(body_phrase):
_expect(body_tag.lower() in sdxl_metadata_prompt, f"SDXL metadata-only built-in route lost body tag: {body_tag}")
_expect(skin.lower() in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost skin")
@@ -1396,7 +1407,7 @@ def smoke_builder_prompt_route_policy() -> None:
_expect(custom_trace.get("branch") == "custom", "Builder custom generation_trace lost branch")
_expect(custom_trace.get("source") == "json_category", "Builder custom generation_trace lost source")
_expect(custom_trace.get("category_slug") == "casual_clothes", "Builder custom generation_trace lost category slug")
_expect(custom_trace.get("content_seed_axis") == "content", "Builder custom generation_trace lost content axis")
_expect(custom_trace.get("content_seed_axis") == "clothing", "Builder custom generation_trace lost clothing axis")
_expect(custom_trace.get("seed_axes", {}).get("content", {}).get("source") == "configured", "Builder custom generation_trace lost configured content seed")
_expect(custom_trace.get("seed_axes", {}).get("content", {}).get("seed") == 3502, "Builder custom generation_trace lost content seed value")
_expect("typed builder route marker" in typed_route.row.get("prompt", ""), "Builder prompt route lost extra positive")
@@ -2493,7 +2504,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 +2518,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 +2594,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,
@@ -5820,6 +5887,8 @@ def smoke_category_subcategory_matrix() -> None:
_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")
elif row_category_route.is_clothing_content_category(category, subcategory):
_expect(row.get("content_seed_axis") == "clothing", f"{name}.content_seed_axis should be clothing")
else:
_expect(row.get("content_seed_axis") == "content", f"{name}.content_seed_axis should be content")
@@ -13902,6 +13971,10 @@ def smoke_node_utility_registration() -> None:
seed_control = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSeedControl"]
seed_inputs = seed_control.INPUT_TYPES().get("required") or {}
_expect("category_seed_mode" in seed_inputs, "Seed Control lost category seed mode input")
_expect("clothing_seed_mode" in seed_inputs, "Seed Control lost clothing seed mode input")
_expect("clothing_seed" in seed_inputs, "Seed Control lost clothing seed input")
seed_control_js = (ROOT / "web" / "seed_control.js").read_text(encoding="utf-8")
_expect('"clothing"' in seed_control_js, "Seed Control frontend random-lock axes should include clothing")
_expect("tooltip" in seed_inputs["category_seed_mode"][1], "Seed Control tooltip injection missing")
_expect(seed_control.RETURN_NAMES == ("seed_config", "summary"), "Seed Control lost visible summary output")
category_seed_tooltip = node_tooltips._tooltip_for_input("SxCPSeedControl", "category_seed_mode")
@@ -13910,6 +13983,8 @@ def smoke_node_utility_registration() -> None:
and "field value stays unchanged" in category_seed_tooltip,
"Node tooltip policy lost Seed Control override",
)
clothing_seed_tooltip = node_tooltips._tooltip_for_input("SxCPSeedControl", "clothing_seed_mode")
_expect("clothing/outfit" in clothing_seed_tooltip, "Node tooltip policy lost Seed Control clothing override")
_expect(
"Autoscaling switch input" in node_tooltips._tooltip_for_input("SxCPIndexSwitch", "input_12"),
"Node tooltip policy lost autoscaling input fallback",
@@ -13933,11 +14008,14 @@ def smoke_node_utility_registration() -> None:
-1,
"fixed",
999,
"fixed",
222,
)
parsed_seed_control = json.loads(seed_control_config)
_expect(parsed_seed_control.get("category_seed") == 0, "Seed Control fixed mode did not clamp negative seed")
_expect(parsed_seed_control.get("subcategory_seed") == -1, "Seed Control follow_main mode should emit -1")
_expect(int(parsed_seed_control.get("content_seed", -1)) >= 0, "Seed Control random mode did not emit resolved seed")
_expect(parsed_seed_control.get("clothing_seed") == 222, "Seed Control fixed clothing seed changed")
_expect(parsed_seed_control.get("person_seed") == 123, "Seed Control auto mode did not preserve explicit value")
_expect(parsed_seed_control.get("pose_seed") == 777, "Seed Control fixed mode did not preserve positive seed")
_expect(parsed_seed_control.get("role_seed") == -1, "Seed Control follow_main role did not emit -1")
@@ -13948,6 +14026,34 @@ def smoke_node_utility_registration() -> None:
f"content={parsed_seed_control['content_seed']}" in seed_control_summary,
"Seed Control summary lost random resolved seed value",
)
legacy_seed_control_config, _legacy_seed_control_summary = seed_control().build(
"fixed",
-1,
"follow_main",
-1,
"random",
-1,
"auto",
123,
"auto",
456,
"fixed",
777,
"follow_main",
888,
"auto",
321,
"fixed",
999,
)
legacy_seed_control = json.loads(legacy_seed_control_config)
_expect(legacy_seed_control.get("clothing_seed") == -1, "Legacy Seed Control call should leave clothing seed unset")
_expect(legacy_seed_control.get("person_seed") == 123, "Legacy Seed Control call shifted person seed")
_expect(legacy_seed_control.get("scene_seed") == 456, "Legacy Seed Control call shifted scene seed")
_expect(legacy_seed_control.get("pose_seed") == 777, "Legacy Seed Control call shifted pose seed")
_expect(legacy_seed_control.get("role_seed") == -1, "Legacy Seed Control call shifted role seed")
_expect(legacy_seed_control.get("expression_seed") == 321, "Legacy Seed Control call shifted expression seed")
_expect(legacy_seed_control.get("composition_seed") == 999, "Legacy Seed Control call shifted composition seed")
seed_locker = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSeedLocker"]
locker_inputs = seed_locker.INPUT_TYPES().get("required") or {}
@@ -14098,6 +14204,11 @@ def smoke_seed_config_policy() -> None:
_expect(pb.normalize_seed_mode("follow main") == "follow_main", "seed mode normalizer should accept spaced labels")
_expect(pb.normalize_seed_mode("FOLLOW-MAIN") == "follow_main", "seed mode normalizer should accept hyphenated labels")
_expect(pb.normalize_reroll_axis("content pose") == "content_pose", "reroll axis normalizer should accept spaced labels")
reroll_choices = pb.seed_reroll_axis_choices()
for expected_axis in ("clothing", "content_clothing", "clothing_pose"):
_expect(expected_axis in reroll_choices, f"seed reroll axis choices missing {expected_axis}")
_expect(pb.normalize_reroll_axis("clothing pose") == "clothing_pose", "reroll axis normalizer should accept clothing pose")
_expect(pb.normalize_reroll_axis("content clothing") == "content_clothing", "reroll axis normalizer should accept content clothing")
fixed_config = json.loads(
pb.build_seed_config_json(
@@ -14116,9 +14227,21 @@ def smoke_seed_config_policy() -> None:
_expect(fixed_config["pose_seed"] == -1, "follow_main seed mode should emit unlocked axis")
_expect(fixed_config["role_seed"] == 789, "auto seed mode should preserve numeric seed")
parsed = pb._parse_seed_config({"item_seed": "44", "pose_seed": "55", "bad": "nope"})
_expect(parsed == {"item_seed": 44, "pose_seed": 55}, "seed parser should keep integer-like values only")
parsed = pb._parse_seed_config({"item_seed": "44", "pose_seed": "55", "outfit_seed": "66", "bad": "nope"})
_expect(
parsed == {"item_seed": 44, "pose_seed": 55, "outfit_seed": 66},
"seed parser should keep integer-like values only",
)
_expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias")
_expect(pb._configured_axis_seed(parsed, "clothing") == 66, "clothing axis should honor outfit_seed alias")
_expect(
pb._configured_axis_seed({"content_seed": 77}, "clothing") == 77,
"clothing axis should keep content_seed as a legacy fallback",
)
_expect(
pb._configured_axis_seed({"content_seed": 77, "clothing_seed": 88}, "clothing") == 88,
"clothing_seed should override legacy content_seed fallback",
)
_expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias")
_expect(
seed_config.configured_seed_from_axes({"camera_seed": "88", "content_seed": "99"}, ("composition", "content")) == 88,
@@ -14133,9 +14256,30 @@ def smoke_seed_config_policy() -> None:
_expect(locked["content_seed"] == 999, "content_pose reroll should alter content seed")
_expect(locked["pose_seed"] == 999 and locked["role_seed"] == 999, "content_pose reroll should alter pose and role seeds")
_expect(locked["scene_seed"] == 100, "content_pose reroll should leave scene locked")
axis_trace = seed_config.axis_seed_trace({"content_seed": 44}, 99, 3, axes=("content", "scene"))
clothing_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="clothing", reroll_seed=777))
_expect(clothing_locked["clothing_seed"] == 777, "clothing reroll should alter clothing seed")
_expect(clothing_locked["content_seed"] == 100, "clothing reroll should leave content locked")
_expect(clothing_locked["pose_seed"] == 100 and clothing_locked["role_seed"] == 100, "clothing reroll should leave pose and role locked")
content_clothing_locked = json.loads(
pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_clothing", reroll_seed=778)
)
_expect(content_clothing_locked["content_seed"] == 778, "content_clothing reroll should alter content seed")
_expect(content_clothing_locked["clothing_seed"] == 778, "content_clothing reroll should alter clothing seed")
_expect(content_clothing_locked["pose_seed"] == 100, "content_clothing reroll should leave pose locked")
clothing_pose_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="clothing_pose", reroll_seed=779))
_expect(clothing_pose_locked["clothing_seed"] == 779, "clothing_pose reroll should alter clothing seed")
_expect(clothing_pose_locked["pose_seed"] == 779 and clothing_pose_locked["role_seed"] == 779, "clothing_pose reroll should alter pose and role seeds")
_expect(clothing_pose_locked["content_seed"] == 100, "clothing_pose reroll should leave content locked")
content_pose_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_pose", reroll_seed=780))
_expect(content_pose_locked["clothing_seed"] == 100, "content_pose reroll should not alter clothing seed")
axis_trace = seed_config.axis_seed_trace({"content_seed": 44, "clothing_seed": 66}, 99, 3, axes=("content", "clothing", "scene"))
_expect(axis_trace["content"]["source"] == "configured", "Seed axis trace lost configured source")
_expect(axis_trace["content"]["seed"] == 44, "Seed axis trace lost configured seed")
_expect(axis_trace["clothing"]["source"] == "configured", "Seed axis trace lost clothing configured source")
_expect(axis_trace["clothing"]["seed"] == 66, "Seed axis trace lost configured clothing seed")
_expect(axis_trace["scene"]["source"] == "main", "Seed axis trace lost main source")
_expect(axis_trace["scene"]["seed"] == 99, "Seed axis trace lost main seed")
_expect(
@@ -14261,6 +14405,261 @@ 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
auto_weighted_seeded = _prompt_row(
name="seed_config_policy_auto_weighted_seed_config",
category="auto_weighted",
subcategory="random",
seed=clothing_axis_seed,
seed_config=pb.build_seed_lock_config_json(base_seed=clothing_axis_seed),
women_count=1,
men_count=0,
)
_expect(auto_weighted_seeded.get("source") == "built_in_generator", "auto_weighted prompt with seed_config should build")
def direct_builtin_woman(seed_config_value: str | dict[str, Any], *, name: str) -> dict[str, Any]:
return _prompt_row(
name=name,
category="woman",
subcategory="random",
seed=43001,
seed_config=seed_config_value,
clothing="full",
poses="standard",
minimal_clothing_ratio=-1,
standard_pose_ratio=-1,
women_count=1,
men_count=0,
)
direct_builtin_locked = direct_builtin_woman(
pb.build_seed_lock_config_json(base_seed=43001),
name="seed_config_policy_direct_builtin_locked",
)
direct_builtin_clothing_reroll = direct_builtin_woman(
pb.build_seed_lock_config_json(base_seed=43001, reroll_axis="clothing", reroll_seed=43002),
name="seed_config_policy_direct_builtin_clothing_reroll",
)
direct_builtin_stable_fields = ("primary_subject", "age_band", "body_type", "scene", "composition", "pose_mode")
_expect(
tuple(direct_builtin_locked.get(key) for key in direct_builtin_stable_fields)
== tuple(direct_builtin_clothing_reroll.get(key) for key in direct_builtin_stable_fields),
"Prompt-level direct built-in clothing reroll should keep non-clothing fields stable",
)
_expect(
(
direct_builtin_locked.get("prompt"),
direct_builtin_locked.get("caption"),
)
!= (
direct_builtin_clothing_reroll.get("prompt"),
direct_builtin_clothing_reroll.get("caption"),
),
"Prompt-level direct built-in clothing reroll should change clothing text",
)
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)
@@ -14355,7 +14754,7 @@ def smoke_prompt_route_simulation_policy() -> None:
"pair_seed.locked_determinism",
"pair_seed.person_reroll",
"pair_seed.scene_reroll",
"pair_seed.content_reroll",
"pair_seed.clothing_reroll",
"pair_seed.pose_reroll",
"pair_seed.expression_reroll",
"pair_seed.composition_reroll",
@@ -14370,15 +14769,15 @@ def smoke_prompt_route_simulation_policy() -> None:
for check_name in (
"pair_seed.person_reroll",
"pair_seed.scene_reroll",
"pair_seed.content_reroll",
"pair_seed.clothing_reroll",
"pair_seed.pose_reroll",
"pair_seed.expression_reroll",
"pair_seed.composition_reroll",
):
_expect(pair_seed_checks[check_name].get("changed") is True, f"{check_name} should prove its axis can reroll")
_expect(
pair_seed_checks["pair_seed.content_reroll"].get("changed") is True,
"Pair content reroll should prove soft outfit/content can reroll while hard action stays locked",
pair_seed_checks["pair_seed.clothing_reroll"].get("changed") is True,
"Pair clothing reroll should prove soft outfit can reroll while hard action stays locked",
)
_expect(
pair_seed_checks["pair_seed.pose_reroll"].get("changed") is True,
@@ -16077,7 +16476,7 @@ def smoke_node_scene_chain_registration() -> None:
-1,
)
seed_fixture_scene = scene
wardrobe_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("wardrobe", "fixed", 9981, "content", "same_for_all_rows", "replace_layer")[0]
wardrobe_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("wardrobe", "fixed", 9981, "clothing", "same_for_all_rows", "replace_layer")[0]
wardrobe_options = nodes["SxCPSceneWardrobeOptions"]().build(
"replace",
"woman",
@@ -16106,7 +16505,34 @@ def smoke_node_scene_chain_registration() -> None:
woman_slot = next(slot for slot in slots if slot.get("subject_type") == "woman")
_expect(woman_slot.get("softcore_outfit") == "simple black dress", "Scene Wardrobe did not update softcore outfit")
_expect(woman_slot.get("hardcore_clothing") == "fully nude", "Scene Wardrobe did not update hardcore clothing")
_expect(json.loads(scene).get("seed_trace", {}).get("wardrobe", {}).get("seed") == 9981, "Scene Wardrobe seed options did not write seed trace")
wardrobe_trace = json.loads(scene).get("seed_trace", {}).get("wardrobe", {})
_expect(wardrobe_trace.get("seed") == 9981, "Scene Wardrobe seed options did not write seed trace")
_expect(wardrobe_trace.get("axes") == ["clothing"], "Scene Wardrobe seed options should write clothing trace axes")
wardrobe_default_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
"wardrobe",
"fixed",
9982,
"none",
"same_for_all_rows",
"replace_layer",
)[0]
wardrobe_default_scene = nodes["SxCPSceneWardrobe"]().build(
seed_fixture_scene,
True,
"woman",
"A",
"full",
"simple black dress",
"fully nude",
"",
wardrobe_options=wardrobe_options,
seed_options=wardrobe_default_seed_options,
)[0]
wardrobe_default_trace = json.loads(wardrobe_default_scene).get("seed_trace", {}).get("wardrobe", {})
_expect(
wardrobe_default_trace.get("axes") == ["clothing"],
"Scene Wardrobe default seed axis should resolve to clothing",
)
location_options = nodes["SxCPSceneLocationLayoutOptions"]().build(
"replace",
@@ -16225,32 +16651,14 @@ def smoke_node_scene_chain_registration() -> None:
hard_trace_axes.get("pose", {}).get("seed") == 7799,
"Scene Pair Output generation trace did not use hardcore branch pose seed",
)
soft_content_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
soft_clothing_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
"softcore_branch",
"fixed",
6679,
"content",
"clothing",
"same_for_all_rows",
"replace_layer",
)[0]
seeded_soft_scene, seeded_hard_scene, _summary, _metadata = nodes["SxCPSceneBranchPair"]().build(
seed_fixture_scene,
"same_creator_same_room",
"hybrid",
branch_options=branch_options,
)
seeded_soft_scene = nodes["SxCPSoftcoreBranchOptions"]().build(
seeded_soft_scene,
"same_as_hardcore",
"lingerie_tease",
True,
0.45,
"from_camera_config",
"compact",
"",
branch_options=branch_options,
seed_options=soft_content_seed_options,
)[0]
default_hardcore_continuity = (
nodes["SxCPHardcoreBranchOptions"].INPUT_TYPES()
.get("required", {})
@@ -16261,28 +16669,76 @@ def smoke_node_scene_chain_registration() -> None:
default_hardcore_continuity == "partially_removed",
"Hardcore Branch Options default should inherit softcore outfit continuity",
)
seeded_hard_scene = nodes["SxCPHardcoreBranchOptions"]().build(
seeded_hard_scene,
"couple",
1,
1,
"hardcore",
True,
0.85,
default_hardcore_continuity,
"from_camera_config",
"compact",
"balanced",
"",
branch_options=branch_options,
)[0]
seeded_pair = json.loads(nodes["SxCPScenePairOutput"]().build(seeded_soft_scene, seeded_hard_scene)[7])
def _soft_clothing_pair(clothing_seed: int, seed_options: str | None = None) -> dict[str, Any]:
soft_seed_options = seed_options or nodes["SxCPSceneLayerSeedOptions"]().build(
"softcore_branch",
"fixed",
clothing_seed,
"clothing",
"same_for_all_rows",
"replace_layer",
)[0]
seeded_soft_scene, seeded_hard_scene, _summary, _metadata = nodes["SxCPSceneBranchPair"]().build(
seed_fixture_scene,
"same_creator_same_room",
"hybrid",
branch_options=branch_options,
)
seeded_soft_scene = nodes["SxCPSoftcoreBranchOptions"]().build(
seeded_soft_scene,
"same_as_hardcore",
"lingerie_tease",
True,
0.45,
"from_camera_config",
"compact",
"",
branch_options=branch_options,
seed_options=soft_seed_options,
)[0]
seeded_hard_scene = nodes["SxCPHardcoreBranchOptions"]().build(
seeded_hard_scene,
"couple",
1,
1,
"hardcore",
True,
0.85,
default_hardcore_continuity,
"from_camera_config",
"compact",
"balanced",
"",
branch_options=branch_options,
)[0]
return json.loads(nodes["SxCPScenePairOutput"]().build(seeded_soft_scene, seeded_hard_scene)[7])
seeded_pair = _soft_clothing_pair(6679, soft_clothing_seed_options)
seeded_soft_item = str(seeded_pair.get("softcore_row", {}).get("item") or "").lower()
seeded_hard_clothing = str(seeded_pair.get("hardcore_clothing_state") or "").lower()
seeded_hard_prompt = str(seeded_pair.get("hardcore_prompt") or "").lower()
seeded_soft_seed_config = (
seeded_pair.get("softcore_row", {}).get("seed_config")
if isinstance(seeded_pair.get("softcore_row"), dict)
else {}
)
seeded_trace = seeded_pair.get("scene_chain", {}).get("softcore", {}).get("seed_trace", {}).get("softcore.softcore_branch", {})
_expect(
seeded_soft_seed_config.get("clothing_seed") == 6679,
"Scene softcore branch clothing seed did not reach softcore row clothing_seed",
)
_expect(
seeded_soft_seed_config.get("content_seed") != 6679,
"Scene softcore branch clothing seed should not write softcore row content_seed",
)
_expect(
seeded_trace.get("axes") == ["clothing"],
"Scene softcore branch clothing seed should write clothing trace axes",
)
_expect(
"black lace lingerie" in seeded_soft_item,
"Scene softcore branch content seed fixture no longer selects the expected outfit",
"Scene softcore branch clothing seed fixture no longer selects the expected outfit",
)
_expect(
"black lace lingerie" in seeded_hard_clothing,
@@ -16292,6 +16748,46 @@ def smoke_node_scene_chain_registration() -> None:
"black lace lingerie" in seeded_hard_prompt,
"Hardcore prompt text did not include the softcore-branch seeded woman outfit",
)
soft_clothing_pairs = [_soft_clothing_pair(seed) for seed in (6677, 6678, 6679, 6680)]
soft_clothing_items = {pair.get("softcore_row", {}).get("item") for pair in soft_clothing_pairs}
soft_clothing_poses = {pair.get("softcore_row", {}).get("pose") for pair in soft_clothing_pairs}
inherited_hard_clothing = {pair.get("hardcore_clothing_state") for pair in soft_clothing_pairs}
_expect(len(soft_clothing_items) > 1, "Softcore branch clothing reroll did not change softcore outfit")
_expect(len(inherited_hard_clothing) > 1, "Softcore branch clothing reroll did not change inherited hard clothing state")
_expect(len(soft_clothing_poses) == 1, "Softcore branch clothing reroll should keep softcore pose stable")
soft_default_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
"softcore_branch",
"fixed",
6681,
"none",
"same_for_all_rows",
"replace_layer",
)[0]
soft_default_pair = _soft_clothing_pair(6681, soft_default_seed_options)
soft_default_trace = (
soft_default_pair.get("scene_chain", {}).get("softcore", {}).get("seed_trace", {}).get("softcore.softcore_branch", {})
)
soft_default_seed_config = (
soft_default_pair.get("softcore_row", {}).get("seed_config")
if isinstance(soft_default_pair.get("softcore_row"), dict)
else {}
)
_expect(
soft_default_trace.get("axes") == ["clothing", "pose", "role"],
"Scene softcore branch default seed axes should resolve to clothing, pose, and role",
)
_expect(
soft_default_seed_config.get("clothing_seed") == 6681,
"Scene softcore branch default seed axis did not write clothing_seed",
)
_expect(
soft_default_seed_config.get("pose_seed") == 6681 and soft_default_seed_config.get("role_seed") == 6681,
"Scene softcore branch default seed axis did not write pose and role seeds",
)
_expect(
soft_default_seed_config.get("content_seed") != 6681,
"Scene softcore branch default seed axis should not write content_seed",
)
content_pose_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
"hardcore_branch",
"fixed",
@@ -16353,6 +16849,10 @@ def smoke_node_scene_chain_registration() -> None:
content_hard_seed_config.get("content_seed") == 8899,
"Hardcore branch content_pose reroll did not reach hardcore content seed",
)
_expect(
content_hard_seed_config.get("clothing_seed") != 8899,
"Hardcore branch content_pose reroll should not write clothing_seed",
)
_expect(
content_hard_seed_config.get("pose_seed") == 8899 and content_hard_seed_config.get("role_seed") == 8899,
"Hardcore branch content_pose reroll did not reach hardcore pose and role seeds",
+1
View File
@@ -12,6 +12,7 @@ const SEED_AXES = [
"role",
"expression",
"composition",
"clothing",
];
function widget(node, name) {