Files
limbicnation 20d5149d02 fix: gate _last_seed mutation to dynamic modes
- 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).
2026-05-10 19:47:38 +02:00

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"]