New nodes: - Isulion Censored Part: blur/pixelate/black bar/noise over a mask and/or a percent rectangle; accepts 2D/3D/4D masks and fails closed (blacks the item out) instead of passing an uncensored image through - Prompt Vault Save/Load: named prompt library stored in ComfyUI's user directory (outside the checkout, so updates never conflict), seeded from prompt_vault.example.json; Load re-runs when the vault changes Refactor: - SimpleSelectorNode base: ~20 selector nodes use an instance RNG instead of reseeding the global random module (same seed => same output) - Theme handlers declare DISPLAY_NAME; theme package auto-discovers modules - __init__ imports node classes directly Fixes: - net_utils: re-validate every redirect hop, unwrap IPv4-mapped IPv6, block NAT64 and all non-global addresses (SSRF) - Multiple Prompt Generator: no duplicate prompts for themes listed in two categories; 6 themes missing from every category are now reachable - Shutdown node: parameter renamed to match its "any" input (TypeError) - ConfigManager: deterministic (sorted) merge order, warns on key collisions; drop stale space_colony copy from tropical_paradise_config - Mega Prompt V3: seed honored again with randomize=disable Breaking: Civitai api_key widget removed (use CIVITAI_API_TOKEN), absolute paths refused by Load Images, Negative Prompt pools de-duplicated (shorter "strict" output), Dynamic Random theme order is now alphabetical. Tooling: pytest suite (51 tests), GitHub CI (compileall, pytest, ruff), ruff config, uv.lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
41 lines
1.6 KiB
Python
41 lines
1.6 KiB
Python
"""Shared seeded-selection core for the simple 'pick from a pool' nodes.
|
|
|
|
Subclasses declare their option pool as a class attribute (POOL or POOLS)
|
|
and call _make_rng() instead of touching the process-global random module,
|
|
so every node's output is reproducible from its own inputs alone.
|
|
"""
|
|
|
|
import random
|
|
from typing import ClassVar, Dict, List, Optional, Sequence, Tuple
|
|
|
|
|
|
class SimpleSelectorNode:
|
|
"""Base for single-choice selector nodes (no ComfyUI imports here)."""
|
|
|
|
POOL: ClassVar[Optional[Sequence[str]]] = None
|
|
POOLS: ClassVar[Optional[Dict[str, Sequence[str]]]] = None
|
|
MAX_SEED: int = 0xFFFFFFFFFFFFFFFF
|
|
|
|
@classmethod
|
|
def _pool_for(cls, category: str = "any") -> List[str]:
|
|
"""Resolve the candidate list, optionally filtered by POOLS key."""
|
|
if cls.POOLS is not None:
|
|
if category and category != "any":
|
|
return list(cls.POOLS.get(category, []))
|
|
return [item for sub in cls.POOLS.values() for item in sub]
|
|
return list(cls.POOL or [])
|
|
|
|
def _make_rng(self, randomize: str, seed: int) -> Tuple[random.Random, int]:
|
|
"""Instance-local RNG honoring the legacy seed contract.
|
|
|
|
enable + seed>0 -> deterministic stream seeded by `seed`.
|
|
enable + seed==0 -> fresh entropy, reported via returned seed.
|
|
disable -> rng is unused; caller keeps the dropdown value.
|
|
"""
|
|
if randomize == "enable":
|
|
if seed is not None and seed > 0:
|
|
return random.Random(seed), seed
|
|
new_seed = random.Random().randint(0, self.MAX_SEED)
|
|
return random.Random(new_seed), new_seed
|
|
return random.Random(seed), seed
|