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: {},