A user found model folders cluttered after scans: four files per model. model_sidecars.py now says where the plugin's own side files are (.info, .anomalous.json, .civitai_bak.*): beside the model, or in the folder's hidden .amb (Settings -> Models -> Files that come with models). Every reader and writer goes through it and reads both places; covers stay beside the model for ComfyUI and other plugins. Scans no longer keep a copy of Civitai's picture identical to the cover: the cover is noted as Civitai's (SHA-256 in the .info) and a copy is kept aside only while the user's own cover shows. api/sidecar_layout.py moves existing files and drops the duplicates to the Recycle Bin. The folder tree leaves .amb out; the duplicated tree builder and cover owner check are shared now (model_catalog.py, backup.py back under 600 lines). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
3.4 KiB
Python
75 lines
3.4 KiB
Python
"""The user's choice to leave a model out of scans, or keep it in: POST /anomalous/scan_choice,
|
|
kept in each model's <model>.anomalous.json ("scan"; model_identity.scan_choice reads it). The
|
|
scan page asks it about new models that look like no drawing model; a model's details change it."""
|
|
|
|
import asyncio
|
|
import os
|
|
|
|
from aiohttp import web
|
|
|
|
from .local_guard import may_change
|
|
from .path_utils import atomic_write_json, resolve_folder_subdir
|
|
from .utils import model_file_within
|
|
|
|
try:
|
|
from ..model_identity import USER_INFO_SUFFIX, read_json
|
|
from ..model_sidecars import target as sidecar_target
|
|
except ImportError: # loaded outside the package (tests)
|
|
from model_identity import USER_INFO_SUFFIX, read_json
|
|
from model_sidecars import target as sidecar_target
|
|
|
|
MAX_CHOICE_FILES = 2000
|
|
|
|
|
|
def _set_scan_choice(targets, choice):
|
|
"""Writes the user's scan choice into each picked model's <model>.anomalous.json ("scan":
|
|
"skip" or "scan"; "" removes it). `targets`: [{type, path_idx, subfolder, files}] as a scan
|
|
takes them. Returns (models changed, file names left as they were: not found, not a
|
|
.safetensors, or an unreadable .anomalous.json)."""
|
|
if choice not in ("skip", "scan", "") or not isinstance(targets, list):
|
|
raise ValueError("Invalid scan choice")
|
|
picked = []
|
|
for target in targets:
|
|
names = target.get("files") if isinstance(target, dict) else None
|
|
if not isinstance(names, list) or not all(isinstance(name, str) for name in names):
|
|
raise ValueError("each target needs a list of file names")
|
|
_, target_dir = resolve_folder_subdir(target.get("type"), int(target.get("path_idx", 0)), target.get("subfolder") or "/")
|
|
picked += [(target_dir, name) for name in names]
|
|
if len(picked) > MAX_CHOICE_FILES:
|
|
raise ValueError("Too many models")
|
|
changed, failed = 0, []
|
|
for target_dir, name in picked:
|
|
try:
|
|
path = model_file_within(target_dir, name)
|
|
except ValueError:
|
|
failed.append(name)
|
|
continue
|
|
user_file = sidecar_target(os.path.splitext(path)[0], USER_INFO_SUFFIX)
|
|
user = read_json(user_file)
|
|
if not name.endswith(".safetensors") or not os.path.isfile(path) or (user is None and os.path.exists(user_file)):
|
|
failed.append(name)
|
|
continue
|
|
user = user or {"format": 1}
|
|
if choice:
|
|
user["scan"] = choice
|
|
else:
|
|
user.pop("scan", None)
|
|
atomic_write_json(user_file, user)
|
|
changed += 1
|
|
return changed, failed
|
|
|
|
|
|
async def api_set_scan_choice(request):
|
|
"""POST /anomalous/scan_choice {targets, choice} - leave models out of scans ("skip"), keep
|
|
them in ("scan"), or forget the choice (""). Only from this computer (local_guard.may_change)."""
|
|
if not may_change(request):
|
|
return web.json_response({"status": "error", "message": "Only from this computer"}, status=403)
|
|
try:
|
|
data = await request.json()
|
|
changed, failed = await asyncio.to_thread(_set_scan_choice, data.get("targets"), data.get("choice", ""))
|
|
except (AttributeError, KeyError, TypeError, ValueError):
|
|
return web.json_response({"status": "error", "message": "Invalid scan choice"}, status=400)
|
|
except OSError:
|
|
return web.json_response({"status": "error", "message": "Could not save the choice"}, status=500)
|
|
return web.json_response({"status": "success", "changed": changed, "failed": failed})
|