feat(builder): finish "Additional Random Seeds" run setting from PR #21

Builder Run Settings already emitted add_random_seeds_to_gens through
_session_settings; this adds the missing new-state default and the
saved-config migration, tests for the emit -> sampler_node unwrap ->
expand_configs path, and a README Run Settings entry.

The extra seeds are now derived from the base seed
(config_utils.derive_extra_seeds) instead of unseeded random.randint, as the
README always promised. Besides reproducibility this makes stop/resume work:
the skip check matches on seed, so an interrupted run can now skip the
extra-seed images it already rendered.

Ported from #21 (it conflicts after the repo restructure). Closes #21.

Co-authored-by: DRuggeri <DRuggeri@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jason Hoku
2026-11-03 23:59:53 -08:00
co-authored by DRuggeri Claude Fable 5.1
parent 198138e51c
commit caf8f35bec
5 changed files with 152 additions and 6 deletions
+13 -2
View File
@@ -380,7 +380,7 @@ This suite consists of three nodes found under the `sampling/testing` category:
* **`add_random_seeds_to_gens`**: Generate X extra random variations per config.
- `0` (default): Only use base seed
- `3`: Generate 3 additional random seed variations per config
- Random seeds are deterministic per base seed — changing base seed generates new random variations
- Extra seeds are deterministic per base seed — the same base seed always gives the same extras, changing the base seed gives new ones
* **`lookup_and_append_lora_triggerwords`**: Automatically fetch and append LoRA trigger words.
- `False` (default): Use prompts as-is
@@ -502,6 +502,12 @@ The Config Builder node replaces manual JSON editing with an intuitive visual in
All configuration data is automatically synchronized with the Ultimate Sampler Grid node.
### Run Settings
The Run Settings section of the Builder holds options that apply to the whole run rather than to one config array (overwrite/resume, flush batch size, VAE batch size, model cache, trigger-word mode, ...). One of them:
* **Additional Random Seeds** (`add_random_seeds_to_gens`, 0-100): render this many extra images per config using extra seeds on top of the base seed. Handy for telling "the base model did that" from "the LoRA did that" by checking whether a quirk survives across seeds. The extra seeds are derived from the base seed, so the same base seed always gives the same extras (and resuming an interrupted run works). Contributed by DRuggeri (PR #21).
### Basic Setup
1. **Add the Node:** Right-click on the canvas → `Add Node` → `sampling/testing` → `Ultimate Config Builder`
@@ -1126,7 +1132,7 @@ Please check the [GitHub Issues list](https://github.com/YOUR_USERNAME/ComfyUI-U
* **"Session not found":** Ensure the `session_name` matches a folder inside `ComfyUI/output/benchmarks/`.
* **OOM Errors:** Lower the `vae_batch_size` to 1 or 2.
* **Images not resuming:** Make sure `overwrite_existing: False`. Check console for skip messages.
* **Random seeds different each run:** Intentional — random seeds are tied to the base seed.
* **Extra random seeds changed:** Additional Random Seeds are derived from the base seed: the same base seed always gives the same extra seeds (so re-runs and resumed runs skip finished images), and a different base seed gives different ones.
* **"mat1 and mat2 shapes cannot be multiplied":** Model architecture mismatch. For SD3/Flux/Z-Image, ensure you connect ALL optional inputs. Check LoRA compatibility.
* **Dashboard not auto-loading:** Ensure the dashboard node is connected to the sampler's `dashboard_html` output. Try the "RELOAD / SHOW SESSION" button.
@@ -1537,6 +1543,11 @@ After modifying backend Python files, restart ComfyUI. After modifying `web/` JS
## 📝 Changelog
### Update 11/3/26 — Additional Random Seeds finished, LTX prompt arrays
* 🌱 **Additional Random Seeds:** Run Settings control (0-100) now has full defaults/migration and tests; extra seeds are deterministic per base seed. Thanks to DRuggeri (PR #21).
* 🎬 **LTX Builder prompts:** Builder prompt arrays (`[["p1","p2"]]`) now expand into separate LTX runs instead of being passed as a list.
* 🔧 **Builder fixes:** empty text-encoder list falls back to `None`; reloading a saved Builder config keeps each model's type.
### Update 3/9/26 — Upscaling, Workflow Packing & Config Builder Enhancements
* 🔍 **Array-Based Upscaling System:** Define multiple upscale configs with Cartesian expansion across models, ratios, and denoise values. Five modes: hires_only, model_only, model_then_hires, seedvr2, and florence2_hires (targeted region inpaint via Florence2 segmentation — needs ComfyUI-Florence2).
* 🖼️ **Upscale Model Discovery:** Auto-detects installed upscale models (RealESRGAN, etc.) from ComfyUI's folder system with searchable dropdown.
+21
View File
@@ -450,6 +450,27 @@ def _expand_ltx_entry(entry):
return expanded
_EXTRA_SEEDS_SALT = "uscg-extra-seeds-v1"
def derive_extra_seeds(base_seed, count):
"""Deterministic extra seeds for "Additional Random Seeds".
The same (base_seed, count prefix) always yields the same seeds, so a
re-run or an interrupted run resumed later (skip matching is by seed)
renders the same images; a different base seed yields different extras.
"""
import random
try:
count = int(count)
except (TypeError, ValueError):
return []
if count <= 0:
return []
rng = random.Random(f"{_EXTRA_SEEDS_SALT}:{base_seed}")
return [rng.randint(0, 2**32 - 1) for _ in range(count)]
def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed, extra_seeds, ckpt_name=None):
"""
Expand raw config entries into full configuration combinations.
+4 -3
View File
@@ -810,7 +810,7 @@ def run_generation_loop(
existing_data["session_name"] = session_name
from .config_utils import (
parse_json_with_error, parse_float_input, parse_prompt_input_nested,
parse_json_with_error, parse_float_input, parse_prompt_input_nested, derive_extra_seeds,
expand_configs, prepare_input_jobs
)
@@ -823,8 +823,9 @@ def run_generation_loop(
extra_seeds = []
if add_random_seeds_to_gens > 0:
import random
extra_seeds = [random.randint(0, 2**32 - 1) for _ in range(add_random_seeds_to_gens)]
# Deterministic per base seed (see config_utils.derive_extra_seeds) so a
# re-run / resumed run matches already-rendered images by seed.
extra_seeds = derive_extra_seeds(seed, add_random_seeds_to_gens)
expanded = expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed, extra_seeds, ckpt_name)
expanded.sort(key=lambda x: (
+111
View File
@@ -0,0 +1,111 @@
"""Additional Random Seeds run setting: Builder emit -> sampler_node unwrap -> expand_configs,
plus the deterministic per-base-seed derivation (config_utils.derive_extra_seeds)."""
import importlib.util
import json
import os
import sys
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config_builder_node import UltimateConfigBuilder
from config_utils import derive_extra_seeds, expand_configs
from tests.test_state_to_configs_json import make_state # noqa: E402
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_PKG = os.path.basename(_ROOT)
def _settings(**kw):
state = make_state()
state.update(kw)
return json.loads(UltimateConfigBuilder.state_to_configs_json(state))["_session_settings"]
def test_emit_value():
assert _settings(add_random_seeds_to_gens=3)["add_random_seeds_to_gens"] == 3
def test_emit_missing_defaults_zero():
assert _settings()["add_random_seeds_to_gens"] == 0
def test_emit_garbage_defaults_zero():
assert _settings(add_random_seeds_to_gens="abc")["add_random_seeds_to_gens"] == 0
def test_emit_numeric_string():
assert _settings(add_random_seeds_to_gens="2")["add_random_seeds_to_gens"] == 2
def test_derive_same_base_seed_same_list():
assert derive_extra_seeds(123, 5) == derive_extra_seeds(123, 5)
assert len(derive_extra_seeds(123, 5)) == 5
def test_derive_different_base_seed_differs():
assert derive_extra_seeds(123, 5) != derive_extra_seeds(124, 5)
def test_derive_prefix_stable_and_zero_empty():
assert derive_extra_seeds(7, 3) == derive_extra_seeds(7, 6)[:3]
assert derive_extra_seeds(7, 0) == []
assert derive_extra_seeds(7, "x") == []
def test_expand_configs_extra_seeds_multiply_count():
raw = [{"model": "m.safetensors", "sampler": "euler", "scheduler": "normal", "steps": 5, "cfg": 1}]
base = expand_configs(raw, [""], [""], [1.0], 0, [])
more = expand_configs(raw, [""], [""], [1.0], 0, [11, 22])
assert len(base) >= 1
assert len(more) == 3 * len(base)
_REAL = {}
def _load_sampler_node():
# conftest registers a STUB `<pkg>.sampler_node`; load the real file under a private name.
if "mod" not in _REAL:
# the conftest stubs for the sibling modules lack some names sampler_node imports;
# add inert placeholders for the missing ones only (never overrides a real attribute).
for modname, names in {
"lora_utils": ["load_and_save_tags"],
"config_utils": ["parse_json_with_error", "parse_float_input", "parse_string_input",
"expand_configs", "prepare_input_jobs", "sanitize_session_name",
"normalize_str", "get_files_from_folder", "parse_lora_definition"],
}.items():
stub = sys.modules.get(f"{_PKG}.{modname}")
for n in names:
if stub is not None and not hasattr(stub, n):
setattr(stub, n, lambda *a, **k: None)
spec = importlib.util.spec_from_file_location(f"{_PKG}.sampler_node_real", os.path.join(_ROOT, "sampler_node.py"))
mod = importlib.util.module_from_spec(spec)
mod.__package__ = _PKG
spec.loader.exec_module(mod)
_REAL["mod"] = mod
return _REAL["mod"]
def _run_tests_arg(monkeypatch, configs_json):
try:
sn = _load_sampler_node()
except Exception as exc: # stubs may be too thin for this module
pytest.skip(f"sampler_node not importable under stubs: {exc}")
captured = {}
monkeypatch.setattr(sys.modules["folder_paths"], "get_filename_list", lambda *_: [], raising=False)
orch = sys.modules[f"{_PKG}.generation_orchestrator"]
monkeypatch.setattr(orch, "run_generation_loop", lambda *a, **k: captured.setdefault("args", a) or ("",))
sn.SamplerGridTester().run_tests(configs_json)
return captured["args"]
def test_sampler_node_unwraps_session_setting(monkeypatch):
args = _run_tests_arg(monkeypatch, json.dumps({"configs": [{}], "_session_settings": {"add_random_seeds_to_gens": 2}}))
assert args[13] == 2
def test_sampler_node_default_zero(monkeypatch):
args = _run_tests_arg(monkeypatch, json.dumps({"configs": [{}]}))
assert args[13] == 0
+3 -1
View File
@@ -210,7 +210,8 @@ app.registerExtension({
lora_triggerwords_mode: "None",
save_conditioning_cache_to_file: false,
enable_model_cache: false,
vae_batch_size: 1
vae_batch_size: 1,
add_random_seeds_to_gens: 0
};
// Create HTML container immediately
@@ -321,6 +322,7 @@ app.registerExtension({
if (this.state.save_conditioning_cache_to_file === undefined) this.state.save_conditioning_cache_to_file = false;
if (this.state.enable_model_cache === undefined) this.state.enable_model_cache = false;
if (this.state.vae_batch_size === undefined) this.state.vae_batch_size = 1;
if (this.state.add_random_seeds_to_gens === undefined) this.state.add_random_seeds_to_gens = 0;
this.saveState();
this.renderUI();