From 2104bb659f4782cf1a705a51d56eaae7da2cba43 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Sat, 3 Oct 2026 13:51:46 +0800 Subject: [PATCH] List GGUF, .pth, .sft and .bin models, in any case The Models page, the folder counts, search and lookups only matched .safetensors, .ckpt and .pt in lower case, so quantized GGUF models and most upscalers never showed. One list, MODEL_EXTENSIONS, and is_model_file() now decide what a model file is everywhere. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + api/model_catalog.py | 27 +++++++++++++-------------- api/model_constants.py | 9 ++++++++- docs/architecture/backend.md | 4 +++- docs/plans/model-source-links.md | 2 +- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82cd891..1d6362e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ - **GPT-SoVITS setup and import (GPT-SoVITS 准备与导入)**: an empty studio is a drop area with **Import character**, which also sits in the toolbar. **GPT-SoVITS settings** at the bottom of the sidebar opens a dialog with where characters are kept, pretrained files and missing Python packages; missing pretrained files only get a small dot on that entry, with "don't remind me", once your characters need them, and the import form notes what the chosen language will download. **Batch import (批量导入)**: the import window takes one or many characters. It first asks what you have (one character, many characters or a package, or files for a character you already have), with three steps at the top and a spotlight tour the first time ("?" replays it). Each character card leads with a checklist that lights up the next step (GPT model, SoVITS model, clips, main voice, name) and offers its button right there; the card says how many steps are left. Drop files or whole folders, or pick a folder on this PC (a GPT-SoVITS package): a folder holding one set of GPT + SoVITS weights and its clips is one character named after the folder (two characters whose weights are both called `model` stay apart; `派蒙/v2` becomes "派蒙 v2"); weights in a package's weight folders pair up by name (latest epoch kept); clips and line files follow their character folder, the weights in their folder or the character their folder or file name points at; an annotation file (`.list`) no character claims goes to every character; two clips with the same name (`开心/01.wav`, `难过/01.wav`) both go in, the second as `难过_01.wav`; anything unclear waits in a card of its own, one line per folder that can be assigned at once, and is uploaded only once it is put into a character. Clips outside 3–10 s are left out with their line files. Python environments and base models (`runtime`, `pretrained_models`, `venv`…) are skipped, and a package's program and training folders (`GPT_SoVITS`, `logs`, `output`, `TEMP`, `tools`) only inside a package: your own folders called `output` or `GPT_SoVITS`, and the folder you chose or dropped, are always read; the window says which folders were skipped or too deep (6 levels) instead of files going missing quietly. A big add opens 20 characters (about 800 files) at a time; **Next batch** opens the rest, and it opens by itself once a batch is imported, so a huge folder does not freeze the window. Every clip has **▶** to listen while importing — uploading, unassigned or picked on this PC — and clicking another clip switches to it for quick comparing. Long clip lists show the first 60, and a very large upload first suggests picking the folder on this PC instead; uploads run three at a time. Each character is a card: one line with its name, a short status and what it holds, unfolding into its model tiles and clips; files move between characters with the picker on each row. **Import N characters** imports each ready one on its own, so one failure leaves the others alone. Status is colour-coded throughout: green ready, amber still missing something, red a failed or refused file, blue uploading; **Import** stays at the bottom. Adding to a character skips files it already has and merges annotation files (Anomalous_TTS interface 5). There is one storage place (default `models/gpt_sovits`); changing it can move the existing characters there with progress. Download pretrained files with progress or use an existing GPT-SoVITS package, all without editing `extra_model_paths.yaml` or restarting. **Import character** builds a character from dropped or picked GPT / SoVITS weights, reference clips and annotation files: the name, SoVITS version, clip lengths and lines are filled in automatically, and everything is copied (originals stay). Drop files on a card, or use **Add files**, to add clips and emotions to an existing character. Lines are found in same-name `.txt` / `.lab` files, annotation files or, as a marked guess, the file name; a text file dropped on a clip's line box fills it. Needs Anomalous_TTS with interface version 11; file actions only work on the computer running ComfyUI. ### Fixes +- **GGUF and .pth models show up (GGUF、.pth 模型能看到了)**: the Models page, the folder list's counts, search and model lookups only knew `.safetensors`, `.ckpt` and `.pt`, in lower case, so quantized GGUF models (Wan, Flux, Qwen…), most upscalers (`.pth`), `.sft` and `.bin` files and names like `MODEL.SAFETENSORS` were missing. They are all listed now. Scans still read only `.safetensors`; the others stay under **Not scanned yet**. - **Language follows ComfyUI again (语言跟随 ComfyUI)**: with the language left on "Follow ComfyUI" (the default), changing ComfyUI's language now switches the browser the next time it opens; before, it kept the old language until the page was reloaded. - **Empty list on the audio pages (音频页左边列表是空的)**: after reloading ComfyUI, a browser that reopened straight onto Voices or Audio showed an empty list column until you switched to the image side and back. - The Material Library's top bar no longer cuts off **Prompt Studio**, **Prompt Notes** and refresh when the window is narrow (docked to the side, or a scaled display): the buttons move to a second row. diff --git a/api/model_catalog.py b/api/model_catalog.py index aeb815a..1fb5364 100644 --- a/api/model_catalog.py +++ b/api/model_catalog.py @@ -9,7 +9,7 @@ from aiohttp import web import folder_paths from .metadata import get_metadata -from .model_constants import MEDIA_EXTENSIONS, MODEL_EXTENSIONS, PREVIEW_SUFFIXES +from .model_constants import MEDIA_EXTENSIONS, PREVIEW_SUFFIXES, is_model_file from .model_media import _cache_token, _preview_url_for_model from .folder_types import ( get_active_folder_types, get_active_model_roots, @@ -32,7 +32,7 @@ def _collect_folder_models(target_dir, folder_type, path_idx, rel_subfolder, pag return {"models": [], "total": 0, "page": page, "limit": limit} valid_files = sorted( - (name for name in file_entries if name.endswith(('.safetensors', '.ckpt', '.pt'))), + (name for name in file_entries if is_model_file(name)), key=str.lower, ) total = len(valid_files) @@ -123,7 +123,7 @@ def _collect_folders(): base_dir = item["base_dir"] tree = {} for root, dirs, files in os.walk(base_dir): - has_models = any(f.endswith(('.safetensors', '.ckpt', '.pt')) for f in files) + has_models = any(is_model_file(f) for f in files) rel = os.path.relpath(root, base_dir) if rel == '.': rel = '/' @@ -133,7 +133,7 @@ def _collect_folders(): "path": rel, "name": os.path.basename(root) if rel != '/' else '[Root]', "has_models": has_models, - "model_count": sum(1 for f in files if f.endswith(('.safetensors', '.ckpt', '.pt'))) + "model_count": sum(1 for f in files if is_model_file(f)) } label = item["bn"] @@ -166,7 +166,7 @@ def _collect_folders(): tree = {} for root, dirs, files in os.walk(base_dir): - has_models = any(f.endswith(('.safetensors', '.ckpt', '.pt')) for f in files) + has_models = any(is_model_file(f) for f in files) rel = os.path.relpath(root, base_dir) if rel == '.': rel = '/' @@ -176,7 +176,7 @@ def _collect_folders(): "path": rel, "name": os.path.basename(root) if rel != '/' else '[Root]', "has_models": has_models, - "model_count": sum(1 for f in files if f.endswith(('.safetensors', '.ckpt', '.pt'))) + "model_count": sum(1 for f in files if is_model_file(f)) } try: @@ -253,7 +253,7 @@ def _iter_search_models(): continue for root, _, files in os.walk(base_dir): for filename in files: - if filename.lower().endswith(MODEL_EXTENSIONS + ('.sft',)): + if is_model_file(filename): yield folder_type, path_idx, base_dir, os.path.join(root, filename) @@ -310,7 +310,7 @@ def _walk_model_files(target_types): continue for root, _, files in os.walk(base_dir): for filename in files: - if not filename.lower().endswith(MODEL_EXTENSIONS): + if not is_model_file(filename): continue file_path = os.path.join(root, filename) real_path = os.path.realpath(file_path) @@ -455,7 +455,7 @@ def _resolve_paths_to_model_info_sync(paths, folder_types=None, exact_only=False continue except ValueError: continue - if os.path.isfile(candidate) and candidate.lower().endswith(('.safetensors', '.ckpt', '.pt', '.bin', '.sft')): + if os.path.isfile(candidate) and is_model_file(candidate): exact_results[original] = _model_info_for_path(folder_type, path_idx, base_dir, candidate) unresolved = [(original, normalized) for original, normalized, _ in requested if original not in exact_results] @@ -469,7 +469,7 @@ def _resolve_paths_to_model_info_sync(paths, folder_types=None, exact_only=False for folder_type, path_idx, base_dir in roots: for root, _, files in os.walk(base_dir): for filename in files: - if not filename.lower().endswith(('.safetensors', '.ckpt', '.pt', '.bin', '.sft')): + if not is_model_file(filename): continue rel_path = os.path.relpath(os.path.join(root, filename), base_dir).replace(os.sep, '/').lower() basename = filename.lower() @@ -542,7 +542,6 @@ async def api_resolve_paths_to_previews(request): def _collect_all_scan_models(page, limit): - source_library_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin', '.sft', '.gguf'} all_tuples = [] for model_root in get_active_model_roots(): t = model_root["type"] @@ -550,7 +549,7 @@ def _collect_all_scan_models(page, limit): base_dir = model_root["base_dir"] for root, dirs, files in os.walk(base_dir): for f in files: - if os.path.splitext(f)[1].lower() in source_library_extensions: + if is_model_file(f): all_tuples.append((t, path_idx, root, base_dir, f)) all_tuples.sort(key=lambda x: (x[0], x[4].lower())) @@ -651,7 +650,7 @@ async def api_batch_select(request): if not os.path.exists(base_dir): continue for root, dirs, files in os.walk(base_dir): for f in files: - if f.endswith(('.safetensors', '.ckpt', '.pt', '.bin', '.sft')): + if is_model_file(f): file_path = os.path.join(root, f) base_name = os.path.splitext(f)[0] if matches_condition(file_path, root, base_name): @@ -681,7 +680,7 @@ async def api_batch_select(request): try: entries = os.listdir(target_dir) except: entries = [] for f in entries: - if f.endswith(('.safetensors', '.ckpt', '.pt', '.bin', '.sft')): + if is_model_file(f): file_path = os.path.join(target_dir, f) if os.path.isfile(file_path): base_name = os.path.splitext(f)[0] diff --git a/api/model_constants.py b/api/model_constants.py index 6a910ef..6816d1f 100644 --- a/api/model_constants.py +++ b/api/model_constants.py @@ -1,7 +1,9 @@ """Shared model catalog, media, and recovery constants.""" MEDIA_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.webp', '.gif', '.avif', '.mp4', '.webm', '.mov', '.avi') -MODEL_EXTENSIONS = ('.safetensors', '.ckpt', '.pt', '.bin') +# Model files the browser lists, counts and looks up: GGUF and .pth too (quantized +# diffusion models, upscalers). Compared without case: see is_model_file(). +MODEL_EXTENSIONS = ('.safetensors', '.ckpt', '.pt', '.pth', '.bin', '.sft', '.gguf') PREVIEW_SUFFIXES = tuple(f'.preview{ext}' for ext in MEDIA_EXTENSIONS) CIVITAI_BACKUP_SUFFIXES = tuple(f'.civitai_bak{ext}' for ext in MEDIA_EXTENSIONS) SIDECAR_SUFFIXES = ( @@ -14,3 +16,8 @@ RESOLVABLE_MODEL_TYPES = ( 'checkpoints', 'loras', 'unet', 'diffusion_models', 'controlnet', 'vae', 'vae_approx', 'clip', 'text_encoders', 'clip_vision', ) + + +def is_model_file(name): + """Whether a file name is a model file (MODEL_EXTENSIONS, any case).""" + return name.lower().endswith(MODEL_EXTENSIONS) diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index 6dbea6b..4147bc2 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -199,7 +199,9 @@ media. - Foundation components—`vae`, `vae_approx`, `clip`, `text_encoders`, and `clip_vision`—are never physical-rename targets. UI and all backend entry points enforce the same denial. -- Main model extensions (`.safetensors`, `.ckpt`, `.pt`, `.bin`) are never +- Model files are `MODEL_EXTENSIONS` in `api/model_constants.py` (`.safetensors`, + `.ckpt`, `.pt`, `.pth`, `.bin`, `.sft`, `.gguf`); listing, counting, search and + lookups test names with `is_model_file()`, without case. They are never sidecar suffixes. Cleanup must not delete a same-stem model with another extension. If such a sibling remains, ambiguous stem-keyed sidecars remain. - Rename/delete/reset uses centralized immutable suffix tuples and a constant diff --git a/docs/plans/model-source-links.md b/docs/plans/model-source-links.md index 0bd495a..e2c110a 100644 --- a/docs/plans/model-source-links.md +++ b/docs/plans/model-source-links.md @@ -13,7 +13,7 @@ - 中控 `fetchAllLibraryModels()` 调用 `/anomalous/all_scan_models`,后端 `_collect_all_scan_models()` 只读分类模式的 `get_active_folder_types()`,没有遵循当前物理目录模式。 - 本次本地配置中,物理目录模式的 `clip` 已开启,但分类配置的 `text_encoders` 关闭。这能解释本地库遗漏 CLIP;不能因此认定所有缺项都只有这一种原因。 -- 此接口只枚举 `.safetensors/.ckpt/.pt/.bin/.sft`,遗漏 `.gguf/.pth`,且扩展名比较区分大小写。 +- 此接口只枚举 `.safetensors/.ckpt/.pt/.bin/.sft`,遗漏 `.gguf/.pth`,且扩展名比较区分大小写。(已修:统一用 `is_model_file()`。) - 上轮提交只补了工作流收集、基础组件标签与对应类别的元数据读取,没有修复本地目录范围。 ### 一、目录范围与文件格式