- 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>
102 lines
4.1 KiB
Python
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))
|