Merge branch 'feat/builder-polish' into int/wave2

This commit is contained in:
Jason Hoku
2026-11-04 01:00:33 -08:00
15 changed files with 253 additions and 47 deletions
+1 -1
View File
@@ -795,7 +795,7 @@ The editor provides a visual chip interface for building and editing these varia
### Job Order & Preview Pass (Run Settings)
Both live in the **Run Settings** section of the Config Builder and default to the old behaviour.
* **Job Order:** `As written (default)` runs one resolution pass after another, exactly as before. `Minimize model/LoRA reloads` re-orders the whole job list so each model, LoRA stack and VAE loads as few times as possible (resolutions interleave, which costs nothing; only a new empty latent). Example: 3 models x 3 LoRA stacks x 3 resolutions loads 9 models and patches 27 LoRA stacks as written, but 3 and 9 with Minimize. The log prints the before/after counts. **Start At Job #** numbers follow the chosen order. Ignored in distributed mode (a warning is logged).
* **Job Order:** `As written (default)` runs one resolution pass after another, exactly as before. `Minimize model/LoRA reloads` re-groups the job list by model, LoRA stack, VAE and resolution. `As written` is already grouped by model (and Builder per-config resolutions stay inside their model group), so Minimize only changes anything when LoRA folders or random LoRA picks expand into different stacks (e.g. a model with 2 LoRA folders alternating A1 B1 A2 B2 becomes A1 A2 B1 B2), or when a batched latent is connected (one pass per latent). LoRA stacks are grouped before VAEs, so a model with several VAEs and several LoRA stacks can switch VAE more often than as written. The log prints the model/LoRA/VAE switch counts before and after and says when nothing changed. **Start At Job #** numbers follow the chosen order. Ignored in distributed mode (a warning is logged).
* **Preview Pass:** renders every cell cheaply first (fewer **Preview Steps**, smaller image via **Preview Scale** x0.25/0.33/0.5/0.75 snapped to the model's latent grid, optional **Preview CFG Override**, upscaling skipped) so you see the whole grid in a fraction of the time. Preview images carry a `PREVIEW` badge on the Dashboard. Reject the losers, then **untick Preview Pass and queue again**: only the surviving cells render at full quality, and each full image replaces its preview in the default Dashboard view. Interrupted preview passes resume (previews already done are skipped), and a preview pass skips cells that already have a full image. Not used when a latent input is connected; LTX video configs always render normally; ignored in distributed mode.
* **Skip Rejected Previews** (default on) and **Only Favorited Previews**: apply when Preview Pass is OFF. Cells whose preview you rejected are skipped, including after **Delete Rejected** (the rejection is remembered in `meta.rejected_preview_fps`). With *Only Favorited*, a cell that has a preview is rendered only if that preview is favorited; cells with no preview are still rendered. `Overwrite Existing` does not override a rejection.
* A cell is identified by a **config fingerprint** (model, LoRA stack, VAE, text encoders, sampler, scheduler, CFG, denoise, seed, raw prompts, full resolution, plus advanced-sampling/SLG/deep-shrink settings; *not* steps). Cells whose seed is re-randomised on every run (`random_before`, extra random seeds) cannot be matched to a preview and simply render again.
+2 -1
View File
@@ -897,9 +897,10 @@ class UltimateConfigBuilder:
# later by the orchestrator (seed, denoise) or only set for
# specific model types (clip_type, text_encoders, gguf_options,
# ltx_*) are intentionally excluded so they don't fire false alarms.
# "vae" is omitted when no VAE is picked (see 'Process VAEs' above), so it is excluded too.
_EXPECTED_CONFIG_KEYS = {
"sampler", "scheduler", "steps", "cfg", "seed_behavior",
"model", "model_type", "lora", "vae",
"model", "model_type", "lora",
"model_sampling_override", "use_advanced_sampling", "use_flux_guidance",
"model_prompt_prefix", "model_prompt_suffix", "attention_mode",
"use_deep_shrink", "use_slg",
+17
View File
@@ -839,6 +839,23 @@ import hashlib
JOB_ORDERS = ("as_written", "minimize_reloads")
def run_meta_extras(session_settings, has_optional_inputs):
"""Manifest-meta keys that let the Dashboard re-queue this session headless (POST /config_tester/run).
meta.configs_json only holds the configs list (sampler_node strips _session_settings)."""
ss = session_settings if isinstance(session_settings, dict) else {}
return {"session_settings": json.loads(json.dumps(ss, default=str)),
"has_optional_inputs": bool(has_optional_inputs)}
def job_order_note(before, after):
"""None when minimize_reloads changed the switch counts, else a note saying it changed nothing."""
if before != after:
return None
return ("minimize_reloads changed nothing: this grid is already grouped by model (it only helps when LoRA "
"folders or random LoRA picks expand into different stacks, or a batched latent makes each model "
"run once per latent)")
def build_job_plan(expanded, input_jobs):
"""Flatten the nested ``for job in input_jobs: for conf in expanded`` loop.
+2 -1
View File
@@ -67,7 +67,8 @@ Times are approximate on an RTX 5060 Ti 16 GB, not counting the first model load
* **What the grid tests:** euler at 4 and 6 steps, 1024x1024, each with the Advanced Sampling pipeline (CFG guider + Flux2 scheduler) and without it (plain KSampler, simple scheduler): the Builder always grids Advanced Sampling on vs off. **4 images**
* **Time:** TBD
* **Notes:** Klein is distilled, so cfg is 1; the official template uses the CFG guider plus the Flux2 scheduler, which is the Advanced Sampling half of the grid. CLIP type `flux2`.
* **Notes:** Klein is distilled, so cfg is 1; the official (distilled) template uses the CFG guider plus the Flux2 scheduler, which is the Advanced Sampling half of the grid. CLIP type `flux2`.
* **Base variant:** this example uses the distilled checkpoint. For the base model use `flux-2-klein-base-9b-fp8.safetensors` (same folder) with ~20 steps and cfg 5 (CFG guider + Flux2 scheduler), as in Comfy's `image_flux2_klein_image_edit_9b_base` template.
## MOSS-Image (`uscg-moss-image.json`)
+16 -6
View File
@@ -840,7 +840,8 @@ def run_generation_loop(
)
from .config_utils import (
JOB_ORDERS, build_job_plan, order_job_plan, count_switches, config_fingerprint,
preview_dims, build_preview_index, should_skip_for_previews, build_full_fp_set
preview_dims, build_preview_index, should_skip_for_previews, build_full_fp_set,
run_meta_extras, job_order_note
)
# ==== JOB ORDER / PREVIEW PASS SETTINGS (defaults reproduce the old behaviour) ====
@@ -963,7 +964,11 @@ def run_generation_loop(
_sw_before = count_switches(_plan_as_written, get_model_cache_key)
_sw_after = count_switches(job_plan, get_model_cache_key)
print(f"[GridTester] 🧭 Job order: minimize_reloads | model loads {_sw_before['model_loads']} -> {_sw_after['model_loads']}, "
f"LoRA switches {_sw_before['lora_switches']} -> {_sw_after['lora_switches']}")
f"LoRA switches {_sw_before['lora_switches']} -> {_sw_after['lora_switches']}, "
f"VAE switches {_sw_before['vae_switches']} -> {_sw_after['vae_switches']}")
_note = job_order_note(_sw_before, _sw_after)
if _note:
print(f"[GridTester] ℹ️ Job order: {_note}")
print(f"{'='*80}")
print(f"[GridTester] 🚀 GENERATION START")
@@ -986,6 +991,7 @@ def run_generation_loop(
optional_negative is not None,
optional_latent is not None
])
_meta_run = run_meta_extras(session_settings, has_optional_inputs)
if has_optional_inputs and not overwrite_existing:
print(f"[GridTester] ⚠️ Optional inputs connected — job skip/resume DISABLED.")
@@ -1421,7 +1427,8 @@ def run_generation_loop(
"image_format": image_format,
"job_order": job_order,
"preview_pass": {"enabled": preview_on, "steps": pv_steps, "scale": pv_scale, "cfg_override": pv_cfg},
"rejected_preview_fps": list((existing_data.get("meta") or {}).get("rejected_preview_fps", []))
"rejected_preview_fps": list((existing_data.get("meta") or {}).get("rejected_preview_fps", [])),
**_meta_run
}
save_manifest(paths["manifest"], existing_data)
@@ -1768,7 +1775,8 @@ def run_generation_loop(
"image_format": image_format,
"job_order": job_order,
"preview_pass": {"enabled": preview_on, "steps": pv_steps, "scale": pv_scale, "cfg_override": pv_cfg},
"rejected_preview_fps": list((existing_data.get("meta") or {}).get("rejected_preview_fps", []))
"rejected_preview_fps": list((existing_data.get("meta") or {}).get("rejected_preview_fps", [])),
**_meta_run
}
save_manifest(paths["manifest"], existing_data)
@@ -2600,7 +2608,8 @@ def run_generation_loop(
"image_format": image_format,
"job_order": job_order,
"preview_pass": {"enabled": preview_on, "steps": pv_steps, "scale": pv_scale, "cfg_override": pv_cfg},
"rejected_preview_fps": list((existing_data.get("meta") or {}).get("rejected_preview_fps", []))
"rejected_preview_fps": list((existing_data.get("meta") or {}).get("rejected_preview_fps", [])),
**_meta_run
}
save_manifest(paths["manifest"], existing_data)
@@ -2709,7 +2718,8 @@ def run_generation_loop(
"image_format": image_format,
"job_order": job_order,
"preview_pass": {"enabled": preview_on, "steps": pv_steps, "scale": pv_scale, "cfg_override": pv_cfg},
"rejected_preview_fps": list((existing_data.get("meta") or {}).get("rejected_preview_fps", []))
"rejected_preview_fps": list((existing_data.get("meta") or {}).get("rejected_preview_fps", [])),
**_meta_run
}
save_manifest(paths["manifest"], existing_data)
+46 -21
View File
@@ -1008,7 +1008,7 @@ setupKeyReloadFullscreen()
// FULL PASS FOR SURVIVORS (preview-pass workflow)
// Strategy (a): ask the parent ComfyUI page (web/dashboard.js) to flip the matching Config Builder to
// "Preview pass off", then queue the prompt. Needs a parent that understands 'uscg_run_full_pass'.
// Strategy (b): STUB for a future run API (see _uscgTryRunApi). Fallback: instructions toast.
// Strategy (b): queue it through the run API (POST /config_tester/run) from the manifest meta. Fallback: instructions toast.
// ---------------------------------------------------------------------------
function _uscgToast(msg, ok) {
var el = document.createElement('div');
@@ -1020,25 +1020,49 @@ function _uscgToast(msg, ok) {
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 9000);
}
// Strategy (b) stub: wire the run API here once it exists. Detection: window.USCG_RUN_API (set by the host
// page) or a '/config_tester/run' route. Resolves to {ok, message} when it handled the request, else null.
// Strategy (b): queue the full pass through POST /config_tester/run, re-using the configs and Run Settings the
// orchestrator recorded in this session's manifest meta (configs_json + session_settings, see run_meta_extras).
function uscgFullPassBody(manifest, session) {
var m = (manifest && manifest.meta) || {};
if (!m.configs_json) return { error: 'This session has no recorded configs (meta.configs_json).' };
if (!m.session_settings || typeof m.session_settings !== 'object')
return { error: 'This session was rendered before the hub recorded its Run Settings in the manifest.' };
if (m.has_optional_inputs)
return { error: "This session used the Generator's optional inputs, which a headless run cannot reproduce." };
var configs;
try { configs = typeof m.configs_json === 'string' ? JSON.parse(m.configs_json) : m.configs_json; }
catch (e) { return { error: 'meta.configs_json is not valid JSON.' }; }
if (configs && !Array.isArray(configs) && Array.isArray(configs.configs)) configs = configs.configs;
if (!Array.isArray(configs) || !configs.length) return { error: 'meta.configs_json holds no configs.' };
var ss = JSON.parse(JSON.stringify(m.session_settings));
ss.preview_pass = Object.assign({ steps: 8, scale: 0.5, cfg_override: null }, ss.preview_pass || {}, { enabled: false });
ss.skip_rejected_previews = true;
ss.start_at_job = 0;
ss.overwrite_existing = false;
ss.session_name = session;
return { body: { configs_json: { configs: configs, _session_settings: ss },
session_name: session, overwrite_existing: false, force: true } };
}
// Resolves to {ok, message} when the run API answered (or the session cannot be re-queued), or null when the
// server has no run API (older USCG), so the caller shows the manual instructions.
async function _uscgTryRunApi(session) {
var built = uscgFullPassBody(typeof fullManifest !== 'undefined' ? fullManifest : null, session);
if (built.error) return { ok: false, message: built.error };
var r;
try {
if (window.USCG_RUN_API && typeof window.USCG_RUN_API.run === 'function') {
// TODO(merge with the run-API task): confirm the real signature; overrides below are the contract.
var res = await window.USCG_RUN_API.run({
session_name: session,
settings_overrides: { preview_pass: { enabled: false }, skip_rejected_previews: true }
});
return { ok: true, message: (res && res.message) || 'Full pass queued.' };
}
var probe = await fetch('/config_tester/run', { method: 'GET' }).catch(function () { return null; });
if (probe && probe.status !== 404) {
// TODO(merge with the run-API task): POST /config_tester/run with the same overrides, then return
// {ok, message}. Intentionally not implemented here (this task does not depend on it).
}
} catch (e) { /* fall through to the instructions toast */ }
return null;
r = await fetch('/config_tester/run', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(built.body) });
} catch (e) { return null; }
if (r.status === 404 || r.status === 405) return null;
var j = null;
try { j = await r.json(); } catch (e) { /* non-JSON error body */ }
if (!r.ok) {
var err = j && j.error ? (typeof j.error === 'string' ? j.error : JSON.stringify(j.error)) : ('HTTP ' + r.status);
return { ok: false, message: 'Run API: ' + String(err).slice(0, 200) + '.' };
}
return { ok: true, message: 'Full pass queued (prompt ' + String((j && j.prompt_id) || '?').slice(0, 8) +
'). Rejected previews are skipped; full images replace their previews as they finish.' };
}
function _uscgAskParentFullPass(session) {
@@ -1049,7 +1073,7 @@ function _uscgAskParentFullPass(session) {
if (!ev.data || ev.data.type !== 'uscg_run_full_pass_result') return;
done = true;
window.removeEventListener('message', onMsg);
resolve({ ok: !!ev.data.ok, message: ev.data.message || '' });
resolve({ ok: !!ev.data.ok, message: ev.data.message || '', reason: ev.data.reason || null });
};
window.addEventListener('message', onMsg);
window.parent.postMessage({ type: 'uscg_run_full_pass', session_name: session }, '*');
@@ -1062,12 +1086,13 @@ function _uscgAskParentFullPass(session) {
}
async function runFullPassForSurvivors() {
var session = (document.getElementById('session-input') && document.getElementById('session-input').value) ||
var session = window._uscgLoadedSession ||
(document.getElementById('session-input') && document.getElementById('session-input').value) ||
(fullManifest && fullManifest.session_name) || '';
var manual = 'In the Config Builder open Run Settings, untick "Preview Pass" and queue the workflow again. ' +
'Only cells whose preview was not rejected will render at full quality.';
var res = await _uscgAskParentFullPass(session);
if (!res) res = await _uscgTryRunApi(session);
if (!res || (!res.ok && (res.reason === 'no_builder' || res.reason === 'no_queue'))) res = await _uscgTryRunApi(session);
if (res && res.ok) { _uscgToast(res.message || 'Full pass queued.', true); return; }
_uscgToast((res && res.message ? res.message + ' ' : '') + manual, false);
}
+1 -1
View File
@@ -566,7 +566,7 @@
<span style="font-weight: bold;">PREVIEW PASS</span>
<span id="preview-bar-text" style="flex: 1;">--</span>
<button id="full-pass-btn" onclick="runFullPassForSurvivors()"
title="Render the survivors at full quality: turns Preview pass off in the Config Builder and queues the workflow"
title="Render the survivors at full quality: turns Preview pass off in the Config Builder and queues the workflow (on the standalone page it queues the run through the run API)"
style="background: #3a2f12; border: 1px solid #8a7330; color: #ffd76a; padding: 3px 10px; border-radius: 4px; cursor: pointer; font-size: 11px;">&#9654; Full pass for survivors</button>
</div>
+12 -5
View File
@@ -57,8 +57,9 @@ def make_item(i, model, sampler, scheduler, cfg, steps, lora):
}
def make_manifest():
"""24 items, every config tuple unique. Newest first, like the real manifest."""
def make_manifest(meta=None):
"""24 items, every config tuple unique. Newest first, like the real manifest.
`meta` (optional) is merged over the default meta dict."""
built = []
i = 0
for model in ("sdxl_base.safetensors", "juggernautXL_v9.safetensors"):
@@ -71,7 +72,7 @@ def make_manifest():
i += 1
return {
"session_name": SESSION,
"meta": {"model": "", "positive": "", "negative": ""},
"meta": {"model": "", "positive": "", "negative": "", **(meta or {})},
"items": list(reversed(built)),
}
@@ -140,9 +141,15 @@ class Dashboard:
@pytest.fixture
def dashboard(page):
def manifest_meta(request):
"""Extra manifest meta for the `dashboard` fixture; override via indirect parametrize."""
return getattr(request, "param", None) or {}
@pytest.fixture
def dashboard(page, manifest_meta):
from playwright.sync_api import expect
d = Dashboard(page, make_manifest())
d = Dashboard(page, make_manifest(manifest_meta))
page.set_viewport_size({"width": 1600, "height": 1200})
page.on("pageerror", lambda e: d.pageerrors.append(str(e)))
page.route("**/*", d.handle)
+61
View File
@@ -0,0 +1,61 @@
"""Dashboard 'Full pass for survivors' via the run API (POST /config_tester/run) on the fake origin."""
import json
import pytest
SESSION = "smoke" # matches tests/e2e/conftest.py
_CONFIGS = [{"model": "sdxl_base.safetensors", "sampler": "euler", "scheduler": "normal",
"steps": 20, "cfg": 7.0, "lora": "None"}]
_META = {
"configs_json": json.dumps(_CONFIGS),
"session_settings": {"preview_pass": {"enabled": True, "steps": 8, "scale": 0.5},
"add_random_seeds_to_gens": 2, "session_name": SESSION},
"has_optional_inputs": False,
}
def _run_posts(d):
return [b for (m, p, b) in d.request_log if m == "POST" and p == "/config_tester/run"]
def _toast_text(page):
page.wait_for_function("[...document.querySelectorAll('div')].some(e => e.style.zIndex === '99999')")
return page.evaluate("[...document.querySelectorAll('div')].filter(e => e.style.zIndex === '99999')"
".map(e => e.textContent).join('|')")
@pytest.mark.parametrize("manifest_meta", [_META], indirect=True)
def test_full_pass_uses_run_api(dashboard):
page = dashboard.page
page.route("**/config_tester/run", lambda route: (
dashboard.request_log.append(("POST", "/config_tester/run", json.loads(route.request.post_data)))
or route.fulfill(status=200, content_type="application/json", body='{"prompt_id":"abcd1234-0000"}')))
page.evaluate("runFullPassForSurvivors()")
toast = _toast_text(page)
assert "Full pass queued" in toast
posts = _run_posts(dashboard)
assert len(posts) == 1
body = posts[0]
ss = body["configs_json"]["_session_settings"]
assert ss["preview_pass"]["enabled"] is False
assert ss["skip_rejected_previews"] is True
assert ss["add_random_seeds_to_gens"] == 2
assert body["overwrite_existing"] is False
assert body["force"] is True
assert body["session_name"] == SESSION
assert body["configs_json"]["configs"] == _CONFIGS
def test_full_pass_without_recorded_settings_posts_nothing(dashboard):
dashboard.page.evaluate("runFullPassForSurvivors()")
toast = _toast_text(dashboard.page)
assert "before the hub recorded" in toast or "no recorded configs" in toast
assert _run_posts(dashboard) == []
@pytest.mark.parametrize("manifest_meta", [{"configs_json": json.dumps(_CONFIGS)}], indirect=True)
def test_full_pass_old_session_without_settings(dashboard):
dashboard.page.evaluate("runFullPassForSurvivors()")
assert "before the hub recorded" in _toast_text(dashboard.page)
assert _run_posts(dashboard) == []
+1 -3
View File
@@ -127,10 +127,8 @@ def test_emits_and_counts(path, capsys):
name = os.path.basename(path)
state = _state(_load(path))
out = json.loads(UltimateConfigBuilder.state_to_configs_json(state))
# state_to_configs_json lists "vae" in _EXPECTED_CONFIG_KEYS but omits it on purpose when no VAE is
# picked (the SDXL checkpoint has its own), so that one key may be reported; anything else is real drift.
drift = [l for l in capsys.readouterr().out.splitlines() if "Config schema drift" in l]
assert all("missing keys {'vae'}" in l for l in drift), drift
assert not drift, drift
assert out["_session_settings"]["add_random_seeds_to_gens"] == 0
count = len(expand_configs(out["configs"], [""], [""], [1.0], 0, [], None))
assert count == EXPECTED_IMAGES[name]
+25
View File
@@ -157,3 +157,28 @@ def test_list_valued_model_keys_are_hashable():
plan = build_job_plan(expanded, _jobs((64, 64)))
after = order_job_plan(plan, "minimize_reloads", lambda c: [c["model"], ["te"]])
assert [p["conf"]["model"] for p in after] == ["A", "A", "B"]
def test_job_order_note():
from config_utils import job_order_note
assert job_order_note({"model_loads": 1}, {"model_loads": 0}) is None
note = job_order_note({"model_loads": 1}, {"model_loads": 1})
assert isinstance(note, str) and "changed nothing" in note
def test_per_config_resolutions_in_model_order_gain_nothing():
expanded = [_conf(model=m, resolution=r) for m in ("A", "B") for r in ((512, 512), (768, 768))]
plan = build_job_plan(expanded, _jobs((1024, 1024)))
before = count_switches(plan, _key)
after = count_switches(order_job_plan(plan, "minimize_reloads", _key), _key)
assert before == after
def test_minimize_reloads_can_trade_vae_switches_for_lora_switches():
# As written, VAE is the slower-changing axis (vae outer, lora inner), but the sort ranks LoRA first.
expanded = [_conf(lora=l, vae=v) for v in ("V1", "V2") for l in ("L1", "L2")]
plan = build_job_plan(expanded, _jobs((1024, 1024)))
before = count_switches(plan, _key)
after = count_switches(order_job_plan(plan, "minimize_reloads", _key), _key)
assert (before["lora_switches"], after["lora_switches"]) == (4, 2)
assert (before["vae_switches"], after["vae_switches"]) == (2, 4)
+47
View File
@@ -0,0 +1,47 @@
"""run_meta_extras (manifest meta that lets the Dashboard re-queue a session) + the orchestrator wiring."""
import json
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config_utils import run_meta_extras
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def test_deep_copy_and_json_safe():
ss = {"add_random_seeds_to_gens": 2, "upscaling": {"enabled": True, "steps": [1, 2]}}
out = run_meta_extras(ss, False)
ss["upscaling"]["steps"].append(3)
ss["add_random_seeds_to_gens"] = 9
assert out["session_settings"]["upscaling"]["steps"] == [1, 2]
assert out["session_settings"]["add_random_seeds_to_gens"] == 2
json.dumps(out)
def test_non_dict_gives_empty_settings():
assert run_meta_extras(None, False)["session_settings"] == {}
assert run_meta_extras("x", False)["session_settings"] == {}
def test_has_optional_inputs_is_bool():
assert run_meta_extras({}, True)["has_optional_inputs"] is True
assert run_meta_extras({}, 0)["has_optional_inputs"] is False
assert run_meta_extras({}, None)["has_optional_inputs"] is False
def test_unserialisable_values_are_stringified():
out = run_meta_extras({"p": object()}, False)
json.dumps(out)
def test_every_run_generation_loop_meta_block_records_extras():
with open(os.path.join(_ROOT, "generation_orchestrator.py"), encoding="utf-8") as f:
src = f.read()
body = src[src.index("def run_generation_loop"):src.index("def _conditioning_to_serializable")]
blocks = re.findall(r'existing_data\["meta"\] = \{(.*?)\n\s*\}\n', body, flags=re.S)
assert len(blocks) == 4
for b in blocks:
assert "**_meta_run" in b
+14
View File
@@ -261,3 +261,17 @@ def test_cfg_override_blank_is_null_and_zero_is_kept():
def test_unknown_job_order_becomes_as_written():
assert _ss(job_order="alphabetical")["job_order"] == "as_written"
def test_checkpoint_without_vae_omits_key_and_reports_no_drift(capsys):
state = make_state(models=["m.safetensors"])
out = json.loads(UltimateConfigBuilder.state_to_configs_json(state))
assert all("vae" not in c for c in out["configs"])
assert "schema drift" not in capsys.readouterr().out.lower()
def test_picked_vae_is_still_emitted(capsys):
state = make_state(models=["m.safetensors"], extra_array_fields={"vaes": ["my.vae.safetensors"]})
out = json.loads(UltimateConfigBuilder.state_to_configs_json(state))
assert any(c.get("vae") for c in out["configs"])
assert "schema drift" not in capsys.readouterr().out.lower()
@@ -4714,9 +4714,8 @@ function renderLTXSection(node, configArray, arrayIdx, modelLists) {
section.appendChild(header);
// Sub-sections — defined in subsequent tasks (A13-A16). For now, stub them
// as no-op functions so this skeleton renders without errors. The real
// implementations will replace these stubs.
// LTX sub-sections (model files, video shape, stage). The typeof guards keep this section rendering
// if one of the render functions is ever missing.
if (typeof renderLTXModelFiles === "function") section.appendChild(renderLTXModelFiles(node, configArray, modelLists));
if (typeof renderLTXVideoShape === "function") section.appendChild(renderLTXVideoShape(node, configArray));
if (typeof renderLTXStage === "function") {
@@ -7167,7 +7166,7 @@ export function renderRunSettingsSection(node, container) {
return node.state.preview_pass;
};
_addRunRow("Job Order", "As written = resolution passes run one after another (the order used before this option existed). Minimize reloads = group jobs so each model, LoRA stack and VAE loads as few times as possible (resolutions interleave, which is free). Note: Start At Job # numbers follow the chosen order. Ignored in distributed mode.", () => {
_addRunRow("Job Order", "As written = the order the configs expand in, already grouped by model, then text encoders, VAE, LoRA and prompt; Builder per-config resolutions stay inside their model group. Minimize reloads = re-group the job list by model, LoRA stack, VAE and resolution. It only changes anything when LoRA folders or random LoRA picks expand into different stacks, or when a batched latent is connected (one pass per latent). LoRA stacks are grouped before VAEs, so a model with several VAEs and LoRAs may switch VAE more often. The log prints the before/after counts and says when nothing changed. Start At Job # numbers follow the chosen order. Ignored in distributed mode.", () => {
const sel = document.createElement("select");
sel.style.cssText = _inputCss;
[["as_written", "As written (default)"], ["minimize_reloads", "Minimize model/LoRA reloads"]].forEach(([v, t]) => {
+5 -4
View File
@@ -162,8 +162,8 @@ app.registerExtension({
// "Preview pass off" and queue the prompt. Feature-detected; the dashboard falls back to an
// instructions toast when this does not answer (or app.queuePrompt is unavailable).
if (event.data?.type === 'uscg_run_full_pass') {
const reply = (ok, message) => {
try { event.source?.postMessage({ type: 'uscg_run_full_pass_result', ok, message }, '*'); } catch (e) { /* ignore */ }
const reply = (ok, message, reason) => {
try { event.source?.postMessage({ type: 'uscg_run_full_pass_result', ok, message, reason }, '*'); } catch (e) { /* ignore */ }
};
try {
const san = (s) => String(s || '').replace(/[^\w\-]/g, '');
@@ -173,11 +173,12 @@ app.registerExtension({
if (builders.length !== 1) {
reply(false, builders.length === 0
? `No Config Builder in this workflow uses session "${wanted}".`
: `${builders.length} Config Builders use session "${wanted}".`);
: `${builders.length} Config Builders use session "${wanted}".`,
builders.length === 0 ? 'no_builder' : undefined);
return;
}
if (typeof app.queuePrompt !== 'function') {
reply(false, 'This ComfyUI frontend does not expose app.queuePrompt.');
reply(false, 'This ComfyUI frontend does not expose app.queuePrompt.', 'no_queue');
return;
}
const b = builders[0];