Merge branch 'claude/hungry-gagarin'

This commit is contained in:
JasonHoku
2026-02-14 23:13:32 -08:00
12 changed files with 900 additions and 193 deletions
+2 -1
View File
@@ -2,7 +2,8 @@
"permissions": {
"allow": [
"Bash(git config --global --add safe.directory '%\\(prefix\\)///DESKTOP-2GIRPC5/comfy/comfy_v0.12.3/ComfyUI/custom_nodes/ComfyUI-Ultimate-Auto-Sampler-Config-Grid-Testing-Suite/.claude/worktrees/hungry-gagarin')",
"Bash(python -c:*)"
"Bash(python -c:*)",
"WebFetch(domain:api.comfy.org)"
]
}
}
-64
View File
@@ -5,7 +5,6 @@ from aiohttp import web
import json
import os
import urllib.parse
import mimetypes
import folder_paths
import shutil
from .sampler_node import SamplerGridTester
@@ -24,69 +23,6 @@ _whitelisted_directories = set()
CONFIGS_DIR = os.path.join(folder_paths.get_output_directory(), "ultimate-configs")
os.makedirs(CONFIGS_DIR, exist_ok=True)
# --- CONFIG BUILDER JS DIRECTORY ---
EXTENSION_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_BUILDER_JS_DIR = os.path.join(EXTENSION_DIR, "js")
# =============================================================================
# SERVE CONFIG BUILDER JS FILES (NOT AUTO-LOADED)
# =============================================================================
@server.PromptServer.instance.routes.get("/ultimate_config_sampler/js/{filename:.*}")
async def serve_config_builder_js(request):
r"""
Serve JavaScript files from /js/ directory with cache-busting headers.
Supports subdirectories with both / and \ (e.g., config-builder/utilities.js)
"""
try:
filename = request.match_info['filename']
print(f"[ConfigBuilder] Request for: {filename}")
# Security: prevent directory traversal with ..
if '..' in filename or filename.startswith('/') or filename.startswith('\\'):
print(f"[ConfigBuilder] FORBIDDEN: {filename}")
return web.Response(status=403, text="Forbidden")
# Normalize path separators (convert backslashes to forward slashes)
filename = filename.replace('\\', '/')
# Build full path and normalize it
file_path = os.path.normpath(os.path.join(CONFIG_BUILDER_JS_DIR, filename))
# Double-check the resolved path is still within our JS directory
normalized_base = os.path.normpath(CONFIG_BUILDER_JS_DIR)
if not file_path.startswith(normalized_base):
print(f"[ConfigBuilder] FORBIDDEN - Outside base dir: {file_path}")
return web.Response(status=403, text="Forbidden - path outside base directory")
if not os.path.exists(file_path):
print(f"[ConfigBuilder] NOT FOUND: {file_path}")
return web.Response(status=404, text=f"File not found: {filename}")
print(f"[ConfigBuilder] Reading file: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
print(f"[ConfigBuilder] File read successfully, {len(content)} bytes")
# Return with cache-busting headers
return web.Response(
text=content,
content_type='application/javascript',
headers={
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
)
except Exception as e:
print(f"[ConfigBuilder] ERROR: {e}")
import traceback
traceback.print_exc()
return web.Response(status=500, text=f"Server error: {str(e)}")
# =============================================================================
# API: CONFIG MANAGEMENT
+73 -4
View File
@@ -510,12 +510,21 @@ class UltimateConfigBuilder:
scheduler_list = self.parse_comma_list(config_array.get("schedulers", "normal"))
steps_list = self.parse_int_list(config_array.get("steps", "20"))
cfg_list = self.parse_number_list(config_array.get("cfg", "7.0"))
models = config_array.get("models", ["None"])
models_raw = config_array.get("models", ["None"])
omit_triggers = config_array.get("lora_omit_triggers", [])
lora_triggerwords_append_settings = config_array.get("lora_triggerwords_append_settings", {})
# Process models
model_strings = [str(m) for m in models if m and m != "None"]
# Process models - handle both object format {path, type} and legacy string format
model_strings = []
model_type = "checkpoint" # default
for m in models_raw:
if isinstance(m, dict):
path = m.get("path", "")
if path and path != "None":
model_strings.append(str(path))
model_type = m.get("type", "checkpoint")
elif isinstance(m, str) and m and m != "None":
model_strings.append(str(m))
# Process loras for this config
lora_strings = self.process_lora_array(config_array, actual_include_none)
@@ -536,6 +545,20 @@ class UltimateConfigBuilder:
if seed_behavior == "randomize":
config["seed_behavior"] = "randomize"
# Add model_type and related fields for non-checkpoint models
if model_type != "checkpoint":
config["model_type"] = model_type
text_encoders = config_array.get("text_encoders", [])
if text_encoders:
config["text_encoders"] = [te for te in text_encoders if te and te != "None"]
clip_type = config_array.get("clip_type", "")
if clip_type:
config["clip_type"] = clip_type
if model_type == "gguf":
gguf_options = config_array.get("gguf_options", {})
if gguf_options:
config["gguf_options"] = gguf_options
# Add omit triggers if present
if omit_triggers:
config["lora_omit_triggers"] = omit_triggers
@@ -706,6 +729,52 @@ async def lookup_lora_metadata_endpoint(request):
}, status=500)
# API endpoint to get all model lists for unified model selector
@server.PromptServer.instance.routes.get("/configbuilder/model_lists")
async def get_model_lists_endpoint(request):
"""Return all model lists for the config builder's unified model selector."""
try:
checkpoints = folder_paths.get_filename_list("checkpoints")
diffusion_models = folder_paths.get_filename_list("diffusion_models")
text_encoders = folder_paths.get_filename_list("text_encoders")
# GGUF lists - may not exist if ComfyUI-GGUF is not installed
try:
unet_gguf = folder_paths.get_filename_list("unet_gguf")
except (KeyError, Exception):
unet_gguf = []
try:
clip_gguf = folder_paths.get_filename_list("clip_gguf")
except (KeyError, Exception):
clip_gguf = []
# CLIPType options (matching ComfyUI's CLIPLoader and DualCLIPLoader)
clip_types = [
"stable_diffusion", "stable_cascade", "sd3", "stable_audio",
"mochi", "ltxv", "pixart", "cosmos", "lumina2", "wan",
"hidream", "chroma", "ace", "flux"
]
dual_clip_types = [
"sdxl", "sd3", "flux", "hunyuan_video", "hidream",
"hunyuan_image", "hunyuan_video_15"
]
return web.json_response({
"checkpoints": checkpoints,
"diffusion_models": diffusion_models,
"unet_gguf": unet_gguf,
"text_encoders": text_encoders,
"clip_gguf": clip_gguf,
"clip_types": clip_types,
"dual_clip_types": dual_clip_types
})
except Exception as e:
print(f"[ConfigBuilder] Error in model_lists endpoint: {e}")
import traceback
traceback.print_exc()
return web.json_response({"error": str(e)}, status=500)
# API endpoint to refresh model/lora lists (triggered by "Update Node Definitions")
@server.PromptServer.instance.routes.post("/configbuilder/refresh_models")
async def refresh_models_endpoint(request):
+19 -2
View File
@@ -332,9 +332,22 @@ def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed,
cfgs = to_list(entry.get("cfg", 7.0))
clip_skips = to_list(entry.get("clip_skip", 0))
# Expand model folders
# Extract model type and related fields for non-checkpoint models
model_type = entry.get("model_type", "checkpoint")
clip_type_str = entry.get("clip_type", "stable_diffusion")
text_encoders = entry.get("text_encoders", [])
gguf_options = entry.get("gguf_options", {})
# Expand model folders (using correct folder type based on model_type)
raw_models = to_list(entry.get("model", "Default"))
expanded_models = []
if model_type == "gguf":
model_folder = "unet_gguf"
elif model_type == "diffusion_model":
model_folder = "diffusion_models"
else:
model_folder = "checkpoints"
for m in raw_models:
if m == "Default":
# Use the checkpoint name from node input instead of "Default"
@@ -343,7 +356,7 @@ def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed,
else:
expanded_models.append("Default")
else:
expanded_models.extend(get_files_from_folder(m, "checkpoints"))
expanded_models.extend(get_files_from_folder(m, model_folder))
# Expand LoRA stacks (strengths are now in the lora string itself)
expanded_loras = expand_lora_stack(entry.get("lora", "None"))
@@ -375,6 +388,10 @@ def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed,
"model": combo[8],
"seed": seed,
"seed_behavior": entry.get("seed_behavior", "fixed"),
"model_type": model_type,
"clip_type": clip_type_str,
"text_encoders": list(text_encoders),
"gguf_options": dict(gguf_options) if gguf_options else {},
"lora_omit_triggers": list(lora_omit_triggers),
"lora_triggerwords_append_settings": dict(lora_triggerwords_append_settings)
})
+125 -51
View File
@@ -19,7 +19,7 @@ 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
print_incompatible_loras_summary, load_diffusion_model_and_clip
)
from .lora_utils import expand_lora_folder
from .image_generation import (
@@ -127,6 +127,45 @@ def check_if_job_completed(existing_items, conf, seed, width, height, batch_idx,
return -1
def get_model_cache_key(conf):
"""Generate a cache key that uniquely identifies the model+clip combination."""
model_type = conf.get("model_type", "checkpoint")
if model_type == "checkpoint":
return conf["model"]
else:
te_key = "|".join(sorted(conf.get("text_encoders", [])))
return f"{conf['model']}::{model_type}::{te_key}"
def load_model_by_type(conf, ckpt_name, use_remote_vae, optional_model, optional_clip, optional_vae,
optional_positive, optional_negative, loaded_clip, loaded_vae, model_cache):
"""Dispatch to correct loader based on model_type in config."""
model_type = conf.get("model_type", "checkpoint")
target = conf["model"]
if model_type == "checkpoint":
return load_checkpoint(
target, ckpt_name, use_remote_vae,
optional_model, optional_clip, optional_vae,
optional_positive, optional_negative,
loaded_clip, loaded_vae, model_cache=model_cache
)
else:
# diffusion_model or gguf
return load_diffusion_model_and_clip(
model_name=target,
model_type=model_type,
text_encoder_paths=conf.get("text_encoders", []),
clip_type_str=conf.get("clip_type", "stable_diffusion"),
gguf_options=conf.get("gguf_options"),
use_remote_vae=use_remote_vae,
optional_model=optional_model,
optional_clip=optional_clip,
optional_vae=optional_vae,
model_cache=model_cache
)
def run_generation_loop(
self,
ckpt_name, positive_text, negative_text, seed, denoise, vae_batch_size,
@@ -180,7 +219,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['model'], x['lora'], x['positive'], x['negative']))
expanded.sort(key=lambda x: (x.get('model_type', 'checkpoint'), x['model'], tuple(x.get('text_encoders', [])), x['lora'], x['positive'], x['negative']))
# ==== EXPAND LORA FOLDERS ====
print(f"[GridTester] 🎲 Expanding LoRA folders and random selections...")
@@ -205,6 +244,16 @@ def run_generation_loop(
conf["lora_expanded"] = "None"
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:
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"
)
# ==== REGISTER SMART CACHE SCHEDULE ====
if model_cache:
model_cache.register_schedule(expanded)
@@ -263,19 +312,22 @@ def run_generation_loop(
)
# ==== PRE-ENCODING STAGE ====
unique_models = set(conf["model"] for conf in expanded)
if not (optional_positive and optional_negative) and expanded and len(unique_models) == 1:
unique_model_keys = set(get_model_cache_key(conf) for conf in expanded)
if not (optional_positive and optional_negative) and expanded and len(unique_model_keys) == 1:
first_conf = expanded[0]
target_model_name = first_conf["model"]
print(f"[GridTester] ✅ Single model detected ({target_model_name}) - enabling pre-encoding")
loaded_model, loaded_clip, loaded_vae = load_checkpoint(
target_model_name, ckpt_name, use_remote_vae,
loaded_model, loaded_clip, loaded_vae = load_model_by_type(
first_conf, ckpt_name, use_remote_vae,
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
if first_conf["lora_expanded"] != "None":
patched_model, patched_clip = load_loras_for_preencoding(
@@ -303,14 +355,14 @@ def run_generation_loop(
patched_clip, unique_positives, unique_negatives, cond_cache, clip_skip, enable_disk_cache=save_conditioning_cache_to_file
)
cached_model_key = target_model_name
cached_model_key = get_model_cache_key(first_conf)
cached_lora_key = first_conf["lora_expanded"]
if model_cache:
cached_lora_cache_key = model_cache._get_lora_cache_key(target_model_name, first_conf["lora_expanded"])
cached_lora_cache_key = model_cache._get_lora_cache_key(cached_model_key, first_conf["lora_expanded"])
else:
cached_lora_cache_key = None
latent_channels = get_latent_channels(loaded_model, optional_latent)
elif len(unique_models) > 1:
elif len(unique_model_keys) > 1:
print(f"[GridTester] ⚠️ Multiple models detected ({len(unique_models)} different models) - pre-encoding DISABLED")
cond_cache = None
latent_channels = 4
@@ -422,7 +474,8 @@ def run_generation_loop(
# ==== MODEL SWITCHING ====
target_model_name = conf["model"]
if target_model_name != cached_model_key:
target_model_key = get_model_cache_key(conf)
if target_model_key != cached_model_key:
if cached_model_key is not None:
patched_model, patched_clip = cleanup_model_references(
patched_model, patched_clip, conditioning_cache
@@ -430,14 +483,17 @@ def run_generation_loop(
# CRITICAL FIX: Restore dict structure after clear
conditioning_cache["positive"] = {}
conditioning_cache["negative"] = {}
loaded_model, loaded_clip, loaded_vae = load_checkpoint(
target_model_name, ckpt_name, use_remote_vae,
loaded_model, loaded_clip, loaded_vae = load_model_by_type(
conf, ckpt_name, use_remote_vae,
optional_model, optional_clip, optional_vae,
optional_positive, optional_negative, loaded_clip, loaded_vae, model_cache=model_cache
)
cached_model_key = target_model_name
# VAE fallback for non-checkpoint models
if loaded_vae is None and optional_vae is not None:
loaded_vae = optional_vae
cached_model_key = target_model_key
cached_lora_key = None
cached_lora_cache_key = None
latent_channels = get_latent_channels(loaded_model, optional_latent)
@@ -449,7 +505,7 @@ def run_generation_loop(
current_lora_string = conf["lora_expanded"]
if model_cache:
current_cache_key = model_cache._get_lora_cache_key(target_model_name, current_lora_string)
current_cache_key = model_cache._get_lora_cache_key(target_model_key, current_lora_string)
need_to_load = (current_cache_key != cached_lora_cache_key) or patched_model is None
else:
need_to_load = (current_lora_string != cached_lora_key) or patched_model is None
@@ -477,7 +533,7 @@ def run_generation_loop(
for future_idx in range(conf_idx, len(expanded)):
future_conf = expanded[future_idx]
if future_conf["model"] == target_model_name:
if get_model_cache_key(future_conf) == target_model_key:
future_positive, _ = build_prompt_with_triggers(
future_conf, lora_triggerwords_mode
)
@@ -557,45 +613,63 @@ def run_generation_loop(
if next_job_idx < len(input_jobs):
next_conf = expanded[next_conf_idx]
# RESOLVE "Default" to actual ckpt name
next_model_raw = next_conf["model"]
next_model_resolved = ckpt_name if next_model_raw == "Default" else next_model_raw
# RESOLVE cache keys for next conf
next_model_key = get_model_cache_key(next_conf)
next_lora = next_conf["lora_expanded"]
# CASE 1: Same Base, Different LoRA
if next_model_resolved == target_model_name and next_lora != current_lora_string:
if next_model_key == target_model_key and next_lora != current_lora_string:
def _preload_lora_worker():
return load_loras(
loaded_model, loaded_clip, next_lora,
next_model_resolved, {}, model_cache=None
target_model_name, {}, model_cache=None
)
model_cache.preload_lora_model(next_model_resolved, next_lora, _preload_lora_worker)
model_cache.preload_lora_model(target_model_key, next_lora, _preload_lora_worker)
# CASE 2: Different Base Model
elif next_model_resolved != target_model_name:
print(f"[GridTester] 🔮 Pre-loading Base Model: {next_model_resolved}")
def _preload_base_worker(path_to_load=next_model_resolved):
try:
import comfy.sd # Re-import locally to be safe in thread
ckpt_path = folder_paths.get_full_path("checkpoints", path_to_load)
if use_remote_vae:
out = comfy.sd.load_checkpoint_guess_config(
ckpt_path, output_vae=False, output_clip=True,
embedding_directory=folder_paths.get_folder_paths("embeddings")
elif next_model_key != target_model_key:
next_model_type = next_conf.get("model_type", "checkpoint")
next_model_name = next_conf["model"]
print(f"[GridTester] 🔮 Pre-loading Base Model: {next_model_name} ({next_model_type})")
if next_model_type == "checkpoint":
next_model_resolved = ckpt_name if next_model_name == "Default" else next_model_name
def _preload_base_worker(path_to_load=next_model_resolved):
try:
import comfy.sd
ckpt_path = folder_paths.get_full_path("checkpoints", path_to_load)
if use_remote_vae:
out = comfy.sd.load_checkpoint_guess_config(
ckpt_path, output_vae=False, output_clip=True,
embedding_directory=folder_paths.get_folder_paths("embeddings")
)
return out[0], out[1], None
else:
out = comfy.sd.load_checkpoint_guess_config(
ckpt_path, output_vae=True, output_clip=True,
embedding_directory=folder_paths.get_folder_paths("embeddings")
)
return out[0], out[1], out[2]
except Exception as e:
print(f"[GridTester] ❌ Async Worker Error: {e}")
return None, None, None
model_cache.preload_base_model(next_model_key, _preload_base_worker)
else:
# GGUF/diffusion model preload
def _preload_diff_worker(conf_to_load=next_conf):
try:
return load_diffusion_model_and_clip(
model_name=conf_to_load["model"],
model_type=conf_to_load.get("model_type"),
text_encoder_paths=conf_to_load.get("text_encoders", []),
clip_type_str=conf_to_load.get("clip_type", "stable_diffusion"),
gguf_options=conf_to_load.get("gguf_options"),
use_remote_vae=use_remote_vae,
)
return out[0], out[1], None
else:
out = comfy.sd.load_checkpoint_guess_config(
ckpt_path, output_vae=True, output_clip=True,
embedding_directory=folder_paths.get_folder_paths("embeddings")
)
return out[0], out[1], out[2]
except Exception as e:
print(f"[GridTester] ❌ Async Worker Error: {e}")
return None, None, None
model_cache.preload_base_model(next_model_resolved, _preload_base_worker)
except Exception as e:
print(f"[GridTester] ❌ Async Worker Error: {e}")
return None, None, None
model_cache.preload_base_model(next_model_key, _preload_diff_worker)
# =========================================================
# Generate image
+2 -1
View File
@@ -137,7 +137,8 @@ def create_image_metadata(config, width, height, duration, seed, batch_idx, actu
global_settings_to_remove = [
"lora_triggerwords_append_settings",
"lora_omit_triggers",
"seed_behavior"
"seed_behavior",
"gguf_options"
]
for key in global_settings_to_remove:
+199
View File
@@ -166,6 +166,205 @@ def load_checkpoint(
return loaded_model, loaded_clip, loaded_vae
def _import_gguf():
"""
Lazy import of ComfyUI-GGUF modules.
Returns (GGMLOps, gguf_sd_loader, gguf_clip_loader, GGUFModelPatcher).
Raises RuntimeError if ComfyUI-GGUF is not installed.
"""
try:
from ComfyUI_GGUF.ops import GGMLOps
from ComfyUI_GGUF.loader import gguf_sd_loader, gguf_clip_loader
from ComfyUI_GGUF.nodes import GGUFModelPatcher
return GGMLOps, gguf_sd_loader, gguf_clip_loader, GGUFModelPatcher
except ImportError:
raise RuntimeError(
"ComfyUI-GGUF is required for GGUF model loading but is not installed.\n"
"Install it from: https://github.com/city96/ComfyUI-GGUF"
)
def resolve_text_encoder_path(filename):
"""
Resolve a text encoder filename to its full path.
Searches text_encoders, clip, and clip_gguf folder paths.
"""
for folder_key in ["text_encoders", "clip", "clip_gguf"]:
try:
path = folder_paths.get_full_path(folder_key, filename)
if path:
return path
except (KeyError, Exception):
continue
raise FileNotFoundError(f"Text encoder not found: {filename}")
def load_diffusion_model_and_clip(
model_name,
model_type,
text_encoder_paths,
clip_type_str,
gguf_options=None,
use_remote_vae=False,
optional_model=None,
optional_clip=None,
optional_vae=None,
model_cache=None
):
"""
Load a diffusion model (unet) + separate CLIP text encoders.
Used for GGUF and standalone diffusion models.
Args:
model_name: Filename of the diffusion model / GGUF model
model_type: "diffusion_model" or "gguf"
text_encoder_paths: List of text encoder filenames
clip_type_str: CLIPType string (e.g. "flux", "sdxl", "stable_diffusion")
gguf_options: Dict with dequant_dtype, patch_dtype, patch_on_device (GGUF only)
use_remote_vae: Whether remote VAE is enabled
optional_model: Optional pre-loaded model override
optional_clip: Optional pre-loaded CLIP override
optional_vae: Optional pre-loaded VAE override
model_cache: ModelCache instance (optional)
Returns:
tuple: (loaded_model, loaded_clip, loaded_vae)
Note: loaded_vae is always optional_vae or None (no bundled VAE)
"""
import comfy.model_management
# Build cache key for this model+text_encoder combo
te_key = "|".join(sorted(text_encoder_paths)) if text_encoder_paths else ""
cache_key = f"{model_name}::{model_type}::{te_key}"
# Check cache first
if model_cache is not None:
cached = model_cache.get_base_model(cache_key)
if cached is not None:
cached_model, cached_clip, cached_vae = cached
final_model = optional_model if optional_model is not None else cached_model
final_clip = optional_clip if optional_clip is not None else cached_clip
final_vae = optional_vae if optional_vae is not None else cached_vae
return final_model, final_clip, final_vae
# Use optional overrides if all provided
if optional_model is not None and optional_clip is not None:
print(f"[GridTester] 🔌 Using optional MODEL and CLIP (skipping {model_type} load)")
return optional_model, optional_clip, optional_vae
start_time = time.time()
# ---- Load the UNet / diffusion model ----
loaded_model = optional_model
if loaded_model is None:
print(f"[GridTester] 💿 LOADING {model_type.upper()}: {model_name}")
if model_type == "gguf":
GGMLOps, gguf_sd_loader, _, GGUFModelPatcher = _import_gguf()
# Resolve path - try unet_gguf first, then unet/diffusion_models
unet_path = None
for folder_key in ["unet_gguf", "unet", "diffusion_models"]:
try:
unet_path = folder_paths.get_full_path(folder_key, model_name)
if unet_path:
break
except (KeyError, Exception):
continue
if unet_path is None:
raise FileNotFoundError(f"GGUF model not found: {model_name}")
ops = GGMLOps()
if gguf_options:
dequant = gguf_options.get("dequant_dtype", "default")
if dequant not in ("default", None):
if dequant == "target":
ops.Linear.dequant_dtype = "target"
else:
ops.Linear.dequant_dtype = getattr(torch, dequant)
patch = gguf_options.get("patch_dtype", "default")
if patch not in ("default", None):
if patch == "target":
ops.Linear.patch_dtype = "target"
else:
ops.Linear.patch_dtype = getattr(torch, patch)
sd, extra = gguf_sd_loader(unet_path)
loaded_model = comfy.sd.load_diffusion_model_state_dict(
sd, model_options={"custom_operations": ops}
)
if loaded_model is None:
raise RuntimeError(f"Could not detect model type of GGUF: {model_name}")
loaded_model = GGUFModelPatcher.clone(loaded_model)
if gguf_options and gguf_options.get("patch_on_device"):
loaded_model.patch_on_device = True
elif model_type == "diffusion_model":
unet_path = folder_paths.get_full_path("diffusion_models", model_name)
if unet_path is None:
# Also try "unet" folder key
unet_path = folder_paths.get_full_path("unet", model_name)
if unet_path is None:
raise FileNotFoundError(f"Diffusion model not found: {model_name}")
loaded_model = comfy.sd.load_diffusion_model(unet_path, model_options={})
print(f"[GridTester] ✅ Model loaded in {time.time() - start_time:.2f}s")
# ---- Load CLIP text encoders ----
loaded_clip = optional_clip
if loaded_clip is None and text_encoder_paths:
clip_start = time.time()
print(f"[GridTester] 💿 LOADING TEXT ENCODERS: {text_encoder_paths}")
clip_type = getattr(comfy.sd.CLIPType, clip_type_str.upper(), comfy.sd.CLIPType.STABLE_DIFFUSION)
resolved_paths = [resolve_text_encoder_path(p) for p in text_encoder_paths]
is_any_gguf = any(p.endswith(".gguf") for p in text_encoder_paths)
if is_any_gguf:
GGMLOps, _, gguf_clip_loader, GGUFModelPatcher = _import_gguf()
clip_data = []
for p in resolved_paths:
if p.endswith(".gguf"):
clip_data.append(gguf_clip_loader(p))
else:
sd_te = comfy.utils.load_torch_file(p, safe_load=True)
clip_data.append(sd_te)
loaded_clip = comfy.sd.load_text_encoder_state_dicts(
clip_type=clip_type,
state_dicts=clip_data,
model_options={
"custom_operations": GGMLOps,
"initial_device": comfy.model_management.text_encoder_offload_device()
},
embedding_directory=folder_paths.get_folder_paths("embeddings")
)
loaded_clip.patcher = GGUFModelPatcher.clone(loaded_clip.patcher)
else:
loaded_clip = comfy.sd.load_clip(
ckpt_paths=resolved_paths,
embedding_directory=folder_paths.get_folder_paths("embeddings"),
clip_type=clip_type
)
print(f"[GridTester] ✅ Text encoders loaded in {time.time() - clip_start:.2f}s")
elapsed = time.time() - start_time
print(f"[GridTester] ✅ Total {model_type} loading: {elapsed:.2f}s")
# VAE: non-checkpoint models have no bundled VAE
loaded_vae = optional_vae # Could be None
# Cache the loaded model
if model_cache is not None and loaded_model is not None:
model_cache.record_disk_load(is_base_model=True, load_time=elapsed)
model_cache.put_base_model(cache_key, loaded_model, loaded_clip, loaded_vae)
return loaded_model, loaded_clip, loaded_vae
def load_loras(base_model, base_clip, lora_string, target_model_name, incompatible_loras, model_cache=None):
"""
Load and patch LoRAs onto model and CLIP.
+1 -1
View File
@@ -52,7 +52,7 @@ class SamplerGridTester:
def INPUT_TYPES(s):
return {
"required": {
"ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),
"ckpt_name": (folder_paths.get_filename_list("checkpoints"), {"tooltip": "Default checkpoint model. Used when config has model set to 'Default'. Configs can also specify GGUF or diffusion models directly via model_type."}),
"positive_text": ("STRING", {"multiline": True, "default": "masterpiece, best quality, 1girl"}),
"negative_text": ("STRING", {"multiline": True, "default": "bad quality, worst quality, lowres"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
@@ -12,14 +12,14 @@ import {
convertStateToConfigs,
countPromptCombinations,
expandPromptPreview
} from '/ultimate_config_sampler/js/conf_builder/conf-builder-utilities.js';
} from './conf-builder-utilities.js';
import {
createSearchableSelect,
createSlider,
createInputGroup,
getStyles
} from '/ultimate_config_sampler/js/conf_builder/conf-builder-ui-components.js';
} from './conf-builder-ui-components.js';
// --- SESSION SECTION RENDERER ---
@@ -108,7 +108,7 @@ export function renderConfigSection(node, container, availableConfigs) {
saveBtn.style.width = "100%";
saveBtn.onclick = async () => {
await node.saveConfigToBackend();
const { getAvailableConfigs } = await import('/ultimate_config_sampler/js/conf_builder/conf-builder-utilities.js');
const { getAvailableConfigs } = await import('./conf-builder-utilities.js');
await getAvailableConfigs();
node.renderUI();
};
@@ -236,15 +236,28 @@ export function createConfigArrayElement(node, configArray, arrayIdx) {
controlsBar.appendChild(deleteBtn);
div.appendChild(controlsBar);
// Label Mode overlay — shows config name at bottom center of card
if (node.state.label_mode && configArray.name) {
const labelOverlay = document.createElement("div");
labelOverlay.className = "cb-config-label-overlay";
labelOverlay.textContent = configArray.name;
div.appendChild(labelOverlay);
}
return div;
}
// --- MODEL ELEMENT CREATOR ---
export function createModelElement(node, modelStr, arrayIdx, modelIdx, availableModels, modelFolders) {
export function createModelElement(node, modelEntry, arrayIdx, modelIdx, modelLists) {
// Handle both string format (legacy) and object format {path, type}
const modelPath = typeof modelEntry === 'string' ? modelEntry : (modelEntry?.path || "None");
const modelType = typeof modelEntry === 'string' ? 'checkpoint' : (modelEntry?.type || 'checkpoint');
const isFolder = modelPath.endsWith("/");
const div = document.createElement("div");
div.className = "cb-item-card model-card";
const isFolder = modelStr.endsWith("/");
const uid = `${arrayIdx}_${modelIdx}`;
// Initial State
@@ -258,7 +271,7 @@ export function createModelElement(node, modelStr, arrayIdx, modelIdx, available
leftGroup.className = "cb-header-left";
const toggleArrow = document.createElement("span");
toggleArrow.textContent = isCollapsed ? "▶" : "▼"; // Right arrow if collapsed, Down if open
toggleArrow.textContent = isCollapsed ? "▶" : "▼";
toggleArrow.style.color = "#aaa";
toggleArrow.style.fontSize = "10px";
toggleArrow.style.width = "12px";
@@ -270,9 +283,17 @@ export function createModelElement(node, modelStr, arrayIdx, modelIdx, available
label.style.whiteSpace = "nowrap";
leftGroup.appendChild(label);
// Show model type badge
if (modelType !== "checkpoint") {
const typeBadge = document.createElement("span");
typeBadge.textContent = modelType === "gguf" ? "GGUF" : "DM";
typeBadge.style.cssText = "background: #553300; color: #ffaa44; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: bold;";
leftGroup.appendChild(typeBadge);
}
const nameSpan = document.createElement("span");
nameSpan.className = "cb-header-name";
nameSpan.textContent = getShortName(modelStr);
nameSpan.textContent = getShortName(modelPath);
leftGroup.appendChild(nameSpan);
header.appendChild(leftGroup);
@@ -290,42 +311,75 @@ export function createModelElement(node, modelStr, arrayIdx, modelIdx, available
header.appendChild(deleteBtn);
div.appendChild(header);
// Content Container (Rendered but hidden if collapsed)
// 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%";
// --- TOGGLE LOGIC (INSTANT) ---
header.onclick = (e) => {
if (e.target.tagName === 'BUTTON') return; // Ignore delete button
if (e.target.tagName === 'BUTTON') return;
const isNowCollapsed = contentDiv.style.display !== "none";
contentDiv.style.display = isNowCollapsed ? "none" : "flex";
toggleArrow.textContent = isNowCollapsed ? "▶" : "▼";
// Save state silently
node.uiState.modelsCollapsed[uid] = isNowCollapsed;
};
// Type Select
// Model Type Select (Checkpoint / Diffusion Model / GGUF)
const modelTypeSelect = document.createElement("select");
modelTypeSelect.className = "cb-select";
modelTypeSelect.innerHTML = `
<option value="checkpoint" ${modelType === 'checkpoint' ? 'selected' : ''}>Checkpoint</option>
<option value="diffusion_model" ${modelType === 'diffusion_model' ? 'selected' : ''}>Diffusion Model</option>
<option value="gguf" ${modelType === 'gguf' ? 'selected' : ''}>GGUF</option>
`;
modelTypeSelect.onchange = () => {
const newType = modelTypeSelect.value;
node.state.config_arrays[arrayIdx].models[modelIdx] = newType === "checkpoint"
? "None"
: { path: "None", type: newType };
node.saveState();
node.renderUI();
};
contentDiv.appendChild(modelTypeSelect);
// File/Folder Type Select
const typeSelect = document.createElement("select");
typeSelect.className = "cb-select";
const fileLabel = modelType === "gguf" ? "GGUF File" : modelType === "diffusion_model" ? "Diffusion File" : "Checkpoint File";
typeSelect.innerHTML = `
<option value="file" ${!isFolder ? 'selected' : ''}>Checkpoint File</option>
<option value="file" ${!isFolder ? 'selected' : ''}>${fileLabel}</option>
<option value="folder" ${isFolder ? 'selected' : ''}>Folder</option>
`;
typeSelect.onchange = () => {
node.state.config_arrays[arrayIdx].models[modelIdx] = typeSelect.value === "folder" ? "/" : "None";
const newVal = typeSelect.value === "folder" ? "/" : "None";
if (modelType === "checkpoint") {
node.state.config_arrays[arrayIdx].models[modelIdx] = newVal;
} else {
node.state.config_arrays[arrayIdx].models[modelIdx] = { path: newVal, type: modelType };
}
node.saveState();
node.renderUI();
};
contentDiv.appendChild(typeSelect);
// Pick the correct file list and folder list based on model type
let fileOptions, folderOptions;
if (modelType === 'gguf') {
fileOptions = modelLists.ggufModels || [];
folderOptions = modelLists.ggufFolders || ["/"];
} else if (modelType === 'diffusion_model') {
fileOptions = modelLists.diffusionModels || [];
folderOptions = modelLists.diffusionFolders || ["/"];
} else {
fileOptions = modelLists.checkpoints || [];
folderOptions = modelLists.checkpointFolders || ["/"];
}
// Searchable Select
const options = isFolder ? modelFolders : availableModels;
const currentVal = modelStr;
const options = isFolder ? folderOptions : fileOptions;
const currentVal = modelPath;
const optionsList = (options && options.includes(currentVal)) || currentVal === "None" || currentVal === "/"
? options || ["None"]
: [currentVal, ...(options || ["None"])];
@@ -334,7 +388,12 @@ export function createModelElement(node, modelStr, arrayIdx, modelIdx, available
optionsList,
currentVal,
(value) => {
node.state.config_arrays[arrayIdx].models[modelIdx] = normalizePath(value);
const normalized = normalizePath(value);
if (modelType === "checkpoint") {
node.state.config_arrays[arrayIdx].models[modelIdx] = normalized;
} else {
node.state.config_arrays[arrayIdx].models[modelIdx] = { path: normalized, type: modelType };
}
node.saveState();
node.renderUI();
},
@@ -342,7 +401,8 @@ export function createModelElement(node, modelStr, arrayIdx, modelIdx, available
);
contentDiv.appendChild(nameSearchable);
if (isFolder && modelStr !== "None" && modelStr !== "/") {
// Folder expand button
if (isFolder && modelPath !== "None" && modelPath !== "/") {
const expandBtn = document.createElement("button");
expandBtn.className = "cb-button";
expandBtn.style.borderLeft = "3px solid #cc6600";
@@ -352,14 +412,15 @@ export function createModelElement(node, modelStr, arrayIdx, modelIdx, available
expandBtn.textContent = "📂 Add all individually";
expandBtn.onclick = () => {
const normalize = (str) => str.replace(/\\/g, "/");
const folderPrefix = normalize(modelStr);
const matchingModels = availableModels ? availableModels.filter(m => normalize(m).startsWith(folderPrefix)) : [];
const folderPrefix = normalize(modelPath);
const matchingModels = fileOptions ? fileOptions.filter(m => normalize(m).startsWith(folderPrefix)) : [];
if (matchingModels.length > 0) {
node.state.config_arrays[arrayIdx].models.splice(modelIdx, 1, ...matchingModels);
const expanded = matchingModels.map(m => modelType === "checkpoint" ? m : { path: m, type: modelType });
node.state.config_arrays[arrayIdx].models.splice(modelIdx, 1, ...expanded);
node.saveState();
node.renderUI();
} else {
alert(`No Checkpoints found in folder: ${folderPrefix}`);
alert(`No models found in folder: ${folderPrefix}`);
}
};
contentDiv.appendChild(expandBtn);
@@ -911,12 +972,11 @@ async function showLoraMetadataModal(node, arrayIdx, loraName) {
// --- RENDER MODELS AND LORAS SECTIONS ---
export function renderModelsSection(node, div, configArray, arrayIdx, availableModels, modelFolders) {
export function renderModelsSection(node, div, configArray, arrayIdx, modelLists) {
if (!configArray.models || configArray.models.length === 0) configArray.models = ["None"];
const isSectionCollapsed = node.uiState.modelsSectionCollapsed[arrayIdx] || false;
// FIX: Restore class for Grid Layout
const modelGrid = document.createElement("div");
modelGrid.className = "cb-list-grid";
@@ -924,13 +984,21 @@ export function renderModelsSection(node, div, configArray, arrayIdx, availableM
modelHeader.className = "cb-section-toggle";
modelHeader.style.cssText = "padding: 8px; background: #3a3a3a; border-radius: 4px; margin-bottom: 8px; font-weight: bold; color: #cc6600;";
// Count total models (handle both string and object format)
let totalModels = 0;
configArray.models.forEach(m => {
if (m === "None") totalModels++;
else if (m.endsWith("/")) {
const norm = normalizePath(m);
if (norm === "/") totalModels += availableModels ? availableModels.length : 1;
else totalModels += availableModels ? availableModels.filter(am => normalizePath(am).startsWith(norm)).length : 1;
const mPath = typeof m === 'string' ? m : (m?.path || "None");
const mType = typeof m === 'string' ? 'checkpoint' : (m?.type || 'checkpoint');
let list;
if (mType === 'gguf') list = modelLists.ggufModels;
else if (mType === 'diffusion_model') list = modelLists.diffusionModels;
else list = modelLists.checkpoints;
if (mPath === "None") totalModels++;
else if (mPath.endsWith("/")) {
const norm = normalizePath(mPath);
if (norm === "/") totalModels += list ? list.length : 1;
else totalModels += list ? list.filter(am => normalizePath(am).startsWith(norm)).length : 1;
} else totalModels++;
});
@@ -939,20 +1007,16 @@ export function renderModelsSection(node, div, configArray, arrayIdx, availableM
modelHeader.appendChild(titleSpan);
const arrowSpan = document.createElement("span");
arrowSpan.textContent = isSectionCollapsed ? "▶" : "▼"; // Initial icon
arrowSpan.textContent = isSectionCollapsed ? "▶" : "▼";
modelHeader.appendChild(arrowSpan);
modelGrid.appendChild(modelHeader);
// CONTENT CONTAINER (Always render, just toggle display)
const contentContainer = document.createElement("div");
contentContainer.style.display = isSectionCollapsed ? "none" : "contents"; // 'contents' keeps the grid layout working!
// Note: 'contents' acts as if the container isn't there, so children become grid items.
contentContainer.style.display = isSectionCollapsed ? "none" : "contents";
// --- HEADER CLICK (INSTANT) ---
modelHeader.onclick = () => {
const isNowCollapsed = contentContainer.style.display === "none";
// Toggle
if (isNowCollapsed) {
contentContainer.style.display = "contents";
arrowSpan.textContent = "▼";
@@ -964,12 +1028,10 @@ export function renderModelsSection(node, div, configArray, arrayIdx, availableM
}
};
// Always render items
configArray.models.forEach((model, modelIdx) => {
contentContainer.appendChild(createModelElement(node, model, arrayIdx, modelIdx, availableModels, modelFolders));
contentContainer.appendChild(createModelElement(node, model, arrayIdx, modelIdx, modelLists));
});
// FIX: Add Model Button (Bottom)
const addRow = document.createElement("div");
addRow.style.width = "100%";
addRow.style.padding = "4px 0";
@@ -987,10 +1049,181 @@ export function renderModelsSection(node, div, configArray, arrayIdx, availableM
addRow.appendChild(addBtn);
contentContainer.appendChild(addRow);
// --- TEXT ENCODERS SECTION (shown when any model is non-checkpoint) ---
const hasNonCheckpoint = configArray.models.some(m =>
typeof m === 'object' && m !== null && m.type && m.type !== 'checkpoint'
);
if (hasNonCheckpoint) {
renderTextEncodersSection(node, contentContainer, configArray, arrayIdx, modelLists);
}
// --- GGUF OPTIONS SECTION (shown when any model is GGUF) ---
const hasGGUF = configArray.models.some(m =>
typeof m === 'object' && m !== null && m.type === 'gguf'
);
if (hasGGUF) {
renderGGUFOptionsSection(node, contentContainer, configArray, arrayIdx);
}
modelGrid.appendChild(contentContainer);
div.appendChild(modelGrid);
}
// --- TEXT ENCODERS SECTION ---
function renderTextEncodersSection(node, container, configArray, arrayIdx, modelLists) {
const section = document.createElement("div");
section.style.cssText = "width: 100%; border-top: 1px solid #444; margin-top: 8px; padding-top: 8px;";
const header = document.createElement("div");
header.style.cssText = "font-size: 11px; font-weight: bold; color: #44aaff; margin-bottom: 6px;";
header.textContent = "TEXT ENCODERS (CLIP)";
section.appendChild(header);
// Clip Type selector
const clipRow = document.createElement("div");
clipRow.style.cssText = "display: flex; gap: 6px; align-items: center; margin-bottom: 6px;";
const clipLabel = document.createElement("label");
clipLabel.textContent = "CLIP Type:";
clipLabel.style.cssText = "color: #aaa; font-size: 11px; white-space: nowrap;";
clipRow.appendChild(clipLabel);
const clipSelect = document.createElement("select");
clipSelect.className = "cb-select";
// Combine single and dual clip types
const allClipTypes = [...new Set([...(modelLists.clipTypes || []), ...(modelLists.dualClipTypes || [])])];
allClipTypes.forEach(ct => {
const opt = document.createElement("option");
opt.value = ct;
opt.textContent = ct;
if (ct === (configArray.clip_type || "stable_diffusion")) opt.selected = true;
clipSelect.appendChild(opt);
});
clipSelect.onchange = () => {
node.state.config_arrays[arrayIdx].clip_type = clipSelect.value;
node.saveState();
};
clipRow.appendChild(clipSelect);
section.appendChild(clipRow);
// Text encoder entries
const teList = configArray.text_encoders || [];
teList.forEach((te, teIdx) => {
const teRow = document.createElement("div");
teRow.style.cssText = "display: flex; gap: 4px; align-items: center; margin-bottom: 4px;";
const teSearchable = createSearchableSelect(
modelLists.textEncoders || ["None"],
te || "None",
(value) => {
node.state.config_arrays[arrayIdx].text_encoders[teIdx] = normalizePath(value);
node.saveState();
},
"Search text encoders..."
);
teSearchable.style.flex = "1";
teRow.appendChild(teSearchable);
const delBtn = document.createElement("button");
delBtn.className = "cb-button danger";
delBtn.style.cssText = "padding: 2px 6px; font-size: 10px;";
delBtn.textContent = "✖";
delBtn.onclick = () => {
node.state.config_arrays[arrayIdx].text_encoders.splice(teIdx, 1);
node.saveState();
node.renderUI();
};
teRow.appendChild(delBtn);
section.appendChild(teRow);
});
// Add text encoder button
const addTeBtn = document.createElement("button");
addTeBtn.className = "cb-button";
addTeBtn.style.cssText = "width: 100%; border: 1px dashed #446; background: rgba(0,50,100,0.2); color: #88bbff; font-size: 11px;";
addTeBtn.textContent = "+ Add Text Encoder";
addTeBtn.onclick = () => {
if (!node.state.config_arrays[arrayIdx].text_encoders) {
node.state.config_arrays[arrayIdx].text_encoders = [];
}
node.state.config_arrays[arrayIdx].text_encoders.push("None");
node.saveState();
node.renderUI();
};
section.appendChild(addTeBtn);
container.appendChild(section);
}
// --- GGUF OPTIONS SECTION ---
function renderGGUFOptionsSection(node, container, configArray, arrayIdx) {
const section = document.createElement("details");
section.style.cssText = "width: 100%; border-top: 1px solid #444; margin-top: 8px; padding-top: 8px;";
const summary = document.createElement("summary");
summary.textContent = "GGUF Options";
summary.style.cssText = "cursor: pointer; color: #aaa; font-size: 11px; font-weight: bold;";
section.appendChild(summary);
const opts = configArray.gguf_options || {};
const grid = document.createElement("div");
grid.style.cssText = "display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 6px;";
// Helper for select rows
const addSelectRow = (label, options, currentVal, onChange) => {
const lbl = document.createElement("label");
lbl.textContent = label;
lbl.style.cssText = "color: #aaa; font-size: 11px; display: flex; align-items: center;";
grid.appendChild(lbl);
const sel = document.createElement("select");
sel.className = "cb-select";
options.forEach(o => {
const opt = document.createElement("option");
opt.value = o;
opt.textContent = o;
if (o === currentVal) opt.selected = true;
sel.appendChild(opt);
});
sel.onchange = () => onChange(sel.value);
grid.appendChild(sel);
};
const dtypeOptions = ["default", "target", "float32", "float16", "bfloat16"];
addSelectRow("Dequant dtype:", dtypeOptions, opts.dequant_dtype || "default", (val) => {
if (!node.state.config_arrays[arrayIdx].gguf_options) node.state.config_arrays[arrayIdx].gguf_options = {};
node.state.config_arrays[arrayIdx].gguf_options.dequant_dtype = val;
node.saveState();
});
addSelectRow("Patch dtype:", dtypeOptions, opts.patch_dtype || "default", (val) => {
if (!node.state.config_arrays[arrayIdx].gguf_options) node.state.config_arrays[arrayIdx].gguf_options = {};
node.state.config_arrays[arrayIdx].gguf_options.patch_dtype = val;
node.saveState();
});
// Patch on device checkbox
const podLabel = document.createElement("label");
podLabel.textContent = "Patch on device:";
podLabel.style.cssText = "color: #aaa; font-size: 11px; display: flex; align-items: center;";
grid.appendChild(podLabel);
const podCheck = document.createElement("input");
podCheck.type = "checkbox";
podCheck.checked = opts.patch_on_device || false;
podCheck.onchange = () => {
if (!node.state.config_arrays[arrayIdx].gguf_options) node.state.config_arrays[arrayIdx].gguf_options = {};
node.state.config_arrays[arrayIdx].gguf_options.patch_on_device = podCheck.checked;
node.saveState();
};
grid.appendChild(podCheck);
section.appendChild(grid);
container.appendChild(section);
}
export function renderLorasSection(node, div, configArray, arrayIdx, availableLoras, loraFolders) {
if (!configArray.loras || configArray.loras.length === 0) configArray.loras = ["None"];
@@ -1807,7 +2040,7 @@ export function updatePreview(node) {
preview.textContent = JSON.stringify(configs, null, 2);
}
export async function renderUI(node, availableLoras, availableModels, loraFolders, modelFolders, availableSessions, availableConfigs, refreshAllConfigBuilders) {
export async function renderUI(node, availableLoras, modelLists, loraFolders, availableSessions, availableConfigs, refreshAllConfigBuilders) {
const scrollContainer = node.htmlContainer.querySelector(".cb-container");
const savedScrollTop = scrollContainer ? scrollContainer.scrollTop : 0;
@@ -1829,7 +2062,7 @@ export async function renderUI(node, availableLoras, availableModels, loraFolder
configSection.innerHTML = '<div class="cb-section-title">⚙️ Config Arrays</div>';
const headerBar = document.createElement("div");
headerBar.style.marginBottom = "10px";
headerBar.style.cssText = "margin-bottom: 10px; display: flex; align-items: center; gap: 12px;";
const addConfigBtn = document.createElement("button");
addConfigBtn.className = "cb-button primary";
addConfigBtn.textContent = "➕ Add Config Array";
@@ -1842,6 +2075,9 @@ export async function renderUI(node, availableLoras, availableModels, loraFolder
cfg: "7.0",
seed_behavior: "fixed",
models: ["None"],
text_encoders: [],
clip_type: "stable_diffusion",
gguf_options: {},
loras: ["None"],
lora_omit_triggers: [],
lora_triggerwords_append_settings: {},
@@ -1854,6 +2090,22 @@ export async function renderUI(node, availableLoras, availableModels, loraFolder
node.renderUI();
};
headerBar.appendChild(addConfigBtn);
const labelModeLabel = document.createElement("label");
labelModeLabel.className = "cb-toggle";
labelModeLabel.style.fontSize = "12px";
const labelModeCheckbox = document.createElement("input");
labelModeCheckbox.type = "checkbox";
labelModeCheckbox.checked = node.state.label_mode || false;
labelModeCheckbox.onchange = () => {
node.state.label_mode = labelModeCheckbox.checked;
node.saveState();
node.renderUI();
};
labelModeLabel.appendChild(labelModeCheckbox);
labelModeLabel.appendChild(document.createTextNode(" \u{1F3F7}\uFE0F Label Mode"));
headerBar.appendChild(labelModeLabel);
configSection.appendChild(headerBar);
const arraysContainer = document.createElement("div");
@@ -1862,7 +2114,7 @@ export async function renderUI(node, availableLoras, availableModels, loraFolder
node.state.config_arrays.forEach((configArray, arrayIdx) => {
const arrayElement = createConfigArrayElement(node, configArray, arrayIdx);
renderConfigPromptsSection(node, arrayElement, configArray, arrayIdx);
renderModelsSection(node, arrayElement, configArray, arrayIdx, availableModels, modelFolders);
renderModelsSection(node, arrayElement, configArray, arrayIdx, modelLists);
renderLorasSection(node, arrayElement, configArray, arrayIdx, availableLoras, loraFolders);
arraysContainer.appendChild(arrayElement);
});
+58 -12
View File
@@ -19,9 +19,9 @@ function ensureModulesLoaded() {
console.log('[ConfigBuilder] Loading modules with cache-bust:', CACHE_BUST);
moduleLoadPromise = (async () => {
const utilitiesModule = await import(`/ultimate_config_sampler/js/conf_builder/conf-builder-utilities.js?v=${CACHE_BUST}`);
const uiComponentsModule = await import(`/ultimate_config_sampler/js/conf_builder/conf-builder-ui-components.js?v=${CACHE_BUST}`);
const configManagementModule = await import(`/ultimate_config_sampler/js/conf_builder/conf-builder-config-management.js?v=${CACHE_BUST}`);
const utilitiesModule = await import(`./conf-builder-utilities.js?v=${CACHE_BUST}`);
const uiComponentsModule = await import(`./conf-builder-ui-components.js?v=${CACHE_BUST}`);
const configManagementModule = await import(`./conf-builder-config-management.js?v=${CACHE_BUST}`);
utilities = utilitiesModule;
uiComponents = uiComponentsModule;
@@ -77,6 +77,7 @@ app.registerExtension({
config_name: "default_config",
auto_save: false,
include_none: false,
label_mode: false,
global_positive_groups: [],
global_negative: "",
config_arrays: [{
@@ -86,6 +87,9 @@ app.registerExtension({
steps: "20, 30",
cfg: "7.0",
models: ["None"],
text_encoders: [],
clip_type: "stable_diffusion",
gguf_options: {},
loras: ["None"],
lora_omit_triggers: [],
lora_triggerwords_append_settings: {},
@@ -169,18 +173,32 @@ app.registerExtension({
if (!utilities || !configManagement) return;
const availableLoras = await utilities.getAvailableLoras();
const availableModels = await utilities.getAvailableModels();
const loraFolders = await utilities.getLoraFolders();
const modelFolders = await utilities.getModelFolders();
const availableSessions = await utilities.getAvailableSessions();
const availableConfigs = await utilities.getAvailableConfigs();
// Fetch all model type lists via unified endpoint
await utilities.getModelLists();
// Build modelLists object for config management
const modelLists = {
checkpoints: await utilities.getAvailableModels(),
checkpointFolders: await utilities.getModelFolders(),
diffusionModels: utilities.getAvailableDiffusionModels(),
diffusionFolders: utilities.getDiffusionModelFolders(),
ggufModels: utilities.getAvailableGGUFModels(),
ggufFolders: utilities.getGGUFModelFolders(),
textEncoders: utilities.getAvailableTextEncoders(),
textEncoderFolders: utilities.getTextEncoderFolders(),
clipTypes: utilities.getClipTypes(),
dualClipTypes: utilities.getDualClipTypes()
};
await configManagement.renderUI(
this,
availableLoras,
availableModels,
modelLists,
loraFolders,
modelFolders,
availableSessions,
availableConfigs,
utilities.refreshAllConfigBuilders
@@ -224,12 +242,26 @@ app.registerExtension({
return loraStr;
});
arr.models = arr.models.map(modelStr => {
const normModel = normalize(modelStr);
// Handle both string and object-format models
arr.models = arr.models.map(modelEntry => {
if (typeof modelEntry === 'object' && modelEntry !== null) {
// Object format: {path, type} — normalize path
return {
...modelEntry,
path: normalize(modelEntry.path || "")
};
}
// String format (checkpoint) — normalize as before
const normModel = normalize(modelEntry);
if (normModel.endsWith("/")) return normModel;
if (modelFolders && modelFolders.includes(normModel + "/")) return normModel + "/";
return normModel;
});
// Ensure new fields exist
if (!arr.text_encoders) arr.text_encoders = [];
if (!arr.clip_type) arr.clip_type = "stable_diffusion";
if (!arr.gguf_options) arr.gguf_options = {};
});
this.state.config_arrays = loadedArrays;
@@ -264,6 +296,9 @@ app.registerExtension({
steps: oldState.steps || "20",
cfg: oldState.cfg || "7.0",
models: oldState.model ? [utilities.normalizePath(oldState.model)] : ["None"],
text_encoders: [],
clip_type: "stable_diffusion",
gguf_options: {},
loras: arr.loras ? arr.loras.map(l => utilities.normalizePath(l)) : ["None"],
lora_omit_triggers: [],
lora_triggerwords_append_settings: {},
@@ -292,6 +327,7 @@ app.registerExtension({
this.state = existing;
if (!this.state.config_name) this.state.config_name = "default_config";
if (this.state.auto_save === undefined) this.state.auto_save = false;
if (this.state.label_mode === undefined) this.state.label_mode = false;
// Migration: ensure global prompt fields exist
if (!this.state.global_positive_groups) this.state.global_positive_groups = [];
@@ -304,7 +340,13 @@ app.registerExtension({
delete arr.model;
}
if (!arr.models) arr.models = ["None"];
arr.models = arr.models.map(utilities.normalizePath);
// Normalize models — handle both string and object formats
arr.models = arr.models.map(m => {
if (typeof m === 'object' && m !== null) {
return { ...m, path: utilities.normalizePath(m.path || "") };
}
return utilities.normalizePath(m);
});
arr.loras = arr.loras ? arr.loras.map(l => {
const p = utilities.parseLoraString(l);
return utilities.buildLoraString(p.name, p.model_str, p.clip_str);
@@ -320,6 +362,11 @@ app.registerExtension({
if (!arr.positive_prompt_groups) arr.positive_prompt_groups = [];
if (arr.negative_prompt === undefined) arr.negative_prompt = "";
if (arr.use_custom_prompts === undefined) arr.use_custom_prompts = false;
// Migration: ensure new model type fields exist
if (!arr.text_encoders) arr.text_encoders = [];
if (!arr.clip_type) arr.clip_type = "stable_diffusion";
if (!arr.gguf_options) arr.gguf_options = {};
});
} else if (existing.lora_config) {
this.state = this.migrateOldFormat(existing);
@@ -330,8 +377,7 @@ app.registerExtension({
await Promise.all([
utilities.getAvailableLoras(),
utilities.getLoraFolders(),
utilities.getAvailableModels(),
utilities.getModelFolders(),
utilities.getModelLists(),
utilities.getAvailableSessions(),
utilities.getAvailableConfigs()
]);
@@ -3,7 +3,7 @@
* Reusable UI elements and component builders
*/
import { normalizePath } from '/ultimate_config_sampler/js/conf_builder/conf-builder-utilities.js';
import { normalizePath } from './conf-builder-utilities.js';
// --- SEARCHABLE SELECT COMPONENT ---
@@ -200,7 +200,8 @@ export function getStyles() {
.cb-button.primary:hover { background: #0077ee; }
.cb-button.danger { background: #cc3333; }
.cb-button.danger:hover { background: #dd4444; }
.cb-array { background: #333; border-radius: 4px; padding: 12px; margin: 8px 0; border: 1px solid #3a3a3a; width: 100%; }
.cb-array { background: #333; border-radius: 4px; padding: 12px; margin: 8px 0; border: 1px solid #3a3a3a; width: 100%; position: relative; }
.cb-config-label-overlay { position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%); background: rgba(0, 102, 204, 0.85); color: #fff; padding: 4px 16px; border-radius: 12px; font-size: 13px; font-weight: bold; font-family: monospace; pointer-events: none; z-index: 10; white-space: nowrap; max-width: 80%; overflow: hidden; text-overflow: ellipsis; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
.cb-arrays-container { display: flex; flex-direction: column; gap: 12px; }
.cb-list-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px dashed #444; }
.cb-item-card { background: #2a2a2a; border-radius: 4px; padding: 10px; border-left: 3px solid #0066cc; flex: 1 1 300px; min-width: 250px; display: flex; flex-direction: column; gap: 6px; }
@@ -8,6 +8,14 @@ let availableLoras = null;
let loraFolders = null;
let availableModels = null;
let modelFolders = null;
let availableDiffusionModels = null;
let diffusionModelFolders = null;
let availableGGUFModels = null;
let ggufModelFolders = null;
let availableTextEncoders = null;
let textEncoderFolders = null;
let clipTypes = [];
let dualClipTypes = [];
let availableSessions = ["None"];
let availableConfigs = ["None"];
@@ -22,6 +30,14 @@ export function clearAllCaches() {
loraFolders = null;
availableModels = null;
modelFolders = null;
availableDiffusionModels = null;
diffusionModelFolders = null;
availableGGUFModels = null;
ggufModelFolders = null;
availableTextEncoders = null;
textEncoderFolders = null;
clipTypes = [];
dualClipTypes = [];
}
export async function refreshAllConfigBuilders() {
@@ -129,6 +145,55 @@ export async function getModelFolders() {
return modelFolders;
}
// --- UNIFIED MODEL LISTS (for GGUF, Diffusion Models, Text Encoders) ---
export async function getModelLists() {
// Fetch all model lists from the unified endpoint
try {
const resp = await fetch("/configbuilder/model_lists", {
headers: { "X-Config-Builder-Internal": "true" }
});
const data = await resp.json();
if (data.checkpoints) {
availableModels = data.checkpoints.map(normalizePath);
modelFolders = extractFolders(availableModels);
}
availableDiffusionModels = (data.diffusion_models || []).map(normalizePath);
diffusionModelFolders = extractFolders(availableDiffusionModels);
availableGGUFModels = (data.unet_gguf || []).map(normalizePath);
ggufModelFolders = extractFolders(availableGGUFModels);
// Combine text_encoders and clip_gguf, deduplicate
const teSet = new Set([
...(data.text_encoders || []).map(normalizePath),
...(data.clip_gguf || []).map(normalizePath)
]);
availableTextEncoders = Array.from(teSet).sort();
textEncoderFolders = extractFolders(availableTextEncoders);
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`);
return data;
} catch (e) {
console.error("[ConfigBuilder] Error fetching model lists:", e);
return {};
}
}
export function getAvailableDiffusionModels() { return availableDiffusionModels || []; }
export function getDiffusionModelFolders() { return diffusionModelFolders || ["/"]; }
export function getAvailableGGUFModels() { return availableGGUFModels || []; }
export function getGGUFModelFolders() { return ggufModelFolders || ["/"]; }
export function getAvailableTextEncoders() { return availableTextEncoders || []; }
export function getTextEncoderFolders() { return textEncoderFolders || ["/"]; }
export function getClipTypes() { return clipTypes; }
export function getDualClipTypes() { return dualClipTypes; }
export async function getAvailableSessions() {
try {
const resp = await fetch("/object_info", { headers: { "X-Config-Builder-Internal": "true" } });
@@ -227,24 +292,31 @@ export function getIterationCount(configArray) {
const st_count = countSplit(configArray.steps);
const c_count = countSplit(configArray.cfg);
// 2. Models
// 2. Models (handles both string format and object format {path, type})
let m_count = 0;
if (!configArray.models || configArray.models.length === 0) {
m_count = 1; // Defaults to None
} else {
configArray.models.forEach(m => {
if (m === "None") {
const modelPath = typeof m === 'string' ? m : (m?.path || "None");
const modelType = typeof m === 'string' ? 'checkpoint' : (m?.type || 'checkpoint');
// Pick the correct file list for folder expansion
let modelList;
if (modelType === 'gguf') modelList = availableGGUFModels;
else if (modelType === 'diffusion_model') modelList = availableDiffusionModels;
else modelList = availableModels;
if (modelPath === "None") {
m_count += 1;
} else if (m.endsWith("/")) {
// Folder
const norm = normalizePath(m);
} else if (modelPath.endsWith("/")) {
const norm = normalizePath(modelPath);
if (norm === "/" || norm === "") {
m_count += availableModels ? availableModels.length : 1;
m_count += modelList ? modelList.length : 1;
} else {
m_count += availableModels ? availableModels.filter(am => normalizePath(am).startsWith(norm)).length : 1;
m_count += modelList ? modelList.filter(am => normalizePath(am).startsWith(norm)).length : 1;
}
} else {
// Single
m_count += 1;
}
});
@@ -314,8 +386,19 @@ export function convertStateToConfigs(state) {
loraValue = loras.join(" + ");
}
// Process Models
let finalModels = configArray.models?.filter(m => m && m !== "None") || [];
// Process Models - handle object format {path, type} and string format
let modelType = "checkpoint";
let finalModels = [];
(configArray.models || []).forEach(m => {
if (typeof m === 'object' && m !== null) {
if (m.path && m.path !== "None") {
finalModels.push(m.path);
modelType = m.type || "checkpoint";
}
} else if (typeof m === 'string' && m && m !== "None") {
finalModels.push(m);
}
});
const config = {
sampler: split(configArray.samplers),
@@ -326,6 +409,17 @@ export function convertStateToConfigs(state) {
model: finalModels.length > 1 ? finalModels : finalModels[0] || "None"
};
// Add model_type and related fields for non-checkpoint models
if (modelType !== "checkpoint") {
config.model_type = modelType;
const textEncoders = (configArray.text_encoders || []).filter(te => te && te !== "None");
if (textEncoders.length > 0) config.text_encoders = textEncoders;
if (configArray.clip_type) config.clip_type = configArray.clip_type;
if (modelType === "gguf" && configArray.gguf_options) {
config.gguf_options = configArray.gguf_options;
}
}
// Add lora_omit_triggers if present
if (configArray.lora_omit_triggers && configArray.lora_omit_triggers.length > 0) {
config.lora_omit_triggers = configArray.lora_omit_triggers;
@@ -388,6 +482,9 @@ export function convertConfigsToConfigArrays(configs) {
cfg: "7.0",
seed_behavior: "fixed",
models: ["None"],
text_encoders: [],
clip_type: "stable_diffusion",
gguf_options: {},
loras: ["None"],
lora_omit_triggers: [],
lora_triggerwords_append_settings: {},
@@ -432,8 +529,16 @@ export function convertConfigsToConfigArrays(configs) {
let models = config.model;
if (!Array.isArray(models)) models = models ? [models] : ["None"];
// Normalize loaded models
models = models.map(normalizePath);
// Determine model type from config
const loadedModelType = config.model_type || "checkpoint";
// Convert models to object format if non-checkpoint, keep string for checkpoint
if (loadedModelType !== "checkpoint") {
models = models.map(m => ({ path: normalizePath(m), type: loadedModelType }));
} else {
models = models.map(normalizePath);
}
// Normalize loaded loras
loras = loras.map(normalizePath);
@@ -497,6 +602,9 @@ export function convertConfigsToConfigArrays(configs) {
cfg: toString(config.cfg || "7.0"),
seed_behavior: config.seed_behavior || "fixed",
models: models,
text_encoders: config.text_encoders || [],
clip_type: config.clip_type || "stable_diffusion",
gguf_options: config.gguf_options || {},
loras: loras,
lora_omit_triggers: omitTriggers,
lora_triggerwords_append_settings: triggerPlacements,
@@ -517,6 +625,9 @@ export function convertConfigsToConfigArrays(configs) {
cfg: "7.0",
seed_behavior: "fixed",
models: ["None"],
text_encoders: [],
clip_type: "stable_diffusion",
gguf_options: {},
loras: ["None"],
lora_omit_triggers: [],
lora_triggerwords_append_settings: {},