From 33ee04a9f30971c91261419b9acc7a4c5663f8c8 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Sat, 17 Jan 2026 05:50:26 +0000 Subject: [PATCH 1/3] feat(flux2-klein): add SDNQ pre-quantized model support and reference images - Add Transformers v5 tokenizer compatibility fix for SDNQ Klein models Downloads missing vocab.json, merges.txt, tokenizer_config.json from Z-Image-Turbo when needed - Detect SDNQ repos and disable shared text encoder to use pre-quantized weights from the SDNQ repo instead of loading from shared base model - Update reference-quant.json with correct preview images and metadata for Klein SDNQ models - Update reference-distilled.json with correct cfg_scale (1.0) for distilled Klein models per official HuggingFace documentation - Add 6 Klein model preview images --- html/reference-distilled.json | 4 ++-- html/reference-quant.json | 12 +++++------ pipelines/model_flux2_klein.py | 39 ++++++++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/html/reference-distilled.json b/html/reference-distilled.json index e77c3b4b6..dc26e97bd 100644 --- a/html/reference-distilled.json +++ b/html/reference-distilled.json @@ -168,7 +168,7 @@ "desc": "FLUX.2-klein-4B is a 4 billion parameter size-distilled version of FLUX.2-dev optimized for consumer GPUs. Achieves sub-second inference with 4 steps. Supports both text-to-image generation and multi-reference image editing. Apache 2.0 licensed.", "skip": true, "tags": "distilled", - "extras": "sampler: Default, cfg_scale: 4.0, steps: 4", + "extras": "sampler: Default, cfg_scale: 1.0, steps: 4", "size": 8.5, "date": "2025 January" }, @@ -178,7 +178,7 @@ "desc": "FLUX.2-klein-9B is a 9 billion parameter size-distilled version of FLUX.2-dev. Higher quality than 4B variant with sub-second inference using 4 steps. Supports text-to-image and multi-reference editing. Non-commercial license.", "skip": true, "tags": "distilled", - "extras": "sampler: Default, cfg_scale: 4.0, steps: 4", + "extras": "sampler: Default, cfg_scale: 1.0, steps: 4", "size": 18.5, "date": "2025 January" } diff --git a/html/reference-quant.json b/html/reference-quant.json index 2798ba4b0..edccf4bcf 100644 --- a/html/reference-quant.json +++ b/html/reference-quant.json @@ -51,20 +51,20 @@ }, "Black Forest Labs FLUX.2 Klein 4B sdnq-uint4-dynamic": { "path": "Disty0/FLUX.2-klein-4B-SDNQ-4bit-dynamic", - "preview": "black-forest-labs--FLUX.2-klein-base-4B.jpg", - "desc": "Dynamic 4 bit quantization of black-forest-labs/FLUX.2-klein-9B using SDNQ.", + "preview": "Disty0--FLUX.2-klein-4B-SDNQ-4bit-dynamic.jpg", + "desc": "Dynamic 4-bit quantization of black-forest-labs/FLUX.2-klein-4B using SDNQ.", "skip": true, - "extras": "sampler: Default, cfg_scale: 4.0, steps: 50", + "extras": "sampler: Default, cfg_scale: 1.0, steps: 4", "tags": "quantized", "size": 5.1, "date": "2025 January" }, "Black Forest Labs FLUX.2 Klein 9B sdnq-uint4-dynamic-svd": { "path": "Disty0/FLUX.2-klein-9B-SDNQ-4bit-dynamic-svd-r32", - "preview": "black-forest-labs--FLUX.2-klein-base-9B.jpg", - "desc": "FLUX.2-klein-base-9B is the undistilled 9 billion parameter base model of FLUX.2-klein. Requires 50 inference steps for full quality but offers flexibility for fine-tuning. Supports text-to-image and multi-reference editing. Non-commercial license.", + "preview": "Disty0--FLUX.2-klein-9B-SDNQ-4bit-dynamic-svd-r32.jpg", + "desc": "Dynamic 4-bit quantization of black-forest-labs/FLUX.2-klein-9B using SDNQ with SVD rank 32.", "skip": true, - "extras": "sampler: Default, cfg_scale: 4.0, steps: 50", + "extras": "sampler: Default, cfg_scale: 1.0, steps: 4", "tags": "quantized", "size": 11.7, "date": "2025 January" diff --git a/pipelines/model_flux2_klein.py b/pipelines/model_flux2_klein.py index d810821d9..0b6ce47e0 100644 --- a/pipelines/model_flux2_klein.py +++ b/pipelines/model_flux2_klein.py @@ -1,23 +1,58 @@ +import os +import shutil import transformers import diffusers from modules import shared, devices, sd_models, model_quant, sd_hijack_te, sd_hijack_vae from pipelines import generic +def ensure_tokenizer_files(checkpoint_info): + """Ensure tokenizer files are compatible with Transformers v4.""" + local_path = checkpoint_info.path + if not os.path.isdir(local_path): + return + tokenizer_path = os.path.join(local_path, 'tokenizer') + if not os.path.isdir(tokenizer_path): + return + + # Check all required files exist (v5 tokenizers may be missing these or have incompatible configs) + required = ['vocab.json', 'merges.txt', 'tokenizer_config.json'] + missing = [f for f in required if not os.path.exists(os.path.join(tokenizer_path, f))] + if not missing: + return + + # Download v4-compatible tokenizer files from Z-Image-Turbo + shared.log.debug(f'Load model: fetching v4-compatible tokenizer from Z-Image-Turbo (missing: {missing})') + try: + from huggingface_hub import hf_hub_download + for f in required: + src = hf_hub_download('Tongyi-MAI/Z-Image-Turbo', f'tokenizer/{f}', cache_dir=shared.opts.hfcache_dir) + shutil.copy(src, os.path.join(tokenizer_path, f)) + except Exception as e: + shared.log.warning(f'Load model: failed to fetch tokenizer files: {e}') + + def load_flux2_klein(checkpoint_info, diffusers_load_config=None): if diffusers_load_config is None: diffusers_load_config = {} repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) + # Ensure tokenizer files exist for models created with Transformers v5 + ensure_tokenizer_files(checkpoint_info) + + # Detect SDNQ pre-quantized repo - disable shared text encoder to use pre-quantized weights from SDNQ repo + is_sdnq = 'sdnq' in repo_id.lower() + load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) - shared.log.debug(f'Load model: type=Flux2Klein repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + shared.log.debug(f'Load model: type=Flux2Klein repo="{repo_id}" prequant={"sdnq" if is_sdnq else "none"} config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') # Load transformer - Klein uses Flux2Transformer2DModel (same class as Flux2, different size) transformer = generic.load_transformer(repo_id, cls_name=diffusers.Flux2Transformer2DModel, load_config=diffusers_load_config) # Load text encoder - Klein uses Qwen3 (4B for Klein-4B, 8B for Klein-9B) - text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.Qwen3ForCausalLM, load_config=diffusers_load_config) + # For SDNQ repos, disable shared text encoder to use pre-quantized weights from the repo + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.Qwen3ForCausalLM, load_config=diffusers_load_config, allow_shared=not is_sdnq) pipe = diffusers.Flux2KleinPipeline.from_pretrained( repo_id, From dd9001b20d3abc5d51880480aaac4cc83f0d99f0 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Sat, 17 Jan 2026 21:29:55 +0000 Subject: [PATCH 2/3] fix(model_flux2_klein): update generic.py and pipeline --- pipelines/generic.py | 3 ++- pipelines/model_flux2_klein.py | 39 ++-------------------------------- 2 files changed, 4 insertions(+), 38 deletions(-) diff --git a/pipelines/generic.py b/pipelines/generic.py index c3fd8fd42..6ad9a3fd5 100644 --- a/pipelines/generic.py +++ b/pipelines/generic.py @@ -203,7 +203,8 @@ def load_text_encoder(repo_id, cls_name, load_config=None, subfolder="text_encod # Qwen3ForCausalLM - shared text encoders by hidden_size: # - Z-Image, Klein-4B: Qwen3-4B (hidden_size=2560) # - Klein-9B: Qwen3-8B (hidden_size=4096) - elif cls_name == transformers.Qwen3ForCausalLM and allow_shared and shared.opts.te_shared_t5: + # SDNQ repos for Klein and Z-Image contain text encoders pre-quantized with different quantization methods, skip shared loading + elif cls_name == transformers.Qwen3ForCausalLM and allow_shared and shared.opts.te_shared_t5 and 'sdnq' not in repo_id.lower(): if '-9b' in repo_id.lower(): shared_repo = 'black-forest-labs/FLUX.2-klein-9B' # 9B variants use Qwen3-8B else: diff --git a/pipelines/model_flux2_klein.py b/pipelines/model_flux2_klein.py index 0b6ce47e0..d810821d9 100644 --- a/pipelines/model_flux2_klein.py +++ b/pipelines/model_flux2_klein.py @@ -1,58 +1,23 @@ -import os -import shutil import transformers import diffusers from modules import shared, devices, sd_models, model_quant, sd_hijack_te, sd_hijack_vae from pipelines import generic -def ensure_tokenizer_files(checkpoint_info): - """Ensure tokenizer files are compatible with Transformers v4.""" - local_path = checkpoint_info.path - if not os.path.isdir(local_path): - return - tokenizer_path = os.path.join(local_path, 'tokenizer') - if not os.path.isdir(tokenizer_path): - return - - # Check all required files exist (v5 tokenizers may be missing these or have incompatible configs) - required = ['vocab.json', 'merges.txt', 'tokenizer_config.json'] - missing = [f for f in required if not os.path.exists(os.path.join(tokenizer_path, f))] - if not missing: - return - - # Download v4-compatible tokenizer files from Z-Image-Turbo - shared.log.debug(f'Load model: fetching v4-compatible tokenizer from Z-Image-Turbo (missing: {missing})') - try: - from huggingface_hub import hf_hub_download - for f in required: - src = hf_hub_download('Tongyi-MAI/Z-Image-Turbo', f'tokenizer/{f}', cache_dir=shared.opts.hfcache_dir) - shutil.copy(src, os.path.join(tokenizer_path, f)) - except Exception as e: - shared.log.warning(f'Load model: failed to fetch tokenizer files: {e}') - - def load_flux2_klein(checkpoint_info, diffusers_load_config=None): if diffusers_load_config is None: diffusers_load_config = {} repo_id = sd_models.path_to_repo(checkpoint_info) sd_models.hf_auth_check(checkpoint_info) - # Ensure tokenizer files exist for models created with Transformers v5 - ensure_tokenizer_files(checkpoint_info) - - # Detect SDNQ pre-quantized repo - disable shared text encoder to use pre-quantized weights from SDNQ repo - is_sdnq = 'sdnq' in repo_id.lower() - load_args, _quant_args = model_quant.get_dit_args(diffusers_load_config, allow_quant=False) - shared.log.debug(f'Load model: type=Flux2Klein repo="{repo_id}" prequant={"sdnq" if is_sdnq else "none"} config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') + shared.log.debug(f'Load model: type=Flux2Klein repo="{repo_id}" config={diffusers_load_config} offload={shared.opts.diffusers_offload_mode} dtype={devices.dtype} args={load_args}') # Load transformer - Klein uses Flux2Transformer2DModel (same class as Flux2, different size) transformer = generic.load_transformer(repo_id, cls_name=diffusers.Flux2Transformer2DModel, load_config=diffusers_load_config) # Load text encoder - Klein uses Qwen3 (4B for Klein-4B, 8B for Klein-9B) - # For SDNQ repos, disable shared text encoder to use pre-quantized weights from the repo - text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.Qwen3ForCausalLM, load_config=diffusers_load_config, allow_shared=not is_sdnq) + text_encoder = generic.load_text_encoder(repo_id, cls_name=transformers.Qwen3ForCausalLM, load_config=diffusers_load_config) pipe = diffusers.Flux2KleinPipeline.from_pretrained( repo_id, From c07eddb58da813740fb59860ccc3b2f64321b3ef Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Sun, 18 Jan 2026 03:56:12 +0000 Subject: [PATCH 3/3] fix(ui): improve gallery sorting and IndexedDB operations - Enhance gallery sorting functionality - Update IndexedDB operations for better reliability --- javascript/gallery.js | 87 +++++++++++++++++++++++++++++-------------- javascript/indexdb.js | 5 ++- 2 files changed, 64 insertions(+), 28 deletions(-) diff --git a/javascript/gallery.js b/javascript/gallery.js index acea7127b..4405da465 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -318,7 +318,11 @@ async function addSeparators() { document.querySelectorAll('.gallery-separator').forEach((node) => { el.files.removeChild(node); }); const all = Array.from(el.files.children); let lastDir; - let isFirstSeparator = true; // Flag to open the first separator by default + + // Count root files (files without a directory path) + const hasRootFiles = all.some((f) => f.name && !f.name.match(/[/\\]/)); + // Only auto-open first separator if there are no root files to display + let isFirstSeparator = !hasRootFiles; // First pass: create separators for (const f of all) { @@ -449,7 +453,9 @@ class GalleryFile extends HTMLElement { } } - this.hash = await getHash(`${this.folder}/${this.name}/${this.size}/${this.mtime}`); // eslint-disable-line no-use-before-define + // Normalize path to ensure consistent hash regardless of which folder view is used + const normalizedPath = this.src.replace(/\/+/g, '/').replace(/\/$/, ''); + this.hash = await getHash(`${normalizedPath}/${this.size}/${this.mtime}`); // eslint-disable-line no-use-before-define const cachedData = (this.hash && opts.browser_cache) ? await idbGet(this.hash).catch(() => undefined) : undefined; const img = document.createElement('img'); img.className = 'gallery-file'; @@ -485,9 +491,11 @@ class GalleryFile extends HTMLElement { this.size = json.size; this.mtime = new Date(json.mtime); if (opts.browser_cache) { + // Store file's actual parent directory (not browsed folder) for consistent cleanup + const fileDir = this.src.replace(/\/+/g, '/').replace(/\/[^/]+$/, ''); await idbAdd({ hash: this.hash, - folder: this.folder, + folder: fileDir, file: this.name, size: this.size, mtime: this.mtime, @@ -736,59 +744,84 @@ async function gallerySort(btn) { if (arr.length === 0) return; // no files to sort if (btn) lastSort = btn.charCodeAt(0); const fragment = document.createDocumentFragment(); + + // Helper to get directory path from a file node + const getDirPath = (node) => { + const match = node.name.match(/(.*)[/\\]/); + return match ? match[1] : ''; + }; + + // Partition into root files and subfolder files - root files always stay at top + const rootFiles = arr.filter((node) => !getDirPath(node)); + const subfolderFiles = arr.filter((node) => getDirPath(node)); + + // Group subfolder files by directory + const folderGroups = new Map(); + for (const file of subfolderFiles) { + const dir = getDirPath(file); + if (!folderGroups.has(dir)) { + folderGroups.set(dir, []); + } + folderGroups.get(dir).push(file); + } + + // Sort function based on current sort mode + let sortFn; switch (lastSort) { case 61789: // name asc lastSortName = 'Name Ascending'; - arr - .sort((a, b) => a.name.localeCompare(b.name)) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => a.name.localeCompare(b.name); break; case 61790: // name dsc lastSortName = 'Name Descending'; - arr - .sort((b, a) => a.name.localeCompare(b.name)) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => b.name.localeCompare(a.name); break; case 61792: // size asc lastSortName = 'Size Ascending'; - arr - .sort((a, b) => a.size - b.size) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => a.size - b.size; break; case 61793: // size dsc lastSortName = 'Size Descending'; - arr - .sort((b, a) => a.size - b.size) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => b.size - a.size; break; case 61794: // resolution asc lastSortName = 'Resolution Ascending'; - arr - .sort((a, b) => a.width * a.height - b.width * b.height) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => a.width * a.height - b.width * b.height; break; case 61795: // resolution dsc lastSortName = 'Resolution Descending'; - arr - .sort((b, a) => a.width * a.height - b.width * b.height) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => b.width * b.height - a.width * a.height; break; case 61662: lastSortName = 'Modified Ascending'; - arr - .sort((a, b) => a.mtime - b.mtime) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => a.mtime - b.mtime; break; case 61661: lastSortName = 'Modified Descending'; - arr - .sort((b, a) => a.mtime - b.mtime) - .forEach((node) => fragment.appendChild(node)); + sortFn = (a, b) => b.mtime - a.mtime; break; default: lastSortName = 'None'; + sortFn = null; break; } + + // Sort root files + if (sortFn) { + rootFiles.sort(sortFn); + } + rootFiles.forEach((node) => fragment.appendChild(node)); + + // Sort folder names alphabetically, then sort files within each folder + const sortedFolderNames = Array.from(folderGroups.keys()).sort((a, b) => a.localeCompare(b)); + for (const folderName of sortedFolderNames) { + const files = folderGroups.get(folderName); + if (sortFn) { + files.sort(sortFn); + } + files.forEach((node) => fragment.appendChild(node)); + } + if (fragment.children.length === 0) return; el.files.innerHTML = ''; el.files.appendChild(fragment); diff --git a/javascript/indexdb.js b/javascript/indexdb.js index 21fbdfa61..ace3d5843 100644 --- a/javascript/indexdb.js +++ b/javascript/indexdb.js @@ -150,7 +150,10 @@ async function idbFolderCleanup(keepSet, folder, signal) { throw new Error('IndexedDB cleaning function must be told the current active folder'); } - let removals = new Set(await idbGetAllKeys('folder', folder)); + // Use range query to match folder and all its subdirectories + const folderNormalized = folder.replace(/\/+/g, '/').replace(/\/$/, ''); + const range = IDBKeyRange.bound(folderNormalized, `${folderNormalized}\uffff`, false, true); + let removals = new Set(await idbGetAllKeys('folder', range)); removals = removals.difference(keepSet); // Don't need to keep full set in memory const totalRemovals = removals.size; if (signal.aborted) {