Merge branch 'feat/builder-polish' into int/wave2
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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`)
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;">▶ Full pass for survivors</button>
|
||||
</div>
|
||||
|
||||
|
||||
+12
-5
@@ -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)
|
||||
|
||||
@@ -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) == []
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user