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 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-09-29 15:36:07 +08:00
co-authored by Claude Opus 5.5
parent a083cfdc54
commit 32a3719d97
12 changed files with 43 additions and 53 deletions
+3
View File
@@ -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.
+5
View File
@@ -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", "")
+2
View File
@@ -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)
+5
View File
@@ -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', '')
+8 -4
View File
@@ -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)
+6 -1
View File
@@ -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"})
+4 -1
View File
@@ -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"
-32
View File
@@ -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",
+1 -9
View File
@@ -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';
+1
View File
@@ -789,5 +789,6 @@ export function createPromptSourceDeck(workbenchGrid, drawer, scope, addSourceCa
refresh: renderSourceCardsList,
sync: syncMaterialsIntoSourceDeck,
hidePreview: (force = false) => hideCardPreviewPopover(force),
focusSearch: () => leftSearch.focus(),
};
}
+1 -1
View File
@@ -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');
};
}
+7 -5
View File
@@ -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') {