Files
DemonGatanjieu-Anomalous_Mo…/api/model_advice.py
T
DemonGatanjieuandClaude Opus 5.5 cb523930da Guide the AI with fewer detours and guesses
- An AI app calling by an older tool list (tool_help or use_tool not offered,
  use_tool for a tool offered directly, a tool the list lacks) is told so, and
  the task header shows how to reconnect until the app fetches tools/list again.
- add_lora / set_model compare the base models on the model line
  (model_advice.py; the page reports the line) and add base_model_note.
- get_model gives author_examples (sampler, steps, CFG, size, a LoRA's weight
  from the Civitai record's example pictures) and the version name.
- run_workflow says ComfyUI's refusal as which node, what is wrong and the tool
  that fixes it (run_refusal.py; mcp_run.js passes the refusal as data).
- Gates "When I don't answer": wait for me, or go with the recommendation once
  the waiting time runs out (choices with a recommended option only), handed to
  the AI in the same call and shown on the stage and under Decisions.
- A tool budget test (count, size, description length, required arguments) and
  the exam tasks with their scorecard (docs/architecture/tool-exam.md,
  tools/tool_exam.mjs). The tool set's text recommends Full for Antigravity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 14:26:41 +08:00

183 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""What Anomalous knows about using a model well, told to the AI so it need not guess:
- the settings its author's example pictures were made with (the Civitai record a scan keeps
beside the file: images[].meta), summed up for get_model: how many pictures, the sampler and
scheduler most used, the range of steps and CFG, the most used size and, for a LoRA, the
weight it was used at: by its file hash, name or Civitai version, else the LoRA most of the
pictures name (their names are the author's own files, and often an older version's);
- whether a LoRA suits the main model it is put on: add_lora and set_model compare their base
models (the scan's, else what the file's header tells) and say so when the families differ
(an SD 1.5 LoRA on an SDXL checkpoint does nothing useful) or, more softly, when two bases of one
family differ (a Pony LoRA on an Illustrious checkpoint). A note only: the call is made.
"""
import json
import os
from collections import Counter
import folder_paths
from . import metadata
# Base model names (Civitai's, or what model_kind.py infers) by the architecture their files share.
FAMILIES = (
("SDXL", ("sdxl", "pony", "illustrious", "noob")), ("SD 1.x", ("sd 1",)), ("SD 2.x", ("sd 2",)),
("SD 3", ("sd 3", "sd3")), ("Flux.1", ("flux.1", "flux 1", "chroma")), ("Flux.2", ("flux.2", "flux 2")),
)
# Bases trained far from their family's: a LoRA of one on another of the family loads but may look off.
OWN_WAYS = ("pony", "illustrious", "noob", "chroma")
MAIN_TYPES = ("checkpoints", "diffusion_models", "unet")
MAX_EXAMPLES = 20
def family(base):
"""The architecture family of a base model name ("" when none is known)."""
lowered = str(base or "").strip().lower()
if not lowered or lowered in ("other", "unknown"):
return ""
for name, marks in FAMILIES:
if lowered.startswith(marks):
return name
return lowered
def base_of(file_path):
"""A model file's base model: the scan's, else what its header tells."""
meta = metadata.get_metadata(file_path)
return meta.get("baseModel") or meta.get("base_guess") or ""
def _main_base(value):
"""The base model of a main loader's choice (its drop-down value), or ""."""
for folder_type in MAIN_TYPES:
try:
path = folder_paths.get_full_path(folder_type, value)
except (KeyError, TypeError):
path = None
if path:
return base_of(path)
return ""
def lora_paths(values):
"""The files of LoRA drop-down values that ComfyUI can find."""
paths = []
for value in values or []:
try:
path = folder_paths.get_full_path("loras", value)
except (KeyError, TypeError):
path = None
if path:
paths.append(path)
return paths
def lora_note(lora_path, line):
"""A note when the LoRA at `lora_path` does not suit the main model of `line` (the page's
{"model": the main loader's choice}), else None."""
main = _main_base((line or {}).get("model") or "")
lora = base_of(lora_path)
if not main or not lora or main.lower() == lora.lower():
return None
if family(main) != family(lora):
return (f"Base model clash: this LoRA is for {lora}, the main model ({line['model']}) is {main}; it will not "
f"work as meant. Pick one with search_models(type='loras', base_model='{main}'), or remove it.")
if lora.lower().startswith(OWN_WAYS) or main.lower().startswith(OWN_WAYS):
return (f"This LoRA was made for {lora}, the main model is {main}: same family, so it loads, but it may "
f"look weaker or off. search_models(type='loras', base_model='{main}') finds ones made for it.")
return None
def _number(value):
try:
number = float(str(value).strip())
except (TypeError, ValueError):
return None
return number if number == number and abs(number) < 1e6 else None
def _span(numbers):
"""'25' or '20–30' for a list of numbers, '' for none."""
if not numbers:
return ""
low, high = min(numbers), max(numbers)
say = lambda number: f"{number:g}"
return say(low) if low == high else f"{say(low)}–{say(high)}"
def _common(values, total):
"""'euler (6 of 10)': the most used value and in how many of the `total` pictures."""
counted = Counter(value for value in values if value)
if not counted:
return ""
value, times = counted.most_common(1)[0]
return str(value) if times == total else f"{value} ({times} of {total})"
def _loras(meta):
"""[(who, weight)] of the LoRAs one picture's settings name: who is a hash, a file name or "v<version>"."""
found = []
for item in meta.get("resources") or []:
if isinstance(item, dict) and str(item.get("type", "")).lower() == "lora":
found.append((str(item.get("hash") or item.get("name") or "").lower(), _number(item.get("weight"))))
for item in meta.get("additionalResources") or []:
if isinstance(item, dict) and str(item.get("type", "lora")).lower() == "lora":
found.append((os.path.splitext(os.path.basename(str(item.get("name", ""))))[0].lower(), _number(item.get("strength"))))
for item in meta.get("civitaiResources") or []:
if isinstance(item, dict) and str(item.get("type", "")).lower() == "lora" and item.get("modelVersionId") is not None:
found.append((f"v{item['modelVersionId']}", _number(item.get("weight"))))
return [(who, weight) for who, weight in found if who and weight is not None]
def _lora_weights(metas, own):
"""The weights this LoRA was used at in its pictures: those naming it (`own`: its hash, file
name, version), else the one LoRA most of them name; none when two are named as often."""
per = [_loras(meta) for meta in metas]
named = Counter(who for found in per for who in {who for who, _weight in found})
mine = {who for who in named if who in own}
if not mine:
common = [(who, times) for who, times in named.most_common(2) if times * 5 >= len(metas) * 3]
if common and (len(common) == 1 or common[0][1] > common[1][1]):
mine = {common[0][0]}
return [weight for found in per for who, weight in found if who in mine]
def _record(file_path):
"""The Civitai record kept beside the model (its .info), or None."""
base_path = os.path.splitext(file_path)[0]
info_base = metadata._info_base(file_path, base_path)
for path in (metadata.find(info_base, ".info"), f"{info_base}.civitai.info"):
if path and os.path.isfile(path):
try:
with open(path, encoding="utf-8") as handle:
data = json.load(handle)
except (OSError, ValueError):
continue
if isinstance(data, dict) and isinstance(data.get("images"), list):
return data
return None
def author_examples(file_path, folder_type, sha256=""):
"""What the author's example pictures were made with, summed up; {} when nothing is kept."""
record = _record(file_path)
metas = [image.get("meta") for image in (record or {}).get("images") or []
if isinstance(image, dict) and isinstance(image.get("meta"), dict)][:MAX_EXAMPLES]
if not metas:
return {}
sizes = [meta.get("Size") or (f"{meta['width']}x{meta['height']}" if meta.get("width") and meta.get("height") else "")
for meta in metas]
found = {
"pictures": len(metas),
"sampler": _common([str(meta.get("sampler") or "") for meta in metas if str(meta.get("sampler")).lower() != "undefined"], len(metas)),
"scheduler": _common([str(meta.get("scheduler") or meta.get("Schedule type") or "") for meta in metas], len(metas)),
"steps": _span([n for n in (_number(meta.get("steps")) for meta in metas) if n is not None]),
"cfg": _span([n for n in (_number(meta.get("cfgScale")) for meta in metas) if n is not None]),
"size": _common([str(size) for size in sizes], len(metas)),
"clip_skip": _span([n for n in (_number(meta.get("clipSkip") or meta.get("Clip skip")) for meta in metas) if n is not None]),
}
if folder_type == "loras":
own = {(sha256 or "")[:10].lower(), os.path.splitext(os.path.basename(file_path))[0].lower(), f"v{record.get('id')}"} - {""}
found["lora_weight"] = _span(_lora_weights(metas, own))
return {key: value for key, value in found.items() if value}