From b23e4c8db69e7086945c444d3a0cd1ec5775dff1 Mon Sep 17 00:00:00 2001 From: JasonHoku Date: Fri, 6 Feb 2026 19:37:47 -0800 Subject: [PATCH] Add Better Model Selections To Config Builder --- pyproject.toml | 2 +- web/config_builder.js | 231 ++++++++++++++++++++++++++---------------- 2 files changed, 142 insertions(+), 91 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 36185bb..c895964 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ultimate-auto-sampler-config-grid-testing-suite" description = "ComfyUI Sampler testing & benchmarking tool for testing Stable Diffusion samplers, schedulers, CFG scales, prompts, img2img denoise values, and LoRA combinations. Features infinite-canvas dashboard image grids with virtual scrolling that can handle thousands of images, multi-model comparison, automatic resume on interrupt, and real-time visualization. Test entire checkpoint folders, stack multiple LoRAs, generate random seed variations, and export optimized configs. Includes fullscreen mode, keyboard navigation, smart filtering by parameters, and one-click regeneration workflow. Perfect for sampler output optimization and hyperparameter tuning." -version = "1.2.0" +version = "1.3.0" license = {file = "LICENSE"} classifiers = [ # # For OS-independent nodes (works on all operating systems) diff --git a/web/config_builder.js b/web/config_builder.js index 4de2cd4..ccd8a02 100644 --- a/web/config_builder.js +++ b/web/config_builder.js @@ -46,12 +46,12 @@ async function getAvailableModels() { // Look for standard CheckpointLoaderSimple or similar const loaderNode = objectInfo["CheckpointLoaderSimple"] || objectInfo["CheckpointLoader"]; if (loaderNode?.input?.required?.ckpt_name) { - const modelInput = loaderNode.input.required.ckpt_name; - if (Array.isArray(modelInput) && Array.isArray(modelInput[0])) { - availableModels = modelInput[0]; - console.log(`[ConfigBuilder] Found ${availableModels.length} Models`); - return availableModels; - } + const modelInput = loaderNode.input.required.ckpt_name; + if (Array.isArray(modelInput) && Array.isArray(modelInput[0])) { + availableModels = modelInput[0]; + console.log(`[ConfigBuilder] Found ${availableModels.length} Models`); + return availableModels; + } } } catch (e) { console.error("[ConfigBuilder] Error fetching Models:", e); } availableModels = ["None"]; @@ -118,8 +118,8 @@ function parseLoraString(loraStr) { function buildLoraString(name, modelStr, clipStr) { if (!name || name === "None") return "None"; - if (name.endsWith("/")) return name; - if (modelStr === 1.0 && clipStr === 1.0) return name; + // if (name.endsWith("/")) return name; + // if (modelStr === 1.0 && clipStr === 1.0) return name; return `${name}:${modelStr.toFixed(2)}:${clipStr.toFixed(2)}`; } @@ -179,9 +179,9 @@ app.registerExtension({ } else if (existing.lora_config) { this.state = this.migrateOldFormat(existing); } - } catch (e) {} + } catch (e) { } - this.migrateOldFormat = function(oldState) { + this.migrateOldFormat = function (oldState) { const arrays = oldState.lora_config?.arrays || []; return { session_name: oldState.session_name || "my_test_session", @@ -199,7 +199,7 @@ app.registerExtension({ }; }; - this.saveState = function() { + this.saveState = function () { configWidget.value = JSON.stringify(this.state, null, 2); this.updatePreview(); }; @@ -209,20 +209,32 @@ app.registerExtension({ this.addDOMWidget("config_ui", "div", this.htmlContainer, { serialize: false, hideOnZoom: false }); // --- UI RENDER --- - this.renderUI = function() { + // --- UI RENDERING --- + this.renderUI = function () { + // 1. SAVE SCROLL POSITION + const scrollContainer = this.htmlContainer.querySelector(".cb-container"); + const savedScrollTop = scrollContainer ? scrollContainer.scrollTop : 0; + this.htmlContainer.innerHTML = `
`; const root = this.htmlContainer.querySelector("#cb-root"); - - // Top Section + const topRow = document.createElement("div"); topRow.className = "cb-sections-row"; this.renderSessionSection(topRow); - // this.renderOptionsSection(topRow); + this.renderOptionsSection(topRow); root.appendChild(topRow); - // Config Arrays const configSection = document.createElement("div"); configSection.className = "cb-section full-width"; configSection.innerHTML = '
⚙️ Config Arrays
'; - + const headerBar = document.createElement("div"); headerBar.style.marginBottom = "10px"; const addConfigBtn = document.createElement("button"); @@ -282,7 +289,7 @@ app.registerExtension({ }; headerBar.appendChild(addConfigBtn); configSection.appendChild(headerBar); - + const arraysContainer = document.createElement("div"); arraysContainer.className = "cb-arrays-container"; this.state.config_arrays.forEach((configArray, arrayIdx) => { @@ -293,9 +300,15 @@ app.registerExtension({ this.renderPreviewSection(root); this.updatePreview(); - }; - this.createInputGroup = function(labelText, inputElement) { + // 2. RESTORE SCROLL POSITION + const newScrollContainer = this.htmlContainer.querySelector(".cb-container"); + if (newScrollContainer) { + newScrollContainer.scrollTop = savedScrollTop; + } + };; + + this.createInputGroup = function (labelText, inputElement) { const group = document.createElement("div"); group.className = "cb-input-group"; const label = document.createElement("label"); @@ -306,7 +319,7 @@ app.registerExtension({ return group; }; - this.renderSessionSection = function(container) { + this.renderSessionSection = function (container) { const section = document.createElement("div"); section.className = "cb-section"; section.innerHTML = '
📁 Session Management
'; @@ -329,7 +342,7 @@ app.registerExtension({ container.appendChild(section); }; - this.renderOptionsSection = function(container) { + this.renderOptionsSection = function (container) { const section = document.createElement("div"); section.className = "cb-section"; section.innerHTML = '
🎯 Options
'; @@ -344,13 +357,13 @@ app.registerExtension({ toggleInput.onchange = () => { this.state.include_none = toggleInput.checked; this.saveState(); }; toggleLabel.appendChild(toggleInput); toggleLabel.appendChild(document.createTextNode(" Include 'None' in Lists")); - + grid.appendChild(toggleLabel); section.appendChild(grid); container.appendChild(section); }; - this.createConfigArrayElement = function(configArray, arrayIdx) { + this.createConfigArrayElement = function (configArray, arrayIdx) { const div = document.createElement("div"); div.className = "cb-array"; @@ -413,7 +426,7 @@ app.registerExtension({ addModelBtn.style.borderLeft = "4px solid #cc6600"; // Orange accent addModelBtn.textContent = `➕ Add Model`; addModelBtn.onclick = () => { - if(!this.state.config_arrays[arrayIdx].models) this.state.config_arrays[arrayIdx].models = []; + if (!this.state.config_arrays[arrayIdx].models) this.state.config_arrays[arrayIdx].models = []; this.state.config_arrays[arrayIdx].models.push("None"); this.saveState(); this.renderUI(); @@ -483,7 +496,7 @@ app.registerExtension({ }; // Create Model Element - this.createModelElement = function(modelStr, arrayIdx, modelIdx) { + this.createModelElement = function (modelStr, arrayIdx, modelIdx) { const div = document.createElement("div"); div.className = "cb-item-card model-card"; const isFolder = modelStr.endsWith("/"); @@ -541,6 +554,7 @@ app.registerExtension({ return div; }; + // Create LoRA Element // Create LoRA Element this.createLoraElement = function(loraStr, arrayIdx, loraIdx) { const div = document.createElement("div"); @@ -548,6 +562,7 @@ app.registerExtension({ const parsed = parseLoraString(loraStr); const isFolder = parsed.name.endsWith("/"); + // Header const header = document.createElement("div"); header.className = "cb-header-bar"; const label = document.createElement("span"); @@ -568,6 +583,7 @@ app.registerExtension({ header.appendChild(deleteBtn); div.appendChild(header); + // Type Select const typeSelect = document.createElement("select"); typeSelect.className = "cb-select"; typeSelect.innerHTML = ` @@ -581,50 +597,85 @@ app.registerExtension({ }; div.appendChild(typeSelect); + // Name Select const nameSelect = document.createElement("select"); nameSelect.className = "cb-select"; const options = isFolder ? loraFolders : availableLoras; const optionsList = (options.includes(parsed.name) || parsed.name === "None") ? options : [parsed.name, ...options]; nameSelect.innerHTML = optionsList.map(opt => ``).join(''); + + // --- FIX IS HERE --- nameSelect.onchange = () => { - if (isFolder) this.state.config_arrays[arrayIdx].loras[loraIdx] = nameSelect.value; - else this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(nameSelect.value, parsed.model_str, parsed.clip_str); + // ALWAYS use buildLoraString to preserve sliders, even for folders + this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString( + nameSelect.value, + parsed.model_str, + parsed.clip_str + ); this.saveState(); + this.renderUI(); }; div.appendChild(nameSelect); - if (!isFolder && parsed.name !== "None") { - // Sliders logic... - const makeSlider = (label, val, onChange) => { - const c = document.createElement("div"); - c.className = "cb-slider-container"; - const l = document.createElement("span"); - l.style.fontSize = "10px"; l.style.color = "#aaa"; l.textContent = label; - c.appendChild(l); - const s = document.createElement("input"); - s.type = "range"; s.className = "cb-slider"; s.min = "-10"; s.max = "10"; s.step = "0.01"; s.value = val; - const v = document.createElement("span"); - v.className = "cb-slider-value"; v.textContent = val.toFixed(2); - s.oninput = () => v.textContent = parseFloat(s.value).toFixed(2); - s.onchange = () => onChange(parseFloat(s.value)); - c.appendChild(s); c.appendChild(v); - return c; + // Expand Button (Folders only) + if (isFolder && parsed.name !== "None" && parsed.name !== "/") { + const expandBtn = document.createElement("button"); + expandBtn.className = "cb-button primary"; + expandBtn.style.width = "100%"; + expandBtn.style.fontSize = "11px"; + expandBtn.style.marginTop = "4px"; + expandBtn.textContent = "📂 Add all individually"; + expandBtn.onclick = () => { + const folderPrefix = parsed.name; + const matchingLoras = availableLoras.filter(l => l.startsWith(folderPrefix)); + if (matchingLoras.length > 0) { + const newEntries = matchingLoras.map(name => + buildLoraString(name, parsed.model_str, parsed.clip_str) + ); + this.state.config_arrays[arrayIdx].loras.splice(loraIdx, 1, ...newEntries); + this.saveState(); + this.renderUI(); + } else { + alert("No LoRAs found in this folder!"); + } }; - - div.appendChild(makeSlider("M", parsed.model_str, (v) => { - this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(parsed.name, v, parsed.clip_str); - this.saveState(); - })); - div.appendChild(makeSlider("C", parsed.clip_str, (v) => { - this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(parsed.name, parsed.model_str, v); - this.saveState(); - })); + div.appendChild(expandBtn); } - return div; - }; - this.renderPreviewSection = function(root) { + // Sliders (Always Visible) + const makeSlider = (label, val, onChange) => { + const c = document.createElement("div"); + c.className = "cb-slider-container"; + const l = document.createElement("span"); + l.style.fontSize = "10px"; l.style.color = "#aaa"; l.textContent = label; + c.appendChild(l); + const s = document.createElement("input"); + s.type = "range"; s.className = "cb-slider"; s.min = "-10"; s.max = "10"; s.step = "0.01"; s.value = val; + + if (parsed.name === "None") s.disabled = true; + + const v = document.createElement("span"); + v.className = "cb-slider-value"; v.textContent = val.toFixed(2); + s.oninput = () => v.textContent = parseFloat(s.value).toFixed(2); + s.onchange = () => onChange(parseFloat(s.value)); + c.appendChild(s); c.appendChild(v); + return c; + }; + + div.appendChild(makeSlider("M", parsed.model_str, (v) => { + this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(parsed.name, v, parsed.clip_str); + this.saveState(); + })); + div.appendChild(makeSlider("C", parsed.clip_str, (v) => { + this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(parsed.name, parsed.model_str, v); + this.saveState(); + })); + + return div; + };;; + + this.renderPreviewSection = function (root) { const section = document.createElement("div"); section.className = "cb-section full-width"; section.innerHTML = '
👁️ Config Preview (Final Output)
'; @@ -635,18 +686,18 @@ app.registerExtension({ root.appendChild(section); }; - this.updatePreview = function() { + this.updatePreview = function () { const preview = this.htmlContainer.querySelector("#cb-preview"); if (!preview) return; - try { preview.textContent = JSON.stringify(this.generateOutput(), null, 2); } + try { preview.textContent = JSON.stringify(this.generateOutput(), null, 2); } catch (e) { preview.textContent = `Error: ${e.message}`; } }; - this.generateOutput = function() { + this.generateOutput = function () { const configs = []; this.state.config_arrays.forEach(configArray => { const split = (str) => str.split(",").map(s => s.trim()).filter(s => s); - + // Process LoRAs let loras = [...configArray.loras]; const nonNoneLoras = loras.filter(l => l !== "None"); @@ -679,44 +730,44 @@ app.registerExtension({ return configs; }; - this.loadSession = async function(sessionName) { + this.loadSession = async function (sessionName) { // (Session loading logic mostly same, just updating models parsing) // Simplified for brevity - assumes previous robust logic handles this // just ensure we map meta.model to array if single string try { const manifestUrl = `/view?filename=manifest.json&type=output&subfolder=benchmarks/${sessionName}&t=${Date.now()}`; const resp = await fetch(manifestUrl); - if(!resp.ok) return; + if (!resp.ok) return; const manifest = await resp.json(); const meta = manifest.meta || {}; - - if(meta.configs_json) { + + if (meta.configs_json) { const configs = JSON.parse(meta.configs_json); // Simple conversion - this.state.config_arrays = configs.map((c, i) => { - // Handle Model Array conversion - let m = c.model; - if(!Array.isArray(m)) m = m ? [m] : ["None"]; - - // Handle Lora logic (same as before) - let l = Array.isArray(c.lora) ? c.lora : [c.lora || "None"]; - - return { - name: `Loaded Config ${i+1}`, + this.state.config_arrays = configs.map((c, i) => { + // Handle Model Array conversion + let m = c.model; + if (!Array.isArray(m)) m = m ? [m] : ["None"]; + + // Handle Lora logic (same as before) + let l = Array.isArray(c.lora) ? c.lora : [c.lora || "None"]; + + return { + name: `Loaded Config ${i + 1}`, samplers: Array.isArray(c.sampler) ? c.sampler.join(", ") : c.sampler, schedulers: Array.isArray(c.scheduler) ? c.scheduler.join(", ") : c.scheduler, steps: Array.isArray(c.steps) ? c.steps.join(", ") : c.steps, cfg: Array.isArray(c.cfg) ? c.cfg.join(", ") : c.cfg, models: m, loras: l, - combine: false - }; - }); + combine: false + }; + }); } this.state.session_name = sessionName; this.saveState(); this.renderUI(); - } catch(e) { console.error(e); } + } catch (e) { console.error(e); } }; this.renderUI();