diff --git a/node_seed_resolution.py b/node_seed_resolution.py index 7da8b77..97b6732 100644 --- a/node_seed_resolution.py +++ b/node_seed_resolution.py @@ -65,6 +65,7 @@ class SxCPSeedControl: "category", "subcategory", "content", + "clothing", "person", "scene", "pose", @@ -117,6 +118,8 @@ class SxCPSeedControl: subcategory_seed, content_seed_mode, content_seed, + clothing_seed_mode, + clothing_seed, person_seed_mode, person_seed, scene_seed_mode, @@ -134,6 +137,7 @@ class SxCPSeedControl: 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, diff --git a/node_tooltips.py b/node_tooltips.py index c3673c3..fd90b2a 100644 --- a/node_tooltips.py +++ b/node_tooltips.py @@ -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": { diff --git a/prompt_builder.py b/prompt_builder.py index cee1d2d..303781c 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -875,6 +875,7 @@ def build_seed_config_json( category_seed: int = -1, subcategory_seed: int = -1, content_seed: int = -1, + clothing_seed: int = -1, person_seed: int = -1, scene_seed: int = -1, pose_seed: int = -1, @@ -884,6 +885,7 @@ def build_seed_config_json( category_seed_mode: str = "auto", subcategory_seed_mode: str = "auto", content_seed_mode: str = "auto", + clothing_seed_mode: str = "auto", person_seed_mode: str = "auto", scene_seed_mode: str = "auto", pose_seed_mode: str = "auto", @@ -895,6 +897,7 @@ def 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, diff --git a/seed_config.py b/seed_config.py index 081df7c..6cb69d2 100644 --- a/seed_config.py +++ b/seed_config.py @@ -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) @@ -101,6 +109,7 @@ def build_seed_config_json( category_seed: int = -1, subcategory_seed: int = -1, content_seed: int = -1, + clothing_seed: int = -1, person_seed: int = -1, scene_seed: int = -1, pose_seed: int = -1, @@ -110,6 +119,7 @@ def build_seed_config_json( category_seed_mode: str = "auto", subcategory_seed_mode: str = "auto", content_seed_mode: str = "auto", + clothing_seed_mode: str = "auto", person_seed_mode: str = "auto", scene_seed_mode: str = "auto", pose_seed_mode: str = "auto", @@ -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), diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 1630b2a..a15c312 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -13902,6 +13902,8 @@ 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") _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 +13912,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", @@ -13921,6 +13925,8 @@ def smoke_node_utility_registration() -> None: -1, "random", -1, + "fixed", + 222, "auto", 123, "auto", @@ -13938,6 +13944,7 @@ def smoke_node_utility_registration() -> None: _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") @@ -14098,6 +14105,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 +14128,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 +14157,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(