Add persistent HuggingFace author index cache and reorganize options UI
This commit is contained in:
+26
@@ -99,6 +99,8 @@ class ModelLinkerExtension:
|
||||
)
|
||||
from .core.sources.huggingface import (
|
||||
search_huggingface_for_file,
|
||||
get_author_fallback_index_status,
|
||||
refresh_author_fallback_index,
|
||||
clear_search_cache as clear_huggingface_search_cache,
|
||||
)
|
||||
from .core.sources.civitai import (
|
||||
@@ -1268,6 +1270,30 @@ class ModelLinkerExtension:
|
||||
log_exception(f"Clear search cache error: {e}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@routes.get("/model_linker/huggingface/author-index/status")
|
||||
async def huggingface_author_index_status_route(request):
|
||||
"""Return local HuggingFace author fallback index status."""
|
||||
try:
|
||||
return web.json_response(get_author_fallback_index_status())
|
||||
except Exception as e:
|
||||
log_exception(f"HuggingFace author index status error: {e}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@routes.post("/model_linker/huggingface/author-index/refresh")
|
||||
async def huggingface_author_index_refresh_route(request):
|
||||
"""Refresh HuggingFace author fallback index."""
|
||||
try:
|
||||
data = await request.json()
|
||||
hf_token = data.get("hf_token", "")
|
||||
result = await asyncio.to_thread(
|
||||
refresh_author_fallback_index, hf_token or None
|
||||
)
|
||||
clear_huggingface_search_cache()
|
||||
return web.json_response(result)
|
||||
except Exception as e:
|
||||
log_exception(f"HuggingFace author index refresh error: {e}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@routes.get("/model_linker/model-list/status")
|
||||
async def model_list_status_route(request):
|
||||
"""Return local model-list status and optionally compare with GitHub."""
|
||||
|
||||
+261
-1
@@ -4,8 +4,11 @@ HuggingFace Source Module
|
||||
Search and download models from HuggingFace Hub.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import requests
|
||||
from typing import Dict, Any, Optional, List
|
||||
from urllib.parse import urlparse, quote
|
||||
@@ -21,9 +24,19 @@ from ..log_system.log_funcs import (
|
||||
HF_API_URL = "https://huggingface.co/api"
|
||||
HF_AUTHOR_FALLBACKS = ["Comfy-Org"]
|
||||
BRAVE_SEARCH_API_URL = "https://api.search.brave.com/res/v1/web/search"
|
||||
HF_AUTHOR_INDEX_CACHE_TTL_SECONDS = 24 * 60 * 60
|
||||
HF_AUTHOR_INDEX_CACHE_VERSION = 1
|
||||
METADATA_DIR = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "metadata"
|
||||
)
|
||||
HF_AUTHOR_INDEX_CACHE_PATH = os.path.join(
|
||||
METADATA_DIR, "huggingface-author-index.json"
|
||||
)
|
||||
|
||||
# Cache for search results
|
||||
_search_cache: Dict[str, Any] = {}
|
||||
_author_index_cache: Dict[str, Dict[str, Any]] = {}
|
||||
_author_index_lock = threading.RLock()
|
||||
|
||||
|
||||
def clear_search_cache():
|
||||
@@ -32,6 +45,186 @@ def clear_search_cache():
|
||||
_search_cache.clear()
|
||||
|
||||
|
||||
def _author_index_cache_key(author: str, headers: Dict[str, str]) -> str:
|
||||
auth_key = "token" if headers.get("Authorization") else "public"
|
||||
return f"{author}::{auth_key}"
|
||||
|
||||
|
||||
def _is_author_index_fresh(index: Optional[Dict[str, Any]]) -> bool:
|
||||
if not index:
|
||||
return False
|
||||
|
||||
updated_at = index.get("updated_at")
|
||||
if not isinstance(updated_at, (int, float)):
|
||||
return False
|
||||
|
||||
return (time.time() - updated_at) < HF_AUTHOR_INDEX_CACHE_TTL_SECONDS
|
||||
|
||||
|
||||
def _read_persistent_author_indexes() -> Dict[str, Any]:
|
||||
if not os.path.exists(HF_AUTHOR_INDEX_CACHE_PATH):
|
||||
return {"version": HF_AUTHOR_INDEX_CACHE_VERSION, "authors": {}}
|
||||
|
||||
try:
|
||||
with open(HF_AUTHOR_INDEX_CACHE_PATH, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if data.get("version") != HF_AUTHOR_INDEX_CACHE_VERSION:
|
||||
return {"version": HF_AUTHOR_INDEX_CACHE_VERSION, "authors": {}}
|
||||
if not isinstance(data.get("authors"), dict):
|
||||
data["authors"] = {}
|
||||
return data
|
||||
except Exception as e:
|
||||
log_debug(f"Error reading HuggingFace author index cache: {e}")
|
||||
return {"version": HF_AUTHOR_INDEX_CACHE_VERSION, "authors": {}}
|
||||
|
||||
|
||||
def _write_persistent_author_index(author: str, index: Dict[str, Any]) -> None:
|
||||
try:
|
||||
os.makedirs(METADATA_DIR, exist_ok=True)
|
||||
data = _read_persistent_author_indexes()
|
||||
data["authors"][author] = index
|
||||
tmp_path = f"{HF_AUTHOR_INDEX_CACHE_PATH}.tmp"
|
||||
with open(tmp_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
|
||||
os.replace(tmp_path, HF_AUTHOR_INDEX_CACHE_PATH)
|
||||
except Exception as e:
|
||||
log_debug(f"Error writing HuggingFace author index cache: {e}")
|
||||
|
||||
|
||||
def _build_author_index_from_models(
|
||||
author: str, repos: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
files = []
|
||||
repo_ids = []
|
||||
|
||||
for repo in repos:
|
||||
repo_id = repo.get("id", "")
|
||||
if not repo_id:
|
||||
continue
|
||||
|
||||
repo_ids.append(repo_id)
|
||||
for sibling in repo.get("siblings") or []:
|
||||
file_path = sibling.get("rfilename") or sibling.get("path") or ""
|
||||
if not file_path:
|
||||
continue
|
||||
|
||||
files.append(
|
||||
{
|
||||
"repo_id": repo_id,
|
||||
"path": file_path,
|
||||
"filename": os.path.basename(file_path),
|
||||
"size": sibling.get("size"),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"author": author,
|
||||
"updated_at": time.time(),
|
||||
"repo_count": len(repo_ids),
|
||||
"file_count": len(files),
|
||||
"repos": repo_ids,
|
||||
"files": files,
|
||||
}
|
||||
|
||||
|
||||
def _fetch_author_index(author: str, headers: Dict[str, str], limit: int = 200):
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{HF_API_URL}/models",
|
||||
params={"author": author, "limit": limit, "full": "true"},
|
||||
headers=headers,
|
||||
timeout=15,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
log_debug(
|
||||
f"HuggingFace author index returned {response.status_code} for author={author}"
|
||||
)
|
||||
return None
|
||||
|
||||
repos = response.json()
|
||||
if not isinstance(repos, list):
|
||||
return None
|
||||
|
||||
index = _build_author_index_from_models(author, repos)
|
||||
log_info(
|
||||
f"HuggingFace author index refreshed for author={author}: repos={index['repo_count']}, files={index['file_count']}"
|
||||
)
|
||||
return index
|
||||
except Exception as e:
|
||||
log_debug(f"Error refreshing HuggingFace author index for author={author}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _get_author_index(
|
||||
author: str, headers: Dict[str, str], force_refresh: bool = False
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
cache_key = _author_index_cache_key(author, headers)
|
||||
can_persist = not headers.get("Authorization")
|
||||
|
||||
with _author_index_lock:
|
||||
memory_index = _author_index_cache.get(cache_key)
|
||||
if not force_refresh and _is_author_index_fresh(memory_index):
|
||||
return memory_index
|
||||
|
||||
persistent_index = None
|
||||
if can_persist:
|
||||
persistent_index = _read_persistent_author_indexes().get("authors", {}).get(
|
||||
author
|
||||
)
|
||||
if not force_refresh and _is_author_index_fresh(persistent_index):
|
||||
_author_index_cache[cache_key] = persistent_index
|
||||
log_debug(f"HuggingFace author index cache hit for author={author}")
|
||||
return persistent_index
|
||||
|
||||
fresh_index = _fetch_author_index(author, headers=headers)
|
||||
if fresh_index:
|
||||
_author_index_cache[cache_key] = fresh_index
|
||||
if can_persist:
|
||||
_write_persistent_author_index(author, fresh_index)
|
||||
return fresh_index
|
||||
|
||||
if persistent_index:
|
||||
log_warn(
|
||||
f"Using stale HuggingFace author index for author={author}; refresh failed"
|
||||
)
|
||||
_author_index_cache[cache_key] = persistent_index
|
||||
return persistent_index
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_author_fallback_index_status(author: str = "Comfy-Org") -> Dict[str, Any]:
|
||||
"""Return persisted public author fallback index status for the options UI."""
|
||||
with _author_index_lock:
|
||||
index = _read_persistent_author_indexes().get("authors", {}).get(author)
|
||||
|
||||
updated_at = index.get("updated_at") if isinstance(index, dict) else None
|
||||
age_seconds = (time.time() - updated_at) if isinstance(updated_at, (int, float)) else None
|
||||
return {
|
||||
"author": author,
|
||||
"exists": bool(index),
|
||||
"updated_at": updated_at,
|
||||
"age_seconds": age_seconds,
|
||||
"ttl_seconds": HF_AUTHOR_INDEX_CACHE_TTL_SECONDS,
|
||||
"stale": bool(index) and not _is_author_index_fresh(index),
|
||||
"repo_count": index.get("repo_count", 0) if isinstance(index, dict) else 0,
|
||||
"file_count": index.get("file_count", 0) if isinstance(index, dict) else 0,
|
||||
}
|
||||
|
||||
|
||||
def refresh_author_fallback_index(
|
||||
token: Optional[str] = None, author: str = "Comfy-Org"
|
||||
) -> Dict[str, Any]:
|
||||
"""Force refresh the public HuggingFace author fallback index."""
|
||||
headers = {}
|
||||
|
||||
index = _get_author_index(author, headers=headers, force_refresh=True)
|
||||
status = get_author_fallback_index_status(author)
|
||||
status["success"] = bool(index)
|
||||
status["persisted"] = bool(index)
|
||||
return status
|
||||
|
||||
|
||||
def parse_huggingface_url(url: str) -> Optional[Dict[str, str]]:
|
||||
"""
|
||||
Parse a HuggingFace URL to extract repo and filename.
|
||||
@@ -168,6 +361,53 @@ def _find_matching_file_in_repo(
|
||||
return partial_match
|
||||
|
||||
|
||||
def _find_matching_file_in_author_index(
|
||||
index: Dict[str, Any], filename: str, exact_only: bool = False
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
files = index.get("files") or []
|
||||
filename_lower = filename.lower()
|
||||
filename_base = os.path.splitext(filename_lower)[0]
|
||||
partial_match = None
|
||||
|
||||
for file_info in files:
|
||||
repo_id = file_info.get("repo_id", "")
|
||||
file_path = file_info.get("path", "")
|
||||
if not repo_id or not file_path:
|
||||
continue
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
file_name_lower = file_name.lower()
|
||||
if file_name_lower == filename_lower or file_path.lower().endswith(
|
||||
filename_lower
|
||||
):
|
||||
return _build_huggingface_result(repo_id, file_path, file_info, "exact")
|
||||
|
||||
if exact_only:
|
||||
return None
|
||||
|
||||
for file_info in files:
|
||||
repo_id = file_info.get("repo_id", "")
|
||||
file_path = file_info.get("path", "")
|
||||
if not repo_id or not file_path:
|
||||
continue
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
file_name_lower = file_name.lower()
|
||||
file_base = os.path.splitext(file_name_lower)[0]
|
||||
file_path_lower = file_path.lower()
|
||||
|
||||
if filename_base in file_base or file_base in filename_base:
|
||||
if file_path_lower.endswith(".safetensors") or file_path_lower.endswith(
|
||||
".ckpt"
|
||||
):
|
||||
partial_match = _build_huggingface_result(
|
||||
repo_id, file_path, file_info, "partial"
|
||||
)
|
||||
break
|
||||
|
||||
return partial_match
|
||||
|
||||
|
||||
def _build_huggingface_search_queries(filename: str) -> List[str]:
|
||||
filename_base = os.path.splitext(os.path.basename(filename))[0].lower()
|
||||
cleaned_base = clean_filename_for_search(filename).lower()
|
||||
@@ -371,8 +611,27 @@ def search_huggingface_for_file(
|
||||
return result
|
||||
|
||||
author_fallback_repos = []
|
||||
author_fallback_repo_count = 0
|
||||
if use_comfy_org_fallback:
|
||||
for author in HF_AUTHOR_FALLBACKS:
|
||||
index = _get_author_index(author, headers={})
|
||||
if index:
|
||||
result = _find_matching_file_in_author_index(
|
||||
index, filename, exact_only=exact_only
|
||||
)
|
||||
author_fallback_repo_count += int(index.get("repo_count") or 0)
|
||||
if result:
|
||||
_search_cache[cache_key] = result
|
||||
log_info(
|
||||
f"Found {result['match_type']} HuggingFace author index match for {filename}: {result['repo_id']}/{result['path']}"
|
||||
)
|
||||
return result
|
||||
|
||||
log_debug(
|
||||
f"HuggingFace author index no match for author={author}, filename={filename}, repos={index.get('repo_count', 0)}, files={index.get('file_count', 0)}"
|
||||
)
|
||||
continue
|
||||
|
||||
repos = _get_repos_by_author(author, headers=headers)
|
||||
log_info(
|
||||
f"HuggingFace author fallback returned {len(repos)} repos for author={author}"
|
||||
@@ -381,6 +640,7 @@ def search_huggingface_for_file(
|
||||
if repo_id not in seen_repos:
|
||||
seen_repos.add(repo_id)
|
||||
author_fallback_repos.append(repo_id)
|
||||
author_fallback_repo_count += 1
|
||||
|
||||
for repo_id in author_fallback_repos:
|
||||
log_debug(
|
||||
@@ -430,7 +690,7 @@ def search_huggingface_for_file(
|
||||
# Not found
|
||||
_search_cache[cache_key] = None
|
||||
log_info(
|
||||
f"HuggingFace search no result: filename={filename}, repos_checked={len(repos_to_check)}, author_fallback_repos={len(author_fallback_repos)}, brave_candidates={len(brave_candidates)}"
|
||||
f"HuggingFace search no result: filename={filename}, repos_checked={len(repos_to_check)}, author_fallback_repos={author_fallback_repo_count}, brave_candidates={len(brave_candidates)}"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,5 +5,5 @@
|
||||
"sha": "5fcb5f0637f9fbdedaf8d37da9a390f76ae0ded8",
|
||||
"size": 268817,
|
||||
"model_count": 527,
|
||||
"updated_at": "2026-06-04T17:47:06+00:00"
|
||||
"updated_at": "2026-06-05T01:04:57+00:00"
|
||||
}
|
||||
|
||||
@@ -3543,6 +3543,42 @@
|
||||
gap: 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
.ml-options-help,
|
||||
#model-linker-modal .ml-options-help {
|
||||
margin: 8px 0 0;
|
||||
color: var(--ml-text-muted);
|
||||
font-size: var(--ml-font-sm);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.ml-options-help summary,
|
||||
#model-linker-modal .ml-options-help summary {
|
||||
width: fit-content;
|
||||
color: var(--ml-text-secondary);
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
.ml-options-help summary:hover,
|
||||
#model-linker-modal .ml-options-help summary:hover {
|
||||
color: var(--ml-accent);
|
||||
}
|
||||
.ml-options-help ol,
|
||||
#model-linker-modal .ml-options-help ol {
|
||||
margin: 8px 0 0 18px;
|
||||
padding: 0;
|
||||
}
|
||||
.ml-options-help li,
|
||||
#model-linker-modal .ml-options-help li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.ml-options-help p,
|
||||
#model-linker-modal .ml-options-help p {
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.ml-options-help code,
|
||||
#model-linker-modal .ml-options-help code {
|
||||
color: var(--ml-text);
|
||||
font-size: var(--ml-font-xs);
|
||||
}
|
||||
.ml-options-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3690,6 +3726,14 @@
|
||||
display: grid;
|
||||
gap: 0;
|
||||
}
|
||||
.ml-options-dependent-block,
|
||||
#model-linker-modal .ml-options-dependent-block {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 0 0 8px 16px;
|
||||
padding: 12px 0 4px 14px;
|
||||
border-left: 1px solid rgba(255,255,255,0.12);
|
||||
}
|
||||
.ml-options-toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+175
-48
@@ -4031,28 +4031,6 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
<h4 class="ml-options-section-title">CivitAI</h4>
|
||||
</div>
|
||||
<div class="ml-options-grid">
|
||||
<div class="ml-options-panel">
|
||||
<div class="ml-options-stack">
|
||||
<div class="ml-options-field">
|
||||
<div class="ml-options-input-row">
|
||||
<label for="ml-options-civitai" class="ml-options-label">CivitAI API Key <a href="https://civitai.com/user/account" target="_blank" rel="noopener noreferrer" class="ml-options-inline-link">Get key</a> <span class="ml-tooltip-badge" data-tooltip="Optional. Used when downloading from CivitAI requires your account. Add this if CivitAI downloads fail or need access to models available to your account.">?</span></label>
|
||||
<input id="ml-options-civitai" class="ml-options-input" type="password" placeholder="Paste CivitAI API key" value="${tokens.civitai_key}">
|
||||
<button id="ml-options-civitai-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved CivitAI API key" data-tooltip="Show saved value">
|
||||
${getSvgIcon('eye')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-options-field">
|
||||
<div class="ml-options-input-row">
|
||||
<label for="ml-options-civitai-session" class="ml-options-label">CivitAI Session Token <span class="ml-tooltip-badge" data-tooltip="Optional. Makes CivitAI search use your logged-in session, so results can match what you see in the browser. Useful for NSFW or account-visible results.">?</span></label>
|
||||
<input id="ml-options-civitai-session" class="ml-options-input" type="password" placeholder="Paste __Secure-civitai-token" value="${tokens.civitai_session_token}">
|
||||
<button id="ml-options-civitai-session-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved CivitAI session token" data-tooltip="Show saved value">
|
||||
${getSvgIcon('eye')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-options-panel">
|
||||
<div class="ml-options-toggle-list">
|
||||
<label class="ml-options-toggle-row">
|
||||
@@ -4064,6 +4042,35 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
<span class="ml-options-switch"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="ml-options-dependent-block">
|
||||
<div class="ml-options-field">
|
||||
<div class="ml-options-input-row">
|
||||
<label for="ml-options-civitai" class="ml-options-label">CivitAI API Key <a href="https://civitai.com/user/account" target="_blank" rel="noopener noreferrer" class="ml-options-inline-link">Get key</a> <span class="ml-tooltip-badge" data-tooltip="Optional. Used when downloading from CivitAI requires your account. Add this if CivitAI downloads fail or need access to models available to your account.">?</span></label>
|
||||
<input id="ml-options-civitai" class="ml-options-input" type="password" placeholder="Paste CivitAI API key" value="${tokens.civitai_key}">
|
||||
<button id="ml-options-civitai-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved CivitAI API key" data-tooltip="Show saved value">
|
||||
${getSvgIcon('eye')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-options-field">
|
||||
<div class="ml-options-input-row">
|
||||
<label for="ml-options-civitai-session" class="ml-options-label">CivitAI Session Token <span class="ml-tooltip-badge" data-tooltip="Optional. Makes CivitAI search use your logged-in session, so results can match what you see in the browser. Useful for NSFW or account-visible results.">?</span></label>
|
||||
<input id="ml-options-civitai-session" class="ml-options-input" type="password" placeholder="Paste __Secure-civitai-token" value="${tokens.civitai_session_token}">
|
||||
<button id="ml-options-civitai-session-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved CivitAI session token" data-tooltip="Show saved value">
|
||||
${getSvgIcon('eye')}
|
||||
</button>
|
||||
</div>
|
||||
<details class="ml-options-help">
|
||||
<summary>How to find the session token</summary>
|
||||
<ol>
|
||||
<li>Sign in on civitai.com in your browser.</li>
|
||||
<li>Open DevTools, then Application or Storage.</li>
|
||||
<li>Open Cookies for civitai.com and copy <code>__Secure-civitai-token</code>.</li>
|
||||
</ol>
|
||||
<p>Treat it like a password. It is only needed when session-aware CivitAI search is required.</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<label class="ml-options-toggle-row">
|
||||
<div class="ml-options-toggle-copy">
|
||||
<span class="ml-options-toggle-title">Use CivitAI HTML fallback <span class="ml-tooltip-badge" data-tooltip="Backup CivitAI search. Leave this enabled to try the regular CivitAI page when the main search does not find enough results.">?</span></span>
|
||||
@@ -4073,11 +4080,13 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
<span class="ml-options-switch"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="ml-options-number-row">
|
||||
<div class="ml-options-number-copy">
|
||||
<span class="ml-options-label">CivitAI Models To Inspect <span class="ml-tooltip-badge" data-tooltip="How many CivitAI results to check for the exact file. Higher values may find more matches, but searches can take longer. Range: 1-20.">?</span></span>
|
||||
<div class="ml-options-dependent-block">
|
||||
<div class="ml-options-number-row">
|
||||
<div class="ml-options-number-copy">
|
||||
<span class="ml-options-label">CivitAI Models To Inspect <span class="ml-tooltip-badge" data-tooltip="How many CivitAI results to check for the exact file. Higher values may find more matches, but searches can take longer. Range: 1-20.">?</span></span>
|
||||
</div>
|
||||
<input id="ml-options-civitai-limit" class="ml-options-input" type="number" min="1" max="20" step="1" value="${tokens.civitai_candidate_limit}">
|
||||
</div>
|
||||
<input id="ml-options-civitai-limit" class="ml-options-input" type="number" min="1" max="20" step="1" value="${tokens.civitai_candidate_limit}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4088,28 +4097,6 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
<h4 class="ml-options-section-title">HuggingFace</h4>
|
||||
</div>
|
||||
<div class="ml-options-grid">
|
||||
<div class="ml-options-panel">
|
||||
<div class="ml-options-stack">
|
||||
<div class="ml-options-field">
|
||||
<div class="ml-options-input-row">
|
||||
<label for="ml-options-hf" class="ml-options-label">HuggingFace Token <a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer" class="ml-options-inline-link">Get key</a> <span class="ml-tooltip-badge" data-tooltip="Optional. Used to search and download files from Hugging Face repos your account can access, including gated repos. A read-only token is enough.">?</span></label>
|
||||
<input id="ml-options-hf" class="ml-options-input" type="password" placeholder="Paste HuggingFace token" value="${tokens.hf_token}">
|
||||
<button id="ml-options-hf-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved HuggingFace token" data-tooltip="Show saved value">
|
||||
${getSvgIcon('eye')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-options-field">
|
||||
<div class="ml-options-input-row">
|
||||
<label for="ml-options-brave" class="ml-options-label">Brave Search API Key <a href="https://api-dashboard.search.brave.com/app/keys" target="_blank" rel="noopener noreferrer" class="ml-options-inline-link">Get key</a> <span class="ml-tooltip-badge" data-tooltip="Optional. Used only by the Brave fallback. It helps find Hugging Face files when Hugging Face search does not show the right repo.">?</span></label>
|
||||
<input id="ml-options-brave" class="ml-options-input" type="password" placeholder="Paste Brave Search API key" value="${tokens.brave_search_api_key}">
|
||||
<button id="ml-options-brave-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved Brave Search API key" data-tooltip="Show saved value">
|
||||
${getSvgIcon('eye')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-options-panel">
|
||||
<div class="ml-options-toggle-list">
|
||||
<label class="ml-options-toggle-row">
|
||||
@@ -4121,6 +4108,17 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
<span class="ml-options-switch"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="ml-options-dependent-block">
|
||||
<div class="ml-options-field">
|
||||
<div class="ml-options-input-row">
|
||||
<label for="ml-options-hf" class="ml-options-label">HuggingFace Token <a href="https://huggingface.co/settings/tokens" target="_blank" rel="noopener noreferrer" class="ml-options-inline-link">Get key</a> <span class="ml-tooltip-badge" data-tooltip="Optional. Used to search and download files from Hugging Face repos your account can access, including gated repos. A read-only token is enough.">?</span></label>
|
||||
<input id="ml-options-hf" class="ml-options-input" type="password" placeholder="Paste HuggingFace token" value="${tokens.hf_token}">
|
||||
<button id="ml-options-hf-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved HuggingFace token" data-tooltip="Show saved value">
|
||||
${getSvgIcon('eye')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="ml-options-toggle-row">
|
||||
<div class="ml-options-toggle-copy">
|
||||
<span class="ml-options-toggle-title">Use Comfy-Org fallback <span class="ml-tooltip-badge" data-tooltip="Checks Comfy-Org repositories directly. Useful for ComfyUI model packs that normal Hugging Face search may miss.">?</span></span>
|
||||
@@ -4130,6 +4128,26 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
<span class="ml-options-switch"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="ml-options-dependent-block">
|
||||
<div class="ml-options-db-summary">
|
||||
<div class="ml-options-db-row">
|
||||
<span>Comfy-Org files</span>
|
||||
<strong id="ml-options-hf-index-count">Loading...</strong>
|
||||
</div>
|
||||
<div class="ml-options-db-row">
|
||||
<span>Last refresh</span>
|
||||
<strong id="ml-options-hf-index-updated">Loading...</strong>
|
||||
</div>
|
||||
<div class="ml-options-db-row">
|
||||
<span>Status</span>
|
||||
<strong id="ml-options-hf-index-state">Checking local index...</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ml-options-hf-index-message" class="ml-options-db-message">Comfy-Org fallback uses a cached HuggingFace file index.</div>
|
||||
<div class="ml-options-db-actions">
|
||||
<button id="ml-options-hf-index-refresh" type="button" class="ml-btn ml-btn-secondary">${getSvgIcon('refreshCw')} Refresh Comfy-Org Index</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="ml-options-toggle-row">
|
||||
<div class="ml-options-toggle-copy">
|
||||
<span class="ml-options-toggle-title">Use Brave fallback <span class="ml-tooltip-badge" data-tooltip="Last-resort web search for the exact filename on huggingface.co. Results are still checked before Model Linker offers them.">?</span></span>
|
||||
@@ -4139,6 +4157,17 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
<span class="ml-options-switch"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="ml-options-dependent-block">
|
||||
<div class="ml-options-field">
|
||||
<div class="ml-options-input-row">
|
||||
<label for="ml-options-brave" class="ml-options-label">Brave Search API Key <a href="https://api-dashboard.search.brave.com/app/keys" target="_blank" rel="noopener noreferrer" class="ml-options-inline-link">Get key</a> <span class="ml-tooltip-badge" data-tooltip="Optional. Used only by the Brave fallback. It helps find Hugging Face files when Hugging Face search does not show the right repo.">?</span></label>
|
||||
<input id="ml-options-brave" class="ml-options-input" type="password" placeholder="Paste Brave Search API key" value="${tokens.brave_search_api_key}">
|
||||
<button id="ml-options-brave-toggle" type="button" class="ml-options-visibility-btn" aria-label="Toggle visibility for saved Brave Search API key" data-tooltip="Show saved value">
|
||||
${getSvgIcon('eye')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4171,6 +4200,11 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const modelListMessageEl = this.contentElement.querySelector('#ml-options-model-list-message');
|
||||
const modelListCheckBtn = this.contentElement.querySelector('#ml-options-model-list-check');
|
||||
const modelListUpdateBtn = this.contentElement.querySelector('#ml-options-model-list-update');
|
||||
const hfIndexCountEl = this.contentElement.querySelector('#ml-options-hf-index-count');
|
||||
const hfIndexUpdatedEl = this.contentElement.querySelector('#ml-options-hf-index-updated');
|
||||
const hfIndexStateEl = this.contentElement.querySelector('#ml-options-hf-index-state');
|
||||
const hfIndexMessageEl = this.contentElement.querySelector('#ml-options-hf-index-message');
|
||||
const hfIndexRefreshBtn = this.contentElement.querySelector('#ml-options-hf-index-refresh');
|
||||
const navButtons = Array.from(this.contentElement.querySelectorAll('.ml-options-nav-btn'));
|
||||
const optionSections = Array.from(this.contentElement.querySelectorAll('.ml-options-section'));
|
||||
const trackedInputs = [
|
||||
@@ -4208,6 +4242,10 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
if (modelListUpdateBtn) modelListUpdateBtn.disabled = busy;
|
||||
};
|
||||
|
||||
const setHfIndexBusy = (busy) => {
|
||||
if (hfIndexRefreshBtn) hfIndexRefreshBtn.disabled = busy;
|
||||
};
|
||||
|
||||
const renderModelListStatus = (data = {}, checkedRemote = false) => {
|
||||
if (modelListCountEl) {
|
||||
modelListCountEl.textContent = Number.isFinite(Number(data.local_count))
|
||||
@@ -4293,6 +4331,86 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
}
|
||||
};
|
||||
|
||||
const renderHfIndexStatus = (data = {}) => {
|
||||
if (hfIndexCountEl) {
|
||||
hfIndexCountEl.textContent = Number.isFinite(Number(data.file_count))
|
||||
? Number(data.file_count).toLocaleString()
|
||||
: '-';
|
||||
}
|
||||
if (hfIndexUpdatedEl) {
|
||||
hfIndexUpdatedEl.textContent = data.updated_at
|
||||
? formatOptionDate(Number(data.updated_at) * 1000)
|
||||
: 'Never';
|
||||
}
|
||||
if (hfIndexStateEl) {
|
||||
if (!data.exists) {
|
||||
hfIndexStateEl.textContent = 'Not cached';
|
||||
} else if (data.stale) {
|
||||
hfIndexStateEl.textContent = 'Refresh due';
|
||||
} else {
|
||||
hfIndexStateEl.textContent = 'Cached';
|
||||
}
|
||||
}
|
||||
if (hfIndexMessageEl) {
|
||||
if (!data.exists) {
|
||||
hfIndexMessageEl.textContent = 'The first Comfy-Org fallback search will build the local HuggingFace file index.';
|
||||
} else if (data.stale) {
|
||||
hfIndexMessageEl.textContent = `Index has ${Number(data.repo_count || 0).toLocaleString()} repos and should be refreshed.`;
|
||||
} else {
|
||||
hfIndexMessageEl.textContent = `Index has ${Number(data.repo_count || 0).toLocaleString()} repos and is used before per-repo fallback checks.`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadHfIndexStatus = async () => {
|
||||
try {
|
||||
setHfIndexBusy(true);
|
||||
if (hfIndexStateEl) hfIndexStateEl.textContent = 'Loading...';
|
||||
const response = await api.fetchApi('/model_linker/huggingface/author-index/status');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status failed: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
renderHfIndexStatus(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Model Linker: HuggingFace index status error:', error);
|
||||
if (hfIndexStateEl) hfIndexStateEl.textContent = 'Error';
|
||||
if (hfIndexMessageEl) hfIndexMessageEl.textContent = error.message || 'Failed to read HuggingFace index status.';
|
||||
return null;
|
||||
} finally {
|
||||
setHfIndexBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshHfIndex = async () => {
|
||||
try {
|
||||
setHfIndexBusy(true);
|
||||
if (hfIndexStateEl) hfIndexStateEl.textContent = 'Refreshing...';
|
||||
if (hfIndexMessageEl) hfIndexMessageEl.textContent = 'Downloading Comfy-Org file index from HuggingFace...';
|
||||
const response = await api.fetchApi('/model_linker/huggingface/author-index/refresh', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ hf_token: hfInput?.value || '' })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok || data.success === false) {
|
||||
throw new Error(data.error || `Refresh failed: ${response.status}`);
|
||||
}
|
||||
await this.clearSearchCaches();
|
||||
renderHfIndexStatus(data);
|
||||
if (hfIndexStateEl) hfIndexStateEl.textContent = 'Refreshed';
|
||||
this.showNotification('HuggingFace Comfy-Org index refreshed', 'success');
|
||||
} catch (error) {
|
||||
console.error('Model Linker: HuggingFace index refresh error:', error);
|
||||
if (hfIndexStateEl) hfIndexStateEl.textContent = 'Error';
|
||||
if (hfIndexMessageEl) hfIndexMessageEl.textContent = error.message || 'Failed to refresh HuggingFace index.';
|
||||
this.showNotification('HuggingFace index refresh failed', 'error');
|
||||
} finally {
|
||||
setHfIndexBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveNav = (targetId) => {
|
||||
navButtons.forEach((btn) => {
|
||||
btn.classList.toggle('is-active', btn.dataset.target === targetId);
|
||||
@@ -4352,6 +4470,8 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
setVisibleSection(targetId);
|
||||
if (targetId === 'ml-options-section-local-db') {
|
||||
loadModelListStatus(false);
|
||||
} else if (targetId === 'ml-options-section-hf') {
|
||||
loadHfIndexStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -4368,7 +4488,14 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
});
|
||||
}
|
||||
|
||||
if (hfIndexRefreshBtn) {
|
||||
hfIndexRefreshBtn.addEventListener('click', () => {
|
||||
refreshHfIndex();
|
||||
});
|
||||
}
|
||||
|
||||
loadModelListStatus(false);
|
||||
loadHfIndexStatus();
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
|
||||
Reference in New Issue
Block a user