diff --git a/README.md b/README.md index 0132b33..55f500b 100644 --- a/README.md +++ b/README.md @@ -532,7 +532,15 @@ alone. ## Seed Control The main `seed` input is still the default master seed. Connect `SxCP Seed -Control` to `seed_config` when you want to lock or vary specific axes. +Control` to `seed_config` when you want to lock or vary specific axes. Each +axis has its own mode plus seed value: + +- `auto`: legacy behavior; `-1` follows the main seed, `0` or higher fixes that + axis to the entered value. +- `follow_main`: always follows the final generator's main `seed` input and + ignores the entered axis seed. +- `fixed`: always uses the entered axis seed. +- `random`: generates a fresh axis seed when the node runs. For normal prompt iteration, `SxCP Seed Locker` is usually simpler: @@ -542,7 +550,7 @@ For normal prompt iteration, `SxCP Seed Locker` is usually simpler: - `reroll_seed`: `-1` makes the selected axis follow the main prompt seed; `0` or higher pins that selected axis to a specific seed. -Seed values: +Seed values in `auto` mode: - `-1`: follow the main seed. - `0` or higher: override only that axis. diff --git a/__init__.py b/__init__.py index 06ff2de..d7056d1 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import random try: from .prompt_builder import ( @@ -40,6 +41,7 @@ try: ethnicity_choices, generation_profile_choices, load_character_profile_json, + seed_mode_choices, subcategory_choices, ) from .caption_naturalizer import naturalize_caption @@ -82,6 +84,7 @@ except ImportError: ethnicity_choices, generation_profile_choices, load_character_profile_json, + seed_mode_choices, subcategory_choices, ) from caption_naturalizer import naturalize_caption @@ -192,21 +195,27 @@ class SxCPPromptBuilder: class SxCPSeedControl: + SEED_AXES = ( + "category", + "subcategory", + "content", + "person", + "scene", + "pose", + "role", + "expression", + "composition", + ) + @classmethod def INPUT_TYPES(cls): seed_spec = {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1} + required = {} + for axis in cls.SEED_AXES: + required[f"{axis}_seed_mode"] = (seed_mode_choices(), {"default": "auto"}) + required[f"{axis}_seed"] = ("INT", seed_spec) return { - "required": { - "category_seed": ("INT", seed_spec), - "subcategory_seed": ("INT", seed_spec), - "content_seed": ("INT", seed_spec), - "person_seed": ("INT", seed_spec), - "scene_seed": ("INT", seed_spec), - "pose_seed": ("INT", seed_spec), - "role_seed": ("INT", seed_spec), - "expression_seed": ("INT", seed_spec), - "composition_seed": ("INT", seed_spec), - } + "required": required } RETURN_TYPES = ("STRING",) @@ -214,16 +223,32 @@ class SxCPSeedControl: FUNCTION = "build" CATEGORY = "prompt_builder" + @classmethod + def IS_CHANGED(cls, *args, **kwargs): + values = list(args) + list(kwargs.values()) + if "random" in values: + return random.random() + return tuple(args), tuple(sorted(kwargs.items())) + def build( self, + category_seed_mode, category_seed, + subcategory_seed_mode, subcategory_seed, + content_seed_mode, content_seed, + person_seed_mode, person_seed, + scene_seed_mode, scene_seed, + pose_seed_mode, pose_seed, + role_seed_mode, role_seed, + expression_seed_mode, expression_seed, + composition_seed_mode, composition_seed, ): return ( @@ -237,6 +262,15 @@ class SxCPSeedControl: role_seed=role_seed, expression_seed=expression_seed, composition_seed=composition_seed, + category_seed_mode=category_seed_mode, + subcategory_seed_mode=subcategory_seed_mode, + content_seed_mode=content_seed_mode, + person_seed_mode=person_seed_mode, + scene_seed_mode=scene_seed_mode, + pose_seed_mode=pose_seed_mode, + role_seed_mode=role_seed_mode, + expression_seed_mode=expression_seed_mode, + composition_seed_mode=composition_seed_mode, ), ) diff --git a/prompt_builder.py b/prompt_builder.py index 1130a9e..d8a3b26 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -60,6 +60,7 @@ SEED_LOCK_AXES = ( "expression", "composition", ) +SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"] ETHNICITY_FILTER_CHOICES = [ "any", @@ -860,6 +861,10 @@ def subcategory_choices() -> list[str]: return choices +def seed_mode_choices() -> list[str]: + return list(SEED_MODE_CHOICES) + + CATEGORY_PRESETS = { "auto_weighted": ("auto_weighted", RANDOM_SUBCATEGORY), "women_casual": ("Casual clothes", RANDOM_SUBCATEGORY), @@ -1192,18 +1197,39 @@ def build_seed_config_json( role_seed: int = -1, expression_seed: int = -1, composition_seed: int = -1, + category_seed_mode: str = "auto", + subcategory_seed_mode: str = "auto", + content_seed_mode: str = "auto", + person_seed_mode: str = "auto", + scene_seed_mode: str = "auto", + pose_seed_mode: str = "auto", + role_seed_mode: str = "auto", + expression_seed_mode: str = "auto", + composition_seed_mode: str = "auto", ) -> str: + rng = random.SystemRandom() + + def axis_seed(value: int, mode: str) -> int: + mode = mode if mode in SEED_MODE_CHOICES else "auto" + if mode == "auto": + return int(value) + if mode == "random": + return rng.randint(0, 0xFFFFFFFF) + if mode == "fixed": + return max(0, int(value)) + return -1 + return json.dumps( { - "category_seed": int(category_seed), - "subcategory_seed": int(subcategory_seed), - "content_seed": int(content_seed), - "person_seed": int(person_seed), - "scene_seed": int(scene_seed), - "pose_seed": int(pose_seed), - "role_seed": int(role_seed), - "expression_seed": int(expression_seed), - "composition_seed": int(composition_seed), + "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), + "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), + "role_seed": axis_seed(role_seed, role_seed_mode), + "expression_seed": axis_seed(expression_seed, expression_seed_mode), + "composition_seed": axis_seed(composition_seed, composition_seed_mode), }, ensure_ascii=True, sort_keys=True,