From f7ec7cca236cdc2fde2fb04a8ee007b30cee7dac Mon Sep 17 00:00:00 2001 From: JasonHoku Date: Sat, 14 Feb 2026 23:36:38 -0800 Subject: [PATCH] implement vae selection in UI & vae iteration tools --- config_builder_node.py | 12 +- config_utils.py | 6 +- generation_orchestrator.py | 64 ++++- model_loader.py | 18 ++ .../conf-builder-config-management.js | 224 +++++++++++++++++- web/conf_builder/conf-builder-main.js | 8 + web/conf_builder/conf-builder-utilities.js | 61 ++++- 7 files changed, 375 insertions(+), 18 deletions(-) diff --git a/config_builder_node.py b/config_builder_node.py index e40950f..d5dcbd3 100644 --- a/config_builder_node.py +++ b/config_builder_node.py @@ -545,6 +545,12 @@ class UltimateConfigBuilder: if seed_behavior == "randomize": config["seed_behavior"] = "randomize" + # Process VAEs + vaes_raw = config_array.get("vaes", ["None"]) + vae_strings = [str(v) for v in vaes_raw if v and v != "None"] + if vae_strings: + config["vae"] = vae_strings if len(vae_strings) > 1 else vae_strings[0] + # Add model_type and related fields for non-checkpoint models if model_type != "checkpoint": config["model_type"] = model_type @@ -759,6 +765,9 @@ async def get_model_lists_endpoint(request): "hunyuan_image", "hunyuan_video_15" ] + # VAE list + vae_list = folder_paths.get_filename_list("vae") + return web.json_response({ "checkpoints": checkpoints, "diffusion_models": diffusion_models, @@ -766,7 +775,8 @@ async def get_model_lists_endpoint(request): "text_encoders": text_encoders, "clip_gguf": clip_gguf, "clip_types": clip_types, - "dual_clip_types": dual_clip_types + "dual_clip_types": dual_clip_types, + "vae": vae_list }) except Exception as e: print(f"[ConfigBuilder] Error in model_lists endpoint: {e}") diff --git a/config_utils.py b/config_utils.py index b6b78d7..02f4eb8 100644 --- a/config_utils.py +++ b/config_utils.py @@ -358,6 +358,9 @@ def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed, else: expanded_models.extend(get_files_from_folder(m, model_folder)) + # Expand VAE list + raw_vaes = to_list(entry.get("vae", "Default")) + # Expand LoRA stacks (strengths are now in the lora string itself) expanded_loras = expand_lora_stack(entry.get("lora", "None")) @@ -374,7 +377,7 @@ def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed, # Build all combinations base_combos = [] for combo in itertools.product(samplers, schedulers, steps_l, cfgs, clip_skips, expanded_loras, - denoise_values, entry_prompt_pairs, expanded_models): + denoise_values, entry_prompt_pairs, expanded_models, raw_vaes): base_combos.append({ "sampler": combo[0], "scheduler": combo[1], @@ -386,6 +389,7 @@ def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed, "positive": combo[7][0], "negative": combo[7][1], "model": combo[8], + "vae": combo[9], "seed": seed, "seed_behavior": entry.get("seed_behavior", "fixed"), "model_type": model_type, diff --git a/generation_orchestrator.py b/generation_orchestrator.py index de8b530..996cda8 100644 --- a/generation_orchestrator.py +++ b/generation_orchestrator.py @@ -19,7 +19,8 @@ from .manifest_utils import load_existing_manifest, save_manifest from .model_loader import ( load_checkpoint, load_loras, cleanup_model_references, get_latent_channels, load_loras_for_preencoding, - print_incompatible_loras_summary, load_diffusion_model_and_clip + print_incompatible_loras_summary, load_diffusion_model_and_clip, + load_vae_by_name ) from .lora_utils import expand_lora_folder from .image_generation import ( @@ -219,7 +220,7 @@ def run_generation_loop( extra_seeds = [random.randint(0, 2**32 - 1) for _ in range(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: (x.get('model_type', 'checkpoint'), x['model'], tuple(x.get('text_encoders', [])), x['lora'], x['positive'], x['negative'])) + expanded.sort(key=lambda x: (x.get('model_type', 'checkpoint'), x['model'], tuple(x.get('text_encoders', [])), x.get('vae', 'Default'), x['lora'], x['positive'], x['negative'])) # ==== EXPAND LORA FOLDERS ==== print(f"[GridTester] 🎲 Expanding LoRA folders and random selections...") @@ -245,13 +246,20 @@ def run_generation_loop( print(f"[GridTester] ✅ LoRA expansion complete") # ==== VAE VALIDATION FOR NON-CHECKPOINT MODELS ==== - has_non_checkpoint = any(c.get("model_type", "checkpoint") != "checkpoint" for c in expanded) - if has_non_checkpoint and not use_remote_vae and optional_vae is None: + # Only error if non-checkpoint model has no VAE source (no optional_vae AND no per-config VAE) + use_remote_vae = remote_vae_endpoint and remote_vae_endpoint != "None" + needs_vae = any( + c.get("model_type", "checkpoint") != "checkpoint" + and c.get("vae", "Default") == "Default" + for c in expanded + ) + if needs_vae and not use_remote_vae and optional_vae is None: raise ValueError( "GGUF and diffusion models do not include a bundled VAE.\n" "You must either:\n" " 1. Connect a VAE to the optional_vae input on the sampler node, or\n" - " 2. Enable remote VAE decoding via remote_vae_endpoint\n" + " 2. Enable remote VAE decoding via remote_vae_endpoint, or\n" + " 3. Set a specific VAE in the config builder for those configs\n" ) # ==== REGISTER SMART CACHE SCHEDULE ==== @@ -273,9 +281,8 @@ def run_generation_loop( else: pos_hash, neg_hash = None, None - # ==== REMOTE VAE SETUP ==== - use_remote_vae = remote_vae_endpoint and remote_vae_endpoint != "None" - + # ==== REMOTE VAE SETUP (use_remote_vae already defined above for VAE validation) ==== + try: if PromptServer is not None: pbar = PromptServer.instance.progress_bar_pool.get_progress_bar(unique_id) @@ -290,6 +297,8 @@ def run_generation_loop( cached_model_key = None cached_lora_key = None cached_lora_cache_key = None + cached_vae_key = None # Track which VAE is currently loaded + default_model_vae = None # Track the model's bundled/default VAE for reverting conditioning_cache = {"positive": {}, "negative": {}} incompatible_loras = {} pending_batch = [] @@ -325,9 +334,21 @@ def run_generation_loop( optional_model, optional_clip, optional_vae, optional_positive, optional_negative, None, None, model_cache=model_cache ) + # VAE fallback for non-checkpoint models if loaded_vae is None and optional_vae is not None: loaded_vae = optional_vae + # Remember this model's default VAE for reverting later + default_model_vae = loaded_vae + + # Per-config VAE: if first config specifies a VAE, load it + target_vae = first_conf.get("vae", "Default") + if target_vae != "Default": + print(f"[GridTester] 🎨 Loading per-config VAE: {target_vae}") + loaded_vae = load_vae_by_name(target_vae) + cached_vae_key = target_vae + else: + cached_vae_key = "Default" if first_conf["lora_expanded"] != "None": patched_model, patched_clip = load_loras_for_preencoding( @@ -363,7 +384,7 @@ def run_generation_loop( cached_lora_cache_key = None latent_channels = get_latent_channels(loaded_model, optional_latent) elif len(unique_model_keys) > 1: - print(f"[GridTester] âš ī¸ Multiple models detected ({len(unique_models)} different models) - pre-encoding DISABLED") + print(f"[GridTester] âš ī¸ Multiple models detected ({len(unique_model_keys)} different models) - pre-encoding DISABLED") cond_cache = None latent_channels = 4 else: @@ -492,6 +513,9 @@ def run_generation_loop( # VAE fallback for non-checkpoint models if loaded_vae is None and optional_vae is not None: loaded_vae = optional_vae + # Remember this model's default VAE for reverting later + default_model_vae = loaded_vae + cached_vae_key = "Default" cached_model_key = target_model_key cached_lora_key = None @@ -500,7 +524,27 @@ def run_generation_loop( model_switched = True else: model_switched = False - + + # ==== PER-CONFIG VAE SWITCHING ==== + target_vae = conf.get("vae", "Default") + if target_vae != cached_vae_key: + # Flush pending batch before switching VAE (they need current VAE for decoding) + if pending_batch: + if use_remote_vae: + flush_batch_with_remote_vae(pending_batch, remote_vae_worker, existing_data, session_name) + else: + flush_batch_with_vae(pending_batch, loaded_vae, paths["images"], existing_data, session_name, paths["manifest"], unique_id) + pending_batch = [] + + if target_vae == "Default": + # Revert to model's bundled/default VAE + loaded_vae = default_model_vae + print(f"[GridTester] 🎨 Reverting to Default VAE") + else: + print(f"[GridTester] 🎨 Loading per-config VAE: {target_vae}") + loaded_vae = load_vae_by_name(target_vae) + cached_vae_key = target_vae + # ==== LORA SWITCHING ==== current_lora_string = conf["lora_expanded"] diff --git a/model_loader.py b/model_loader.py index 046cfe7..0c2e9bf 100644 --- a/model_loader.py +++ b/model_loader.py @@ -14,6 +14,24 @@ from .lora_utils import LoRAFileNotFoundError from .config_utils import parse_lora_definition +def load_vae_by_name(vae_name): + """ + Load a VAE by filename from the vae folder. + Follows the pattern of ComfyUI's VAELoader.load_vae(). + + Args: + vae_name: Filename of the VAE model + + Returns: + comfy.sd.VAE: Loaded VAE instance + """ + vae_path = folder_paths.get_full_path_or_raise("vae", vae_name) + sd, metadata = comfy.utils.load_torch_file(vae_path, return_metadata=True) + vae = comfy.sd.VAE(sd=sd, metadata=metadata) + vae.throw_exception_if_invalid() + return vae + + def load_checkpoint( target_model_name, ckpt_name, diff --git a/web/conf_builder/conf-builder-config-management.js b/web/conf_builder/conf-builder-config-management.js index 2855d97..c040f7c 100644 --- a/web/conf_builder/conf-builder-config-management.js +++ b/web/conf_builder/conf-builder-config-management.js @@ -11,7 +11,9 @@ import { getIterationCount, convertStateToConfigs, countPromptCombinations, - expandPromptPreview + expandPromptPreview, + getAvailableVAEs, + getVAEFolders } from './conf-builder-utilities.js'; import { @@ -199,6 +201,18 @@ export function createConfigArrayElement(node, configArray, arrayIdx) { }; controlsBar.appendChild(addModelBtn); + const addVaeBtn = document.createElement("button"); + addVaeBtn.className = "cb-button"; + addVaeBtn.style.borderLeft = "4px solid #9900cc"; + addVaeBtn.textContent = `➕ Add VAE`; + addVaeBtn.onclick = () => { + if (!node.state.config_arrays[arrayIdx].vaes) node.state.config_arrays[arrayIdx].vaes = []; + node.state.config_arrays[arrayIdx].vaes.push("None"); + node.saveState(); + node.renderUI(); + }; + controlsBar.appendChild(addVaeBtn); + const addLoraBtn = document.createElement("button"); addLoraBtn.className = "cb-button"; addLoraBtn.style.borderLeft = "4px solid #0066cc"; @@ -1224,6 +1238,212 @@ function renderGGUFOptionsSection(node, container, configArray, arrayIdx) { container.appendChild(section); } +// --- VAE SECTION RENDERER --- + +export function renderVAEsSection(node, div, configArray, arrayIdx) { + if (!configArray.vaes || configArray.vaes.length === 0) return; // Don't show section if no VAEs added + + // Filter out "only None" case — if vaes is ["None"], don't show section unless explicitly added + const hasRealVAEs = configArray.vaes.some(v => v && v !== "None"); + if (!hasRealVAEs && configArray.vaes.length <= 1) return; + + const vaeList = getAvailableVAEs(); + const vFolders = getVAEFolders(); + + const isSectionCollapsed = node.uiState.vaesSectionCollapsed?.[arrayIdx] || false; + + const vaeGrid = document.createElement("div"); + vaeGrid.className = "cb-list-grid"; + + const vaeHeader = document.createElement("div"); + vaeHeader.className = "cb-section-toggle"; + vaeHeader.style.cssText = "padding: 8px; background: #3a3a3a; border-radius: 4px; margin-bottom: 8px; font-weight: bold; color: #9900cc;"; + + const activeVaes = configArray.vaes.filter(v => v && v !== "None").length; + const titleSpan = document.createElement("span"); + titleSpan.textContent = `VAEs (${configArray.vaes.length} Entries, ${activeVaes} Active)`; + vaeHeader.appendChild(titleSpan); + + const arrowSpan = document.createElement("span"); + arrowSpan.textContent = isSectionCollapsed ? "â–ļ" : "â–ŧ"; + vaeHeader.appendChild(arrowSpan); + + vaeGrid.appendChild(vaeHeader); + + const contentContainer = document.createElement("div"); + contentContainer.style.display = isSectionCollapsed ? "none" : "contents"; + + vaeHeader.onclick = () => { + const isNowCollapsed = contentContainer.style.display === "none"; + if (isNowCollapsed) { + contentContainer.style.display = "contents"; + arrowSpan.textContent = "â–ŧ"; + if (!node.uiState.vaesSectionCollapsed) node.uiState.vaesSectionCollapsed = {}; + node.uiState.vaesSectionCollapsed[arrayIdx] = false; + } else { + contentContainer.style.display = "none"; + arrowSpan.textContent = "â–ļ"; + if (!node.uiState.vaesSectionCollapsed) node.uiState.vaesSectionCollapsed = {}; + node.uiState.vaesSectionCollapsed[arrayIdx] = true; + } + }; + + configArray.vaes.forEach((vae, vaeIdx) => { + contentContainer.appendChild(createVAEElement(node, vae, arrayIdx, vaeIdx, vaeList, vFolders)); + }); + + const addRow = document.createElement("div"); + addRow.style.width = "100%"; + addRow.style.padding = "4px 0"; + const addBtn = document.createElement("button"); + addBtn.className = "cb-button"; + addBtn.style.cssText = "width: 100%; border: 1px dashed #555; background: rgba(0,0,0,0.2); color: #aaa;"; + addBtn.textContent = "➕ Add New VAE"; + addBtn.onmouseover = () => addBtn.style.background = "rgba(255,255,255,0.1)"; + addBtn.onmouseout = () => addBtn.style.background = "rgba(0,0,0,0.2)"; + addBtn.onclick = () => { + node.state.config_arrays[arrayIdx].vaes.push("None"); + node.saveState(); + node.renderUI(); + }; + addRow.appendChild(addBtn); + contentContainer.appendChild(addRow); + + vaeGrid.appendChild(contentContainer); + div.appendChild(vaeGrid); +} + +// --- VAE ELEMENT CREATOR --- + +function createVAEElement(node, vaeName, arrayIdx, vaeIdx, vaeList, vFolders) { + const isFolder = vaeName && vaeName.endsWith("/"); + + const div = document.createElement("div"); + div.className = "cb-item-card"; + div.style.borderLeft = "3px solid #9900cc"; + const uid = `vae_${arrayIdx}_${vaeIdx}`; + + const isCollapsed = node.uiState.vaesCollapsed?.[uid] || false; + + // Header + const header = document.createElement("div"); + header.className = "cb-header-bar"; + + const leftGroup = document.createElement("div"); + leftGroup.className = "cb-header-left"; + + const toggleArrow = document.createElement("span"); + toggleArrow.textContent = isCollapsed ? "â–ļ" : "â–ŧ"; + toggleArrow.style.color = "#aaa"; + toggleArrow.style.fontSize = "10px"; + toggleArrow.style.width = "12px"; + leftGroup.appendChild(toggleArrow); + + const label = document.createElement("span"); + label.textContent = `VAE #${vaeIdx + 1}`; + label.style.color = "#9900cc"; + label.style.fontSize = "10px"; + label.style.marginRight = "6px"; + leftGroup.appendChild(label); + + const nameSpan = document.createElement("span"); + nameSpan.className = "cb-header-name"; + nameSpan.textContent = getShortName(vaeName || "None"); + leftGroup.appendChild(nameSpan); + + header.appendChild(leftGroup); + + const deleteBtn = document.createElement("button"); + deleteBtn.className = "cb-button danger"; + deleteBtn.style.padding = "2px 6px"; + deleteBtn.style.fontSize = "10px"; + deleteBtn.textContent = "✖"; + deleteBtn.onclick = () => { + node.state.config_arrays[arrayIdx].vaes.splice(vaeIdx, 1); + if (node.state.config_arrays[arrayIdx].vaes.length === 0) { + node.state.config_arrays[arrayIdx].vaes = ["None"]; + } + node.saveState(); + node.renderUI(); + }; + header.appendChild(deleteBtn); + div.appendChild(header); + + // Content container + const contentDiv = document.createElement("div"); + contentDiv.style.display = isCollapsed ? "none" : "flex"; + contentDiv.style.flexDirection = "column"; + contentDiv.style.gap = "6px"; + contentDiv.style.width = "100%"; + + header.onclick = (e) => { + if (e.target.tagName === 'BUTTON') return; + const isNowCollapsed = contentDiv.style.display !== "none"; + contentDiv.style.display = isNowCollapsed ? "none" : "flex"; + toggleArrow.textContent = isNowCollapsed ? "â–ļ" : "â–ŧ"; + if (!node.uiState.vaesCollapsed) node.uiState.vaesCollapsed = {}; + node.uiState.vaesCollapsed[uid] = isNowCollapsed; + }; + + // File/Folder Type Select + const typeSelect = document.createElement("select"); + typeSelect.className = "cb-select"; + typeSelect.innerHTML = ` + + + `; + typeSelect.onchange = () => { + const newVal = typeSelect.value === "folder" ? "/" : "None"; + node.state.config_arrays[arrayIdx].vaes[vaeIdx] = newVal; + node.saveState(); + node.renderUI(); + }; + contentDiv.appendChild(typeSelect); + + // Searchable Select for VAE + const options = isFolder ? vFolders : vaeList; + const currentVal = vaeName || "None"; + const optionsList = (options && options.includes(currentVal)) || currentVal === "None" || currentVal === "/" + ? options || ["None"] + : [currentVal, ...(options || ["None"])]; + + const nameSearchable = createSearchableSelect( + optionsList, + currentVal, + (value) => { + node.state.config_arrays[arrayIdx].vaes[vaeIdx] = normalizePath(value); + node.saveState(); + node.renderUI(); + }, + isFolder ? "Search folders..." : "Search VAEs..." + ); + contentDiv.appendChild(nameSearchable); + + // Folder expand button + if (isFolder && vaeName !== "None" && vaeName !== "/") { + const expandBtn = document.createElement("button"); + expandBtn.className = "cb-button"; + expandBtn.style.cssText = "width: 100%; border-left: 3px solid #9900cc; font-size: 11px; margin-top: 4px;"; + expandBtn.textContent = "📂 Add all individually"; + expandBtn.onclick = () => { + const normalize = (str) => str.replace(/\\/g, "/"); + const folderPrefix = normalize(vaeName); + const matchingVAEs = vaeList ? vaeList.filter(v => normalize(v).startsWith(folderPrefix)) : []; + if (matchingVAEs.length > 0) { + node.state.config_arrays[arrayIdx].vaes.splice(vaeIdx, 1, ...matchingVAEs); + node.saveState(); + node.renderUI(); + } else { + alert(`No VAEs found in folder: ${folderPrefix}`); + } + }; + contentDiv.appendChild(expandBtn); + } + + div.appendChild(contentDiv); + return div; +} + export function renderLorasSection(node, div, configArray, arrayIdx, availableLoras, loraFolders) { if (!configArray.loras || configArray.loras.length === 0) configArray.loras = ["None"]; @@ -2075,6 +2295,7 @@ export async function renderUI(node, availableLoras, modelLists, loraFolders, av cfg: "7.0", seed_behavior: "fixed", models: ["None"], + vaes: ["None"], text_encoders: [], clip_type: "stable_diffusion", gguf_options: {}, @@ -2115,6 +2336,7 @@ export async function renderUI(node, availableLoras, modelLists, loraFolders, av const arrayElement = createConfigArrayElement(node, configArray, arrayIdx); renderConfigPromptsSection(node, arrayElement, configArray, arrayIdx); renderModelsSection(node, arrayElement, configArray, arrayIdx, modelLists); + renderVAEsSection(node, arrayElement, configArray, arrayIdx); renderLorasSection(node, arrayElement, configArray, arrayIdx, availableLoras, loraFolders); arraysContainer.appendChild(arrayElement); }); diff --git a/web/conf_builder/conf-builder-main.js b/web/conf_builder/conf-builder-main.js index c148bc9..92511eb 100644 --- a/web/conf_builder/conf-builder-main.js +++ b/web/conf_builder/conf-builder-main.js @@ -65,8 +65,10 @@ app.registerExtension({ this.uiState = { modelsSectionCollapsed: {}, lorasSectionCollapsed: {}, + vaesSectionCollapsed: {}, modelsCollapsed: {}, lorasCollapsed: {}, + vaesCollapsed: {}, promptsSectionCollapsed: {}, globalPromptsSectionCollapsed: false }; @@ -87,6 +89,7 @@ app.registerExtension({ steps: "20, 30", cfg: "7.0", models: ["None"], + vaes: ["None"], text_encoders: [], clip_type: "stable_diffusion", gguf_options: {}, @@ -262,6 +265,7 @@ app.registerExtension({ if (!arr.text_encoders) arr.text_encoders = []; if (!arr.clip_type) arr.clip_type = "stable_diffusion"; if (!arr.gguf_options) arr.gguf_options = {}; + if (!arr.vaes) arr.vaes = ["None"]; }); this.state.config_arrays = loadedArrays; @@ -296,6 +300,7 @@ app.registerExtension({ steps: oldState.steps || "20", cfg: oldState.cfg || "7.0", models: oldState.model ? [utilities.normalizePath(oldState.model)] : ["None"], + vaes: ["None"], text_encoders: [], clip_type: "stable_diffusion", gguf_options: {}, @@ -367,6 +372,9 @@ app.registerExtension({ if (!arr.text_encoders) arr.text_encoders = []; if (!arr.clip_type) arr.clip_type = "stable_diffusion"; if (!arr.gguf_options) arr.gguf_options = {}; + + // Migration: ensure VAE field exists + if (!arr.vaes) arr.vaes = ["None"]; }); } else if (existing.lora_config) { this.state = this.migrateOldFormat(existing); diff --git a/web/conf_builder/conf-builder-utilities.js b/web/conf_builder/conf-builder-utilities.js index 7a955a1..1da3303 100644 --- a/web/conf_builder/conf-builder-utilities.js +++ b/web/conf_builder/conf-builder-utilities.js @@ -14,6 +14,8 @@ let availableGGUFModels = null; let ggufModelFolders = null; let availableTextEncoders = null; let textEncoderFolders = null; +let availableVAEs = null; +let vaeFolders = null; let clipTypes = []; let dualClipTypes = []; let availableSessions = ["None"]; @@ -36,6 +38,8 @@ export function clearAllCaches() { ggufModelFolders = null; availableTextEncoders = null; textEncoderFolders = null; + availableVAEs = null; + vaeFolders = null; clipTypes = []; dualClipTypes = []; } @@ -175,9 +179,19 @@ export async function getModelLists() { clipTypes = data.clip_types || []; dualClipTypes = data.dual_clip_types || []; - console.log(`[ConfigBuilder] Model lists: ${availableModels?.length || 0} checkpoints, ` + - `${availableDiffusionModels.length} diffusion, ${availableGGUFModels.length} GGUF, ` + - `${availableTextEncoders.length} text encoders`); + // VAE list + availableVAEs = (data.vae || []).map(normalizePath); + vaeFolders = extractFolders(availableVAEs); + + console.log(`[ConfigBuilder] Model lists loaded: ${availableModels?.length || 0} checkpoints, ` + + `${availableDiffusionModels.length} diffusion models, ${availableGGUFModels.length} GGUFs, ` + + `${availableTextEncoders.length} text encoders, ${availableVAEs.length} VAEs`); + if (availableDiffusionModels.length === 0) { + console.log(`[ConfigBuilder] â„šī¸ No diffusion models found. Place .safetensors files in ComfyUI/models/unet/ or ComfyUI/models/diffusion_models/`); + } + if (availableGGUFModels.length === 0) { + console.log(`[ConfigBuilder] â„šī¸ No GGUF models found. Install ComfyUI-GGUF and place .gguf files in the unet_gguf folder.`); + } return data; } catch (e) { console.error("[ConfigBuilder] Error fetching model lists:", e); @@ -191,6 +205,8 @@ export function getAvailableGGUFModels() { return availableGGUFModels || []; } export function getGGUFModelFolders() { return ggufModelFolders || ["/"]; } export function getAvailableTextEncoders() { return availableTextEncoders || []; } export function getTextEncoderFolders() { return textEncoderFolders || ["/"]; } +export function getAvailableVAEs() { return availableVAEs || []; } +export function getVAEFolders() { return vaeFolders || ["/"]; } export function getClipTypes() { return clipTypes; } export function getDualClipTypes() { return dualClipTypes; } @@ -352,13 +368,28 @@ export function getIterationCount(configArray) { } if (l_count === 0) l_count = 1; - // 4. Prompts (per-config custom prompts multiply the iteration count) + // 4. VAEs + let v_count = 0; + if (!configArray.vaes || configArray.vaes.length === 0) { + v_count = 1; // Default VAE + } else { + configArray.vaes.forEach(v => { + if (!v || v === "None") { + v_count += 1; // "None" = use Default + } else { + v_count += 1; + } + }); + } + if (v_count === 0) v_count = 1; + + // 5. Prompts (per-config custom prompts multiply the iteration count) let p_count = 1; if (configArray.use_custom_prompts && configArray.positive_prompt_groups && configArray.positive_prompt_groups.length > 0) { p_count = countPromptCombinations(configArray.positive_prompt_groups); } - return m_count * l_count * s_count * sch_count * st_count * c_count * p_count; + return m_count * l_count * v_count * s_count * sch_count * st_count * c_count * p_count; } // --- CONFIG CONVERSION --- @@ -400,6 +431,9 @@ export function convertStateToConfigs(state) { } }); + // Process VAEs + const vaes = (configArray.vaes || []).filter(v => v && v !== "None"); + const config = { sampler: split(configArray.samplers), scheduler: split(configArray.schedulers), @@ -409,6 +443,11 @@ export function convertStateToConfigs(state) { model: finalModels.length > 1 ? finalModels : finalModels[0] || "None" }; + // Add VAE if any are selected (not "None") + if (vaes.length > 0) { + config.vae = vaes.length > 1 ? vaes : vaes[0]; + } + // Add model_type and related fields for non-checkpoint models if (modelType !== "checkpoint") { config.model_type = modelType; @@ -563,6 +602,16 @@ export function convertConfigsToConfigArrays(configs) { strengthLock = { ...config.lora_strength_lock }; } + // Parse VAE + let vaes = ["None"]; + if (config.vae) { + if (Array.isArray(config.vae)) { + vaes = config.vae.map(v => normalizePath(String(v))); + } else { + vaes = [normalizePath(String(config.vae))]; + } + } + // Parse prompt fields from loaded config let positivePromptGroups = []; let negativePrompt = ""; @@ -602,6 +651,7 @@ export function convertConfigsToConfigArrays(configs) { cfg: toString(config.cfg || "7.0"), seed_behavior: config.seed_behavior || "fixed", models: models, + vaes: vaes, text_encoders: config.text_encoders || [], clip_type: config.clip_type || "stable_diffusion", gguf_options: config.gguf_options || {}, @@ -625,6 +675,7 @@ export function convertConfigsToConfigArrays(configs) { cfg: "7.0", seed_behavior: "fixed", models: ["None"], + vaes: ["None"], text_encoders: [], clip_type: "stable_diffusion", gguf_options: {},