From 42d8c7bbfcd6c80fcb23d9b71dd601f3e0141318 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Sun, 27 Sep 2026 17:28:18 +0800 Subject: [PATCH] fix(backend): restore imports lost in the module split Saving a workflow recipe failed for any workflow with a model loader: recipe_images.py called _resolve_exact_model_reference and hashlib, which stayed behind in recipe_schema.py when 6ad975c split the module. The resolver moves to path_utils.py so both recipe modules can import it without a cycle. The same split left media_routes.py without time/tempfile (card thumbnails silently fell back to full-size covers) and model_metadata.py without _resolve_paths_to_model_info_sync (editing a moved model failed). Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 3 ++- CHANGELOG.md | 7 ++++++- api/media_routes.py | 2 ++ api/model_metadata.py | 1 + api/path_utils.py | 35 +++++++++++++++++++++++++++++++++++ api/recipe_images.py | 2 ++ api/recipe_schema.py | 34 +--------------------------------- 7 files changed, 49 insertions(+), 35 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a961b0c..880a14e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -64,7 +64,8 @@ DOM or live LiteGraph state. ### Backend - `api/config.py` owns configured paths and active model-folder types. -- `api/path_utils.py` owns containment, filename validation, and atomic JSON writes; +- `api/path_utils.py` owns containment, filename validation, atomic JSON writes, and resolving + a saved model value inside the model folders; `api/utils.py` is a compatibility export surface. - `api/metadata.py` owns sidecar and safetensors metadata extraction. - `api/model_catalog.py`, `api/model_resolution.py`, `api/model_metadata.py`, and diff --git a/CHANGELOG.md b/CHANGELOG.md index c6ed4c8..c75c416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # 📈 Anomalous Model Browser Changelog -## v1.57.2 Beta (Canvas Performance Hotfix) — 2026-09-26 +## v1.57.2 Beta (Hotfix: Canvas Performance, Saving Workflows) — 2026-09-27 + +### 💾 Saving workflows and Prompt Notes (保存工作流与提示词笔记) +- **Saving a workflow works again (保存工作流失败)**: since v1.57, **Save Current Workflow** failed with "Could not save this Workflow Recipe" for any workflow with a model loader, because splitting the backend into smaller files left the save path calling a function that was no longer imported. The same split also made model cards load full-size cover images instead of thumbnails (slow on big folders), and made editing details of a model that had moved fail. +- **Prompt Notes with UNet models (提示词笔记与 UNet 模型)**: sending a note with a UNet / diffusion model to the canvas built a graph that could not run, because its LoRAs waited for a CLIP the UNet does not have. LoRAs of a UNet now load model-only, and the note says to connect your own text encoder to the two prompt nodes. Models whose metadata names no base model (most models not downloaded from Civitai) were missing from every group; they now appear under **No base model listed**. Listing models no longer blocks ComfyUI while it reads the folders. +- Installed v1.57.2 before 2026-09-27 through the version panel? Pick v1.57.2 there once more to get this build; updating through ComfyUI Manager gets it as usual. ### ⚡ Smooth canvas dragging again (画布拖动卡顿修复) - **Dragging and zooming the canvas no longer slows down with this plugin installed (安装插件后拖动画布掉帧)**: one stylesheet rule, which only hid the sidebar tooltip while the Toolbox or Settings popup was open, made the browser re-check the whole page every time any element's inline style changed. ComfyUI moves every text-box widget on every frame while you pan or zoom, so each frame paid for it, even with the browser closed. Measured on a 300-node workflow in ComfyUI: style work while panning drops from about 11× the no-plugin cost back to the same as without the plugin. diff --git a/api/media_routes.py b/api/media_routes.py index c6896dd..77f7944 100644 --- a/api/media_routes.py +++ b/api/media_routes.py @@ -5,7 +5,9 @@ import hashlib import json import os import struct +import tempfile import threading +import time import urllib.parse from aiohttp import web diff --git a/api/model_metadata.py b/api/model_metadata.py index edcb4a0..0cf3ff9 100644 --- a/api/model_metadata.py +++ b/api/model_metadata.py @@ -14,6 +14,7 @@ from .model_constants import ( CIVITAI_BACKUP_SUFFIXES, MEDIA_EXTENSIONS, MODEL_EXTENSIONS, PREVIEW_SUFFIXES, SIDECAR_SUFFIXES, ) +from .model_catalog import _resolve_paths_to_model_info_sync from .utils import require_filename, resolve_folder_subdir, resolve_within def _first_existing_sidecar(base_path, suffixes): diff --git a/api/path_utils.py b/api/path_utils.py index d6bc0f2..b2f931b 100644 --- a/api/path_utils.py +++ b/api/path_utils.py @@ -6,6 +6,8 @@ import tempfile import folder_paths +from .recipe_constants import MODEL_FILE_SUFFIXES + def resolve_within(base_dir, *parts): """Resolve a path and require it to stay inside base_dir, including through symlinks.""" @@ -19,6 +21,39 @@ def resolve_within(base_dir, *parts): return candidate +def _model_roots(): + for folder_type in getattr(folder_paths, "folder_names_and_paths", {}): + try: + paths = folder_paths.get_folder_paths(folder_type) + except Exception: + continue + for path_index, base_dir in enumerate(paths or []): + if os.path.isdir(base_dir): + yield folder_type, path_index, os.path.realpath(base_dir) + + +def _resolve_exact_model_reference(saved_value): + """Resolve one saved model value without recursive scanning or hashing.""" + if not isinstance(saved_value, str) or not saved_value.strip(): + return None + relative_value = saved_value.replace("/", os.sep) + for folder_type, path_index, base_dir in _model_roots(): + candidate = os.path.realpath(os.path.join(base_dir, relative_value)) + try: + if os.path.commonpath((base_dir, candidate)) != base_dir: + continue + except ValueError: + continue + if not os.path.isfile(candidate) or not candidate.lower().endswith(MODEL_FILE_SUFFIXES): + continue + return { + "path": candidate, + "folder_type": folder_type, + "path_index": path_index, + } + return None + + def get_folder_root(folder_type, path_idx=0): paths = folder_paths.get_folder_paths(folder_type) if not paths or not isinstance(path_idx, int) or path_idx < 0 or path_idx >= len(paths): diff --git a/api/recipe_images.py b/api/recipe_images.py index 82920e8..a58d90d 100644 --- a/api/recipe_images.py +++ b/api/recipe_images.py @@ -1,6 +1,7 @@ """Contained Recipe image assets and bounded output-gallery inspection.""" from io import BytesIO +import hashlib import heapq import json import os @@ -8,6 +9,7 @@ import os import folder_paths from PIL import Image +from .path_utils import _resolve_exact_model_reference from .recipe_constants import * from .utils import require_filename, resolve_within from .workflow_schema import _node_type, _parameter_signature, _workflow_node_signature diff --git a/api/recipe_schema.py b/api/recipe_schema.py index e3d9d8a..d4a9a8e 100644 --- a/api/recipe_schema.py +++ b/api/recipe_schema.py @@ -10,6 +10,7 @@ import folder_paths from .metadata import get_metadata from .recipe_constants import * +from .path_utils import _model_roots, _resolve_exact_model_reference from .recipe_images import _attach_preview_snapshots from .utils import require_filename, resolve_within from .workflow_schema import ( @@ -44,39 +45,6 @@ def _model_reference_specs(node): return specs -def _model_roots(): - for folder_type in getattr(folder_paths, "folder_names_and_paths", {}): - try: - paths = folder_paths.get_folder_paths(folder_type) - except Exception: - continue - for path_index, base_dir in enumerate(paths or []): - if os.path.isdir(base_dir): - yield folder_type, path_index, os.path.realpath(base_dir) - - -def _resolve_exact_model_reference(saved_value): - """Resolve one saved model value without recursive scanning or hashing.""" - if not isinstance(saved_value, str) or not saved_value.strip(): - return None - relative_value = saved_value.replace("/", os.sep) - for folder_type, path_index, base_dir in _model_roots(): - candidate = os.path.realpath(os.path.join(base_dir, relative_value)) - try: - if os.path.commonpath((base_dir, candidate)) != base_dir: - continue - except ValueError: - continue - if not os.path.isfile(candidate) or not candidate.lower().endswith(MODEL_FILE_SUFFIXES): - continue - return { - "path": candidate, - "folder_type": folder_type, - "path_index": path_index, - } - return None - - def _identity_for_reference(saved_value): resolved = _resolve_exact_model_reference(saved_value) if not resolved: