Harden seed control normalization

This commit is contained in:
2026-06-27 14:12:53 +02:00
parent 0289a94153
commit c7e4bdc373
4 changed files with 87 additions and 37 deletions
+8 -16
View File
@@ -8,12 +8,16 @@ try:
from .seed_config import ( from .seed_config import (
build_seed_config_json, build_seed_config_json,
build_seed_lock_config_json, build_seed_lock_config_json,
normalize_reroll_axis,
seed_reroll_axis_choices,
seed_mode_choices, seed_mode_choices,
) )
except ImportError: # Allows local smoke tests from the repository root. except ImportError: # Allows local smoke tests from the repository root.
from seed_config import ( from seed_config import (
build_seed_config_json, build_seed_config_json,
build_seed_lock_config_json, build_seed_lock_config_json,
normalize_reroll_axis,
seed_reroll_axis_choices,
seed_mode_choices, seed_mode_choices,
) )
@@ -180,20 +184,7 @@ class SxCPSeedLocker:
"required": { "required": {
"base_seed": ("INT", seed_spec), "base_seed": ("INT", seed_spec),
"reroll_axis": ( "reroll_axis": (
[ seed_reroll_axis_choices(),
"none",
"category",
"subcategory",
"content",
"person",
"scene",
"pose",
"role",
"expression",
"composition",
"content_pose",
"scene_pose",
],
{"default": "none"}, {"default": "none"},
), ),
"reroll_seed": ("INT", reroll_seed_spec), "reroll_seed": ("INT", reroll_seed_spec),
@@ -206,8 +197,9 @@ class SxCPSeedLocker:
CATEGORY = "prompt_builder" CATEGORY = "prompt_builder"
def build(self, base_seed, reroll_axis, reroll_seed): def build(self, base_seed, reroll_axis, reroll_seed):
config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed) normalized_axis = normalize_reroll_axis(reroll_axis)
summary = f"base {base_seed}; reroll {reroll_axis} with {'main seed' if int(reroll_seed) < 0 else reroll_seed}" config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=normalized_axis, reroll_seed=reroll_seed)
summary = f"base {base_seed}; reroll {normalized_axis} with {'main seed' if int(reroll_seed) < 0 else reroll_seed}"
return config, summary return config, summary
+12
View File
@@ -356,6 +356,18 @@ def seed_mode_choices() -> list[str]:
return seed_policy.seed_mode_choices() return seed_policy.seed_mode_choices()
def seed_reroll_axis_choices() -> list[str]:
return seed_policy.seed_reroll_axis_choices()
def normalize_seed_mode(value: Any) -> str:
return seed_policy.normalize_seed_mode(value)
def normalize_reroll_axis(value: Any) -> str:
return seed_policy.normalize_reroll_axis(value)
CATEGORY_PRESETS = category_cast_policy.CATEGORY_PRESETS CATEGORY_PRESETS = category_cast_policy.CATEGORY_PRESETS
CAST_PRESETS = category_cast_policy.CAST_PRESETS CAST_PRESETS = category_cast_policy.CAST_PRESETS
+48 -16
View File
@@ -41,12 +41,58 @@ SEED_LOCK_AXES = (
"composition", "composition",
) )
SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"] SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"]
SEED_REROLL_GROUPS = {
"none": (),
"category": ("category",),
"subcategory": ("subcategory",),
"content": ("content",),
"person": ("person",),
"scene": ("scene",),
"pose": ("pose", "role"),
"role": ("role",),
"expression": ("expression",),
"composition": ("composition",),
"content_pose": ("content", "pose", "role"),
"scene_pose": ("scene", "pose", "role"),
}
SEED_REROLL_AXIS_CHOICES = list(SEED_REROLL_GROUPS.keys())
def _normal_key(value: Any) -> str:
return str(value or "").strip().lower().replace("-", "_").replace(" ", "_")
def seed_mode_choices() -> list[str]: def seed_mode_choices() -> list[str]:
return list(SEED_MODE_CHOICES) return list(SEED_MODE_CHOICES)
def normalize_seed_mode(value: Any) -> str:
normalized = _normal_key(value)
aliases = {
"follow": "follow_main",
"followmain": "follow_main",
"follow_main_seed": "follow_main",
"main": "follow_main",
"main_seed": "follow_main",
}
normalized = aliases.get(normalized, normalized)
return normalized if normalized in SEED_MODE_CHOICES else "auto"
def seed_reroll_axis_choices() -> list[str]:
return list(SEED_REROLL_AXIS_CHOICES)
def normalize_reroll_axis(value: Any) -> str:
normalized = _normal_key(value)
aliases = {
"contentpose": "content_pose",
"scenepose": "scene_pose",
}
normalized = aliases.get(normalized, normalized)
return normalized if normalized in SEED_REROLL_GROUPS else "none"
def row_seed(seed: int, row_number: int, salt: int = 0) -> int: def row_seed(seed: int, row_number: int, salt: int = 0) -> int:
return int(seed) + int(row_number) * 1009 + salt * 9176 return int(seed) + int(row_number) * 1009 + salt * 9176
@@ -74,7 +120,7 @@ def build_seed_config_json(
rng = random.SystemRandom() rng = random.SystemRandom()
def axis_seed(value: int, mode: str) -> int: def axis_seed(value: int, mode: str) -> int:
mode = mode if mode in SEED_MODE_CHOICES else "auto" mode = normalize_seed_mode(mode)
if mode == "auto": if mode == "auto":
return int(value) return int(value)
if mode == "random": if mode == "random":
@@ -107,21 +153,7 @@ def build_seed_lock_config_json(
) -> str: ) -> str:
base_seed = int(base_seed) base_seed = int(base_seed)
reroll_seed = int(reroll_seed) reroll_seed = int(reroll_seed)
reroll_groups = { reroll = set(SEED_REROLL_GROUPS[normalize_reroll_axis(reroll_axis)])
"none": (),
"category": ("category",),
"subcategory": ("subcategory",),
"content": ("content",),
"person": ("person",),
"scene": ("scene",),
"pose": ("pose", "role"),
"role": ("role",),
"expression": ("expression",),
"composition": ("composition",),
"content_pose": ("content", "pose", "role"),
"scene_pose": ("scene", "pose", "role"),
}
reroll = set(reroll_groups.get(str(reroll_axis or "none"), ()))
config: dict[str, int] = {} config: dict[str, int] = {}
for axis in SEED_LOCK_AXES: for axis in SEED_LOCK_AXES:
config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed
+19 -5
View File
@@ -5738,14 +5738,21 @@ def smoke_node_utility_registration() -> None:
"Seed Control summary lost random resolved seed value", "Seed Control summary lost random resolved seed value",
) )
seed, seed_config, summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPGlobalSeed"]().build(12345) seed_locker = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSeedLocker"]
parsed_seed = json.loads(seed_config) locker_inputs = seed_locker.INPUT_TYPES().get("required") or {}
_expect(
list(locker_inputs["reroll_axis"][0]) == seed_config.seed_reroll_axis_choices(),
"Seed Locker reroll choices drifted from seed_config",
)
seed, global_seed_config, summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPGlobalSeed"]().build(12345)
parsed_seed = json.loads(global_seed_config)
_expect(seed == 12345, "Global Seed did not return the clamped seed") _expect(seed == 12345, "Global Seed did not return the clamped seed")
_expect(parsed_seed, "Global Seed config should not be empty") _expect(parsed_seed, "Global Seed config should not be empty")
_expect(all(int(value) == 12345 for value in parsed_seed.values()), "Global Seed config did not lock every axis") _expect(all(int(value) == 12345 for value in parsed_seed.values()), "Global Seed config did not lock every axis")
_expect("all axes locked" in summary, "Global Seed summary changed unexpectedly") _expect("all axes locked" in summary, "Global Seed summary changed unexpectedly")
locker_config, locker_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSeedLocker"]().build(12345, "pose", 999) locker_config, locker_summary = seed_locker().build(12345, "pose", 999)
parsed_locker = json.loads(locker_config) parsed_locker = json.loads(locker_config)
_expect(parsed_locker.get("pose_seed") == 999, "Seed Locker did not apply pose reroll seed") _expect(parsed_locker.get("pose_seed") == 999, "Seed Locker did not apply pose reroll seed")
_expect("reroll pose" in locker_summary, "Seed Locker summary lost reroll axis") _expect("reroll pose" in locker_summary, "Seed Locker summary lost reroll axis")
@@ -5852,6 +5859,13 @@ def smoke_server_route_payload_policy() -> None:
def smoke_seed_config_policy() -> None: def smoke_seed_config_policy() -> None:
_expect(pb.SEED_AXIS_SALTS is seed_config.SEED_AXIS_SALTS, "prompt_builder seed salts should delegate to seed_config") _expect(pb.SEED_AXIS_SALTS is seed_config.SEED_AXIS_SALTS, "prompt_builder seed salts should delegate to seed_config")
_expect(pb.seed_mode_choices() == seed_config.seed_mode_choices(), "seed mode choices drifted from seed_config") _expect(pb.seed_mode_choices() == seed_config.seed_mode_choices(), "seed mode choices drifted from seed_config")
_expect(
pb.seed_reroll_axis_choices() == seed_config.seed_reroll_axis_choices(),
"seed reroll axis choices drifted from seed_config",
)
_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")
fixed_config = json.loads( fixed_config = json.loads(
pb.build_seed_config_json( pb.build_seed_config_json(
@@ -5861,7 +5875,7 @@ def smoke_seed_config_policy() -> None:
role_seed=789, role_seed=789,
category_seed_mode="fixed", category_seed_mode="fixed",
content_seed_mode="fixed", content_seed_mode="fixed",
pose_seed_mode="follow_main", pose_seed_mode="follow-main",
role_seed_mode="auto", role_seed_mode="auto",
) )
) )
@@ -5875,7 +5889,7 @@ def smoke_seed_config_policy() -> None:
_expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias") _expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias")
_expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias") _expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias")
locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_pose", reroll_seed=999)) locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content pose", reroll_seed=999))
_expect(locked["content_seed"] == 999, "content_pose reroll should alter content seed") _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["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") _expect(locked["scene_seed"] == 100, "content_pose reroll should leave scene locked")