83 lines
3.7 KiB
Python
83 lines
3.7 KiB
Python
"""Sampler settings recommended for a model.
|
|
|
|
Layers, later ones winning: generic defaults, the family, hints in the file name (turbo / distilled ...), and what
|
|
the model's own sidecar says. The sidecar (`<model>.metadata.json`, written by LoRA Manager / CivitAI helpers)
|
|
lists the sampler, scheduler, steps and CFG of the author's example images; the most common value of each is used.
|
|
Only those four fields are read from it, never the prompts.
|
|
|
|
Family values come from community workflow documentation (ComfyUI docs for LTXVScheduler, the MiniMax H3 Turbo
|
|
and Krea 2 Turbo node pages), not from the model makers, so treat them as starting points.
|
|
"""
|
|
import json
|
|
import re
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
import comfy.samplers
|
|
|
|
GENERIC = {"sampler_name": "euler", "scheduler": "simple", "steps": 20, "cfg": 4.0}
|
|
# keyed by family; a name hint adds the distilled values on top
|
|
FAMILY = {
|
|
"krea2": {"sampler_name": "res_multistep", "scheduler": "simple"},
|
|
"minimax_h3": {"sampler_name": "euler", "scheduler": "simple"},
|
|
"ltxv": {"scheduler": "ltxv"},
|
|
"moss_image": {"sampler_name": "er_sde", "scheduler": "simple", "steps": 20, "cfg": 3.0},
|
|
}
|
|
DISTILLED_HINT = re.compile(r"turbo|distill|lightning|hyper|lcm|schnell|dmd|\b[2-9]step|\b[1-9]\d?step", re.I)
|
|
DISTILLED = {"steps": 8, "cfg": 1.0}
|
|
# names used by A1111-style metadata -> ComfyUI names
|
|
SAMPLER_ALIASES = {
|
|
"euler a": "euler_ancestral", "euler ancestral": "euler_ancestral", "er sde": "er_sde", "dpm++ 2m": "dpmpp_2m",
|
|
"dpm++ 2m sde": "dpmpp_2m_sde", "dpm++ sde": "dpmpp_sde", "dpm++ 3m sde": "dpmpp_3m_sde", "dpm++ 2s a": "dpmpp_2s_ancestral",
|
|
"unipc": "uni_pc", "heun": "heun", "lms": "lms", "ddim": "ddim", "lcm": "lcm",
|
|
}
|
|
|
|
|
|
def _sampler(name):
|
|
key = str(name).strip().lower()
|
|
key = SAMPLER_ALIASES.get(key, key.replace(" ", "_"))
|
|
return key if key in comfy.samplers.KSampler.SAMPLERS else None
|
|
|
|
|
|
def _scheduler(name):
|
|
key = str(name).strip().lower().replace(" ", "_")
|
|
return key if key in comfy.samplers.KSampler.SCHEDULERS else None
|
|
|
|
|
|
def from_sidecar(model_path):
|
|
"""Most common sampler, scheduler, steps and cfg among the author's example images."""
|
|
sidecar = Path(str(Path(model_path).with_suffix("")) + ".metadata.json")
|
|
try:
|
|
images = (json.loads(sidecar.read_text(encoding="utf-8")).get("civitai") or {}).get("images") or []
|
|
except (OSError, ValueError, AttributeError):
|
|
return {}
|
|
votes = {"sampler_name": Counter(), "scheduler": Counter(), "steps": Counter(), "cfg": Counter()}
|
|
for image in images:
|
|
meta = image.get("meta") or {} if isinstance(image, dict) else {}
|
|
for field, key, convert in (("sampler_name", "sampler", _sampler), ("scheduler", "scheduler", _scheduler),
|
|
("steps", "steps", lambda v: int(v) or None), ("cfg", "cfgScale", lambda v: float(v) or None)):
|
|
if key in meta:
|
|
try:
|
|
value = convert(meta[key])
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if value is not None:
|
|
votes[field][value] += 1
|
|
return {field: counter.most_common(1)[0][0] for field, counter in votes.items() if counter}
|
|
|
|
|
|
def recommend(family_key="", model_name="", model_path=None):
|
|
"""(settings, where each came from)."""
|
|
settings, source = dict(GENERIC), {key: "default" for key in GENERIC}
|
|
|
|
def apply(values, origin):
|
|
for key, value in values.items():
|
|
settings[key], source[key] = value, origin
|
|
|
|
apply(FAMILY.get(family_key, {}), f"{family_key} family")
|
|
if DISTILLED_HINT.search(model_name or ""):
|
|
apply(DISTILLED, "name looks distilled")
|
|
if model_path:
|
|
apply(from_sidecar(model_path), "model sidecar")
|
|
return settings, source
|