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/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) {
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: