From 32a3719d9724b1a6c8937e432b0fd3e7046b400a Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Tue, 29 Sep 2026 15:36:07 +0800 Subject: [PATCH] fix: offline scans stay offline, workshop snap dock, 400 on malformed requests From an outside audit: - The scan wizard's offline mode still sent the greyed-out rename and overwrite options, and scraper.py fetched Civitai model pages and preview media for models whose .info held a Civitai id. Offline now implies no media and no model page; the folder scan and scan_all refuse a missing or non-object body instead of defaulting to an online scan. - The Prompt Workshop's snap dock referenced leftSearch from the source deck module (ReferenceError on click); the deck now exposes focusSearch(). - translate, delete_model, delete_gallery_image and the image-material routes answer malformed bodies with 400 instead of 500. - Removed six unused imports in ui_doctor.js and 16 duplicate locale keys per language (the later, effective entry is kept, so nothing changes on screen). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ api/gallery_routes.py | 5 +++++ api/material_assets.py | 2 ++ api/model_metadata.py | 5 +++++ api/scanner.py | 12 +++++++---- api/translation_routes.py | 7 +++++- scraper.py | 5 ++++- web/modules/locales.js | 32 ---------------------------- web/modules/ui_doctor.js | 10 +-------- web/modules/ui_prompt_source_deck.js | 1 + web/modules/ui_prompt_workbench.js | 2 +- web/modules/ui_scan_wizard.js | 12 ++++++----- 12 files changed, 43 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 562a688..1e255aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ - **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 +- **Offline scan stays offline (离线扫描不再联网)**: "Offline fast read" no longer sends the greyed-out rename and overwrite options, and an offline scan never fetches Civitai model pages or preview images, even for models whose `.info` came from Civitai. Scan requests without options are refused instead of starting an online scan. +- Clicking the drop zone at the end of the Prompt Workshop's assembly track focuses the card search again instead of failing silently. +- Malformed requests to translation, deletion, scanning and image-material routes get a clear 400 instead of a server error. - All-in-one loader recipes and cross-node prompt injection use one verified adapter table; prompt text never crosses between positive and negative. - The Workflows, Prompt Notes and Material Library workspace no longer covers the header: the Models / Gallery / Workflows tabs stay clickable while it is open, and **Esc** closes it (back to what was open before) unless a dialog is above it or a text field has focus. Closing the Prompt Translator brings back the browser it folded away. diff --git a/api/gallery_routes.py b/api/gallery_routes.py index 4a424d0..e0c3e03 100644 --- a/api/gallery_routes.py +++ b/api/gallery_routes.py @@ -83,6 +83,11 @@ async def api_get_gallery_images(request): async def api_delete_gallery_image(request): try: data = await request.json() + except ValueError: + data = None + if not isinstance(data, dict): + return web.json_response({"status": "error", "message": "Invalid request body"}, status=400) + try: filename = data.get("filename") subfolder = data.get("subfolder", "") diff --git a/api/material_assets.py b/api/material_assets.py index bcdadca..cb22e50 100644 --- a/api/material_assets.py +++ b/api/material_assets.py @@ -25,6 +25,8 @@ def _material_assets_dir(materials_dir, filename, create=False): def _inspect_source_image(source_image): source = _normalise_source_image(source_image) + if source is None: + raise ValueError("Missing source image") if not source["filename"].lower().endswith(".png"): raise ValueError("Only PNG output images can contain reusable workflow metadata") source_path = _output_source_path(source) diff --git a/api/model_metadata.py b/api/model_metadata.py index 0cf3ff9..20b0ffe 100644 --- a/api/model_metadata.py +++ b/api/model_metadata.py @@ -75,6 +75,11 @@ def _reset_model_cover(base_path): async def api_delete_model(request): try: data = await request.json() + except ValueError: + data = None + if not isinstance(data, dict): + return web.json_response({"status": "error", "message": "Invalid request body"}, status=400) + try: folder_type = data.get('type', 'checkpoints') subfolder = data.get('subfolder', '/') filename = data.get('filename', '') diff --git a/api/scanner.py b/api/scanner.py index 84de821..959e053 100644 --- a/api/scanner.py +++ b/api/scanner.py @@ -274,8 +274,10 @@ async def api_scan_folder(request): try: data = await request.json() - except Exception: - data = {} + except ValueError: + data = None + if not isinstance(data, dict): + return web.json_response({"status": "error", "message": "Invalid request body"}, status=400) offline_only = data.get("offline_only", False) skip_rename = data.get("skip_rename", False) @@ -361,8 +363,10 @@ async def api_scan_all(request): try: data = await request.json() - except Exception: - data = {} + except ValueError: + data = None + if not isinstance(data, dict): + return web.json_response({"status": "error", "message": "Invalid request body"}, status=400) offline_only = data.get("offline_only", False) use_local_metadata = data.get("use_local_metadata", True) diff --git a/api/translation_routes.py b/api/translation_routes.py index f8cd987..c6bf49b 100644 --- a/api/translation_routes.py +++ b/api/translation_routes.py @@ -73,7 +73,12 @@ def _translate_with_mymemory(text, tl): async def api_translate(request): try: data = await request.json() - text = str(data.get("text", "")).strip() + except ValueError: + data = None + if not isinstance(data, dict): + return web.json_response({"status": "error", "message": "Invalid request body"}, status=400) + text = str(data.get("text", "")).strip() + try: tl = str(data.get("target_lang", "zh-CN")).strip() if not text: return web.json_response({"translated": "", "status": "success"}) diff --git a/scraper.py b/scraper.py index d534c4a..4cf9a13 100644 --- a/scraper.py +++ b/scraper.py @@ -230,6 +230,9 @@ def main(): parser.add_argument("--folder-type", default="", help="由 ComfyUI 传入的模型目录类型,用于执行安全策略") parser.add_argument("--progress-file", default="", help=argparse.SUPPRESS) args = parser.parse_args() + if args.offline_only: + # Offline means no network at all: previews and model pages come from Civitai. + args.skip_media = True target_folder = args.folder if not os.path.isdir(target_folder): @@ -396,7 +399,7 @@ def main(): # --- 额外获取模型主页的说明文字 --- model_id = civitai_data.get("modelId") - if model_id and model_id != -1: + if model_id and model_id != -1 and not args.offline_only: try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" diff --git a/web/modules/locales.js b/web/modules/locales.js index 0597966..135fab0 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -207,11 +207,7 @@ export const i18n = { recipeScopePill_partial: "局部子图", recipeViewGrid: "网格视图", recipeViewList: "列表视图", - recipeDragHint: "按住拖拽至画布打开新工作流", - recipeDragTargetCanvas: "释放以在画布打开新工作流", - recipeStatusReady: "模型全部就绪", recipeStatusUnverified: "模型待校验", - recipeStatusMissing: "模型缺失", recipeReadinessHint: "检测工作流所需模型在本地环境中的就绪状态,点击卡片进入详情可进行智能比对与替换", promptEnablePart: "启用这个片段", promptUnnamedPart: "未命名片段", @@ -251,21 +247,9 @@ export const i18n = { notebookCompanionModels: "配套模型", notebookMore: "更多", notebookDoneEditing: "📑 查看双语分词", - editRaw: "✏️ 编辑原始文本", - saveNotebook: "保存笔记", - deleteNotebook: "删除笔记", - delSure: "确认删除?", - sendToCanvas: "发送到画布", - createNotebook: "新建笔记", - newNotebookName: "新笔记名称...", recipeSearchAndReplace: "词条替换与目标语言", - findPlaceholder: "查找...", - replacePlaceholder: "替换为...", - replaceAll: "全部替换", recipeDiffNone: "未选择", loras: "LoRA 模型", - baseModel: "底模", - mainModel: "主模型", nbNavHeading: "快速直达", nbNotesHeading: "我的笔记", nbNavModels: "配套模型", @@ -1948,11 +1932,7 @@ export const i18n = { recipeScopePill_partial: "Sub-graph", recipeViewGrid: "Grid View", recipeViewList: "List View", - recipeDragHint: "Hold & drag onto canvas to load workflow", - recipeDragTargetCanvas: "Drop to load workflow in canvas", - recipeStatusReady: "All Models Ready", recipeStatusUnverified: "Models to Verify", - recipeStatusMissing: "Models Missing", recipeReadinessHint: "Detects readiness of required models in local environment; click card details for smart matching and replacement", promptEnablePart: "Enable this fragment", promptUnnamedPart: "Untitled fragment", @@ -1992,21 +1972,9 @@ export const i18n = { notebookCompanionModels: "Companion models", notebookMore: "More", notebookDoneEditing: "📑 View Bilingual Tags", - editRaw: "✏️ Edit Raw Prompt", - saveNotebook: "Save Note", - deleteNotebook: "Delete Note", - delSure: "Confirm Delete?", - sendToCanvas: "Send to Canvas", - createNotebook: "New Note", - newNotebookName: "New note name...", recipeSearchAndReplace: "Find, Replace & Language", - findPlaceholder: "Find...", - replacePlaceholder: "Replace with...", - replaceAll: "Replace All", recipeDiffNone: "None", loras: "LoRA Models", - baseModel: "Base Model", - mainModel: "Main Model", nbNavHeading: "Quick Jump", nbNotesHeading: "My Notes", nbNavModels: "Companion Models", diff --git a/web/modules/ui_doctor.js b/web/modules/ui_doctor.js index dfa4515..fb6141d 100644 --- a/web/modules/ui_doctor.js +++ b/web/modules/ui_doctor.js @@ -1,19 +1,11 @@ -import { applyNodeMaterialValues } from './node_material_actions.js'; -import { applyMaterialToSelectedNode } from './ui_material_application.js'; import { app } from "../../../scripts/app.js"; import { translate } from './locales.js'; import { escapeHtml } from './safe_dom.js'; import { analyzeModelChainInsertion, getModelChainInsertionCapabilities, - spliceModelChainNode, } from './graph_splice.js'; -import { - collectMainModelContextRequests, - formatModelTypeLabel, - getBaseModelFamily, - inferPickerModelType, -} from './model_picker.js'; +import { inferPickerModelType } from './model_picker.js'; import { requiresHashForModelRecovery } from './model_policies.js'; import { findModelComboWidget, getNativeWidgetValues } from './ui_node_model_picker.js'; import { renderParameterPresets } from './ui_node_presets.js'; diff --git a/web/modules/ui_prompt_source_deck.js b/web/modules/ui_prompt_source_deck.js index 0529d77..9d042f6 100644 --- a/web/modules/ui_prompt_source_deck.js +++ b/web/modules/ui_prompt_source_deck.js @@ -789,5 +789,6 @@ export function createPromptSourceDeck(workbenchGrid, drawer, scope, addSourceCa refresh: renderSourceCardsList, sync: syncMaterialsIntoSourceDeck, hidePreview: (force = false) => hideCardPreviewPopover(force), + focusSearch: () => leftSearch.focus(), }; } diff --git a/web/modules/ui_prompt_workbench.js b/web/modules/ui_prompt_workbench.js index 43460cb..9036d49 100644 --- a/web/modules/ui_prompt_workbench.js +++ b/web/modules/ui_prompt_workbench.js @@ -704,7 +704,7 @@ export function createPromptWorkbench(owner, container, scope, options) { `; setupDropzoneListeners(snapDock); snapDock.onclick = () => { - leftSearch?.focus?.(); + sourceDeck.focusSearch(); showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? '点击左侧词卡即可直接加入此处' : 'Click any card on left to add here'); }; } diff --git a/web/modules/ui_scan_wizard.js b/web/modules/ui_scan_wizard.js index 3f83b1b..c552a34 100644 --- a/web/modules/ui_scan_wizard.js +++ b/web/modules/ui_scan_wizard.js @@ -533,12 +533,14 @@ export function openScanWizard({ isGlobal = false, targetFiles = null } = {}) { // Execute Scan Logic const doScan = async () => { try { + // Offline greys out renaming and overwriting, so none of them may be sent. + const offline = scanMode === 'offline'; const reqBody = { - offline_only: scanMode === 'offline', - skip_rename: !enableRename, - virtual_rename: enableRename ? enableVirtualRename : false, - physical_rename: enableRename ? enablePhysicalRename : false, - force_overwrite: enableForceOverwrite + offline_only: offline, + skip_rename: offline || !enableRename, + virtual_rename: !offline && enableRename && enableVirtualRename, + physical_rename: !offline && enableRename && enablePhysicalRename, + force_overwrite: !offline && enableForceOverwrite }; if (targetMode === 'custom') {