Files
DemonGatanjieuandClaude Opus 5.5 414e76c935 Read a LoRA's guessed trigger words from its header, and tell the AI what a model needs
- header_words.py: modelspec.trigger_phrase, else the tags most training pictures share
  (ss_tag_frequency, generic ones left out); the scanner keeps them as
  anomalous_header_words when Civitai gives no trigger words, metadata gives headerWords,
  model search finds them.
- model_kind: Flux Schnell told from Dev (no guidance_in), Z-Image from Lumina 2 (caption
  embedder width, a GGUF by parameter count); RULES 3 so kept header answers are read again.
- api/model_guide.py + model_profiles.json: GET /anomalous/model_guide for the model details
  (example settings, header words, the text encoder and VAE a diffusion models file needs,
  found by name), and get_model's header_words and usage for the AI.
- model_advice.example_settings gives the author's example settings as values;
  author_examples says them in words as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:43:51 +08:00

102 lines
4.1 KiB
Python

"""What using a model takes besides its file, for the model details (GET /anomalous/model_guide)
and the AI's get_model: the settings of its author's example pictures (model_advice), the words
its own header suggests when Civitai gave no trigger words (header_words.py: kept by the scan,
else read now), and, by its base model family (model_profiles.json), the files a drawing model
loaded in parts needs beside it, with the one this ComfyUI has by that name, and notes for the AI.
The page names a model by type, folder index, subfolder and file name, never by a path."""
import asyncio
import json
import os
import re
import folder_paths
from aiohttp import web
from . import model_advice
from .metadata import get_metadata
from .model_constants import MODEL_EXTENSIONS
from .path_utils import model_file_within, resolve_folder_subdir
try:
from ..header_words import of_file as header_words_of
except ImportError: # loaded outside the package (tests)
from header_words import of_file as header_words_of
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "model_profiles.json"), encoding="utf-8") as _source:
FAMILIES = json.load(_source)["families"]
PART_FOLDERS = ("diffusion_models", "unet") # a checkpoint carries its text encoders and VAE
def profile(base):
"""The family whose marks start `base` (a base model name), else None."""
lowered = str(base or "").strip().lower()
return next((family for family in FAMILIES if lowered and lowered.startswith(tuple(family["marks"]))), None)
def _plain(name):
return re.sub(r"[^a-z0-9]", "", os.path.splitext(os.path.basename(name))[0].lower())
def _found(part):
"""The first file in the part's folder with one of its names in its own, "" when none."""
try:
listed = folder_paths.get_filename_list(part["folder"])
except KeyError:
return ""
for name in listed:
plain = _plain(name)
if any(plain == wanted or (len(wanted) > 3 and wanted in plain) for wanted in part["names"]):
return name
return ""
def parts(folder_type, base):
"""[{"folder", "file", "found"}]: what a drawing model loaded in parts needs beside it; [] for others."""
family = profile(base)
if folder_type not in PART_FOLDERS or not family:
return []
return [{"folder": part["folder"], "file": part["file"], "found": _found(part)} for part in family.get("parts", [])]
def words(path, folder_type, meta):
"""What a LoRA's header suggests when Civitai gave no trigger words: kept by the scan, else read now."""
if folder_type != "loras" or meta.get("trainedWords"):
return []
return list(meta.get("headerWords") or []) or header_words_of(path)
def usage(folder_type, base):
"""For the AI: the family's notes and the files it needs here, {} when nothing is known."""
family = profile(base)
if not family:
return {}
found = {"family": family["family"]}
needed = parts(folder_type, base)
if needed:
found["needs"] = [f"{part['folder']}: {part['file']}" + (f" (here: {part['found']})" if part["found"] else " (none here by that name)")
for part in needed]
if family.get("notes"):
found["notes"] = family["notes"]
return found
def guide(path, folder_type):
meta = get_metadata(path)
base = meta.get("baseModel") or meta.get("base_guess") or ""
return {"examples": model_advice.example_settings(path, folder_type, meta.get("hash", "")),
"words": words(path, folder_type, meta), "parts": parts(folder_type, base)}
async def api_model_guide(request):
query = request.query
folder_type = query.get("type", "")
try:
_base, folder = resolve_folder_subdir(folder_type, int(query.get("path_idx", 0)), query.get("subfolder", ""))
path = model_file_within(folder, query.get("filename", ""))
except (ValueError, KeyError):
return web.json_response({"error": "bad request"}, status=400)
if not path.lower().endswith(MODEL_EXTENSIONS) or not os.path.isfile(path):
return web.json_response({"error": "not found"}, status=404)
return web.json_response(await asyncio.to_thread(guide, path, folder_type))