From 7a2c3c905ec912195305d09663a6e20ef7ea32bf Mon Sep 17 00:00:00 2001 From: Jason Hoku Date: Mon, 11 May 2026 09:21:14 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20Builder=20UI=20Run=20Settings=20?= =?UTF-8?q?=E2=80=94=20top-of-UI=20placement=20+=206=20generator-node=20ov?= =?UTF-8?q?errides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of consolidating run-level config into the Builder UI. The Builder UI's "Run Settings" section is now rendered at the TOP of the UI (was previously below config arrays) and gains six new fields with help tooltips: - overwrite_existing (bool, default false) - flush_batch_every (int, default 4) - lora_triggerwords_mode (enum: None / Append To End / Append To Start / Read From Config, default None) - save_conditioning_cache_to_file (bool, default false) - enable_model_cache (bool, default false) - vae_batch_size (int, default 4, range -1..64) State plumbs through state_to_configs_json -> _session_settings -> sampler_node.run_tests -> run_generation_loop. Builder UI values override the corresponding widgets on UltimateSamplerGrid when set (non-default). Widget values remain the fallback so existing workflows keep working without changes. Logs an "⚙️ overridden by Builder UI: " line whenever an override fires, so it's obvious in the console. Next: once the user has tested this thoroughly, the corresponding widgets on the SamplerGridTester node can be removed/hidden to simplify the generator UX (deferred — coordinate with user). Co-Authored-By: Claude Opus 4.7 (1M context) --- config_builder_node.py | 36 +++++++++ sampler_node.py | 31 ++++++++ .../conf-builder-config-management.js | 78 ++++++++++++++++++- web/conf_builder/conf-builder-main.js | 16 +++- 4 files changed, 159 insertions(+), 2 deletions(-) diff --git a/config_builder_node.py b/config_builder_node.py index 31acb61..04cda6e 100644 --- a/config_builder_node.py +++ b/config_builder_node.py @@ -832,6 +832,42 @@ class UltimateConfigBuilder: image_format = state.get("image_format", "webp") if image_format and image_format != "webp": session_settings["image_format"] = image_format + # Generator-node override settings — emit only when non-default so the + # widget value on the SamplerGridTester node wins for users who haven't + # explicitly set them in the Builder UI. When the Generator widgets are + # eventually removed, these can be made unconditional. + ov_existing = state.get("overwrite_existing", False) + if ov_existing: + session_settings["overwrite_existing"] = True + + flush_be = state.get("flush_batch_every", 4) + try: + flush_be = int(flush_be) + except (TypeError, ValueError): + flush_be = 4 + if flush_be != 4: + session_settings["flush_batch_every"] = flush_be + + lora_tw = state.get("lora_triggerwords_mode", "None") + if lora_tw and lora_tw != "None": + session_settings["lora_triggerwords_mode"] = lora_tw + + save_cc = state.get("save_conditioning_cache_to_file", False) + if save_cc: + session_settings["save_conditioning_cache_to_file"] = True + + en_mc = state.get("enable_model_cache", False) + if en_mc: + session_settings["enable_model_cache"] = True + + vae_bs = state.get("vae_batch_size", 4) + try: + vae_bs = int(vae_bs) + except (TypeError, ValueError): + vae_bs = 4 + if vae_bs != 4: + session_settings["vae_batch_size"] = vae_bs + if session_settings: output_obj["_session_settings"] = session_settings diff --git a/sampler_node.py b/sampler_node.py index 076892c..a83a89d 100644 --- a/sampler_node.py +++ b/sampler_node.py @@ -292,6 +292,37 @@ class SamplerGridTester: except Exception as e: print(f"[GridTester] ⚠️ Error parsing configs_json for distribution: {e}") + # Builder UI Run Settings override widget values when present. + # When a user sets these in the Builder UI, _session_settings carries + # them through configs_json and they take precedence over what's + # wired to the SamplerGridTester widgets. Generator widgets are still + # the fallback for backward compat. + if session_settings: + if "overwrite_existing" in session_settings: + overwrite_existing = bool(session_settings["overwrite_existing"]) + print(f"[GridTester] ⚙️ overwrite_existing overridden by Builder UI: {overwrite_existing}") + if "flush_batch_every" in session_settings: + try: + flush_batch_every = int(session_settings["flush_batch_every"]) + print(f"[GridTester] ⚙️ flush_batch_every overridden by Builder UI: {flush_batch_every}") + except (TypeError, ValueError): + pass + if "lora_triggerwords_mode" in session_settings: + lora_triggerwords_mode = str(session_settings["lora_triggerwords_mode"]) + print(f"[GridTester] ⚙️ lora_triggerwords_mode overridden by Builder UI: {lora_triggerwords_mode}") + if "save_conditioning_cache_to_file" in session_settings: + save_conditioning_cache_to_file = bool(session_settings["save_conditioning_cache_to_file"]) + print(f"[GridTester] ⚙️ save_conditioning_cache_to_file overridden by Builder UI: {save_conditioning_cache_to_file}") + if "enable_model_cache" in session_settings: + enable_model_cache = bool(session_settings["enable_model_cache"]) + print(f"[GridTester] ⚙️ enable_model_cache overridden by Builder UI: {enable_model_cache}") + if "vae_batch_size" in session_settings: + try: + vae_batch_size = int(session_settings["vae_batch_size"]) + print(f"[GridTester] ⚙️ vae_batch_size overridden by Builder UI: {vae_batch_size}") + except (TypeError, ValueError): + pass + return run_generation_loop( self, ckpt_name, positive_text, negative_text, seed, denoise, vae_batch_size, diff --git a/web/conf_builder/conf-builder-config-management.js b/web/conf_builder/conf-builder-config-management.js index 6b13d1e..dca558d 100644 --- a/web/conf_builder/conf-builder-config-management.js +++ b/web/conf_builder/conf-builder-config-management.js @@ -6344,6 +6344,12 @@ export function renderCooldownSection(node, container) { export function renderRunSettingsSection(node, container) { if (node.state.start_at_job === undefined) node.state.start_at_job = 0; if (node.state.image_format === undefined) node.state.image_format = "webp"; + if (node.state.overwrite_existing === undefined) node.state.overwrite_existing = false; + if (node.state.flush_batch_every === undefined) node.state.flush_batch_every = 4; + if (node.state.lora_triggerwords_mode === undefined) node.state.lora_triggerwords_mode = "None"; + if (node.state.save_conditioning_cache_to_file === undefined) node.state.save_conditioning_cache_to_file = false; + if (node.state.enable_model_cache === undefined) node.state.enable_model_cache = false; + if (node.state.vae_batch_size === undefined) node.state.vae_batch_size = 4; const section = document.createElement("div"); section.className = "cb-section full-width"; @@ -6414,6 +6420,73 @@ export function renderRunSettingsSection(node, container) { content.appendChild(fmtDiv); content.appendChild(fmtDesc); + // Helper: renders a labeled row with a "?" tooltip icon, binding to node.state[stateKey]. + function _addRunSetting(content, opts) { + // opts: { stateKey, label, tooltip, kind: "bool"|"int"|"select", options?, min?, max?, defaultValue } + const row = document.createElement("div"); + row.style.cssText = "margin-top: 10px; display: flex; align-items: center; gap: 8px;"; + + const label = document.createElement("label"); + label.textContent = opts.label + ":"; + label.style.cssText = "font-size: 12px; color: #ccc; white-space: nowrap;"; + + const help = document.createElement("span"); + help.textContent = "?"; + help.title = opts.tooltip; + help.style.cssText = "display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: #2a2a2a; color: #8af; font-size: 10px; font-weight: bold; cursor: help; user-select: none;"; + + let input; + if (opts.kind === "bool") { + input = document.createElement("input"); + input.type = "checkbox"; + input.checked = !!node.state[opts.stateKey]; + input.style.cssText = "transform: scale(1.2); cursor: pointer;"; + input.onchange = () => { node.state[opts.stateKey] = input.checked; node.saveState(); }; + } else if (opts.kind === "int") { + input = document.createElement("input"); + input.type = "number"; + if (opts.min !== undefined) input.min = opts.min; + if (opts.max !== undefined) input.max = opts.max; + input.value = node.state[opts.stateKey] !== undefined ? node.state[opts.stateKey] : opts.defaultValue; + input.style.cssText = "width: 70px; background: #1a1a1a; color: #ccc; border: 1px solid #444; border-radius: 4px; padding: 4px 6px; font-size: 12px;"; + input.onchange = () => { + const v = parseInt(input.value); + node.state[opts.stateKey] = isNaN(v) ? opts.defaultValue : v; + node.saveState(); + }; + } else if (opts.kind === "select") { + input = document.createElement("select"); + input.style.cssText = "background: #1a1a1a; color: #ccc; border: 1px solid #444; border-radius: 4px; padding: 4px 6px; font-size: 12px;"; + (opts.options || []).forEach(o => { + const opt = document.createElement("option"); + opt.value = o; + opt.textContent = o; + if (o === (node.state[opts.stateKey] || opts.defaultValue)) opt.selected = true; + input.appendChild(opt); + }); + input.onchange = () => { node.state[opts.stateKey] = input.value; node.saveState(); }; + } + + row.appendChild(label); + row.appendChild(help); + row.appendChild(input); + content.appendChild(row); + } + + _addRunSetting(content, { stateKey: "overwrite_existing", label: "Overwrite Existing", kind: "bool", + tooltip: "True = Re-run everything. False = Skip already generated images (Resume)." }); + _addRunSetting(content, { stateKey: "flush_batch_every", label: "Flush Batch Every", kind: "int", min: 0, max: 64, defaultValue: 4, + tooltip: "Update dashboard every X images. 0 = Use VAE Batch Size." }); + _addRunSetting(content, { stateKey: "lora_triggerwords_mode", label: "LoRA Triggerwords Mode", kind: "select", + options: ["None", "Append To End", "Append To Start", "Read From Config"], defaultValue: "None", + tooltip: "None = Don't fetch/append trigger words. Append To End = Add triggers at end of prompt (default behavior). Append To Start = Add triggers at start of prompt. Read From Config = Use lora_triggerwords_append_settings in config JSON to specify per-lora placement." }); + _addRunSetting(content, { stateKey: "save_conditioning_cache_to_file", label: "Save Conditioning Cache To File", kind: "bool", + tooltip: "Save CLIP conditioning cache to disk. Useful when experimenting with the same prompts/models — skips text encoding on resume. WARNING: Can create very large files in output/benchmarks. Automatically disabled when optional inputs (model/clip/conditioning) are connected." }); + _addRunSetting(content, { stateKey: "enable_model_cache", label: "Enable Model Cache", kind: "bool", + tooltip: "Experimental: intelligent model/LoRA caching with async background preloading. Speeds up generation when switching LoRAs frequently. Loads cached LoRAs from RAM instead of disk. Disable to reduce RAM/VRAM usage." }); + _addRunSetting(content, { stateKey: "vae_batch_size", label: "VAE Batch Size", kind: "int", min: -1, max: 64, defaultValue: 4, + tooltip: "How many images to encode/decode per VAE pass. Lower = less VRAM. -1 = process all at once. Default: 4." }); + section.appendChild(header); section.appendChild(content); container.appendChild(section); @@ -6583,6 +6656,10 @@ export async function renderUI(node, availableLoras, modelLists, loraFolders, av // Global Prompts Section renderGlobalPromptsSection(node, mainContent); + // Run Settings Section — session-level generator overrides, shown at top + // so users see them before diving into config arrays. + renderRunSettingsSection(node, mainContent); + // Config Arrays Section const configSection = document.createElement("div"); configSection.className = "cb-section full-width"; @@ -6774,7 +6851,6 @@ export async function renderUI(node, availableLoras, modelLists, loraFolders, av // Session-level settings (applies to all configs) renderUpscalingSection(node, mainContent, modelLists); renderCooldownSection(node, mainContent); - renderRunSettingsSection(node, mainContent); // Output Preview — rendered as a SIBLING of layoutWrapper (not inside // mainContent), so it sits as a fixed/resizable panel at the bottom of diff --git a/web/conf_builder/conf-builder-main.js b/web/conf_builder/conf-builder-main.js index 8164a37..36b92d9 100644 --- a/web/conf_builder/conf-builder-main.js +++ b/web/conf_builder/conf-builder-main.js @@ -189,7 +189,13 @@ app.registerExtension({ every_n: 1, clear_vram: false }, - image_format: "webp" + image_format: "webp", + overwrite_existing: false, + flush_batch_every: 4, + lora_triggerwords_mode: "None", + save_conditioning_cache_to_file: false, + enable_model_cache: false, + vae_batch_size: 4 }; // Create HTML container immediately @@ -267,6 +273,14 @@ app.registerExtension({ }); } + // Migration: ensure top-level Run Settings fields exist + if (this.state.overwrite_existing === undefined) this.state.overwrite_existing = false; + if (this.state.flush_batch_every === undefined) this.state.flush_batch_every = 4; + if (this.state.lora_triggerwords_mode === undefined) this.state.lora_triggerwords_mode = "None"; + 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 = 4; + this.saveState(); this.renderUI(); }