- random_seed_generator.py: only update the shared _last_seed counter for random/increment/decrement modes. fixed mode must be side-effect-free so interleaved fixed reads do not clobber the counter used by other AdvancedSeedGenerator nodes in the same workflow. - tests: add regression coverage for the new gating (fixed leaves _last_seed alone; random and increment write through). Rewrite reset_state test to set state directly instead of relying on the removed fixed side effect. - pyproject.toml: rewrite description to drop the stale "cross-library synchronization" claim (feature removed in 2.3.0). - README: replace the unrealistic "ComfyUI Python console" reset advice with three actionable options (restart, random-then-increment, and reset_state for scripts).
108 lines
3.4 KiB
Python
108 lines
3.4 KiB
Python
"""ComfyUI node for reproducible seed generation with fixed, random, increment, and decrement modes."""
|
|
|
|
import random
|
|
import time
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Seed bounds (64-bit unsigned integer range)
|
|
MIN_SEED = 0
|
|
MAX_SEED = 0xFFFFFFFFFFFFFFFF # 2^64 - 1
|
|
|
|
|
|
class AdvancedSeedGenerator:
|
|
"""Generates seed values for reproducible or exploratory image generation.
|
|
|
|
Modes:
|
|
fixed: Returns the exact seed value provided.
|
|
random: Generates a new random seed (0 to 2^64-1) each execution.
|
|
increment: Increments the last seed by 1 (wraps at MAX to 0).
|
|
decrement: Decrements the last seed by 1 (wraps at 0 to MAX).
|
|
|
|
Note:
|
|
_last_seed is class-level state shared across all node instances.
|
|
ComfyUI does not expose per-node identity to IS_CHANGED or generate_seed,
|
|
so per-instance isolation is not possible without upstream API changes.
|
|
"""
|
|
|
|
_last_seed: int = 0
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"mode": (["fixed", "increment", "decrement", "random"],),
|
|
"seed": ("INT", {
|
|
"default": 0,
|
|
"min": MIN_SEED,
|
|
"max": MAX_SEED,
|
|
"step": 1,
|
|
"display": "number",
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("INT",)
|
|
RETURN_NAMES = ("seed",)
|
|
FUNCTION = "generate_seed"
|
|
CATEGORY = "utils"
|
|
|
|
def generate_seed(self, mode: str, seed: int) -> tuple[int]:
|
|
"""Generate a seed value based on the selected mode.
|
|
|
|
Args:
|
|
mode: One of "fixed", "increment", "decrement", "random".
|
|
seed: The user-provided seed value (used directly in fixed mode).
|
|
|
|
Returns:
|
|
A single-element tuple containing the generated seed.
|
|
|
|
Raises:
|
|
ValueError: If mode is not recognized.
|
|
"""
|
|
if mode == "fixed":
|
|
result = seed
|
|
elif mode == "random":
|
|
result = random.randint(MIN_SEED, MAX_SEED)
|
|
elif mode == "increment":
|
|
result = self.__class__._last_seed + 1
|
|
if result > MAX_SEED:
|
|
result = MIN_SEED
|
|
elif mode == "decrement":
|
|
result = self.__class__._last_seed - 1
|
|
if result < MIN_SEED:
|
|
result = MAX_SEED
|
|
else:
|
|
raise ValueError(f"Unknown mode: {mode!r}")
|
|
|
|
# Only dynamic modes update the shared counter; fixed must be side-effect-free
|
|
# so users can interleave fixed reads without disturbing increment/decrement state.
|
|
if mode in ("random", "increment", "decrement"):
|
|
self.__class__._last_seed = result
|
|
logger.debug("Generated seed %d (mode=%s)", result, mode)
|
|
return (result,)
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, mode: str, seed: int):
|
|
"""Force re-execution for dynamic modes; cache for fixed mode."""
|
|
if mode in ("random", "increment", "decrement"):
|
|
return time.time()
|
|
return f"fixed_{seed}"
|
|
|
|
@classmethod
|
|
def reset_state(cls) -> None:
|
|
"""Reset class-level state. Useful for testing."""
|
|
cls._last_seed = 0
|
|
|
|
|
|
# ComfyUI Registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"AdvancedSeedGenerator": AdvancedSeedGenerator,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"AdvancedSeedGenerator": "🎲 Advanced Seed Generator",
|
|
}
|
|
|
|
__all__ = ["AdvancedSeedGenerator", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] |