mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-23 03:54:09 -03:00
fix(organize): exclude Civitai meta tags from folder names (#1119)
Follow-up to the keyword-dump guard. The reported model's tag list is ["lora, character, ... face", "base model"], so skipping the dump left the "base model" label to be picked as the folder name. That label describes Civitai's listing rather than the model's content, which makes it as meaningless as a folder as the blob was. Add CIVITAI_META_TAGS and is_civitai_meta_tag(), and skip those labels in the automatic fallback. An explicit priority entry still matches them, so a user who does want a "base model" folder can configure one. The reported model now resolves to "Krea 2/no tags" instead of "Krea 2/base model". Also correct a comment that listed ".civitai.info" among the files sitting next to a model. LoRA Manager only reads that sidecar -- other tools write it -- and writes ".metadata.json" itself.
This commit is contained in:
@@ -32,6 +32,7 @@ When your path template contains `{first_tag}`, the app picks a folder name base
|
||||
- It checks the priority list from top to bottom. If a canonical tag or any of its aliases appear in the model tags, that canonical name becomes the folder name.
|
||||
- If no priority tags are found but the model has tags, the first tag that can be used as a folder name is chosen.
|
||||
- Tags that contain a comma, or that are longer than 50 characters, are treated as unusable and skipped: some uploaders pack their whole keyword list into a single tag. If every tag is unusable, the folder falls back to `no tags`.
|
||||
- Civitai's structural labels, such as `base model`, describe the listing rather than the model, so the automatic fallback skips them too. Add one to your priority list if you really want it as a folder name.
|
||||
- If the model has no tags at all, the folder falls back to `no tags`.
|
||||
|
||||
### Example
|
||||
@@ -44,6 +45,7 @@ With a template like `/{model_type}/{first_tag}` and the priority entry list `ch
|
||||
| `["anime", "portrait"]` | `style` | `anime` hits the `style` entry, so its canonical label is used. |
|
||||
| `["portrait", "bw"]` | `portrait` | No priority match, so the first model tag is used. |
|
||||
| `["lora, character, rosie, ... face"]` | `no tags` | The only tag is a keyword dump, so it is skipped. |
|
||||
| `["lora, character, ... face", "base model"]` | `no tags` | A keyword dump plus a Civitai label: nothing usable is left. |
|
||||
| `[]` | `no tags` | Nothing to match, so the fallback is applied. |
|
||||
|
||||
## 3. Save the Settings
|
||||
|
||||
@@ -47,6 +47,7 @@ from ..utils.settings_paths import (
|
||||
from ..utils.tag_priorities import (
|
||||
PriorityTagEntry,
|
||||
collect_canonical_tags,
|
||||
is_civitai_meta_tag,
|
||||
is_usable_path_tag,
|
||||
parse_priority_tag_string,
|
||||
resolve_priority_tag,
|
||||
@@ -1572,8 +1573,11 @@ class SettingsManager:
|
||||
|
||||
# Fall back to the first tag that is usable as a folder name. The raw
|
||||
# tag list can contain keyword dumps that would become unusable folders
|
||||
# and break path length limits, so skip those (#1119).
|
||||
# and break path length limits, and Civitai mixes in structural labels
|
||||
# like "base model" that mean nothing as a folder, so skip both (#1119).
|
||||
for tag in tags:
|
||||
if is_civitai_meta_tag(tag):
|
||||
continue
|
||||
if is_usable_path_tag(tag):
|
||||
return tag.strip()
|
||||
return ""
|
||||
|
||||
+15
-3
@@ -273,6 +273,16 @@ CIVITAI_MODEL_TAGS = [
|
||||
"action",
|
||||
]
|
||||
|
||||
# Civitai tags that describe the listing rather than the model's content.
|
||||
# Uploaders can also set these by hand, so they must not be picked as an
|
||||
# automatic folder name; a user who wants one can still name it explicitly in
|
||||
# their priority tag list.
|
||||
CIVITAI_META_TAGS = frozenset(
|
||||
{
|
||||
"base model",
|
||||
}
|
||||
)
|
||||
|
||||
# Default priority tag configuration strings for each model type
|
||||
DEFAULT_PRIORITY_TAG_CONFIG = {
|
||||
"lora": ", ".join(CIVITAI_MODEL_TAGS),
|
||||
@@ -295,9 +305,11 @@ DEFAULT_DOWNLOAD_PATH_TEMPLATES: Dict[str, str] = {
|
||||
|
||||
# Length guards for template placeholders that end up in file and folder names.
|
||||
# Windows enforces MAX_PATH (260 characters) on the full path and 255 on a
|
||||
# single path component, and a model folder also has to leave room for the
|
||||
# model file, its ".civitai.info"/".json" sidecars and preview images. Values
|
||||
# stay well below those limits so the surrounding files still fit.
|
||||
# single path component. A model folder also holds the model file, the
|
||||
# ".metadata.json" sidecar written by LoRA Manager, preview images and the
|
||||
# metadata files other tools drop next to the model (for example
|
||||
# ".civitai.info", which LoRA Manager only reads), so names stay well below
|
||||
# those limits.
|
||||
#
|
||||
# Tags get a much tighter budget than other names: some CivitAI uploaders dump
|
||||
# their whole keyword list into a single tag (see issue #1119), and such a tag
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterable, List, Optional, Sequence, Set
|
||||
|
||||
from .constants import MAX_PATH_TAG_LENGTH
|
||||
from .constants import CIVITAI_META_TAGS, MAX_PATH_TAG_LENGTH
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -128,3 +128,19 @@ def is_usable_path_tag(tag: object) -> bool:
|
||||
return False
|
||||
|
||||
return len(candidate) <= MAX_PATH_TAG_LENGTH
|
||||
|
||||
|
||||
def is_civitai_meta_tag(tag: object) -> bool:
|
||||
"""Return True for Civitai labels that describe the listing, not content.
|
||||
|
||||
Civitai attaches structural tags such as "base model" to the same list as
|
||||
real content tags. They carry no organisational meaning, so the automatic
|
||||
fallback must not turn one into a folder name. A user who does want such a
|
||||
folder can still put the label in their priority tag list, because explicit
|
||||
priority matches bypass this check.
|
||||
"""
|
||||
|
||||
if not isinstance(tag, str):
|
||||
return False
|
||||
|
||||
return tag.strip().casefold() in CIVITAI_META_TAGS
|
||||
|
||||
@@ -239,7 +239,11 @@ async def test_successful_download_uses_defaults(
|
||||
|
||||
|
||||
def test_calculate_relative_path_ignores_keyword_dump_tag():
|
||||
"""The #1119 download flow: a keyword-dump tag must not become a folder."""
|
||||
"""The #1119 download flow: the real tag list must not become a folder.
|
||||
|
||||
The model's only two tags are the keyword dump and Civitai's "base model"
|
||||
label, so nothing usable is left and the template falls back to "no tags".
|
||||
"""
|
||||
keyword_dump = (
|
||||
"lora, character, rosie, irish, redhead, auburn, freckles, green eyes, "
|
||||
"curly hair, woman, female, photorealistic, realistic, krea2, dark beast, "
|
||||
@@ -257,7 +261,7 @@ def test_calculate_relative_path_ignores_keyword_dump_tag():
|
||||
"lora",
|
||||
)
|
||||
|
||||
assert relative_path == "MappedModel/base model"
|
||||
assert relative_path == "MappedModel/no tags"
|
||||
assert keyword_dump not in relative_path
|
||||
assert len(relative_path) < 50
|
||||
|
||||
|
||||
@@ -412,6 +412,40 @@ def test_resolve_priority_tag_skips_unusable_tags(manager):
|
||||
assert manager.resolve_priority_tag_for_model([" portrait "], "lora") == "portrait"
|
||||
|
||||
|
||||
def test_resolve_priority_tag_skips_civitai_meta_tags(manager):
|
||||
"""Civitai's structural labels are not content, so they cannot be folders."""
|
||||
assert manager.resolve_priority_tag_for_model(["base model"], "lora") == ""
|
||||
assert (
|
||||
manager.resolve_priority_tag_for_model(["Base Model"], "lora") == ""
|
||||
), "the meta tag check must be case-insensitive"
|
||||
assert manager.resolve_priority_tag_for_model(["base model", " "], "lora") == ""
|
||||
# A real tag after the label is still used.
|
||||
assert (
|
||||
manager.resolve_priority_tag_for_model(["base model", "portrait"], "lora")
|
||||
== "portrait"
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_priority_tag_meta_tag_can_be_opted_into(manager):
|
||||
"""An explicit priority entry still wins over the meta tag exclusion."""
|
||||
manager.settings["priority_tags"] = {"lora": "base model"}
|
||||
|
||||
assert (
|
||||
manager.resolve_priority_tag_for_model(["base model", "portrait"], "lora")
|
||||
== "base model"
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_priority_tag_real_1119_tag_list(manager):
|
||||
"""End to end for the reported model: both of its tags are unusable."""
|
||||
assert (
|
||||
manager.resolve_priority_tag_for_model(
|
||||
[KEYWORD_DUMP_TAG, "base model"], "lora"
|
||||
)
|
||||
== ""
|
||||
)
|
||||
|
||||
|
||||
def test_auto_set_default_roots(manager):
|
||||
# Clear any previously auto-set values to test fresh behavior
|
||||
manager.settings["default_lora_root"] = ""
|
||||
|
||||
@@ -177,6 +177,20 @@ def test_calculate_relative_path_uses_next_usable_tag(isolated_settings):
|
||||
assert calculate_relative_path_for_model(model_data, "lora") == "Krea 2/portrait"
|
||||
|
||||
|
||||
def test_calculate_relative_path_ignores_civitai_meta_tag(isolated_settings):
|
||||
"""Civitai's "base model" label is not content, so it is not a folder."""
|
||||
model_data = {"base_model": "Krea 2", "tags": ["base model"]}
|
||||
|
||||
assert calculate_relative_path_for_model(model_data, "lora") == "Krea 2/no tags"
|
||||
|
||||
|
||||
def test_calculate_relative_path_ignores_full_1119_tag_list(isolated_settings):
|
||||
"""The reported model carries only a keyword dump and the meta label."""
|
||||
model_data = {"base_model": "Krea 2", "tags": [KEYWORD_DUMP_TAG, "base model"]}
|
||||
|
||||
assert calculate_relative_path_for_model(model_data, "lora") == "Krea 2/no tags"
|
||||
|
||||
|
||||
def test_calculate_relative_path_sanitizes_tag_segment(isolated_settings):
|
||||
"""A tag with path separators must not create nested folders."""
|
||||
model_data = {"base_model": "SDXL", "tags": ["a/b:c"]}
|
||||
|
||||
Reference in New Issue
Block a user