feat: add base model filtering across search sources, API routes, and frontend UI
This commit is contained in:
+46
@@ -138,6 +138,52 @@ class ModelResolverExtension:
|
||||
)
|
||||
download_available = False
|
||||
|
||||
# ==================== BASE MODELS CONFIG ROUTE ====================
|
||||
|
||||
@routes.get("/model_resolver/base-models")
|
||||
async def get_base_models(request):
|
||||
"""Return the base models config from metadata/base-models.json.
|
||||
|
||||
Returns {base_models: [{name, aliases}]} so the frontend
|
||||
dropdown can populate correctly via baseModels.base_models.
|
||||
"""
|
||||
try:
|
||||
from .core.sources.popular import get_base_models_config
|
||||
data = get_base_models_config()
|
||||
return web.json_response(data)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Model Resolver base-models error: {e}", exc_info=True
|
||||
)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@routes.get("/model_resolver/base-models/status")
|
||||
async def get_base_models_status_route(request):
|
||||
"""Get local and optional remote base models status."""
|
||||
try:
|
||||
check_remote = request.query.get("check_remote") == "1"
|
||||
from .core.sources.popular import get_base_models_status
|
||||
status = await asyncio.to_thread(get_base_models_status, check_remote)
|
||||
return web.json_response(status)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Model Resolver base-models status error: {e}", exc_info=True
|
||||
)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@routes.post("/model_resolver/base-models/update")
|
||||
async def update_base_models_route(request):
|
||||
"""Update base models list from CivitAI."""
|
||||
try:
|
||||
from .core.sources.popular import update_base_models_from_remote
|
||||
status = await asyncio.to_thread(update_base_models_from_remote)
|
||||
return web.json_response(status)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
f"Model Resolver base-models update error: {e}", exc_info=True
|
||||
)
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
# ==================== ANALYZE ROUTES ====================
|
||||
|
||||
@routes.post("/model_resolver/analyze")
|
||||
|
||||
@@ -486,6 +486,34 @@ def _calculate_confidence(
|
||||
return round(best * 100, 1)
|
||||
|
||||
|
||||
def _normalize_base_model(value: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "", str(value or "").lower())
|
||||
|
||||
|
||||
def _base_model_matches(candidate: str, preferred: Optional[str]) -> bool:
|
||||
preferred_norm = _normalize_base_model(preferred or "")
|
||||
if not preferred_norm:
|
||||
return True
|
||||
|
||||
candidate_norm = _normalize_base_model(candidate or "")
|
||||
if not candidate_norm:
|
||||
return False
|
||||
|
||||
from .popular import load_base_model_aliases
|
||||
aliases = load_base_model_aliases()
|
||||
preferred_tokens = aliases.get(preferred_norm, [preferred_norm])
|
||||
return any(
|
||||
token and (token in candidate_norm or candidate_norm in token)
|
||||
for token in preferred_tokens
|
||||
)
|
||||
|
||||
|
||||
def _base_model_score(candidate: str, preferred: Optional[str]) -> float:
|
||||
if not preferred:
|
||||
return 0.0
|
||||
return 1000.0 if _base_model_matches(candidate, preferred) else -1000.0
|
||||
|
||||
|
||||
def _build_result_from_payload(
|
||||
payload: Dict[str, Any],
|
||||
query: str = "",
|
||||
@@ -751,6 +779,7 @@ def search_civarchive(
|
||||
def search_civarchive_for_file(
|
||||
filename: str,
|
||||
model_type: Optional[str] = None,
|
||||
base_model_context: Optional[str] = None,
|
||||
exact_only: bool = False,
|
||||
limit: int = DEFAULT_CIVARCHIVE_CANDIDATE_LIMIT,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
@@ -762,8 +791,9 @@ def search_civarchive_for_file(
|
||||
return None
|
||||
|
||||
limit = max(1, min(int(limit), MAX_CIVARCHIVE_CANDIDATE_LIMIT))
|
||||
base_model_key = _normalize_base_model(base_model_context or "")
|
||||
cache_key = (
|
||||
f"file::{normalized_filename.lower()}::{model_type or ''}::{exact_only}::{limit}"
|
||||
f"file::{normalized_filename.lower()}::{model_type or ''}::{base_model_key}::{exact_only}::{limit}"
|
||||
)
|
||||
if cache_key in _search_cache:
|
||||
return _search_cache[cache_key]
|
||||
@@ -780,6 +810,7 @@ def search_civarchive_for_file(
|
||||
|
||||
best_match = None
|
||||
best_confidence = 0.0
|
||||
best_rank = -9999.0
|
||||
seen = set()
|
||||
|
||||
try:
|
||||
@@ -810,19 +841,29 @@ def search_civarchive_for_file(
|
||||
|
||||
resolved["confidence"] = confidence
|
||||
resolved["match_type"] = "exact" if confidence == 100.0 else "similar"
|
||||
base_model_matches = _base_model_matches(
|
||||
resolved.get("base_model"), base_model_context
|
||||
)
|
||||
rank = confidence + _base_model_score(
|
||||
resolved.get("base_model"), base_model_context
|
||||
)
|
||||
|
||||
if confidence > best_confidence:
|
||||
if rank > best_rank:
|
||||
best_rank = rank
|
||||
best_confidence = confidence
|
||||
best_match = resolved
|
||||
|
||||
if confidence == 100.0:
|
||||
if confidence == 100.0 and base_model_matches:
|
||||
_search_cache[cache_key] = best_match
|
||||
return best_match
|
||||
|
||||
if len(seen) >= limit:
|
||||
break
|
||||
|
||||
if best_match:
|
||||
if best_match and (
|
||||
not base_model_context
|
||||
or _base_model_matches(best_match.get("base_model"), base_model_context)
|
||||
):
|
||||
break
|
||||
except CivArchiveSearchError:
|
||||
raise
|
||||
@@ -832,6 +873,10 @@ def search_civarchive_for_file(
|
||||
|
||||
if best_match and best_confidence < 40:
|
||||
best_match = None
|
||||
if best_match and base_model_context and not _base_model_matches(
|
||||
best_match.get("base_model"), base_model_context
|
||||
):
|
||||
best_match = None
|
||||
|
||||
_search_cache[cache_key] = best_match
|
||||
return best_match
|
||||
|
||||
+48
-6
@@ -261,6 +261,34 @@ def _calculate_filename_confidence(target_filename: str, candidate_filename: str
|
||||
return round(max(similarity, similarity_no_ext) * 100, 1)
|
||||
|
||||
|
||||
def _normalize_base_model(value: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "", str(value or "").lower())
|
||||
|
||||
|
||||
def _base_model_matches(candidate: str, preferred: Optional[str]) -> bool:
|
||||
preferred_norm = _normalize_base_model(preferred or "")
|
||||
if not preferred_norm:
|
||||
return True
|
||||
|
||||
candidate_norm = _normalize_base_model(candidate or "")
|
||||
if not candidate_norm:
|
||||
return False
|
||||
|
||||
from .popular import load_base_model_aliases
|
||||
aliases = load_base_model_aliases()
|
||||
preferred_tokens = aliases.get(preferred_norm, [preferred_norm])
|
||||
return any(
|
||||
token and (token in candidate_norm or candidate_norm in token)
|
||||
for token in preferred_tokens
|
||||
)
|
||||
|
||||
|
||||
def _base_model_score(candidate: str, preferred: Optional[str]) -> float:
|
||||
if not preferred:
|
||||
return 0.0
|
||||
return 1000.0 if _base_model_matches(candidate, preferred) else -1000.0
|
||||
|
||||
|
||||
def _find_matching_file_in_versions(
|
||||
versions: List[Dict[str, Any]],
|
||||
filename: str,
|
||||
@@ -502,6 +530,7 @@ def _find_civitai_file_in_model(
|
||||
api_key: Optional[str] = None,
|
||||
exact_only: bool = False,
|
||||
preferred_version_id: Optional[int] = None,
|
||||
base_model_context: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Load one CivitAI model and search all its versions for the requested file."""
|
||||
filename_lower = filename.lower()
|
||||
@@ -554,7 +583,9 @@ def _find_civitai_file_in_model(
|
||||
if preferred_version_id is not None:
|
||||
resolved = resolve_urn(model_id, preferred_version_id, api_key)
|
||||
if resolved:
|
||||
if resolved_version_matches(resolved):
|
||||
if resolved_version_matches(resolved) and _base_model_matches(
|
||||
resolved.get("base_model"), base_model_context
|
||||
):
|
||||
return build_result_from_resolved_version(resolved, preferred_version_id)
|
||||
|
||||
best_resolved_result = None
|
||||
@@ -589,17 +620,22 @@ def _find_civitai_file_in_model(
|
||||
|
||||
resolved = resolve_urn(model_id, version_id, api_key)
|
||||
if resolved:
|
||||
if resolved_version_matches(resolved):
|
||||
if resolved_version_matches(resolved) and _base_model_matches(
|
||||
resolved.get("base_model"), base_model_context
|
||||
):
|
||||
return build_result_from_resolved_version(resolved, version_id)
|
||||
|
||||
if not exact_only:
|
||||
expected_filename = resolved.get("expected_filename", "")
|
||||
confidence = _calculate_filename_confidence(filename, expected_filename)
|
||||
ranking_score = confidence + _base_model_score(
|
||||
resolved.get("base_model"), base_model_context
|
||||
)
|
||||
log_debug(
|
||||
f"CivitAI resolved version confidence: model_id={model_id}, version_id={version_id}, expected_filename={expected_filename}, confidence={confidence}"
|
||||
)
|
||||
if confidence > best_resolved_confidence:
|
||||
best_resolved_confidence = confidence
|
||||
if ranking_score > best_resolved_confidence:
|
||||
best_resolved_confidence = ranking_score
|
||||
best_resolved_result = build_result_from_resolved_version(
|
||||
resolved, version_id
|
||||
)
|
||||
@@ -639,7 +675,10 @@ def _find_civitai_file_in_model(
|
||||
log_info(
|
||||
f"CivitAI version-list probable match: model_id={model_id}, version_id={result.get('version_id')}, filename={result.get('filename')}, confidence={result['confidence']}"
|
||||
)
|
||||
return result
|
||||
if _base_model_matches(result.get("base_model"), base_model_context):
|
||||
return result
|
||||
if not base_model_context:
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
@@ -716,6 +755,7 @@ def search_civitai_for_file(
|
||||
api_key: Optional[str] = None,
|
||||
exact_only: bool = False,
|
||||
model_type: Optional[str] = None,
|
||||
base_model_context: Optional[str] = None,
|
||||
session_token: Optional[str] = None,
|
||||
candidate_limit: int = DEFAULT_CIVITAI_CANDIDATE_LIMIT,
|
||||
use_trpc_search: bool = True,
|
||||
@@ -739,9 +779,10 @@ def search_civitai_for_file(
|
||||
candidate_limit = max(1, min(int(candidate_limit), MAX_CIVITAI_CANDIDATE_LIMIT))
|
||||
session_key = "session" if session_token else "anon"
|
||||
model_type_key = str(model_type or "").lower()
|
||||
base_model_key = _normalize_base_model(base_model_context or "")
|
||||
methods_key = f"trpc{int(bool(use_trpc_search))}_html{int(bool(use_html_fallback))}"
|
||||
cache_key = (
|
||||
f"civit_{filename}_exact{exact_only}_type{model_type_key}_session{session_key}_limit{candidate_limit}_{methods_key}"
|
||||
f"civit_{filename}_exact{exact_only}_type{model_type_key}_base{base_model_key}_session{session_key}_limit{candidate_limit}_{methods_key}"
|
||||
)
|
||||
if cache_key in _search_cache:
|
||||
log_debug(f"CivitAI search cache hit for {filename} (exact_only={exact_only})")
|
||||
@@ -798,6 +839,7 @@ def search_civitai_for_file(
|
||||
api_key=api_key,
|
||||
exact_only=exact_only,
|
||||
preferred_version_id=version_id,
|
||||
base_model_context=base_model_context,
|
||||
)
|
||||
if result:
|
||||
confidence = float(result.get("confidence") or 0.0)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""
|
||||
"""
|
||||
LoRA Manager Archive Source Module
|
||||
|
||||
Search archived CivitAI metadata stored by comfyui-lora-manager.
|
||||
@@ -153,6 +153,34 @@ def _calculate_confidence(
|
||||
return round(best * 100, 1)
|
||||
|
||||
|
||||
def _normalize_base_model(value: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "", str(value or "").lower())
|
||||
|
||||
|
||||
def _base_model_matches(candidate: str, preferred: Optional[str]) -> bool:
|
||||
preferred_norm = _normalize_base_model(preferred or "")
|
||||
if not preferred_norm:
|
||||
return True
|
||||
|
||||
candidate_norm = _normalize_base_model(candidate or "")
|
||||
if not candidate_norm:
|
||||
return False
|
||||
|
||||
from .popular import load_base_model_aliases
|
||||
aliases = load_base_model_aliases()
|
||||
preferred_tokens = aliases.get(preferred_norm, [preferred_norm])
|
||||
return any(
|
||||
token and (token in candidate_norm or candidate_norm in token)
|
||||
for token in preferred_tokens
|
||||
)
|
||||
|
||||
|
||||
def _base_model_score(candidate: str, preferred: Optional[str]) -> float:
|
||||
if not preferred:
|
||||
return 0.0
|
||||
return 1000.0 if _base_model_matches(candidate, preferred) else -1000.0
|
||||
|
||||
|
||||
def _extract_search_tokens(query: str) -> Dict[str, List[str]]:
|
||||
"""Split filename/model query into useful content/version tokens."""
|
||||
base_query = os.path.splitext(os.path.basename(query or ""))[0]
|
||||
@@ -513,7 +541,10 @@ def _build_result_from_row(
|
||||
|
||||
|
||||
def search_lora_manager_archive(
|
||||
query: str, model_type: Optional[str] = None, limit: int = 10
|
||||
query: str,
|
||||
model_type: Optional[str] = None,
|
||||
base_model_context: Optional[str] = None,
|
||||
limit: int = 10,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search the local comfyui-lora-manager CivitAI archive by model name.
|
||||
@@ -531,7 +562,10 @@ def search_lora_manager_archive(
|
||||
return []
|
||||
|
||||
normalized_type = _normalize_model_type(model_type)
|
||||
cache_key = f"search::{normalized_query.lower()}::{normalized_type}::{limit}"
|
||||
base_model_key = _normalize_base_model(base_model_context or "")
|
||||
cache_key = (
|
||||
f"search::{normalized_query.lower()}::{normalized_type}::{base_model_key}::{limit}"
|
||||
)
|
||||
if cache_key in _search_cache:
|
||||
return list(_search_cache[cache_key])
|
||||
|
||||
@@ -562,22 +596,27 @@ def search_lora_manager_archive(
|
||||
)
|
||||
if confidence < 40:
|
||||
continue
|
||||
scored_candidates.append((confidence, row))
|
||||
rank = confidence + _base_model_score(
|
||||
row["base_model"], base_model_context
|
||||
)
|
||||
scored_candidates.append((rank, confidence, row))
|
||||
|
||||
scored_candidates.sort(
|
||||
key=lambda item: (
|
||||
item[0],
|
||||
1 if item[1]["version_name"] else 0,
|
||||
item[1],
|
||||
1 if item[2]["version_name"] else 0,
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
selected_version_ids = [
|
||||
row["version_id"] for _, row in scored_candidates[: max(limit * 3, limit)]
|
||||
row["version_id"]
|
||||
for _, _, row in scored_candidates[: max(limit * 3, limit)]
|
||||
]
|
||||
full_rows = _load_full_rows_for_versions(conn, selected_version_ids)
|
||||
|
||||
results = []
|
||||
for _, candidate_row in scored_candidates:
|
||||
for _, _, candidate_row in scored_candidates:
|
||||
version_id = candidate_row["version_id"]
|
||||
full_row = full_rows.get(version_id)
|
||||
if not full_row:
|
||||
@@ -585,10 +624,15 @@ def search_lora_manager_archive(
|
||||
result = _build_result_from_row(conn, full_row, normalized_query)
|
||||
if not result or result["confidence"] < 40:
|
||||
continue
|
||||
if base_model_context and not _base_model_matches(
|
||||
result.get("base_model"), base_model_context
|
||||
):
|
||||
continue
|
||||
results.append(result)
|
||||
|
||||
results.sort(
|
||||
key=lambda item: (
|
||||
_base_model_score(item.get("base_model"), base_model_context),
|
||||
item.get("confidence", 0),
|
||||
1 if item.get("match_type") == "exact" else 0,
|
||||
),
|
||||
@@ -611,6 +655,7 @@ def search_lora_manager_archive(
|
||||
def search_lora_manager_archive_for_file(
|
||||
filename: str,
|
||||
model_type: Optional[str] = None,
|
||||
base_model_context: Optional[str] = None,
|
||||
exact_only: bool = False,
|
||||
limit: int = 10,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
@@ -621,29 +666,43 @@ def search_lora_manager_archive_for_file(
|
||||
if not search_query:
|
||||
return None
|
||||
|
||||
base_model_key = _normalize_base_model(base_model_context or "")
|
||||
cache_key = (
|
||||
f"file::{filename.lower()}::{_normalize_model_type(model_type)}::{exact_only}::{limit}"
|
||||
f"file::{filename.lower()}::{_normalize_model_type(model_type)}::{base_model_key}::{exact_only}::{limit}"
|
||||
)
|
||||
if cache_key in _search_cache:
|
||||
return _search_cache[cache_key]
|
||||
|
||||
candidates = search_lora_manager_archive(
|
||||
search_query, model_type=model_type, limit=limit
|
||||
search_query,
|
||||
model_type=model_type,
|
||||
base_model_context=base_model_context,
|
||||
limit=limit,
|
||||
)
|
||||
best_match = None
|
||||
best_confidence = 0.0
|
||||
best_rank = -9999.0
|
||||
|
||||
for candidate in candidates:
|
||||
candidate_filename = candidate.get("filename", "")
|
||||
confidence = _calculate_confidence(filename, candidate.get("name", ""), candidate.get("version_name", ""), candidate_filename)
|
||||
if exact_only and confidence < 100.0:
|
||||
continue
|
||||
if confidence > best_confidence:
|
||||
base_model_matches = _base_model_matches(
|
||||
candidate.get("base_model"), base_model_context
|
||||
)
|
||||
if base_model_context and not base_model_matches:
|
||||
continue
|
||||
rank = confidence + _base_model_score(
|
||||
candidate.get("base_model"), base_model_context
|
||||
)
|
||||
if rank > best_rank:
|
||||
best_rank = rank
|
||||
best_confidence = confidence
|
||||
best_match = dict(candidate)
|
||||
best_match["confidence"] = confidence
|
||||
best_match["match_type"] = "exact" if confidence == 100.0 else "similar"
|
||||
if confidence == 100.0:
|
||||
if confidence == 100.0 and base_model_matches:
|
||||
break
|
||||
|
||||
if best_match is None and exact_only:
|
||||
|
||||
+305
-1
@@ -6,6 +6,10 @@ Curated list of common models with known download URLs.
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from ..log_system.log_funcs import (
|
||||
@@ -22,10 +26,12 @@ METADATA_DIR = os.path.join(
|
||||
)
|
||||
POPULAR_MODELS_FILE = os.path.join(METADATA_DIR, "popular-models.json")
|
||||
MODEL_ALIASES_FILE = os.path.join(METADATA_DIR, "model-aliases.json")
|
||||
BASE_MODELS_FILE = os.path.join(METADATA_DIR, "base-models.json")
|
||||
|
||||
# Cache for loaded data
|
||||
_popular_models_cache: Optional[Dict] = None
|
||||
_model_aliases_cache: Optional[Dict] = None
|
||||
_base_models_aliases_cache: Optional[Dict[str, List[str]]] = None
|
||||
|
||||
|
||||
def _load_popular_models() -> Dict[str, Any]:
|
||||
@@ -68,6 +74,71 @@ def _load_model_aliases() -> Dict[str, List[str]]:
|
||||
return _model_aliases_cache
|
||||
|
||||
|
||||
def _normalize_base_model(value: str) -> str:
|
||||
import re
|
||||
return re.sub(r"[^a-z0-9]+", "", str(value or "").lower())
|
||||
|
||||
|
||||
def load_base_model_aliases() -> Dict[str, List[str]]:
|
||||
"""Load and normalize base model aliases from base-models.json."""
|
||||
global _base_models_aliases_cache
|
||||
|
||||
if _base_models_aliases_cache is not None:
|
||||
return _base_models_aliases_cache
|
||||
|
||||
aliases_dict = {}
|
||||
try:
|
||||
if os.path.exists(BASE_MODELS_FILE):
|
||||
with open(BASE_MODELS_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
base_models = data.get("base_models", [])
|
||||
for model in base_models:
|
||||
name = model.get("name", "")
|
||||
normalized_name = _normalize_base_model(name)
|
||||
if not normalized_name:
|
||||
continue
|
||||
|
||||
aliases = model.get("aliases", [])
|
||||
# Normalize each alias token and deduplicate
|
||||
normalized_tokens = []
|
||||
seen_tokens = set()
|
||||
for alias in aliases:
|
||||
if not alias:
|
||||
continue
|
||||
normalized_alias = _normalize_base_model(alias)
|
||||
if normalized_alias and normalized_alias not in seen_tokens:
|
||||
seen_tokens.add(normalized_alias)
|
||||
normalized_tokens.append(normalized_alias)
|
||||
|
||||
# Ensure normalized name itself is in preferred tokens if not already
|
||||
if normalized_name not in seen_tokens:
|
||||
normalized_tokens.append(normalized_name)
|
||||
|
||||
aliases_dict[normalized_name] = normalized_tokens
|
||||
except Exception as e:
|
||||
log_error(f"Error loading base model aliases: {e}")
|
||||
|
||||
# Fallback to hardcoded defaults if file is missing/corrupted
|
||||
if not aliases_dict:
|
||||
aliases_dict = {
|
||||
"zimage": ["zimage", "zimageturbo"],
|
||||
"pony": ["pony", "ponyxl"],
|
||||
"illustrious": ["illustrious", "illustriousxl"],
|
||||
"sdxl10": ["sdxl", "sdxl10", "sdxl100", "sdxl1"],
|
||||
"sd15": ["sd15", "sd1", "stablediffusion15"],
|
||||
"flux1d": ["flux", "flux1", "flux1d", "fluxdev"],
|
||||
"flux1s": ["flux1s", "fluxschnell"],
|
||||
"qwenimage": ["qwenimage"],
|
||||
"hunyuan1": ["hunyuan", "hunyuan1"],
|
||||
"wanvideo": ["wan", "wanvideo"],
|
||||
"noobai": ["noobai"],
|
||||
"hidream": ["hidream"],
|
||||
}
|
||||
|
||||
_base_models_aliases_cache = aliases_dict
|
||||
return _base_models_aliases_cache
|
||||
|
||||
|
||||
def get_popular_model_url(filename: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Look up a model filename in the popular models database.
|
||||
@@ -136,8 +207,241 @@ def get_all_popular_models() -> Dict[str, Any]:
|
||||
|
||||
def reload_databases():
|
||||
"""Force reload of all databases."""
|
||||
global _popular_models_cache, _model_aliases_cache
|
||||
global _popular_models_cache, _model_aliases_cache, _base_models_aliases_cache
|
||||
_popular_models_cache = None
|
||||
_model_aliases_cache = None
|
||||
_base_models_aliases_cache = None
|
||||
_load_popular_models()
|
||||
_load_model_aliases()
|
||||
load_base_model_aliases()
|
||||
|
||||
|
||||
BASE_MODELS_META_FILE = os.path.join(METADATA_DIR, "base-models.meta.json")
|
||||
|
||||
|
||||
def get_base_models_config() -> Dict[str, Any]:
|
||||
"""Return the raw base-models config (base_models array) for the frontend dropdown."""
|
||||
return _read_base_models_file()
|
||||
|
||||
|
||||
def _read_base_models_file() -> Dict[str, Any]:
|
||||
try:
|
||||
if os.path.exists(BASE_MODELS_FILE):
|
||||
with open(BASE_MODELS_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
log_warn(f"Error reading base-models.json: {e}")
|
||||
return {"base_models": []}
|
||||
|
||||
|
||||
def _read_base_models_meta() -> Dict[str, Any]:
|
||||
try:
|
||||
if os.path.exists(BASE_MODELS_META_FILE):
|
||||
with open(BASE_MODELS_META_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
log_warn(f"Error reading base-models.meta.json: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def generate_aliases(name: str) -> List[str]:
|
||||
aliases = {name.lower()}
|
||||
|
||||
# Replace dots, dashes, underscores with spaces, then normalize spaces
|
||||
normalized = re.sub(r"[\._\-]+", " ", name.lower())
|
||||
normalized = re.sub(r"\s+", " ", normalized).strip()
|
||||
aliases.add(normalized)
|
||||
|
||||
# Remove all spaces/dashes/dots/underscores
|
||||
collapsed = re.sub(r"[\s\._\-]+", "", name.lower())
|
||||
aliases.add(collapsed)
|
||||
|
||||
# If there are dots/dashes/underscores, add specific clean replacements
|
||||
if "." in name:
|
||||
aliases.add(name.lower().replace(".", ""))
|
||||
aliases.add(name.lower().replace(".", " "))
|
||||
if "-" in name:
|
||||
aliases.add(name.lower().replace("-", ""))
|
||||
aliases.add(name.lower().replace("-", " "))
|
||||
if "_":
|
||||
aliases.add(name.lower().replace("_", ""))
|
||||
aliases.add(name.lower().replace("_", " "))
|
||||
|
||||
# Extract first word if it has multiple words
|
||||
words = name.lower().split()
|
||||
if len(words) > 1:
|
||||
first_word = words[0]
|
||||
if len(first_word) > 2 and first_word not in {"stable", "flux", "pony", "sdxl"}:
|
||||
aliases.add(first_word)
|
||||
|
||||
return sorted(list({a for a in aliases if len(a) > 1}))
|
||||
|
||||
|
||||
def get_base_models_status(check_remote: bool = False) -> Dict[str, Any]:
|
||||
"""Return local base-models metadata and optionally compare with CivitAI."""
|
||||
local_data = _read_base_models_file()
|
||||
base_models = local_data.get("base_models", [])
|
||||
meta = _read_base_models_meta()
|
||||
|
||||
local_count = len(base_models)
|
||||
local_updated_at = meta.get("updated_at") or ""
|
||||
|
||||
status = {
|
||||
"local_count": local_count,
|
||||
"local_updated_at": local_updated_at,
|
||||
"remote_checked_at": meta.get("last_checked_at") or "",
|
||||
"update_available": False,
|
||||
}
|
||||
|
||||
if check_remote:
|
||||
try:
|
||||
import requests
|
||||
# Fetch from CivitAI API
|
||||
response = requests.get("https://civitai.com/api/v1/enums", timeout=15)
|
||||
if response.status_code == 200:
|
||||
enums = response.json()
|
||||
remote_models = enums.get("BaseModel", [])
|
||||
|
||||
# Check if there are new models by comparing normalized names and aliases
|
||||
all_existing_normalized = set()
|
||||
for m in base_models:
|
||||
all_existing_normalized.add(_normalize_base_model(m.get("name", "")))
|
||||
for alias in m.get("aliases", []):
|
||||
all_existing_normalized.add(_normalize_base_model(alias))
|
||||
|
||||
new_models_found = False
|
||||
for remote_name in remote_models:
|
||||
norm_remote = _normalize_base_model(remote_name)
|
||||
if norm_remote and norm_remote not in all_existing_normalized:
|
||||
new_models_found = True
|
||||
break
|
||||
|
||||
status.update({
|
||||
"remote_checked_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
|
||||
"update_available": new_models_found
|
||||
})
|
||||
except Exception as e:
|
||||
log_warn(f"Error checking remote base models: {e}")
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def update_base_models_from_remote() -> Dict[str, Any]:
|
||||
"""Fetch live BaseModel enums from CivitAI and merge into base-models.json."""
|
||||
import requests
|
||||
response = requests.get("https://civitai.com/api/v1/enums", timeout=15)
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"CivitAI enums endpoint returned HTTP {response.status_code}")
|
||||
|
||||
enums = response.json()
|
||||
remote_names = enums.get("BaseModel", [])
|
||||
if not remote_names or not isinstance(remote_names, list):
|
||||
raise ValueError("CivitAI enums did not return a valid BaseModel list")
|
||||
|
||||
local_data = _read_base_models_file()
|
||||
base_models = local_data.get("base_models", [])
|
||||
|
||||
# 1. Build a set of all normalized aliases currently in base-models.json
|
||||
all_known_normalized = set()
|
||||
for m in base_models:
|
||||
for alias in m.get("aliases", []):
|
||||
all_known_normalized.add(_normalize_base_model(alias))
|
||||
all_known_normalized.add(_normalize_base_model(m.get("name", "")))
|
||||
|
||||
updated_models = list(base_models)
|
||||
new_added_count = 0
|
||||
|
||||
for name in remote_names:
|
||||
norm_name = _normalize_base_model(name)
|
||||
if not norm_name:
|
||||
continue
|
||||
|
||||
# If the normalized name is already a known alias or name, skip it
|
||||
if norm_name in all_known_normalized:
|
||||
continue
|
||||
|
||||
# Generate candidate aliases
|
||||
candidates = generate_aliases(name)
|
||||
|
||||
# Filter out aliases that are already registered/known
|
||||
filtered_aliases = []
|
||||
for alias in candidates:
|
||||
norm_alias = _normalize_base_model(alias)
|
||||
if norm_alias not in all_known_normalized:
|
||||
filtered_aliases.append(alias)
|
||||
all_known_normalized.add(norm_alias)
|
||||
|
||||
# If we have valid aliases, add the new base model entry
|
||||
if filtered_aliases:
|
||||
if name.lower() not in [a.lower() for a in filtered_aliases]:
|
||||
filtered_aliases.insert(0, name)
|
||||
all_known_normalized.add(norm_name)
|
||||
|
||||
updated_models.append({
|
||||
"name": name,
|
||||
"aliases": filtered_aliases
|
||||
})
|
||||
new_added_count += 1
|
||||
|
||||
# Write updated models to base-models.json atomically
|
||||
os.makedirs(METADATA_DIR, exist_ok=True)
|
||||
|
||||
# Backup existing
|
||||
if os.path.exists(BASE_MODELS_FILE):
|
||||
shutil.copy2(BASE_MODELS_FILE, f"{BASE_MODELS_FILE}.bak")
|
||||
|
||||
# Write atomically
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
prefix=f".{os.path.basename(BASE_MODELS_FILE)}.",
|
||||
suffix=".tmp",
|
||||
dir=os.path.dirname(BASE_MODELS_FILE),
|
||||
text=True,
|
||||
)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump({"base_models": updated_models}, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
os.replace(tmp_path, BASE_MODELS_FILE)
|
||||
except Exception:
|
||||
try:
|
||||
os.remove(tmp_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
# Write meta file
|
||||
now_str = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
||||
meta = {
|
||||
"updated_at": now_str,
|
||||
"last_checked_at": now_str,
|
||||
"local_count": len(updated_models),
|
||||
"new_models_added": new_added_count
|
||||
}
|
||||
|
||||
fd_meta, tmp_meta_path = tempfile.mkstemp(
|
||||
prefix=f".{os.path.basename(BASE_MODELS_META_FILE)}.",
|
||||
suffix=".tmp",
|
||||
dir=os.path.dirname(BASE_MODELS_META_FILE),
|
||||
text=True,
|
||||
)
|
||||
try:
|
||||
with os.fdopen(fd_meta, "w", encoding="utf-8") as f:
|
||||
json.dump(meta, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
os.replace(tmp_meta_path, BASE_MODELS_META_FILE)
|
||||
except Exception:
|
||||
try:
|
||||
os.remove(tmp_meta_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
|
||||
# Force reload database in-memory cache
|
||||
reload_databases()
|
||||
|
||||
return {
|
||||
"local_count": len(updated_models),
|
||||
"local_updated_at": now_str,
|
||||
"new_models_added": new_added_count,
|
||||
"updated": True
|
||||
}
|
||||
|
||||
@@ -0,0 +1,627 @@
|
||||
{
|
||||
"base_models": [
|
||||
{
|
||||
"name": "Z-Image",
|
||||
"aliases": [
|
||||
"zimage",
|
||||
"z image",
|
||||
"z-image",
|
||||
"z_image",
|
||||
"zImageTurbo",
|
||||
"z image turbo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Pony",
|
||||
"aliases": [
|
||||
"pony",
|
||||
"ponyxl",
|
||||
"pony diffusion",
|
||||
"pony realism"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Illustrious",
|
||||
"aliases": [
|
||||
"illustrious",
|
||||
"illustriousxl",
|
||||
"illustrious xl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SDXL 1.0",
|
||||
"aliases": [
|
||||
"sdxl",
|
||||
"sdxl10",
|
||||
"sdxl 1.0",
|
||||
"stable diffusion xl",
|
||||
"sdxl100",
|
||||
"sdxl1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 1.5",
|
||||
"aliases": [
|
||||
"sd15",
|
||||
"sd 1.5",
|
||||
"sd1.5",
|
||||
"stable diffusion 1.5",
|
||||
"sd1",
|
||||
"stablediffusion15"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flux.1 D",
|
||||
"aliases": [
|
||||
"flux",
|
||||
"flux1",
|
||||
"flux.1",
|
||||
"flux dev",
|
||||
"flux.1 d",
|
||||
"flux1d",
|
||||
"fluxdev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flux.1 S",
|
||||
"aliases": [
|
||||
"flux schnell",
|
||||
"flux.1 s",
|
||||
"flux1s",
|
||||
"fluxschnell"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Qwen Image",
|
||||
"aliases": [
|
||||
"qwen image",
|
||||
"qwenimage",
|
||||
"qwen-image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hunyuan 1",
|
||||
"aliases": [
|
||||
"hunyuan",
|
||||
"hunyuan1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "WAN Video",
|
||||
"aliases": [
|
||||
"wan",
|
||||
"wan video",
|
||||
"wanvideo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NoobAI",
|
||||
"aliases": [
|
||||
"noobai",
|
||||
"noob ai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "HiDream",
|
||||
"aliases": [
|
||||
"hidream",
|
||||
"hi dream"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Anima",
|
||||
"aliases": [
|
||||
"anima"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AuraFlow",
|
||||
"aliases": [
|
||||
"auraflow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Chroma",
|
||||
"aliases": [
|
||||
"chroma"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CogVideoX",
|
||||
"aliases": [
|
||||
"cogvideox"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ernie",
|
||||
"aliases": [
|
||||
"ernie"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flux.1 Krea",
|
||||
"aliases": [
|
||||
"Flux.1 Krea",
|
||||
"flux 1 krea"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flux.1 Kontext",
|
||||
"aliases": [
|
||||
"Flux.1 Kontext",
|
||||
"flux 1 kontext"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flux.2 D",
|
||||
"aliases": [
|
||||
"Flux.2 D",
|
||||
"flux 2 d",
|
||||
"flux.2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flux.2 Klein 9B",
|
||||
"aliases": [
|
||||
"Flux.2 Klein 9B",
|
||||
"flux 2 klein 9b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flux.2 Klein 9B-base",
|
||||
"aliases": [
|
||||
"Flux.2 Klein 9B-base",
|
||||
"flux 2 klein 9b base"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flux.2 Klein 4B",
|
||||
"aliases": [
|
||||
"Flux.2 Klein 4B",
|
||||
"flux 2 klein 4b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flux.2 Klein 4B-base",
|
||||
"aliases": [
|
||||
"Flux.2 Klein 4B-base",
|
||||
"flux 2 klein 4b base"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Grok",
|
||||
"aliases": [
|
||||
"grok"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "HappyHorse",
|
||||
"aliases": [
|
||||
"happyhorse"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "HiDream-O1",
|
||||
"aliases": [
|
||||
"HiDream-O1",
|
||||
"hidream o1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hunyuan Video",
|
||||
"aliases": [
|
||||
"hunyuan video"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Imagen4",
|
||||
"aliases": [
|
||||
"imagen4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Kolors",
|
||||
"aliases": [
|
||||
"kolors"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Krea 2",
|
||||
"aliases": [
|
||||
"krea",
|
||||
"krea 2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LTXV",
|
||||
"aliases": [
|
||||
"ltxv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LTXV2",
|
||||
"aliases": [
|
||||
"ltxv2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LTXV 2.3",
|
||||
"aliases": [
|
||||
"LTXV 2.3",
|
||||
"ltxv 2 3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Lens",
|
||||
"aliases": [
|
||||
"lens"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Lumina",
|
||||
"aliases": [
|
||||
"lumina"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MAI",
|
||||
"aliases": [
|
||||
"mai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Mochi",
|
||||
"aliases": [
|
||||
"mochi"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Nano Banana",
|
||||
"aliases": [
|
||||
"nano",
|
||||
"nano banana"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ODOR",
|
||||
"aliases": [
|
||||
"odor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "OpenAI",
|
||||
"aliases": [
|
||||
"openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Upscaler",
|
||||
"aliases": [
|
||||
"upscaler"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Other",
|
||||
"aliases": [
|
||||
"other"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PixArt a",
|
||||
"aliases": [
|
||||
"pixart",
|
||||
"pixart a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PixArt E",
|
||||
"aliases": [
|
||||
"pixart e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Playground v2",
|
||||
"aliases": [
|
||||
"playground",
|
||||
"playground v2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Pony V7",
|
||||
"aliases": [
|
||||
"pony v7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Qwen",
|
||||
"aliases": [
|
||||
"qwen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Qwen 2",
|
||||
"aliases": [
|
||||
"qwen 2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Stable Cascade",
|
||||
"aliases": [
|
||||
"stable cascade"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 1.4",
|
||||
"aliases": [
|
||||
"SD 1.4",
|
||||
"sd 1 4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 1.5 LCM",
|
||||
"aliases": [
|
||||
"SD 1.5 LCM",
|
||||
"sd 1 5 lcm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 1.5 Hyper",
|
||||
"aliases": [
|
||||
"SD 1.5 Hyper",
|
||||
"sd 1 5 hyper"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 2.0",
|
||||
"aliases": [
|
||||
"SD 2.0",
|
||||
"sd 2 0"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 2.0 768",
|
||||
"aliases": [
|
||||
"SD 2.0 768",
|
||||
"sd 2 0 768"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 2.1",
|
||||
"aliases": [
|
||||
"SD 2.1",
|
||||
"sd 2 1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 2.1 768",
|
||||
"aliases": [
|
||||
"SD 2.1 768",
|
||||
"sd 2 1 768"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 2.1 Unclip",
|
||||
"aliases": [
|
||||
"SD 2.1 Unclip",
|
||||
"sd 2 1 unclip"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 3",
|
||||
"aliases": [
|
||||
"sd 3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 3.5",
|
||||
"aliases": [
|
||||
"SD 3.5",
|
||||
"sd 3 5"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 3.5 Large",
|
||||
"aliases": [
|
||||
"SD 3.5 Large",
|
||||
"sd 3 5 large"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 3.5 Large Turbo",
|
||||
"aliases": [
|
||||
"SD 3.5 Large Turbo",
|
||||
"sd 3 5 large turbo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SD 3.5 Medium",
|
||||
"aliases": [
|
||||
"SD 3.5 Medium",
|
||||
"sd 3 5 medium"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SDXL 0.9",
|
||||
"aliases": [
|
||||
"SDXL 0.9",
|
||||
"sdxl 0 9"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SDXL 1.0 LCM",
|
||||
"aliases": [
|
||||
"SDXL 1.0 LCM",
|
||||
"sdxl 1 0 lcm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SDXL Lightning",
|
||||
"aliases": [
|
||||
"sdxl lightning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SDXL Hyper",
|
||||
"aliases": [
|
||||
"sdxl hyper"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SDXL Turbo",
|
||||
"aliases": [
|
||||
"sdxl turbo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SDXL Distilled",
|
||||
"aliases": [
|
||||
"sdxl distilled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Seedream",
|
||||
"aliases": [
|
||||
"seedream"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SVD",
|
||||
"aliases": [
|
||||
"svd"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SVD XT",
|
||||
"aliases": [
|
||||
"svd xt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Sora 2",
|
||||
"aliases": [
|
||||
"sora",
|
||||
"sora 2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Veo 3",
|
||||
"aliases": [
|
||||
"veo",
|
||||
"veo 3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 1.3B t2v",
|
||||
"aliases": [
|
||||
"Wan Video 1.3B t2v",
|
||||
"wan video 1 3b t2v"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 14B t2v",
|
||||
"aliases": [
|
||||
"wan video 14b t2v"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 14B i2v 480p",
|
||||
"aliases": [
|
||||
"wan video 14b i2v 480p"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 14B i2v 720p",
|
||||
"aliases": [
|
||||
"wan video 14b i2v 720p"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 2.2 TI2V-5B",
|
||||
"aliases": [
|
||||
"Wan Video 2.2 TI2V-5B",
|
||||
"wan video 2 2 ti2v 5b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 2.2 I2V-A14B",
|
||||
"aliases": [
|
||||
"Wan Video 2.2 I2V-A14B",
|
||||
"wan video 2 2 i2v a14b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 2.2 T2V-A14B",
|
||||
"aliases": [
|
||||
"Wan Video 2.2 T2V-A14B",
|
||||
"wan video 2 2 t2v a14b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 2.5 T2V",
|
||||
"aliases": [
|
||||
"Wan Video 2.5 T2V",
|
||||
"wan video 2 5 t2v"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 2.5 I2V",
|
||||
"aliases": [
|
||||
"Wan Video 2.5 I2V",
|
||||
"wan video 2 5 i2v"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Image 2.7",
|
||||
"aliases": [
|
||||
"Wan Image 2.7",
|
||||
"wan image 2 7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wan Video 2.7",
|
||||
"aliases": [
|
||||
"Wan Video 2.7",
|
||||
"wan video 2 7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ZImageBase",
|
||||
"aliases": [
|
||||
"zimagebase"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Vidu Q1",
|
||||
"aliases": [
|
||||
"vidu",
|
||||
"vidu q1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hailuo by MiniMax",
|
||||
"aliases": [
|
||||
"hailuo",
|
||||
"hailuo by minimax"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Kling",
|
||||
"aliases": [
|
||||
"kling"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Seedance",
|
||||
"aliases": [
|
||||
"seedance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ACE Audio",
|
||||
"aliases": [
|
||||
"ace",
|
||||
"ace audio"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"updated_at": "2026-06-06T17:28:12+00:00",
|
||||
"last_checked_at": "2026-06-06T17:28:12+00:00",
|
||||
"local_count": 91,
|
||||
"new_models_added": 79
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1822,7 +1822,7 @@
|
||||
}
|
||||
.mr-search-source-bar {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(178px, 1fr);
|
||||
grid-template-columns: auto minmax(150px, 0.85fr) minmax(150px, 1fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 0;
|
||||
@@ -1863,7 +1863,8 @@
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mr-search-source-select {
|
||||
.mr-search-source-select,
|
||||
.mr-search-base-select {
|
||||
width: 100%;
|
||||
height: var(--mr-download-control-height);
|
||||
min-height: var(--mr-download-control-height);
|
||||
@@ -1878,16 +1879,19 @@
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mr-missing-browser .mr-search-source-select {
|
||||
.mr-missing-browser .mr-search-source-select,
|
||||
.mr-missing-browser .mr-search-base-select {
|
||||
background: var(--mr-missing-control-bg, rgba(255,255,255,0.035));
|
||||
border-color: var(--mr-missing-control-border, var(--mr-missing-line-strong, rgba(255,255,255,0.12)));
|
||||
border-radius: var(--mr-missing-control-radius, 7px);
|
||||
}
|
||||
.mr-missing-browser .mr-search-source-select:focus {
|
||||
.mr-missing-browser .mr-search-source-select:focus,
|
||||
.mr-missing-browser .mr-search-base-select:focus {
|
||||
border-color: var(--mr-missing-control-focus-border, rgba(114,211,138,0.4));
|
||||
box-shadow: 0 0 0 2px var(--mr-missing-control-focus-ring, rgba(114,211,138,0.12));
|
||||
}
|
||||
.mr-search-source-select:focus {
|
||||
.mr-search-source-select:focus,
|
||||
.mr-search-base-select:focus {
|
||||
border-color: rgba(114,211,138,0.4);
|
||||
box-shadow: 0 0 0 3px rgba(114,211,138,0.12);
|
||||
}
|
||||
@@ -4362,6 +4366,7 @@
|
||||
}
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-combo-input,
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-search-source-select,
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-search-base-select,
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-download-target-input,
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-download-target-select {
|
||||
min-height: var(--mr-missing-field-height, 38px);
|
||||
@@ -4374,6 +4379,7 @@
|
||||
}
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-combo-input:focus,
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-search-source-select:focus,
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-search-base-select:focus,
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-download-target-input:focus,
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-download-target-select:focus {
|
||||
background: var(--mr-missing-control-bg-hover, rgba(255,255,255,0.045));
|
||||
|
||||
@@ -846,6 +846,7 @@ export const resolveDownloadMethods = {
|
||||
}
|
||||
const selectedSourceLabel = this.getSearchSourceLabel(selectedSource);
|
||||
const sourceIds = this.getSearchSourcesForSelection(selectedSource, missing);
|
||||
const baseModelContext = this.getSearchBaseModelContext(missing);
|
||||
|
||||
// For URNs, use the resolved file/model name for searching instead of the URN itself
|
||||
// and pass the URN type as category (CivitAI expects specific type names)
|
||||
@@ -907,6 +908,7 @@ export const resolveDownloadMethods = {
|
||||
state.lastAttemptSources = sourceIds;
|
||||
state.lastAttemptFound = null;
|
||||
state.lastAttemptError = null;
|
||||
state.lastAttemptBaseModelContext = baseModelContext;
|
||||
state.sourceProgress = {};
|
||||
for (const source of sourceIds) {
|
||||
this.setSourceProgress(state, source, {
|
||||
@@ -944,6 +946,7 @@ export const resolveDownloadMethods = {
|
||||
filename,
|
||||
category,
|
||||
is_urn: isUrn,
|
||||
base_model_context: baseModelContext,
|
||||
civitai_session_token: tokens.civitai_session_token,
|
||||
civitai_candidate_limit: tokens.civitai_candidate_limit,
|
||||
civitai_use_trpc_search: tokens.civitai_use_trpc_search,
|
||||
@@ -991,6 +994,9 @@ export const resolveDownloadMethods = {
|
||||
if (!this.isBackgroundSearchRunActive(workflowKey, missingSearchKey, searchRunId)) {
|
||||
return { source, stale: true };
|
||||
}
|
||||
if (baseModelContext !== this.getSearchBaseModelContext(missing)) {
|
||||
return { source, stale: true };
|
||||
}
|
||||
|
||||
const responseSources = Array.isArray(data.searched_sources) && data.searched_sources.length
|
||||
? data.searched_sources
|
||||
@@ -1300,6 +1306,53 @@ export const resolveDownloadMethods = {
|
||||
this.syncSearchSourceUi(missing, container);
|
||||
}
|
||||
|
||||
const baseSelect = container.querySelector(`#search-base-select-${missing.node_id}-${missing.widget_index}`);
|
||||
const baseList = container.querySelector(`#search-base-list-${missing.node_id}-${missing.widget_index}`);
|
||||
if (baseSelect && baseList && baseSelect.dataset.mlSearchBaseBound !== 'true') {
|
||||
baseSelect.dataset.mlSearchBaseBound = 'true';
|
||||
|
||||
const renderBaseOptions = () => {
|
||||
const options = this.getKnownBaseModelOptions();
|
||||
baseList.innerHTML = options
|
||||
.map(option => {
|
||||
const label = option.value === 'auto'
|
||||
? this.getSearchBaseModelLabel('auto')
|
||||
: option.label;
|
||||
return `<div class="mr-download-target-option" data-value="${encodeURIComponent(option.value)}" data-label="${encodeURIComponent(label)}">${this.escapeHtml(label)}</div>`;
|
||||
})
|
||||
.join('');
|
||||
baseList.style.display = 'block';
|
||||
baseList.querySelectorAll('.mr-download-target-option').forEach(optionEl => {
|
||||
optionEl.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
const value = decodeURIComponent(optionEl.dataset.value || '');
|
||||
const label = decodeURIComponent(optionEl.dataset.label || optionEl.dataset.value || '');
|
||||
this.setDropdownValue(baseSelect, value, label);
|
||||
this.setSearchBaseModel(missing, value, container);
|
||||
baseList.style.display = 'none';
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const hideBaseList = () => {
|
||||
setTimeout(() => {
|
||||
baseList.style.display = 'none';
|
||||
}, 150);
|
||||
};
|
||||
|
||||
this.enableWheelScrollChaining(baseList);
|
||||
baseSelect.addEventListener('focus', () => renderBaseOptions());
|
||||
baseSelect.addEventListener('click', () => renderBaseOptions());
|
||||
baseSelect.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
renderBaseOptions();
|
||||
}
|
||||
});
|
||||
baseSelect.addEventListener('blur', hideBaseList);
|
||||
this.syncSearchSourceUi(missing, container);
|
||||
}
|
||||
|
||||
this.wireDownloadTargetAutocomplete(container, missing);
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
|
||||
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
|
||||
import { dialogShellMethods } from "./shell/dialog_shell_methods.js";
|
||||
import { lifecycleGraphMethods } from "./shell/lifecycle_graph_methods.js";
|
||||
import { workflowStateMethods } from "./shell/workflow_state_methods.js";
|
||||
@@ -21,6 +21,7 @@ export class ResolverManagerDialog extends ComfyDialog {
|
||||
this.allModels = null; // list of all available models for dropdown
|
||||
this.downloadDirectories = null;
|
||||
this.capabilities = null;
|
||||
this.baseModels = null;
|
||||
this.downloadSubfolders = new Map();
|
||||
this.downloadTargetSelections = new Map(); // missing model key -> user/suggested download target
|
||||
this.pendingResolutions = [];
|
||||
@@ -85,21 +86,21 @@ export class ResolverManagerDialog extends ComfyDialog {
|
||||
$el("div.mr-context-menu-item", {
|
||||
onclick: () => this.handleContextMenuAction('showInfo')
|
||||
}, [
|
||||
$el("span.mr-context-menu-item-icon", { textContent: "ℹ" }),
|
||||
$el("span.mr-context-menu-item-icon", { textContent: "?" }),
|
||||
$el("span", { textContent: "Show Info" })
|
||||
]),
|
||||
$el("div.mr-context-menu-divider"),
|
||||
$el("div.mr-context-menu-item", {
|
||||
onclick: () => this.handleContextMenuAction('civitai')
|
||||
}, [
|
||||
$el("span.mr-context-menu-item-icon", { textContent: "🌐" }),
|
||||
$el("span.mr-context-menu-item-icon", { textContent: "??" }),
|
||||
$el("span", { textContent: "Open in CivitAI" })
|
||||
]),
|
||||
$el("div.mr-context-menu-divider"),
|
||||
$el("div.mr-context-menu-item", {
|
||||
onclick: () => this.handleContextMenuAction('openFolder')
|
||||
}, [
|
||||
$el("span.mr-context-menu-item-icon", { textContent: "📁" }),
|
||||
$el("span.mr-context-menu-item-icon", { textContent: "??" }),
|
||||
$el("span", { textContent: "Open Containing Folder" })
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -50,7 +50,21 @@ export const downloadTargetMethods = {
|
||||
}
|
||||
},
|
||||
|
||||
async ensureBaseModelsLoaded() {
|
||||
if (this.baseModels) return;
|
||||
try {
|
||||
const resp = await api.fetchApi('/model_resolver/base-models');
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
this.baseModels = data && typeof data === 'object' ? data : {};
|
||||
} catch (e) {
|
||||
console.warn('Model Resolver: could not load base-models config', e);
|
||||
this.baseModels = {};
|
||||
}
|
||||
},
|
||||
|
||||
isSourceAvailable(source) {
|
||||
|
||||
if (!source || ['all', 'local', 'huggingface', 'civitai', 'civarchive'].includes(source)) {
|
||||
return true;
|
||||
}
|
||||
@@ -675,11 +689,13 @@ export const downloadTargetMethods = {
|
||||
this.allModels = null;
|
||||
this.downloadDirectories = null;
|
||||
this.capabilities = null;
|
||||
this.baseModels = null;
|
||||
this.downloadSubfolders.clear();
|
||||
this.downloadTargetSelections?.clear();
|
||||
this._analysisProgressToken = null;
|
||||
|
||||
await this.ensureCapabilitiesLoaded();
|
||||
await this.ensureBaseModelsLoaded();
|
||||
this.refreshMissingListStats?.();
|
||||
this.updateBatchFooterButtons?.();
|
||||
this.updateDownloadAllButtonState?.();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app } from "../../../../../scripts/app.js";
|
||||
import { app } from "../../../../../scripts/app.js";
|
||||
import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
@@ -29,6 +29,7 @@ export const searchPanelMethods = {
|
||||
createEmptySearchState() {
|
||||
return {
|
||||
selectedSource: 'all',
|
||||
selectedBaseModel: 'auto',
|
||||
results: {
|
||||
popular: null,
|
||||
model_list: null,
|
||||
@@ -38,6 +39,7 @@ export const searchPanelMethods = {
|
||||
lora_manager_archive: null
|
||||
},
|
||||
lastAttemptSources: [],
|
||||
lastAttemptBaseModelContext: '',
|
||||
lastAttemptFound: null,
|
||||
lastAttemptError: null,
|
||||
sourceProgress: {},
|
||||
@@ -45,6 +47,119 @@ export const searchPanelMethods = {
|
||||
};
|
||||
},
|
||||
|
||||
getKnownBaseModelOptions() {
|
||||
const baseModelsList = this.baseModels?.base_models;
|
||||
if (Array.isArray(baseModelsList) && baseModelsList.length > 0) {
|
||||
return [
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'none', label: 'Any model' },
|
||||
...baseModelsList.map(m => ({ value: m.name, label: m.name }))
|
||||
];
|
||||
}
|
||||
// Fallback to hardcoded list
|
||||
return [
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'none', label: 'Any model' },
|
||||
{ value: 'Z-Image', label: 'Z-Image' },
|
||||
{ value: 'Pony', label: 'Pony' },
|
||||
{ value: 'Illustrious', label: 'Illustrious' },
|
||||
{ value: 'SDXL 1.0', label: 'SDXL 1.0' },
|
||||
{ value: 'SD 1.5', label: 'SD 1.5' },
|
||||
{ value: 'Flux.1 D', label: 'Flux.1 D' },
|
||||
{ value: 'Flux.1 S', label: 'Flux.1 S' },
|
||||
{ value: 'Qwen Image', label: 'Qwen Image' },
|
||||
{ value: 'Hunyuan 1', label: 'Hunyuan 1' },
|
||||
{ value: 'WAN Video', label: 'WAN Video' },
|
||||
{ value: 'NoobAI', label: 'NoobAI' },
|
||||
{ value: 'HiDream', label: 'HiDream' }
|
||||
];
|
||||
},
|
||||
|
||||
normalizeBaseModelToken(value = '') {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '');
|
||||
},
|
||||
|
||||
getBaseModelAliases() {
|
||||
const baseModelsList = this.baseModels?.base_models;
|
||||
if (Array.isArray(baseModelsList) && baseModelsList.length > 0) {
|
||||
return baseModelsList.map(m => ({ value: m.name, aliases: m.aliases || [] }));
|
||||
}
|
||||
// Fallback to hardcoded list
|
||||
return [
|
||||
{ value: 'Z-Image', aliases: ['zimage', 'z image', 'z-image', 'z_image', 'zImageTurbo', 'z image turbo'] },
|
||||
{ value: 'Pony', aliases: ['pony', 'ponyxl', 'pony diffusion', 'pony realism'] },
|
||||
{ value: 'Illustrious', aliases: ['illustrious', 'illustriousxl', 'illustrious xl'] },
|
||||
{ value: 'SDXL 1.0', aliases: ['sdxl', 'sdxl10', 'sdxl 1.0', 'stable diffusion xl'] },
|
||||
{ value: 'SD 1.5', aliases: ['sd15', 'sd 1.5', 'sd1.5', 'stable diffusion 1.5'] },
|
||||
{ value: 'Flux.1 D', aliases: ['flux', 'flux1', 'flux.1', 'flux dev', 'flux.1 d', 'flux1d'] },
|
||||
{ value: 'Flux.1 S', aliases: ['flux schnell', 'flux.1 s', 'flux1s'] },
|
||||
{ value: 'Qwen Image', aliases: ['qwen image', 'qwenimage', 'qwen-image'] },
|
||||
{ value: 'Hunyuan 1', aliases: ['hunyuan', 'hunyuan1'] },
|
||||
{ value: 'WAN Video', aliases: ['wan', 'wan video', 'wanvideo'] },
|
||||
{ value: 'NoobAI', aliases: ['noobai', 'noob ai'] },
|
||||
{ value: 'HiDream', aliases: ['hidream', 'hi dream'] }
|
||||
];
|
||||
},
|
||||
|
||||
getWorkflowModelReferenceText() {
|
||||
const workflow = this.getCurrentWorkflow?.();
|
||||
if (!workflow) return '';
|
||||
const values = [];
|
||||
const visit = (value, key = '') => {
|
||||
if (typeof value === 'string') {
|
||||
if (/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)\b/i.test(value) || /model|checkpoint|unet|diffusion/i.test(key)) {
|
||||
values.push(value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => visit(item, key));
|
||||
return;
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
Object.entries(value).forEach(([childKey, childValue]) => visit(childValue, childKey));
|
||||
}
|
||||
};
|
||||
visit(workflow);
|
||||
return values.join(' ');
|
||||
},
|
||||
|
||||
getDominantWorkflowBaseModel() {
|
||||
const text = this.getWorkflowModelReferenceText();
|
||||
if (!text) return '';
|
||||
const normalizedText = this.normalizeBaseModelToken(text);
|
||||
let best = null;
|
||||
for (const entry of this.getBaseModelAliases()) {
|
||||
const score = entry.aliases.reduce((count, alias) => {
|
||||
const token = this.normalizeBaseModelToken(alias);
|
||||
return token && normalizedText.includes(token) ? count + 1 : count;
|
||||
}, 0);
|
||||
if (score > 0 && (!best || score > best.score)) {
|
||||
best = { value: entry.value, score };
|
||||
}
|
||||
}
|
||||
return best?.value || '';
|
||||
},
|
||||
|
||||
getSearchBaseModelLabel(value = 'auto') {
|
||||
if (value === 'auto') {
|
||||
const detected = this.getDominantWorkflowBaseModel();
|
||||
return detected ? `Auto (${detected})` : 'Auto';
|
||||
}
|
||||
const option = this.getKnownBaseModelOptions().find(item => item.value === value);
|
||||
return option?.label || value || 'Auto';
|
||||
},
|
||||
|
||||
getSearchBaseModelContext(missing = {}) {
|
||||
const state = this.getSearchState(missing);
|
||||
const selected = state.selectedBaseModel || 'auto';
|
||||
if (selected === 'none') return '';
|
||||
if (selected === 'auto') return this.getDominantWorkflowBaseModel();
|
||||
return selected;
|
||||
},
|
||||
|
||||
getBackgroundSearchJobKey(workflowKey, missingSearchKey) {
|
||||
return `${workflowKey || 'workflow'}\n${missingSearchKey || 'missing'}`;
|
||||
},
|
||||
@@ -636,6 +751,15 @@ export const searchPanelMethods = {
|
||||
}
|
||||
this.setDropdownValue(selectEl, state.selectedSource, this.getSearchSourceLabel(state.selectedSource));
|
||||
}
|
||||
|
||||
const baseEl = container.querySelector(`#search-base-select-${missing.node_id}-${missing.widget_index}`);
|
||||
if (baseEl) {
|
||||
const options = this.getKnownBaseModelOptions();
|
||||
if (!options.some(option => option.value === state.selectedBaseModel)) {
|
||||
state.selectedBaseModel = 'auto';
|
||||
}
|
||||
this.setDropdownValue(baseEl, state.selectedBaseModel, this.getSearchBaseModelLabel(state.selectedBaseModel));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -648,6 +772,37 @@ export const searchPanelMethods = {
|
||||
this.syncSearchSourceUi(missing, container);
|
||||
},
|
||||
|
||||
setSearchBaseModel(missing, baseModel, container) {
|
||||
const state = this.getSearchState(missing);
|
||||
const nextBaseModel = baseModel || 'auto';
|
||||
const changed = (state.selectedBaseModel || 'auto') !== nextBaseModel;
|
||||
if (changed) {
|
||||
this.clearSearchProgressTimers(state.activeSearchRunId);
|
||||
state.results = this.createEmptySearchState().results;
|
||||
state.lastAttemptSources = [];
|
||||
state.lastAttemptBaseModelContext = '';
|
||||
state.lastAttemptFound = null;
|
||||
state.lastAttemptError = null;
|
||||
state.sourceProgress = {};
|
||||
state.activeSearchRunId = null;
|
||||
const resultsDiv = container?.querySelector?.(`#search-results-${missing.node_id}-${missing.widget_index}`);
|
||||
if (resultsDiv) {
|
||||
resultsDiv.classList.remove('mr-is-visible');
|
||||
resultsDiv.classList.add('mr-is-hidden');
|
||||
resultsDiv.innerHTML = '';
|
||||
}
|
||||
const searchBtn = container?.querySelector?.(`#search-${missing.node_id}-${missing.widget_index}`);
|
||||
if (searchBtn) {
|
||||
searchBtn.disabled = false;
|
||||
searchBtn.innerHTML = `${this.getSearchIconHtml()} ${this.hasSearchResultsForMissing(missing) ? 'Search Again' : 'Search'}`;
|
||||
}
|
||||
}
|
||||
state.selectedBaseModel = nextBaseModel;
|
||||
this.persistSearchStateForActiveWorkflow();
|
||||
this.syncSearchSourceUi(missing, container);
|
||||
this.refreshSearchUiForMissing?.(missing, state);
|
||||
},
|
||||
|
||||
getDropdownValue(el) {
|
||||
return el?.dataset?.value || el?.value || '';
|
||||
},
|
||||
@@ -985,8 +1140,11 @@ export const searchPanelMethods = {
|
||||
const searchSourcesId = `search-sources-${missing.node_id}-${missing.widget_index}`;
|
||||
const searchSourceSelectId = `search-source-select-${missing.node_id}-${missing.widget_index}`;
|
||||
const searchSourceListId = `search-source-list-${missing.node_id}-${missing.widget_index}`;
|
||||
const searchBaseSelectId = `search-base-select-${missing.node_id}-${missing.widget_index}`;
|
||||
const searchBaseListId = `search-base-list-${missing.node_id}-${missing.widget_index}`;
|
||||
const state = this.getSearchState(missing);
|
||||
const selectedSource = state.selectedSource || 'all';
|
||||
const selectedBaseModel = state.selectedBaseModel || 'auto';
|
||||
const buttonText = options.buttonText
|
||||
|| (this.hasSearchResultsForMissing(missing) ? 'Search Again' : 'Search');
|
||||
|
||||
@@ -1001,6 +1159,13 @@ export const searchPanelMethods = {
|
||||
html += `<div id="${searchSourceListId}" class="mr-download-target-list mr-search-source-list"></div>`;
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="mr-search-source-picker mr-search-base-picker">`;
|
||||
html += `<label class="mr-search-source-picker-label" for="${searchBaseSelectId}">Model</label>`;
|
||||
html += `<div class="mr-download-target-wrap">`;
|
||||
html += `<input id="${searchBaseSelectId}" class="mr-download-target-input mr-search-base-select" type="text" readonly autocomplete="off" data-value="${this.escapeHtml(selectedBaseModel)}" value="${this.escapeHtml(this.getSearchBaseModelLabel(selectedBaseModel))}">`;
|
||||
html += `<div id="${searchBaseListId}" class="mr-download-target-list mr-search-base-list"></div>`;
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
return html;
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@ export const lifecycleGraphMethods = {
|
||||
|
||||
// Ensure all models are loaded for dropdown
|
||||
await this.ensureCapabilitiesLoaded();
|
||||
await this.ensureBaseModelsLoaded();
|
||||
await this.ensureAllModelsLoaded();
|
||||
await this.ensureDownloadDirectoriesLoaded();
|
||||
|
||||
|
||||
@@ -186,10 +186,12 @@ export const workflowStateMethods = {
|
||||
}
|
||||
|
||||
clone.selectedSource = clone.selectedSource || 'all';
|
||||
clone.selectedBaseModel = clone.selectedBaseModel || 'auto';
|
||||
clone.results = this.mergeSearchResults({}, clone.results || {});
|
||||
clone.lastAttemptSources = Array.isArray(clone.lastAttemptSources)
|
||||
? clone.lastAttemptSources
|
||||
: [];
|
||||
clone.lastAttemptBaseModelContext = clone.lastAttemptBaseModelContext || '';
|
||||
clone.lastAttemptFound = clone.lastAttemptFound ?? null;
|
||||
clone.lastAttemptError = clone.lastAttemptError || null;
|
||||
clone.sourceProgress = clone.sourceProgress || {};
|
||||
@@ -229,6 +231,7 @@ export const workflowStateMethods = {
|
||||
|
||||
isPersistableSearchState(state = {}) {
|
||||
if ((state?.selectedSource || 'all') !== 'all') return true;
|
||||
if ((state?.selectedBaseModel || 'auto') !== 'auto') return true;
|
||||
if (state?.lastAttemptError) return true;
|
||||
if (this.hasSearchResults(state?.results || {})) return true;
|
||||
|
||||
|
||||
@@ -136,6 +136,35 @@ export const optionsMethods = {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-options-section-head" style="margin-top: 24px;">
|
||||
<h4 class="mr-options-section-title">Base Models Support</h4>
|
||||
</div>
|
||||
<div class="mr-options-grid">
|
||||
<div class="mr-options-panel">
|
||||
<div class="mr-options-stack">
|
||||
<div class="mr-options-db-summary">
|
||||
<div class="mr-options-db-row">
|
||||
<span>Base Models</span>
|
||||
<strong id="mr-options-base-models-count">Loading...</strong>
|
||||
</div>
|
||||
<div class="mr-options-db-row">
|
||||
<span>Last update</span>
|
||||
<strong id="mr-options-base-models-updated">Loading...</strong>
|
||||
</div>
|
||||
<div class="mr-options-db-row">
|
||||
<span>Status</span>
|
||||
<strong id="mr-options-base-models-state">Checking local file...</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mr-options-base-models-message" class="mr-options-db-message">Base Models list maps new CivitAI models to local categories.</div>
|
||||
<div class="mr-options-db-actions">
|
||||
<button id="mr-options-base-models-check" type="button" class="mr-btn mr-btn-secondary">${getSvgIcon('refreshCw')} Check latest</button>
|
||||
<button id="mr-options-base-models-update" type="button" class="mr-btn mr-btn-primary">${getSvgIcon('download')} Update Base Models</button>
|
||||
<a class="mr-options-inline-link" href="https://civitai.com/api/v1/enums" target="_blank" rel="noopener noreferrer">Source</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="mr-options-section-civitai" class="mr-options-card mr-options-section">
|
||||
<div class="mr-options-section-head">
|
||||
@@ -353,6 +382,12 @@ export const optionsMethods = {
|
||||
const modelListMessageEl = this.contentElement.querySelector('#mr-options-model-list-message');
|
||||
const modelListCheckBtn = this.contentElement.querySelector('#mr-options-model-list-check');
|
||||
const modelListUpdateBtn = this.contentElement.querySelector('#mr-options-model-list-update');
|
||||
const baseModelsCountEl = this.contentElement.querySelector('#mr-options-base-models-count');
|
||||
const baseModelsUpdatedEl = this.contentElement.querySelector('#mr-options-base-models-updated');
|
||||
const baseModelsStateEl = this.contentElement.querySelector('#mr-options-base-models-state');
|
||||
const baseModelsMessageEl = this.contentElement.querySelector('#mr-options-base-models-message');
|
||||
const baseModelsCheckBtn = this.contentElement.querySelector('#mr-options-base-models-check');
|
||||
const baseModelsUpdateBtn = this.contentElement.querySelector('#mr-options-base-models-update');
|
||||
const hfIndexCountEl = this.contentElement.querySelector('#mr-options-hf-index-count');
|
||||
const hfIndexUpdatedEl = this.contentElement.querySelector('#mr-options-hf-index-updated');
|
||||
const hfIndexStateEl = this.contentElement.querySelector('#mr-options-hf-index-state');
|
||||
@@ -397,6 +432,11 @@ export const optionsMethods = {
|
||||
if (modelListUpdateBtn) modelListUpdateBtn.disabled = busy;
|
||||
};
|
||||
|
||||
const setBaseModelsBusy = (busy) => {
|
||||
if (baseModelsCheckBtn) baseModelsCheckBtn.disabled = busy;
|
||||
if (baseModelsUpdateBtn) baseModelsUpdateBtn.disabled = busy;
|
||||
};
|
||||
|
||||
const setHfIndexBusy = (busy) => {
|
||||
if (hfIndexRefreshBtn) hfIndexRefreshBtn.disabled = busy;
|
||||
};
|
||||
@@ -525,6 +565,89 @@ export const optionsMethods = {
|
||||
}
|
||||
};
|
||||
|
||||
const renderBaseModelsStatus = (data = {}, checkedRemote = false) => {
|
||||
if (baseModelsCountEl) {
|
||||
baseModelsCountEl.textContent = Number.isFinite(Number(data.local_count))
|
||||
? Number(data.local_count).toLocaleString()
|
||||
: '-';
|
||||
}
|
||||
if (baseModelsUpdatedEl) {
|
||||
baseModelsUpdatedEl.textContent = formatOptionDate(data.local_updated_at);
|
||||
}
|
||||
if (baseModelsStateEl) {
|
||||
if (checkedRemote && data.update_available) {
|
||||
baseModelsStateEl.textContent = 'Update available';
|
||||
} else if (checkedRemote) {
|
||||
baseModelsStateEl.textContent = 'Up to date';
|
||||
} else {
|
||||
baseModelsStateEl.textContent = data.local_updated_at ? 'Tracked' : 'Bundled';
|
||||
}
|
||||
}
|
||||
if (baseModelsMessageEl) {
|
||||
if (checkedRemote && data.update_available) {
|
||||
baseModelsMessageEl.textContent = `New base models are available from CivitAI.`;
|
||||
} else if (checkedRemote) {
|
||||
baseModelsMessageEl.textContent = `Base Models list is current.`;
|
||||
} else {
|
||||
baseModelsMessageEl.textContent = `Base Models list maps new CivitAI models to local categories.`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadBaseModelsStatus = async (checkRemote = false) => {
|
||||
try {
|
||||
setBaseModelsBusy(true);
|
||||
if (baseModelsStateEl) {
|
||||
baseModelsStateEl.textContent = checkRemote ? 'Checking CivitAI...' : 'Loading...';
|
||||
}
|
||||
const response = await api.fetchApi(`/model_resolver/base-models/status${checkRemote ? '?check_remote=1' : ''}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status failed: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
renderBaseModelsStatus(data, checkRemote);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Model Resolver: base-models status error:', error);
|
||||
if (baseModelsStateEl) baseModelsStateEl.textContent = 'Error';
|
||||
if (baseModelsMessageEl) baseModelsMessageEl.textContent = error.message || 'Failed to read Base Models status.';
|
||||
return null;
|
||||
} finally {
|
||||
setBaseModelsBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateBaseModels = async () => {
|
||||
try {
|
||||
setBaseModelsBusy(true);
|
||||
if (baseModelsStateEl) baseModelsStateEl.textContent = 'Updating...';
|
||||
if (baseModelsMessageEl) baseModelsMessageEl.textContent = 'Downloading latest base models from CivitAI...';
|
||||
const response = await api.fetchApi('/model_resolver/base-models/update', {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `Update failed: ${response.status}`);
|
||||
}
|
||||
await this.clearSearchCaches();
|
||||
// Invalidate the in-memory cache so the base model dropdown
|
||||
// reloads fresh data from the server next time it is opened.
|
||||
this.baseModels = null;
|
||||
await this.ensureBaseModelsLoaded();
|
||||
renderBaseModelsStatus(data, false);
|
||||
if (baseModelsStateEl) baseModelsStateEl.textContent = 'Updated';
|
||||
if (baseModelsMessageEl) baseModelsMessageEl.textContent = `Base Models updated. ${Number(data.local_count || 0).toLocaleString()} base models loaded.`;
|
||||
this.showNotification('Base Models updated', 'success');
|
||||
} catch (error) {
|
||||
console.error('Model Resolver: base-models update error:', error);
|
||||
if (baseModelsStateEl) baseModelsStateEl.textContent = 'Error';
|
||||
if (baseModelsMessageEl) baseModelsMessageEl.textContent = error.message || 'Failed to update Base Models.';
|
||||
this.showNotification('Base Models update failed', 'error');
|
||||
} finally {
|
||||
setBaseModelsBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderHfIndexStatus = (data = {}) => {
|
||||
if (hfIndexCountEl) {
|
||||
hfIndexCountEl.textContent = Number.isFinite(Number(data.file_count))
|
||||
@@ -683,6 +806,7 @@ export const optionsMethods = {
|
||||
setVisibleSection(targetId);
|
||||
if (targetId === 'mr-options-section-local-db') {
|
||||
loadModelListStatus(false);
|
||||
loadBaseModelsStatus(false);
|
||||
} else if (targetId === 'mr-options-section-hf') {
|
||||
loadHfIndexStatus();
|
||||
}
|
||||
@@ -701,6 +825,18 @@ export const optionsMethods = {
|
||||
});
|
||||
}
|
||||
|
||||
if (baseModelsCheckBtn) {
|
||||
baseModelsCheckBtn.addEventListener('click', () => {
|
||||
loadBaseModelsStatus(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (baseModelsUpdateBtn) {
|
||||
baseModelsUpdateBtn.addEventListener('click', () => {
|
||||
updateBaseModels();
|
||||
});
|
||||
}
|
||||
|
||||
if (hfIndexRefreshBtn) {
|
||||
hfIndexRefreshBtn.addEventListener('click', () => {
|
||||
refreshHfIndex();
|
||||
@@ -766,6 +902,7 @@ export const optionsMethods = {
|
||||
}
|
||||
|
||||
loadModelListStatus(false);
|
||||
loadBaseModelsStatus(false);
|
||||
loadHfIndexStatus();
|
||||
|
||||
if (saveBtn) {
|
||||
|
||||
Reference in New Issue
Block a user