mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-13 12:52:13 -03:00
Download root rules now accept match_type "tag": the rule hits when any of its free-text tags equals one of the model's CivitAI tags (trimmed, case-insensitive), so e.g. all "character" models can be routed to a NAS root regardless of base model. Rules stay ordered — first match wins across mixed base_model/tag rules. - settings normalize keeps tag entries as free text (case-insensitive dedupe, no base-model whitelist); resolve_download_root gained a tags parameter and a match_download_root_rule helper - the download flow passes the version's model tags into rule resolution - the routing endpoint accepts tags and reports the matched rule; the versions endpoint ships model-level tags with each version so the location step can preview tag matches - the settings rule editor gained a per-rule match-type dropdown; tag rules edit a comma-separated list (committed on blur/Enter) instead of the base model checkbox panel - the download confirmation modal shows "Routed by rule: ..." next to the preselected root
222 lines
7.3 KiB
Python
222 lines
7.3 KiB
Python
"""Shared download routing logic.
|
|
|
|
Decides whether a download initiated from the checkpoint library should be
|
|
routed to the unet/diffusion-model roots instead of the checkpoint roots.
|
|
Used by both the download manager (at download time) and the download
|
|
routing HTTP endpoint (when the user picks a location in the UI), so the
|
|
two can never disagree.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Iterable, Mapping, Optional, Sequence
|
|
|
|
from ..utils.constants import (
|
|
CHECKPOINT_BASE_MODELS,
|
|
CIVITAI_FILE_TYPE_TO_OTHER_SUB_TYPE,
|
|
CIVITAI_TYPE_TO_OTHER_SUB_TYPE,
|
|
DIFFUSION_MODEL_BASE_MODELS,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# File types reported by the CivitAI API that indicate a raw diffusion
|
|
# model (loaded via UNETLoader in ComfyUI) rather than a full checkpoint.
|
|
DIFFUSION_FILE_TYPES = frozenset({"UNet", "Diffusion Model"})
|
|
|
|
# Allowed values for the "unknown_base_model_routing" setting / the
|
|
# unknown_base_model_default parameter below.
|
|
ROUTING_DIFFUSION_MODEL = "diffusion_model"
|
|
ROUTING_CHECKPOINT = "checkpoint"
|
|
|
|
# Download-time model type -> download_root_rules folder key. The rule set is
|
|
# keyed by folder_paths keys, so a checkpoint download that resolves to the
|
|
# unet roots consults the "unet" rules, not the "checkpoints" ones.
|
|
MODEL_TYPE_TO_ROOT_RULE_KEY = {
|
|
"lora": "loras",
|
|
"checkpoint": "checkpoints",
|
|
"unet": "unet",
|
|
"embedding": "embeddings",
|
|
}
|
|
|
|
# Download-time model type -> the setting holding its default download root.
|
|
MODEL_TYPE_TO_DEFAULT_ROOT_SETTING = {
|
|
"lora": "default_lora_root",
|
|
"checkpoint": "default_checkpoint_root",
|
|
"unet": "default_unet_root",
|
|
"embedding": "default_embedding_root",
|
|
}
|
|
|
|
# match_type values understood by download_root_rules.
|
|
RULE_MATCH_TYPE_BASE_MODEL = "base_model"
|
|
RULE_MATCH_TYPE_TAG = "tag"
|
|
|
|
|
|
def _normalize_tag_set(tags: Optional[Iterable[object]]) -> frozenset:
|
|
"""Case-folded, trimmed tag set for rule matching (never None)."""
|
|
if not tags:
|
|
return frozenset()
|
|
return frozenset(
|
|
tag.strip().casefold()
|
|
for tag in tags
|
|
if isinstance(tag, str) and tag.strip()
|
|
)
|
|
|
|
|
|
def match_download_root_rule(
|
|
rules: Optional[Sequence[Mapping[str, object]]],
|
|
base_model: str,
|
|
tags: Optional[Iterable[object]] = None,
|
|
) -> Optional[Mapping[str, object]]:
|
|
"""Return the first rule matching this download, else ``None``.
|
|
|
|
Rules are ordered; the first match wins. ``base_model`` rules hit when
|
|
their ``match`` list contains ``base_model`` (exact match); ``tag`` rules
|
|
hit when any ``match`` entry equals one of the model's tags (trimmed,
|
|
case-insensitive). A rule with an empty/invalid root never matches.
|
|
"""
|
|
if not rules:
|
|
return None
|
|
|
|
model_tags = _normalize_tag_set(tags)
|
|
|
|
for rule in rules:
|
|
if not isinstance(rule, Mapping):
|
|
continue
|
|
match = rule.get("match")
|
|
if not isinstance(match, Sequence) or isinstance(match, (str, bytes)):
|
|
continue
|
|
match_type = rule.get("match_type")
|
|
if match_type == RULE_MATCH_TYPE_BASE_MODEL:
|
|
if not base_model or base_model not in match:
|
|
continue
|
|
elif match_type == RULE_MATCH_TYPE_TAG:
|
|
if not model_tags or not any(
|
|
isinstance(entry, str) and entry.strip().casefold() in model_tags
|
|
for entry in match
|
|
):
|
|
continue
|
|
else:
|
|
continue
|
|
root = rule.get("root")
|
|
if isinstance(root, str) and root.strip():
|
|
return rule
|
|
|
|
return None
|
|
|
|
|
|
def resolve_download_root(
|
|
rules: Optional[Sequence[Mapping[str, object]]],
|
|
base_model: str,
|
|
default_root: str,
|
|
tags: Optional[Iterable[object]] = None,
|
|
) -> str:
|
|
"""Return the root from the first matching rule, else ``default_root``.
|
|
|
|
See :func:`match_download_root_rule` for the matching semantics.
|
|
"""
|
|
rule = match_download_root_rule(rules, base_model, tags)
|
|
if rule is not None:
|
|
root = rule["root"]
|
|
logger.info(
|
|
"Download root rule matched (%s), routing to %s",
|
|
rule.get("match_type"),
|
|
root,
|
|
)
|
|
return root
|
|
|
|
return default_root
|
|
|
|
|
|
def is_diffusion_model_download(
|
|
model_type: str,
|
|
file_types: Iterable[str] = (),
|
|
base_model: str = "",
|
|
unknown_base_model_default: str = ROUTING_DIFFUSION_MODEL,
|
|
) -> bool:
|
|
"""Return True when a download should be routed to the unet roots.
|
|
|
|
Only applies to downloads initiated from the checkpoint library.
|
|
Priority: (1) any file has type "UNet" or "Diffusion Model" (the more
|
|
direct signal from CivitAI), (2) baseModel is a known diffusion model,
|
|
(3) baseModel is a known full checkpoint -> not diffusion, (4) unknown
|
|
or empty baseModel -> the ``unknown_base_model_default`` setting, which
|
|
defaults to diffusion because the set of true checkpoint families is
|
|
closed while new DiT base models appear all the time.
|
|
"""
|
|
if model_type != "checkpoint":
|
|
return False
|
|
|
|
for file_type in file_types:
|
|
if file_type in DIFFUSION_FILE_TYPES:
|
|
logger.info(
|
|
"File type '%s' detected, routing checkpoint to unet folder",
|
|
file_type,
|
|
)
|
|
return True
|
|
|
|
if base_model in DIFFUSION_MODEL_BASE_MODELS:
|
|
logger.info(
|
|
"baseModel '%s' is a known diffusion model, routing to unet folder",
|
|
base_model,
|
|
)
|
|
return True
|
|
|
|
if base_model in CHECKPOINT_BASE_MODELS:
|
|
return False
|
|
|
|
is_diffusion = unknown_base_model_default != ROUTING_CHECKPOINT
|
|
logger.info(
|
|
"baseModel '%s' is unknown, routing to %s folder (unknown_base_model_routing)",
|
|
base_model,
|
|
"unet" if is_diffusion else "checkpoint",
|
|
)
|
|
return is_diffusion
|
|
|
|
|
|
def resolve_other_download_sub_type(
|
|
civitai_model_type: str,
|
|
file_types: Iterable[str] = (),
|
|
selected_file_type: Optional[str] = None,
|
|
) -> Optional[str]:
|
|
"""Resolve the "other"-page sub_type for a download.
|
|
|
|
Fixed priority (locked design, docs/plans/other-models-page.md §9.2):
|
|
|
|
1. Explicit user file pick — when the picked file's type maps, it wins
|
|
even when model.type maps to something else.
|
|
2. model.type via CIVITAI_TYPE_TO_OTHER_SUB_TYPE.
|
|
3. file.type fallback — only when model.type maps to nothing. Must NOT
|
|
override a mapped model.type: checkpoint models routinely bundle
|
|
VAE/Text Encoder component files.
|
|
4. Still undecidable -> None (caller must ask the user for a folder).
|
|
"""
|
|
if selected_file_type:
|
|
mapped = CIVITAI_FILE_TYPE_TO_OTHER_SUB_TYPE.get(selected_file_type)
|
|
if mapped:
|
|
logger.info(
|
|
"Explicit file pick type '%s' routes other download to '%s'",
|
|
selected_file_type,
|
|
mapped,
|
|
)
|
|
return mapped
|
|
|
|
normalized_model_type = (civitai_model_type or "").strip().lower()
|
|
mapped = CIVITAI_TYPE_TO_OTHER_SUB_TYPE.get(normalized_model_type)
|
|
if mapped:
|
|
return mapped
|
|
|
|
for file_type in file_types:
|
|
mapped = CIVITAI_FILE_TYPE_TO_OTHER_SUB_TYPE.get(file_type)
|
|
if mapped:
|
|
logger.info(
|
|
"model.type '%s' unmapped; file type '%s' routes other download to '%s'",
|
|
civitai_model_type,
|
|
file_type,
|
|
mapped,
|
|
)
|
|
return mapped
|
|
|
|
return None
|