mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-22 11:21:15 -03:00
Compare commits
3 Commits
33e5f3d85d
...
818fa34a48
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
818fa34a48 | ||
|
|
78303b2a5e | ||
|
|
9ce56dd40c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ model_cache/
|
||||
.claude/
|
||||
.sisyphus/
|
||||
.codex
|
||||
.omo
|
||||
|
||||
# Vue widgets development cache (but keep build output)
|
||||
vue-widgets/node_modules/
|
||||
|
||||
@@ -45,10 +45,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_lora_name(lora_path):
|
||||
"""Extract the lora name from a lora path (e.g., 'IL\\aorunIllstrious.safetensors' -> 'aorunIllstrious')"""
|
||||
# Get the basename without extension
|
||||
basename = os.path.basename(lora_path)
|
||||
return os.path.splitext(basename)[0]
|
||||
normalized = lora_path.replace("\\", "/")
|
||||
basename = os.path.basename(normalized)
|
||||
name_no_ext = os.path.splitext(basename)[0]
|
||||
dirname = os.path.dirname(normalized)
|
||||
if dirname and dirname not in (".", "/") and not normalized.startswith("/"):
|
||||
return f"{dirname}/{name_no_ext}"
|
||||
return name_no_ext
|
||||
|
||||
|
||||
def get_loras_list(kwargs):
|
||||
|
||||
@@ -788,7 +788,7 @@ class ModelManagementHandler:
|
||||
|
||||
metadata_updates = {k: v for k, v in data.items() if k != "file_path"}
|
||||
|
||||
await self._metadata_sync.save_metadata_updates(
|
||||
updated_metadata = await self._metadata_sync.save_metadata_updates(
|
||||
file_path=file_path,
|
||||
updates=metadata_updates,
|
||||
metadata_loader=self._metadata_sync.load_local_metadata,
|
||||
@@ -799,7 +799,12 @@ class ModelManagementHandler:
|
||||
cache = await self._service.scanner.get_cached_data()
|
||||
await cache.resort()
|
||||
|
||||
return web.json_response({"success": True})
|
||||
from ...services.auto_tag_service import extract_auto_tags
|
||||
auto_tags = extract_auto_tags(updated_metadata)
|
||||
|
||||
return web.json_response(
|
||||
{"success": True, "auto_tags": auto_tags}
|
||||
)
|
||||
except Exception as exc:
|
||||
self._logger.error("Error saving metadata: %s", exc, exc_info=True)
|
||||
return web.Response(text=str(exc), status=500)
|
||||
@@ -816,14 +821,16 @@ class ModelManagementHandler:
|
||||
if not isinstance(new_tags, list):
|
||||
return web.Response(text="Tags must be a list", status=400)
|
||||
|
||||
tags = await self._tag_update_service.add_tags(
|
||||
tags, auto_tags = await self._tag_update_service.add_tags(
|
||||
file_path=file_path,
|
||||
new_tags=new_tags,
|
||||
metadata_loader=self._metadata_sync.load_local_metadata,
|
||||
update_cache=self._service.scanner.update_single_model_cache,
|
||||
)
|
||||
|
||||
return web.json_response({"success": True, "tags": tags})
|
||||
return web.json_response(
|
||||
{"success": True, "tags": tags, "auto_tags": auto_tags}
|
||||
)
|
||||
except Exception as exc:
|
||||
self._logger.error("Error adding tags: %s", exc, exc_info=True)
|
||||
return web.Response(text=str(exc), status=500)
|
||||
|
||||
@@ -76,46 +76,64 @@ def _collect_sources(model_data: Dict) -> List[str]:
|
||||
def extract_auto_tags(model_data: Dict) -> List[str]:
|
||||
"""Extract auto-detected tags from model metadata.
|
||||
|
||||
Matches predefined patterns against filename, base_model, and
|
||||
CivitAI version name. Returns a sorted, deduplicated list of tag labels.
|
||||
Uses a two-layer approach:
|
||||
Layer 1 — Regex-based detection against filename, base_model, and
|
||||
CivitAI version name.
|
||||
Layer 2 — Merge in any user-defined tags that overlap with known
|
||||
auto-tag categories. This provides a manual fallback when
|
||||
auto-detection fails (e.g. "I2V HN" or unlabeled models).
|
||||
|
||||
HIGH/LOW tags are only returned when the base_model indicates a Wan
|
||||
family model — no other model architecture uses this distinction.
|
||||
|
||||
Args:
|
||||
model_data: Model metadata dict with keys:
|
||||
file_name, base_model, civitai (with optional 'name' field).
|
||||
file_name, base_model, civitai (with optional 'name' field),
|
||||
tags (user-defined tag list, used as fallback).
|
||||
|
||||
Returns:
|
||||
Sorted list of unique auto-tag strings (e.g. ["I2V"]).
|
||||
"""
|
||||
sources = _collect_sources(model_data)
|
||||
if not sources:
|
||||
return []
|
||||
|
||||
base_model = model_data.get("base_model", "")
|
||||
is_wan = "wan" in base_model.lower()
|
||||
|
||||
found: Set[str] = set()
|
||||
|
||||
for label, pattern in AUTO_TAG_CATEGORIES.items():
|
||||
# HIGH/LOW are Wan-specific — skip for non-Wan to avoid noise
|
||||
if label in ("HIGH", "LOW"):
|
||||
if not is_wan:
|
||||
continue
|
||||
# Use case-insensitive character class + case-sensitive boundary,
|
||||
# so "HighNoise" (camelCase) matches but "highlight" doesn't.
|
||||
# Boundary: not followed by lowercase letter (= word has ended).
|
||||
ci = "".join(f"[{c.lower()}{c.upper()}]" for c in label)
|
||||
if label == "LOW":
|
||||
regex = re.compile(r"(?<![Ff])" + ci + r"(?![a-z])")
|
||||
# ── Layer 1: regex-based detection ────────────────────────────
|
||||
if sources:
|
||||
for label, pattern in AUTO_TAG_CATEGORIES.items():
|
||||
# HIGH/LOW are Wan-specific — skip for non-Wan to avoid noise
|
||||
if label in ("HIGH", "LOW"):
|
||||
if not is_wan:
|
||||
continue
|
||||
# Use case-insensitive character class + case-sensitive boundary,
|
||||
# so "HighNoise" (camelCase) matches but "highlight" doesn't.
|
||||
# Boundary: not followed by lowercase letter (= word has ended).
|
||||
ci = "".join(f"[{c.lower()}{c.upper()}]" for c in label)
|
||||
if label == "LOW":
|
||||
regex = re.compile(r"(?<![Ff])" + ci + r"(?![a-z])")
|
||||
else:
|
||||
regex = re.compile(ci + r"(?![a-z])")
|
||||
else:
|
||||
regex = re.compile(ci + r"(?![a-z])")
|
||||
else:
|
||||
regex = re.compile(pattern, re.IGNORECASE)
|
||||
for source in sources:
|
||||
if regex.search(source):
|
||||
found.add(label)
|
||||
break
|
||||
regex = re.compile(pattern, re.IGNORECASE)
|
||||
for source in sources:
|
||||
if regex.search(source):
|
||||
found.add(label)
|
||||
break
|
||||
|
||||
# ── Layer 2: user-defined tags as manual fallback ─────────────
|
||||
# When auto-detection fails (abbreviated names like "Hi"/"Lo",
|
||||
# "I2V HN", or unlabeled models), users can add canonical tags
|
||||
# (HIGH, LOW, I2V, etc.) to the model's regular tags for correct
|
||||
# badge display and filtering. Matching is case-insensitive so
|
||||
# "high"/"High"/"HIGH" all resolve to the canonical label.
|
||||
user_tags = model_data.get("tags")
|
||||
if user_tags:
|
||||
label_map = {label.lower(): label for label in AUTO_TAG_CATEGORIES}
|
||||
for t in user_tags:
|
||||
canonical = label_map.get(t.lower())
|
||||
if canonical:
|
||||
found.add(canonical)
|
||||
|
||||
return sorted(found)
|
||||
|
||||
@@ -870,22 +870,75 @@ class BaseModelService(ABC):
|
||||
"""Get the static preview URL for a model file"""
|
||||
cache = await self.scanner.get_cached_data()
|
||||
|
||||
name_normalized = model_name.replace("\\", "/")
|
||||
name_no_ext = name_normalized
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if name_no_ext.lower().endswith(ext):
|
||||
name_no_ext = name_no_ext[: -len(ext)]
|
||||
break
|
||||
|
||||
has_path = "/" in name_no_ext
|
||||
basename = os.path.basename(name_no_ext) if has_path else name_no_ext
|
||||
best_fallback = None
|
||||
|
||||
for model in cache.raw_data:
|
||||
if model["file_name"] == model_name:
|
||||
file_name = model.get("file_name", "")
|
||||
folder = model.get("folder", "")
|
||||
file_name_no_ext = file_name
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if file_name_no_ext.lower().endswith(ext):
|
||||
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||
break
|
||||
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||
|
||||
if name_no_ext == file_name_no_ext or name_no_ext == path_name:
|
||||
preview_url = model.get("preview_url")
|
||||
if preview_url:
|
||||
from ..config import config
|
||||
|
||||
return config.get_preview_static_url(preview_url)
|
||||
|
||||
if has_path and file_name_no_ext == basename:
|
||||
if folder and name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||
best_fallback = model
|
||||
elif best_fallback is None:
|
||||
best_fallback = model
|
||||
|
||||
if best_fallback:
|
||||
preview_url = best_fallback.get("preview_url")
|
||||
if preview_url:
|
||||
from ..config import config
|
||||
|
||||
return config.get_preview_static_url(preview_url)
|
||||
|
||||
return "/loras_static/images/no-preview.png"
|
||||
|
||||
async def get_model_civitai_url(self, model_name: str) -> Dict[str, Optional[str]]:
|
||||
"""Get the Civitai URL for a model file"""
|
||||
cache = await self.scanner.get_cached_data()
|
||||
|
||||
name_normalized = model_name.replace("\\", "/")
|
||||
name_no_ext = name_normalized
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if name_no_ext.lower().endswith(ext):
|
||||
name_no_ext = name_no_ext[: -len(ext)]
|
||||
break
|
||||
|
||||
has_path = "/" in name_no_ext
|
||||
basename = os.path.basename(name_no_ext) if has_path else name_no_ext
|
||||
best_fallback = None
|
||||
|
||||
for model in cache.raw_data:
|
||||
if model["file_name"] == model_name:
|
||||
file_name = model.get("file_name", "")
|
||||
folder = model.get("folder", "")
|
||||
file_name_no_ext = file_name
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if file_name_no_ext.lower().endswith(ext):
|
||||
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||
break
|
||||
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||
|
||||
if name_no_ext == file_name_no_ext or name_no_ext == path_name:
|
||||
civitai_data = model.get("civitai", {})
|
||||
model_id = civitai_data.get("modelId")
|
||||
version_id = civitai_data.get("id")
|
||||
@@ -904,6 +957,27 @@ class BaseModelService(ABC):
|
||||
"version_id": str(version_id) if version_id else None,
|
||||
}
|
||||
|
||||
if has_path and file_name_no_ext == basename:
|
||||
if folder and name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||
best_fallback = model
|
||||
elif best_fallback is None:
|
||||
best_fallback = model
|
||||
|
||||
if best_fallback:
|
||||
civitai_data = best_fallback.get("civitai", {})
|
||||
model_id = civitai_data.get("modelId")
|
||||
if model_id:
|
||||
version_id = civitai_data.get("id")
|
||||
civitai_host = self.settings.get("civitai_host", "civitai.com")
|
||||
civitai_url = build_civitai_model_page_url(
|
||||
model_id, version_id, host=civitai_host
|
||||
)
|
||||
return {
|
||||
"civitai_url": civitai_url,
|
||||
"model_id": str(model_id),
|
||||
"version_id": str(version_id) if version_id else None,
|
||||
}
|
||||
|
||||
return {"civitai_url": None, "model_id": None, "version_id": None}
|
||||
|
||||
async def get_model_metadata(self, file_path: str) -> Optional[Dict]:
|
||||
|
||||
@@ -312,8 +312,23 @@ class LoraService(BaseModelService):
|
||||
"""Return cached raw metadata for a LoRA matching the given filename."""
|
||||
cache = await self.scanner.get_cached_data(force_refresh=False)
|
||||
|
||||
fn_normalized = filename.replace("\\", "/")
|
||||
fn_no_ext = fn_normalized
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if fn_no_ext.lower().endswith(ext):
|
||||
fn_no_ext = fn_no_ext[: -len(ext)]
|
||||
break
|
||||
|
||||
for lora in cache.raw_data if cache else []:
|
||||
if lora.get("file_name") == filename:
|
||||
file_name = lora.get("file_name", "")
|
||||
folder = lora.get("folder", "")
|
||||
file_name_no_ext = file_name
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if file_name_no_ext.lower().endswith(ext):
|
||||
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||
break
|
||||
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||
if fn_no_ext in (file_name_no_ext, path_name):
|
||||
return lora
|
||||
|
||||
return None
|
||||
@@ -401,7 +416,10 @@ class LoraService(BaseModelService):
|
||||
locked_loras = locked_loras[:target_count]
|
||||
|
||||
# Filter out locked LoRAs from available pool
|
||||
locked_names = {lora["name"] for lora in locked_loras}
|
||||
locked_names = {
|
||||
os.path.basename(lora["name"]) if "/" in str(lora.get("name", "")) else lora["name"]
|
||||
for lora in locked_loras
|
||||
}
|
||||
available_pool = [
|
||||
l for l in available_loras if l["file_name"] not in locked_names
|
||||
]
|
||||
@@ -456,7 +474,7 @@ class LoraService(BaseModelService):
|
||||
|
||||
result_loras.append(
|
||||
{
|
||||
"name": lora["file_name"],
|
||||
"name": f"{lora['folder']}/{lora['file_name']}" if lora.get("folder") else lora["file_name"],
|
||||
"strength": model_str,
|
||||
"clipStrength": clip_str,
|
||||
"active": True,
|
||||
@@ -672,8 +690,9 @@ class LoraService(BaseModelService):
|
||||
# Return minimal data needed for cycling
|
||||
return [
|
||||
{
|
||||
"file_name": lora["file_name"],
|
||||
"file_name": f"{lora['folder']}/{lora['file_name']}" if lora.get("folder") else lora["file_name"],
|
||||
"model_name": lora.get("model_name", lora["file_name"]),
|
||||
"folder": lora.get("folder", ""),
|
||||
}
|
||||
for lora in available_loras
|
||||
]
|
||||
|
||||
@@ -209,7 +209,9 @@ class ModelHashIndex:
|
||||
return self._filename_to_hash.get(filename)
|
||||
|
||||
def get_hash_by_filename(self, filename: str) -> Optional[str]:
|
||||
"""Get hash for a filename without extension"""
|
||||
"""Get hash for a filename (bare basename or path-prefixed name)"""
|
||||
if "/" in filename or "\\" in filename:
|
||||
filename = os.path.splitext(os.path.basename(filename.replace("\\", "/")))[0]
|
||||
return self._filename_to_hash.get(filename)
|
||||
|
||||
def clear(self) -> None:
|
||||
|
||||
@@ -1597,12 +1597,39 @@ class ModelScanner:
|
||||
"""Get model information by name"""
|
||||
try:
|
||||
cache = await self.get_cached_data()
|
||||
|
||||
|
||||
name_normalized = name.replace("\\", "/")
|
||||
name_no_ext = name_normalized
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if name_no_ext.lower().endswith(ext):
|
||||
name_no_ext = name_no_ext[: -len(ext)]
|
||||
break
|
||||
|
||||
has_path = "/" in name_no_ext
|
||||
basename = os.path.basename(name_no_ext) if has_path else name_no_ext
|
||||
best_fallback = None
|
||||
|
||||
for model in cache.raw_data:
|
||||
if model.get("file_name") == name:
|
||||
file_name = model.get("file_name", "")
|
||||
folder = model.get("folder", "")
|
||||
file_name_no_ext = file_name
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if file_name_no_ext.lower().endswith(ext):
|
||||
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||
break
|
||||
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||
|
||||
if name_no_ext == file_name_no_ext or name_no_ext == path_name:
|
||||
return model
|
||||
|
||||
return None
|
||||
|
||||
if has_path and file_name_no_ext == basename:
|
||||
if folder and name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||
best_fallback = model
|
||||
elif best_fallback is None:
|
||||
best_fallback = model
|
||||
|
||||
return best_fallback
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting model info by name: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
@@ -2517,6 +2517,7 @@ class RecipeScanner:
|
||||
continue
|
||||
|
||||
file_name = None
|
||||
folder = ""
|
||||
hash_value = (lora.get("hash") or "").lower()
|
||||
if (
|
||||
hash_value
|
||||
@@ -2526,6 +2527,11 @@ class RecipeScanner:
|
||||
file_path = self._lora_scanner._hash_index.get_path(hash_value)
|
||||
if file_path:
|
||||
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||
if lora_cache is not None:
|
||||
for cached_lora in getattr(lora_cache, "raw_data", []):
|
||||
if cached_lora.get("file_path") == file_path:
|
||||
folder = cached_lora.get("folder", "")
|
||||
break
|
||||
|
||||
if not file_name and lora.get("modelVersionId") and lora_cache is not None:
|
||||
for cached_lora in getattr(lora_cache, "raw_data", []):
|
||||
@@ -2540,13 +2546,16 @@ class RecipeScanner:
|
||||
file_name = os.path.splitext(os.path.basename(cached_path))[
|
||||
0
|
||||
]
|
||||
folder = cached_lora.get("folder", "")
|
||||
break
|
||||
|
||||
if not file_name:
|
||||
file_name = lora.get("file_name", "unknown-lora")
|
||||
folder = lora.get("folder", "")
|
||||
|
||||
lora_name = f"{folder}/{file_name}" if folder else file_name
|
||||
strength = lora.get("strength", 1.0)
|
||||
syntax_parts.append(f"<lora:{file_name}:{strength}>")
|
||||
syntax_parts.append(f"<lora:{lora_name}:{strength}>")
|
||||
|
||||
return syntax_parts
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from typing import Awaitable, Callable, Dict, List, Sequence
|
||||
from typing import Awaitable, Callable, Dict, List, Sequence, Tuple
|
||||
|
||||
from .auto_tag_service import extract_auto_tags
|
||||
|
||||
|
||||
class TagUpdateService:
|
||||
@@ -20,9 +22,8 @@ class TagUpdateService:
|
||||
new_tags: Sequence[str],
|
||||
metadata_loader: Callable[[str], Awaitable[Dict[str, object]]],
|
||||
update_cache: Callable[[str, str, Dict[str, object]], Awaitable[bool]],
|
||||
) -> List[str]:
|
||||
"""Add tags to a metadata entry while keeping case-insensitive uniqueness."""
|
||||
|
||||
) -> Tuple[List[str], List[str]]:
|
||||
"""Add tags to a metadata entry and return updated tags and auto_tags."""
|
||||
base, _ = os.path.splitext(file_path)
|
||||
metadata_path = f"{base}.metadata.json"
|
||||
metadata = await metadata_loader(metadata_path)
|
||||
@@ -44,5 +45,6 @@ class TagUpdateService:
|
||||
await self._metadata_manager.save_metadata(file_path, metadata)
|
||||
await update_cache(file_path, file_path, metadata)
|
||||
|
||||
return existing_tags
|
||||
auto_tags = extract_auto_tags(metadata)
|
||||
return existing_tags, auto_tags
|
||||
|
||||
|
||||
@@ -15,30 +15,64 @@ def get_lora_info(lora_name):
|
||||
scanner = await ServiceRegistry.get_lora_scanner()
|
||||
cache = await scanner.get_cached_data()
|
||||
|
||||
lora_name_normalized = lora_name.replace("\\", "/")
|
||||
lora_name_no_ext = lora_name_normalized
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if lora_name_no_ext.lower().endswith(ext):
|
||||
lora_name_no_ext = lora_name_no_ext[: -len(ext)]
|
||||
break
|
||||
|
||||
has_path = "/" in lora_name_no_ext
|
||||
basename = os.path.basename(lora_name_no_ext) if has_path else lora_name_no_ext
|
||||
best_fallback = None
|
||||
|
||||
for item in cache.raw_data:
|
||||
if item.get("file_name") == lora_name:
|
||||
file_path = item.get("file_path")
|
||||
if file_path:
|
||||
# Check all lora roots including extra paths
|
||||
all_roots = list(config.loras_roots or []) + list(
|
||||
config.extra_loras_roots or []
|
||||
file_name = item.get("file_name", "")
|
||||
folder = item.get("folder", "")
|
||||
file_name_no_ext = file_name
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if file_name_no_ext.lower().endswith(ext):
|
||||
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||
break
|
||||
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||
|
||||
if lora_name_no_ext not in (file_name_no_ext, path_name):
|
||||
if has_path and file_name_no_ext == basename:
|
||||
if folder and lora_name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||
best_fallback = item
|
||||
elif best_fallback is None:
|
||||
best_fallback = item
|
||||
continue
|
||||
|
||||
file_path = item.get("file_path")
|
||||
if not file_path:
|
||||
continue
|
||||
|
||||
all_roots = list(config.loras_roots or []) + list(
|
||||
config.extra_loras_roots or []
|
||||
)
|
||||
for root in all_roots:
|
||||
root = root.replace(os.sep, "/")
|
||||
if file_path.startswith(root):
|
||||
relative_path = os.path.relpath(file_path, root).replace(
|
||||
os.sep, "/"
|
||||
)
|
||||
for root in all_roots:
|
||||
root = root.replace(os.sep, "/")
|
||||
if file_path.startswith(root):
|
||||
relative_path = os.path.relpath(file_path, root).replace(
|
||||
os.sep, "/"
|
||||
)
|
||||
# Get trigger words from civitai metadata
|
||||
civitai = item.get("civitai", {})
|
||||
trigger_words = (
|
||||
civitai.get("trainedWords", []) if civitai else []
|
||||
)
|
||||
return relative_path, trigger_words
|
||||
# If not found in any root, return path with trigger words from cache
|
||||
civitai = item.get("civitai", {})
|
||||
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||
return file_path, trigger_words
|
||||
trigger_words = (
|
||||
civitai.get("trainedWords", []) if civitai else []
|
||||
)
|
||||
return relative_path, trigger_words
|
||||
civitai = item.get("civitai", {})
|
||||
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||
return file_path, trigger_words
|
||||
|
||||
if best_fallback:
|
||||
file_path = best_fallback.get("file_path")
|
||||
if file_path:
|
||||
civitai = best_fallback.get("civitai", {})
|
||||
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||
return file_path, trigger_words
|
||||
|
||||
return lora_name, []
|
||||
|
||||
try:
|
||||
@@ -77,15 +111,54 @@ def get_lora_info_absolute(lora_name):
|
||||
scanner = await ServiceRegistry.get_lora_scanner()
|
||||
cache = await scanner.get_cached_data()
|
||||
|
||||
lora_name_normalized = lora_name.replace("\\", "/")
|
||||
lora_name_no_ext = lora_name_normalized
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if lora_name_no_ext.lower().endswith(ext):
|
||||
lora_name_no_ext = lora_name_no_ext[: -len(ext)]
|
||||
break
|
||||
|
||||
has_path = "/" in lora_name_no_ext
|
||||
basename = os.path.basename(lora_name_no_ext) if has_path else lora_name_no_ext
|
||||
best_fallback = None
|
||||
|
||||
for item in cache.raw_data:
|
||||
if item.get("file_name") == lora_name:
|
||||
file_name = item.get("file_name", "")
|
||||
folder = item.get("folder", "")
|
||||
file_name_no_ext = file_name
|
||||
for ext in (".safetensors", ".ckpt", ".pt", ".bin"):
|
||||
if file_name_no_ext.lower().endswith(ext):
|
||||
file_name_no_ext = file_name_no_ext[: -len(ext)]
|
||||
break
|
||||
path_name = f"{folder}/{file_name_no_ext}".replace("\\", "/") if folder else file_name_no_ext
|
||||
|
||||
if lora_name_no_ext == file_name_no_ext:
|
||||
file_path = item.get("file_path")
|
||||
if file_path:
|
||||
# Return absolute path directly
|
||||
# Get trigger words from civitai metadata
|
||||
civitai = item.get("civitai", {})
|
||||
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||
return file_path, trigger_words
|
||||
|
||||
if lora_name_no_ext == path_name:
|
||||
file_path = item.get("file_path")
|
||||
if file_path:
|
||||
civitai = item.get("civitai", {})
|
||||
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||
return file_path, trigger_words
|
||||
|
||||
if has_path and file_name_no_ext == basename:
|
||||
if folder and lora_name_no_ext.startswith(folder.replace("\\", "/") + "/"):
|
||||
best_fallback = item
|
||||
elif best_fallback is None:
|
||||
best_fallback = item
|
||||
|
||||
if best_fallback:
|
||||
file_path = best_fallback.get("file_path")
|
||||
if file_path:
|
||||
civitai = best_fallback.get("civitai", {})
|
||||
trigger_words = civitai.get("trainedWords", []) if civitai else []
|
||||
return file_path, trigger_words
|
||||
|
||||
return lora_name, []
|
||||
|
||||
try:
|
||||
|
||||
@@ -422,8 +422,12 @@ export class BaseModelApiClient {
|
||||
throw new Error('Failed to save metadata');
|
||||
}
|
||||
|
||||
state.virtualScroller.updateSingleItem(filePath, data);
|
||||
return response.json();
|
||||
const result = await response.json();
|
||||
state.virtualScroller.updateSingleItem(filePath, {
|
||||
...data,
|
||||
auto_tags: result.auto_tags,
|
||||
});
|
||||
return result;
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
@@ -448,7 +452,10 @@ export class BaseModelApiClient {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.tags) {
|
||||
state.virtualScroller.updateSingleItem(filePath, { tags: result.tags });
|
||||
state.virtualScroller.updateSingleItem(filePath, {
|
||||
tags: result.tags,
|
||||
auto_tags: result.auto_tags,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -166,7 +166,9 @@ async function toggleFavorite(card) {
|
||||
function handleSendToWorkflow(card, replaceMode, modelType) {
|
||||
if (modelType === MODEL_TYPES.LORA) {
|
||||
const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
|
||||
const loraSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
|
||||
const folder = card.dataset.folder || '';
|
||||
const loraName = folder ? `${folder}/${card.dataset.file_name}` : card.dataset.file_name;
|
||||
const loraSyntax = buildLoraSyntax(loraName, usageTips);
|
||||
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
|
||||
} else if (modelType === MODEL_TYPES.CHECKPOINT) {
|
||||
const modelPath = card.dataset.filepath;
|
||||
|
||||
@@ -274,7 +274,17 @@ async function saveTags() {
|
||||
|
||||
const filePath = editBtn.dataset.filePath;
|
||||
const tagElements = document.querySelectorAll('.metadata-item');
|
||||
const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||
let tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||
|
||||
// Flush uncommitted input as a tag so it's not silently lost on save
|
||||
const tagInput = document.querySelector('.metadata-input');
|
||||
if (tagInput) {
|
||||
const pendingTag = tagInput.value.trim().toLowerCase();
|
||||
if (pendingTag && !tags.includes(pendingTag)) {
|
||||
tags.push(pendingTag);
|
||||
}
|
||||
tagInput.value = '';
|
||||
}
|
||||
|
||||
// Get original tags to compare
|
||||
const originalTagElements = document.querySelectorAll('.tooltip-tag');
|
||||
@@ -465,6 +475,7 @@ function setupTagInput() {
|
||||
const tagInput = document.querySelector('.metadata-input');
|
||||
|
||||
if (tagInput) {
|
||||
tagInput.focus();
|
||||
tagInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -785,6 +785,7 @@ export class BulkManager {
|
||||
// Setup tag input behavior
|
||||
const tagInput = document.querySelector('.bulk-metadata-input');
|
||||
if (tagInput) {
|
||||
tagInput.focus();
|
||||
tagInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
@@ -1008,7 +1009,17 @@ export class BulkManager {
|
||||
|
||||
async saveBulkTags(mode = 'append') {
|
||||
const tagElements = document.querySelectorAll('#bulkTagsItems .metadata-item');
|
||||
const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||
let tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||
|
||||
// Flush uncommitted input as a tag so it's not silently lost on save
|
||||
const tagInput = document.querySelector('.bulk-metadata-input');
|
||||
if (tagInput) {
|
||||
const pendingTag = tagInput.value.trim().toLowerCase();
|
||||
if (pendingTag && !tags.includes(pendingTag)) {
|
||||
tags.push(pendingTag);
|
||||
}
|
||||
tagInput.value = '';
|
||||
}
|
||||
|
||||
if (tags.length === 0) {
|
||||
showToast('toast.models.noTagsToAdd', {}, 'warning');
|
||||
|
||||
@@ -430,7 +430,9 @@ export function buildLoraSyntax(fileName, usageTips = {}) {
|
||||
|
||||
export function copyLoraSyntax(card) {
|
||||
const usageTips = JSON.parse(card.dataset.usage_tips || "{}");
|
||||
const baseSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
|
||||
const folder = card.dataset.folder || '';
|
||||
const loraName = folder ? `${folder}/${card.dataset.file_name}` : card.dataset.file_name;
|
||||
const baseSyntax = buildLoraSyntax(loraName, usageTips);
|
||||
|
||||
// Check if trigger words should be included
|
||||
const includeTriggerWords = state.global.settings.include_trigger_words;
|
||||
|
||||
@@ -185,7 +185,7 @@ describe('AutoComplete widget interactions', () => {
|
||||
expect(fetchApiMock).toHaveBeenCalledWith(
|
||||
'/lm/loras/usage-tips-by-path?relative_path=models%2Fexample.safetensors',
|
||||
);
|
||||
expect(input.value).toContain('<lora:example:1.5:0.9>,');
|
||||
expect(input.value).toContain('<lora:models/example:1.5:0.9>,');
|
||||
expect(autoComplete.dropdown.style.display).toBe('none');
|
||||
expect(input.focus).toHaveBeenCalled();
|
||||
expect(input.setSelectionRange).toHaveBeenCalled();
|
||||
@@ -1624,8 +1624,8 @@ describe('AutoComplete widget interactions', () => {
|
||||
|
||||
await autoComplete.insertSelection('models/example.safetensors');
|
||||
|
||||
expect(input.value).toContain('<lora:example:1.2>');
|
||||
expect(input.value).not.toContain('<lora:example:1.2>,');
|
||||
expect(input.value).toContain('<lora:models/example:1.2>');
|
||||
expect(input.value).not.toContain('<lora:models/example:1.2>,');
|
||||
});
|
||||
|
||||
it('replaces entire phrase when selected tag ends with underscore version of search term (suffix match)', async () => {
|
||||
|
||||
@@ -255,7 +255,7 @@ def test_tag_update_service_adds_unique_tags(tmp_path: Path) -> None:
|
||||
cache_updates.append(metadata)
|
||||
return True
|
||||
|
||||
tags = asyncio.run(
|
||||
tags, auto_tags = asyncio.run(
|
||||
service.add_tags(
|
||||
file_path=str(tmp_path / "model.safetensors"),
|
||||
new_tags=["new", "existing"],
|
||||
@@ -265,5 +265,6 @@ def test_tag_update_service_adds_unique_tags(tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
assert tags == ["existing", "new"]
|
||||
assert auto_tags == []
|
||||
assert manager.saved
|
||||
assert cache_updates
|
||||
|
||||
@@ -43,7 +43,7 @@ async def test_tag_update_service_handles_case_insensitive_tags(tmp_path: Path)
|
||||
return True
|
||||
|
||||
# Try to add "Test" (different case) - should not be added since "test" already exists
|
||||
tags = await service.add_tags(
|
||||
tags, auto_tags = await service.add_tags(
|
||||
file_path=str(tmp_path / "model.safetensors"),
|
||||
new_tags=["Test"],
|
||||
metadata_loader=loader,
|
||||
@@ -52,6 +52,7 @@ async def test_tag_update_service_handles_case_insensitive_tags(tmp_path: Path)
|
||||
|
||||
# Should still only have "test" (lowercase) in the tags
|
||||
assert tags == ["test"]
|
||||
assert auto_tags == [] # no file_name/base_model in metadata, so no auto-detection
|
||||
assert len(manager.saved) == 1
|
||||
saved_metadata = manager.saved[0][1]
|
||||
assert saved_metadata["tags"] == ["test"]
|
||||
@@ -76,7 +77,7 @@ async def test_tag_update_service_adds_new_tags_in_lowercase(tmp_path: Path) ->
|
||||
return True
|
||||
|
||||
# Add new tags with mixed case
|
||||
tags = await service.add_tags(
|
||||
tags, auto_tags = await service.add_tags(
|
||||
file_path=str(tmp_path / "model.safetensors"),
|
||||
new_tags=["NewTag", "ANOTHER_TAG"],
|
||||
metadata_loader=loader,
|
||||
@@ -87,6 +88,7 @@ async def test_tag_update_service_adds_new_tags_in_lowercase(tmp_path: Path) ->
|
||||
assert "existing" in tags
|
||||
assert "newtag" in tags
|
||||
assert "another_tag" in tags
|
||||
assert auto_tags == []
|
||||
assert len(manager.saved) == 1
|
||||
saved_metadata = manager.saved[0][1]
|
||||
assert "newtag" in saved_metadata["tags"]
|
||||
|
||||
@@ -126,6 +126,80 @@ class TestExtractAutoTags:
|
||||
})
|
||||
assert set(result) == {"HIGH", "I2V"}
|
||||
|
||||
# ── Layer 2: user-defined tags as manual fallback ───────────
|
||||
|
||||
def test_user_tags_fallback_when_detection_fails(self):
|
||||
result = extract_auto_tags({
|
||||
"file_name": "BOTH-v1.0",
|
||||
"base_model": "Wan 2.2",
|
||||
"civitai": {},
|
||||
"tags": ["HIGH", "I2V", "T2V"],
|
||||
})
|
||||
assert set(result) == {"HIGH", "I2V", "T2V"}
|
||||
|
||||
def test_user_tags_augment_partial_detection(self):
|
||||
result = extract_auto_tags({
|
||||
"file_name": "wan_i2v_hn_v2",
|
||||
"base_model": "Wan 2.2 I2V",
|
||||
"civitai": {},
|
||||
"tags": ["HIGH"],
|
||||
})
|
||||
assert set(result) == {"HIGH", "I2V"}
|
||||
|
||||
def test_user_tags_non_auto_tag_ignored(self):
|
||||
result = extract_auto_tags({
|
||||
"file_name": "model_v1",
|
||||
"base_model": "Wan 2.2",
|
||||
"civitai": {},
|
||||
"tags": ["HIGH", "character", "style", "nsfw"],
|
||||
})
|
||||
assert set(result) == {"HIGH"}
|
||||
|
||||
def test_user_tags_overrides_non_wan_gate(self):
|
||||
result = extract_auto_tags({
|
||||
"file_name": "flux_model_v1",
|
||||
"base_model": "Flux.1 D",
|
||||
"civitai": {},
|
||||
"tags": ["HIGH", "LOW", "Turbo"],
|
||||
})
|
||||
assert set(result) == {"HIGH", "LOW", "Turbo"}
|
||||
|
||||
def test_user_tags_no_duplication(self):
|
||||
result = extract_auto_tags({
|
||||
"file_name": "wan_i2v_high_v3",
|
||||
"base_model": "Wan 2.2",
|
||||
"civitai": {},
|
||||
"tags": ["HIGH", "I2V"],
|
||||
})
|
||||
assert set(result) == {"HIGH", "I2V"}
|
||||
|
||||
def test_user_tags_lightning_turbo_manual(self):
|
||||
result = extract_auto_tags({
|
||||
"file_name": "sdxl_model_v1",
|
||||
"base_model": "SDXL",
|
||||
"civitai": {},
|
||||
"tags": ["Lightning"],
|
||||
})
|
||||
assert set(result) == {"Lightning"}
|
||||
|
||||
def test_user_tags_case_insensitive_lowercase(self):
|
||||
result = extract_auto_tags({
|
||||
"file_name": "wan_masterpieces_v2",
|
||||
"base_model": "Wan Video 14B t2v",
|
||||
"civitai": {},
|
||||
"tags": ["high"],
|
||||
})
|
||||
assert set(result) == {"HIGH", "T2V"}
|
||||
|
||||
def test_user_tags_case_insensitive_mixed(self):
|
||||
result = extract_auto_tags({
|
||||
"file_name": "model_v1",
|
||||
"base_model": "SDXL",
|
||||
"civitai": {},
|
||||
"tags": ["lightning", "turbo", "i2v"],
|
||||
})
|
||||
assert set(result) == {"Lightning", "Turbo", "I2V"}
|
||||
|
||||
|
||||
class TestAutoTagCategories:
|
||||
def test_all_patterns_compile(self):
|
||||
|
||||
@@ -1,13 +1,43 @@
|
||||
import pytest
|
||||
|
||||
from py.services.settings_manager import SettingsManager, get_settings_manager
|
||||
from py.services.service_registry import ServiceRegistry
|
||||
from py.utils.utils import (
|
||||
calculate_recipe_fingerprint,
|
||||
calculate_relative_path_for_model,
|
||||
get_lora_info,
|
||||
get_lora_info_absolute,
|
||||
sanitize_folder_name,
|
||||
)
|
||||
|
||||
|
||||
class _FakeCache:
|
||||
def __init__(self, items):
|
||||
self.raw_data = list(items)
|
||||
|
||||
|
||||
class _FakeScanner:
|
||||
def __init__(self, items):
|
||||
self._cache = _FakeCache(items)
|
||||
|
||||
async def get_cached_data(self):
|
||||
return self._cache
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_lora_scanner(monkeypatch):
|
||||
def _setup(items):
|
||||
scanner = _FakeScanner(items)
|
||||
|
||||
async def get_scanner():
|
||||
return scanner
|
||||
|
||||
monkeypatch.setattr(ServiceRegistry, "get_lora_scanner", get_scanner)
|
||||
return scanner
|
||||
|
||||
return _setup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_settings(monkeypatch):
|
||||
manager = get_settings_manager()
|
||||
@@ -114,3 +144,114 @@ def test_calculate_recipe_fingerprint_empty_input():
|
||||
)
|
||||
def test_sanitize_folder_name(original, expected):
|
||||
assert sanitize_folder_name(original) == expected
|
||||
|
||||
|
||||
def test_get_lora_info_absolute_bare_name(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {"trainedWords": ["trigger1"]}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info_absolute("mylora")
|
||||
|
||||
assert path == "/models/Lora/SDXL/mylora.safetensors"
|
||||
assert triggers == ["trigger1"]
|
||||
|
||||
|
||||
def test_get_lora_info_absolute_with_path(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "SDXL/Styles", "file_path": "/models/Lora/SDXL/Styles/mylora.safetensors", "civitai": {"trainedWords": ["artistic"]}},
|
||||
{"file_name": "other", "folder": "", "file_path": "/models/Lora/other.safetensors", "civitai": {}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info_absolute("SDXL/Styles/mylora")
|
||||
|
||||
assert path == "/models/Lora/SDXL/Styles/mylora.safetensors"
|
||||
assert triggers == ["artistic"]
|
||||
|
||||
|
||||
def test_get_lora_info_absolute_path_fallback_to_basename(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "RenamedFolder", "file_path": "/models/Lora/RenamedFolder/mylora.safetensors", "civitai": {"trainedWords": ["trigger1"]}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info_absolute("OldFolder/mylora")
|
||||
|
||||
assert path == "/models/Lora/RenamedFolder/mylora.safetensors"
|
||||
assert triggers == ["trigger1"]
|
||||
|
||||
|
||||
def test_get_lora_info_absolute_prefers_folder_match(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "V1", "file_path": "/models/Lora/V1/mylora.safetensors", "civitai": {"trainedWords": ["v1"]}},
|
||||
{"file_name": "mylora", "folder": "V2", "file_path": "/models/Lora/V2/mylora.safetensors", "civitai": {"trainedWords": ["v2"]}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info_absolute("V2/mylora")
|
||||
|
||||
assert path == "/models/Lora/V2/mylora.safetensors"
|
||||
assert triggers == ["v2"]
|
||||
|
||||
|
||||
def test_get_lora_info_absolute_no_folder_in_cache_no_path_in_name(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "", "file_path": "/models/Lora/mylora.safetensors", "civitai": {}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info_absolute("mylora")
|
||||
|
||||
assert path == "/models/Lora/mylora.safetensors"
|
||||
assert triggers == []
|
||||
|
||||
|
||||
def test_get_lora_info_absolute_strips_extension(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {"trainedWords": ["hello"]}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info_absolute("SDXL/mylora.safetensors")
|
||||
|
||||
assert path == "/models/Lora/SDXL/mylora.safetensors"
|
||||
assert triggers == ["hello"]
|
||||
|
||||
|
||||
def test_get_lora_info_absolute_not_found_returns_original(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info_absolute("nonexistent")
|
||||
|
||||
assert path == "nonexistent"
|
||||
assert triggers == []
|
||||
|
||||
|
||||
def test_get_lora_info_bare_name(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {"trainedWords": ["trigger1"]}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info("mylora")
|
||||
|
||||
assert triggers == ["trigger1"]
|
||||
|
||||
|
||||
def test_get_lora_info_with_path(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "SDXL/Styles", "file_path": "/models/Lora/SDXL/Styles/mylora.safetensors", "civitai": {"trainedWords": ["artistic"]}},
|
||||
{"file_name": "other", "folder": "", "file_path": "/models/Lora/other.safetensors", "civitai": {}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info("SDXL/Styles/mylora")
|
||||
|
||||
assert triggers == ["artistic"]
|
||||
|
||||
|
||||
def test_get_lora_info_not_found_returns_original(mock_lora_scanner):
|
||||
mock_lora_scanner([
|
||||
{"file_name": "mylora", "folder": "SDXL", "file_path": "/models/Lora/SDXL/mylora.safetensors", "civitai": {}},
|
||||
])
|
||||
|
||||
path, triggers = get_lora_info("nonexistent")
|
||||
|
||||
assert path == "nonexistent"
|
||||
assert triggers == []
|
||||
|
||||
@@ -226,7 +226,10 @@ const MODEL_BEHAVIORS = {
|
||||
}
|
||||
},
|
||||
async getInsertText(_instance, relativePath) {
|
||||
const fileName = removeLoraExtension(splitRelativePath(relativePath).fileName);
|
||||
const { directories, fileName } = splitRelativePath(relativePath);
|
||||
const baseName = removeLoraExtension(fileName);
|
||||
const folder = directories.length ? directories.join('/') + '/' : '';
|
||||
const loraName = folder + baseName;
|
||||
|
||||
let strength = 1.0;
|
||||
let hasStrength = false;
|
||||
@@ -262,9 +265,9 @@ const MODEL_BEHAVIORS = {
|
||||
}
|
||||
|
||||
if (clipStrength !== null) {
|
||||
return formatAutocompleteInsertion(`<lora:${fileName}:${strength}:${clipStrength}>`);
|
||||
return formatAutocompleteInsertion(`<lora:${loraName}:${strength}:${clipStrength}>`);
|
||||
}
|
||||
return formatAutocompleteInsertion(`<lora:${fileName}:${strength}>`);
|
||||
return formatAutocompleteInsertion(`<lora:${loraName}:${strength}>`);
|
||||
}
|
||||
},
|
||||
embeddings: {
|
||||
|
||||
Reference in New Issue
Block a user