feat: add SHA-256 search support and refresh HuggingFace author index
- Propagate sha256 through search orchestrator and providers - Add hash-based lookup for CivitAI, HuggingFace, CivArchive, and LoRA Manager Archive - Update search UI to accept SHA-256 hashes and refine link/name toggle styling - Refresh HuggingFace author index metadata
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Request orchestration for model source searches."""
|
||||
|
||||
from ..routes.context import RouteContext
|
||||
from ..type_utils import normalize_sha256
|
||||
from .search_cache import SearchResultCache
|
||||
from .search_dependencies import SearchDependencies
|
||||
from .search_providers import (
|
||||
@@ -135,7 +136,10 @@ class SearchOrchestrator:
|
||||
"""Execute all requested sources and return a JSON response."""
|
||||
try:
|
||||
data = await request.json()
|
||||
filename = data.get("filename", "")
|
||||
filename = str(data.get("filename", "") or "").strip()
|
||||
sha256 = normalize_sha256(
|
||||
data.get("sha256") or data.get("hash") or data.get("file_hash")
|
||||
)
|
||||
category = data.get("category", "")
|
||||
base_model_context = data.get("base_model_context", "")
|
||||
progress_id = str(data.get("progress_id") or "").strip()
|
||||
@@ -186,7 +190,7 @@ class SearchOrchestrator:
|
||||
|
||||
model_id = data.get("model_id")
|
||||
version_id = data.get("version_id")
|
||||
if not filename and not (is_urn and model_id and version_id):
|
||||
if not filename and not sha256 and not (is_urn and model_id and version_id):
|
||||
return self.web.json_response(
|
||||
{
|
||||
"error": (
|
||||
@@ -250,6 +254,7 @@ class SearchOrchestrator:
|
||||
hf_use_brave_fallback=hf_use_brave_fallback,
|
||||
force_search=force_search,
|
||||
normalized_sources=normalized_sources,
|
||||
sha256=sha256,
|
||||
)
|
||||
raise_if_search_cancelled = (
|
||||
self.provider_runner.raise_if_search_cancelled
|
||||
@@ -303,6 +308,7 @@ class SearchOrchestrator:
|
||||
else model_id or version_id
|
||||
),
|
||||
base=base_model_context,
|
||||
sha256=sha256,
|
||||
force=force_search,
|
||||
)
|
||||
)
|
||||
@@ -319,6 +325,8 @@ class SearchOrchestrator:
|
||||
"searched_sources": sorted(normalized_sources),
|
||||
"source_errors": {},
|
||||
"source_status": {},
|
||||
"search_mode": "sha256" if sha256 else "name",
|
||||
"search_sha256": sha256,
|
||||
}
|
||||
|
||||
def iter_result_items(result):
|
||||
@@ -333,65 +341,83 @@ class SearchOrchestrator:
|
||||
matches = []
|
||||
seen_match_paths = set()
|
||||
seen_hashes = set()
|
||||
for source_key in (
|
||||
"huggingface",
|
||||
"civitai",
|
||||
"civarchive",
|
||||
"lora_manager_archive",
|
||||
"popular",
|
||||
"model_list",
|
||||
):
|
||||
for source_result in iter_result_items(payload.get(source_key)):
|
||||
raise_if_search_cancelled(search_request, source_key)
|
||||
sha256 = self.extract_sha256_from_metadata(source_result)
|
||||
if not sha256 or sha256 in seen_hashes:
|
||||
continue
|
||||
seen_hashes.add(sha256)
|
||||
|
||||
self.search_tracker.update(
|
||||
progress_id,
|
||||
progress_source,
|
||||
"local_hash",
|
||||
"Checking local metadata hashes",
|
||||
94,
|
||||
source_entries = []
|
||||
if search_request.sha256:
|
||||
source_entries.append(
|
||||
(
|
||||
"sha256_query",
|
||||
{
|
||||
"filename": filename,
|
||||
"sha256": search_request.sha256,
|
||||
},
|
||||
)
|
||||
try:
|
||||
hash_matches = self.search_local_matches_by_hash(
|
||||
sha256,
|
||||
category=category or None,
|
||||
max_matches=20,
|
||||
force_rescan=force_search,
|
||||
)
|
||||
except Exception as hash_error:
|
||||
self.logger.warning(
|
||||
"Local metadata hash lookup failed "
|
||||
f"for {source_key}:{sha256}: {hash_error}"
|
||||
)
|
||||
continue
|
||||
)
|
||||
source_entries.extend(
|
||||
(
|
||||
source_key,
|
||||
source_result,
|
||||
)
|
||||
for source_key in (
|
||||
"huggingface",
|
||||
"civitai",
|
||||
"civarchive",
|
||||
"lora_manager_archive",
|
||||
"popular",
|
||||
"model_list",
|
||||
)
|
||||
for source_result in iter_result_items(payload.get(source_key))
|
||||
)
|
||||
for source_key, source_result in source_entries:
|
||||
raise_if_search_cancelled(search_request, source_key)
|
||||
sha256 = self.extract_sha256_from_metadata(source_result)
|
||||
if not sha256 or sha256 in seen_hashes:
|
||||
continue
|
||||
seen_hashes.add(sha256)
|
||||
|
||||
for match in hash_matches:
|
||||
model_path = (
|
||||
match.get("model", {}).get("path")
|
||||
or match.get("path")
|
||||
or ""
|
||||
)
|
||||
path_key = model_path.lower()
|
||||
if path_key and path_key in seen_match_paths:
|
||||
continue
|
||||
if path_key:
|
||||
seen_match_paths.add(path_key)
|
||||
matches.append(
|
||||
{
|
||||
**match,
|
||||
"hash_lookup_source": source_key,
|
||||
"hash_lookup_filename": (
|
||||
source_result.get("filename")
|
||||
or source_result.get("path")
|
||||
or filename
|
||||
),
|
||||
"hash_lookup_sha256": sha256,
|
||||
}
|
||||
)
|
||||
self.search_tracker.update(
|
||||
progress_id,
|
||||
progress_source,
|
||||
"local_hash",
|
||||
"Checking local metadata hashes",
|
||||
94,
|
||||
)
|
||||
try:
|
||||
hash_matches = self.search_local_matches_by_hash(
|
||||
sha256,
|
||||
category=category or None,
|
||||
max_matches=20,
|
||||
force_rescan=force_search,
|
||||
)
|
||||
except Exception as hash_error:
|
||||
self.logger.warning(
|
||||
"Local metadata hash lookup failed "
|
||||
f"for {source_key}:{sha256}: {hash_error}"
|
||||
)
|
||||
continue
|
||||
|
||||
for match in hash_matches:
|
||||
model_path = (
|
||||
match.get("model", {}).get("path")
|
||||
or match.get("path")
|
||||
or ""
|
||||
)
|
||||
path_key = model_path.lower()
|
||||
if path_key and path_key in seen_match_paths:
|
||||
continue
|
||||
if path_key:
|
||||
seen_match_paths.add(path_key)
|
||||
matches.append(
|
||||
{
|
||||
**match,
|
||||
"hash_lookup_source": source_key,
|
||||
"hash_lookup_filename": (
|
||||
source_result.get("filename")
|
||||
or source_result.get("path")
|
||||
or filename
|
||||
),
|
||||
"hash_lookup_sha256": sha256,
|
||||
}
|
||||
)
|
||||
|
||||
if matches:
|
||||
self.logger.info(
|
||||
@@ -433,6 +459,8 @@ class SearchOrchestrator:
|
||||
|
||||
raise_if_search_cancelled(search_request, progress_source)
|
||||
results["local_hash_matches"] = collect_local_hash_matches(results)
|
||||
if results["local_hash_matches"]:
|
||||
results["found"] = True
|
||||
raise_if_search_cancelled(search_request, progress_source)
|
||||
|
||||
self.logger.info(
|
||||
|
||||
@@ -33,6 +33,7 @@ class SearchRequest:
|
||||
hf_use_brave_fallback: bool
|
||||
force_search: bool
|
||||
normalized_sources: frozenset[str]
|
||||
sha256: str = ""
|
||||
|
||||
@property
|
||||
def search_local(self):
|
||||
@@ -226,6 +227,17 @@ class SearchProviderRunner:
|
||||
def task():
|
||||
source_results = {"popular": None, "model_list": None}
|
||||
source_found = False
|
||||
request_sha256 = getattr(request, "sha256", "")
|
||||
|
||||
if request_sha256:
|
||||
self.owner.search_tracker.update(
|
||||
request.progress_id,
|
||||
"local",
|
||||
"hash",
|
||||
"Searching local models by SHA-256",
|
||||
58,
|
||||
)
|
||||
return source_results, source_found
|
||||
|
||||
popular_info = self.owner.get_popular_model_url(request.filename)
|
||||
self.owner.log_search_result("popular", popular_info)
|
||||
@@ -289,6 +301,7 @@ class SearchProviderRunner:
|
||||
def task():
|
||||
hf_result = self.owner.search_huggingface_for_file(
|
||||
request.filename,
|
||||
sha256=getattr(request, "sha256", ""),
|
||||
token=request.hf_token or None,
|
||||
brave_api_key=request.brave_search_api_key or None,
|
||||
use_api_search=request.hf_use_api_search,
|
||||
@@ -435,6 +448,7 @@ class SearchProviderRunner:
|
||||
"civitai",
|
||||
lambda base_ctx, callback: self.owner.search_civitai_for_file(
|
||||
request.filename,
|
||||
sha256=getattr(request, "sha256", ""),
|
||||
api_key=request.civitai_key or None,
|
||||
model_type=request.category,
|
||||
base_model_context=base_ctx,
|
||||
@@ -508,6 +522,7 @@ class SearchProviderRunner:
|
||||
"civarchive",
|
||||
lambda base_ctx, callback: self.owner.search_civarchive_for_file(
|
||||
request.filename,
|
||||
sha256=getattr(request, "sha256", ""),
|
||||
model_type=request.category,
|
||||
base_model_context=base_ctx,
|
||||
limit=request.civarchive_candidate_limit,
|
||||
@@ -589,6 +604,7 @@ class SearchProviderRunner:
|
||||
"lora_manager_archive",
|
||||
lambda base_ctx, callback: self.owner.search_lora_manager_archive_for_file(
|
||||
request.filename,
|
||||
sha256=getattr(request, "sha256", ""),
|
||||
model_type=request.category,
|
||||
base_model_context=base_ctx,
|
||||
progress_callback=callback,
|
||||
@@ -626,7 +642,13 @@ class SearchProviderRunner:
|
||||
)
|
||||
)
|
||||
if request.search_civarchive_source and (
|
||||
request.filename or (request.is_urn and request.data.get("model_id") and request.data.get("version_id"))
|
||||
request.filename
|
||||
or getattr(request, "sha256", "")
|
||||
or (
|
||||
request.is_urn
|
||||
and request.data.get("model_id")
|
||||
and request.data.get("version_id")
|
||||
)
|
||||
):
|
||||
tasks.append(
|
||||
self.owner.asyncio.to_thread(
|
||||
@@ -634,7 +656,9 @@ class SearchProviderRunner:
|
||||
request,
|
||||
)
|
||||
)
|
||||
if request.search_lora_manager_archive_source and request.filename:
|
||||
if request.search_lora_manager_archive_source and (
|
||||
request.filename or getattr(request, "sha256", "")
|
||||
):
|
||||
tasks.append(
|
||||
self.owner.asyncio.to_thread(
|
||||
self.search_lora_manager_archive_source_task,
|
||||
|
||||
@@ -2137,19 +2137,21 @@ def search_civarchive_for_file(
|
||||
exact_only: bool = False,
|
||||
limit: int = DEFAULT_CIVARCHIVE_CANDIDATE_LIMIT,
|
||||
progress_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
|
||||
sha256: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Search CivArchive for the best downloadable match for a model filename.
|
||||
"""
|
||||
normalized_filename = (filename or "").strip()
|
||||
if not normalized_filename:
|
||||
requested_sha256 = _extract_sha256(sha256 or "")
|
||||
if not normalized_filename and not requested_sha256:
|
||||
return None
|
||||
|
||||
limit = max(1, min(int(limit), MAX_CIVARCHIVE_CANDIDATE_LIMIT))
|
||||
detail_limit = min(limit, SEARCH_RESULT_DETAIL_LIMIT)
|
||||
base_model_key = _normalize_base_model(base_model_context or "")
|
||||
cache_key = (
|
||||
f"file::{normalized_filename.lower()}::{model_type or ''}::{base_model_key}::{exact_only}::{limit}"
|
||||
f"file::{normalized_filename.lower()}::hash::{requested_sha256}::{model_type or ''}::{base_model_key}::{exact_only}::{limit}"
|
||||
)
|
||||
if cache_key in _search_cache:
|
||||
_report_progress(
|
||||
@@ -2160,17 +2162,17 @@ def search_civarchive_for_file(
|
||||
)
|
||||
return _search_cache[cache_key]
|
||||
|
||||
sha256 = _extract_sha256(normalized_filename)
|
||||
if sha256:
|
||||
hash_query = requested_sha256 or _extract_sha256(normalized_filename)
|
||||
if hash_query:
|
||||
_report_progress(
|
||||
progress_callback,
|
||||
"hash",
|
||||
"Resolving CivArchive hash",
|
||||
40,
|
||||
sha256=sha256,
|
||||
sha256=hash_query,
|
||||
)
|
||||
result = resolve_civarchive_by_hash(
|
||||
sha256,
|
||||
hash_query,
|
||||
query=normalized_filename,
|
||||
exact_only=exact_only,
|
||||
model_type=model_type,
|
||||
|
||||
+85
-1
@@ -49,6 +49,7 @@ from ..type_utils import (
|
||||
first_non_empty,
|
||||
normalize_model_file_info,
|
||||
normalize_model_image,
|
||||
normalize_sha256,
|
||||
parse_size_to_bytes,
|
||||
resolve_model_category,
|
||||
select_primary_model_file,
|
||||
@@ -879,6 +880,7 @@ def search_civitai_for_file(
|
||||
use_api_search: bool = True,
|
||||
use_html_fallback: bool = True,
|
||||
progress_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
|
||||
sha256: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Search CivitAI for a specific model file.
|
||||
@@ -896,6 +898,7 @@ def search_civitai_for_file(
|
||||
global _search_cache
|
||||
|
||||
candidate_limit = max(1, min(int(candidate_limit), MAX_CIVITAI_CANDIDATE_LIMIT))
|
||||
requested_sha256 = normalize_sha256(sha256)
|
||||
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 "")
|
||||
@@ -905,7 +908,7 @@ def search_civitai_for_file(
|
||||
f"_html{int(bool(use_html_fallback))}"
|
||||
)
|
||||
cache_key = (
|
||||
f"civit_{filename}_exact{exact_only}_type{model_type_key}_base{base_model_key}_session{session_key}_limit{candidate_limit}_{methods_key}"
|
||||
f"civit_{filename}_sha{requested_sha256}_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})")
|
||||
@@ -918,6 +921,61 @@ def search_civitai_for_file(
|
||||
return _search_cache[cache_key]
|
||||
|
||||
try:
|
||||
if requested_sha256:
|
||||
_report_progress(
|
||||
progress_callback,
|
||||
"hash",
|
||||
"Resolving CivitAI SHA-256",
|
||||
62,
|
||||
sha256=requested_sha256,
|
||||
)
|
||||
hash_result = get_model_info_by_hash(
|
||||
requested_sha256,
|
||||
api_key=api_key,
|
||||
)
|
||||
if hash_result:
|
||||
result = build_unified_search_result(
|
||||
"civitai",
|
||||
model_id=hash_result.get("model_id"),
|
||||
version_id=hash_result.get("version_id"),
|
||||
name=hash_result.get("model_name") or hash_result.get("name") or "",
|
||||
version_name=hash_result.get("version_name") or "",
|
||||
type=hash_result.get("model_type") or hash_result.get("type") or "",
|
||||
filename=hash_result.get("filename") or filename,
|
||||
url=hash_result.get("version_url") or hash_result.get("url") or "",
|
||||
download_url=hash_result.get("download_url"),
|
||||
size=hash_result.get("size"),
|
||||
base_model=hash_result.get("base_model"),
|
||||
tags=hash_result.get("tags") or [],
|
||||
trained_words=hash_result.get("trained_words") or [],
|
||||
images=hash_result.get("images") or [],
|
||||
match_type="hash",
|
||||
confidence=100.0,
|
||||
sha256=requested_sha256,
|
||||
hashes=hash_result.get("hashes") or {"SHA256": requested_sha256},
|
||||
description=hash_result.get("description") or "",
|
||||
model_description=hash_result.get("model_description") or "",
|
||||
)
|
||||
_search_cache[cache_key] = result
|
||||
_report_progress(
|
||||
progress_callback,
|
||||
"found",
|
||||
"Found exact CivitAI SHA-256 match",
|
||||
92,
|
||||
confidence=100.0,
|
||||
)
|
||||
return result
|
||||
|
||||
_search_cache[cache_key] = None
|
||||
_report_progress(
|
||||
progress_callback,
|
||||
"done",
|
||||
"CivitAI SHA-256 not found",
|
||||
92,
|
||||
found=False,
|
||||
)
|
||||
return None
|
||||
|
||||
# Prefer the CivitAI.red tRPC search because it is much closer to what
|
||||
# the browser search UI returns than the broad public /models API.
|
||||
if use_trpc_search:
|
||||
@@ -1504,6 +1562,7 @@ def get_model_info_by_hash(
|
||||
"""
|
||||
global _hash_cache
|
||||
|
||||
file_hash = normalize_sha256(file_hash)
|
||||
if not file_hash:
|
||||
return None
|
||||
|
||||
@@ -1546,6 +1605,9 @@ def get_model_info_by_hash(
|
||||
"version_id": version_info.get("id"),
|
||||
"version_name": version_info.get("name"),
|
||||
"sha256": file_hash,
|
||||
"filename": "",
|
||||
"size": None,
|
||||
"hashes": {},
|
||||
"url": f"https://civitai.com/models/{model_id}" if model_id else None,
|
||||
"version_url": f"https://civitai.com/models/{model_id}?modelVersionId={version_info.get('id')}"
|
||||
if model_id
|
||||
@@ -1560,6 +1622,28 @@ def get_model_info_by_hash(
|
||||
"model_description": model_info.get("description", ""),
|
||||
}
|
||||
|
||||
files = version_info.get("files") or []
|
||||
matching_file = next(
|
||||
(
|
||||
file_info
|
||||
for file_info in files
|
||||
if isinstance(file_info, dict)
|
||||
and normalize_sha256(
|
||||
file_info.get("sha256")
|
||||
or (file_info.get("hashes") or {}).get("SHA256")
|
||||
or (file_info.get("hashes") or {}).get("sha256")
|
||||
)
|
||||
== file_hash.lower()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if matching_file:
|
||||
result["filename"] = matching_file.get("name") or matching_file.get("filename") or ""
|
||||
result["size"] = matching_file.get("size")
|
||||
if result["size"] is None and matching_file.get("sizeKB") is not None:
|
||||
result["size"] = matching_file.get("sizeKB") * 1024
|
||||
result["hashes"] = matching_file.get("hashes") or {}
|
||||
|
||||
result = _enrich_model_info_with_details(
|
||||
result,
|
||||
model_id,
|
||||
|
||||
@@ -896,6 +896,7 @@ def search_huggingface_for_file(
|
||||
use_brave_fallback: bool = True,
|
||||
force_refresh: bool = False,
|
||||
progress_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
|
||||
sha256: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Search HuggingFace for a specific model file.
|
||||
@@ -912,6 +913,7 @@ def search_huggingface_for_file(
|
||||
"""
|
||||
global _search_cache
|
||||
|
||||
requested_sha256 = normalize_sha256(sha256)
|
||||
brave_key = "brave" if brave_api_key and use_brave_fallback else "nobrave"
|
||||
token_key = "token" if token else "notoken"
|
||||
methods_key = (
|
||||
@@ -920,7 +922,7 @@ def search_huggingface_for_file(
|
||||
f"brave{int(bool(use_brave_fallback))}_"
|
||||
f"force{int(bool(force_refresh))}"
|
||||
)
|
||||
cache_key = f"hf_{filename}_exact{exact_only}_{token_key}_{brave_key}_{methods_key}"
|
||||
cache_key = f"hf_{filename}_sha{requested_sha256}_exact{exact_only}_{token_key}_{brave_key}_{methods_key}"
|
||||
if cache_key in _search_cache:
|
||||
log.debug(f"HuggingFace cache hit file={filename} exact={exact_only}")
|
||||
_report_progress(
|
||||
@@ -932,6 +934,16 @@ def search_huggingface_for_file(
|
||||
return _search_cache[cache_key]
|
||||
|
||||
try:
|
||||
def result_matches_requested_hash(result: Optional[Dict[str, Any]]) -> bool:
|
||||
if not requested_sha256:
|
||||
return True
|
||||
return normalize_sha256(
|
||||
result.get("sha256")
|
||||
or result.get("hash")
|
||||
or (result.get("hashes") or {}).get("SHA256")
|
||||
or (result.get("hashes") or {}).get("sha256")
|
||||
) == requested_sha256
|
||||
|
||||
headers = {}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
@@ -1015,7 +1027,7 @@ def search_huggingface_for_file(
|
||||
exact_only=exact_only,
|
||||
headers=headers,
|
||||
)
|
||||
if result:
|
||||
if result and result_matches_requested_hash(result):
|
||||
_search_cache[cache_key] = result
|
||||
_report_progress(
|
||||
progress_callback,
|
||||
@@ -1057,7 +1069,7 @@ def search_huggingface_for_file(
|
||||
headers={},
|
||||
)
|
||||
author_fallback_repo_count += int(index.get("repo_count") or 0)
|
||||
if result:
|
||||
if result and result_matches_requested_hash(result):
|
||||
_search_cache[cache_key] = result
|
||||
_report_progress(
|
||||
progress_callback,
|
||||
@@ -1117,7 +1129,7 @@ def search_huggingface_for_file(
|
||||
exact_only=exact_only,
|
||||
headers=headers,
|
||||
)
|
||||
if result:
|
||||
if result and result_matches_requested_hash(result):
|
||||
_search_cache[cache_key] = result
|
||||
_report_progress(
|
||||
progress_callback,
|
||||
@@ -1178,7 +1190,11 @@ def search_huggingface_for_file(
|
||||
exact_only=True,
|
||||
headers=headers,
|
||||
)
|
||||
if result and result.get("filename", "").lower() == filename.lower():
|
||||
if (
|
||||
result
|
||||
and result.get("filename", "").lower() == filename.lower()
|
||||
and result_matches_requested_hash(result)
|
||||
):
|
||||
_search_cache[cache_key] = result
|
||||
_report_progress(
|
||||
progress_callback,
|
||||
|
||||
@@ -32,6 +32,7 @@ from ..type_utils import (
|
||||
build_search_result,
|
||||
get_generic_filename_tokens,
|
||||
normalize_lora_manager_type,
|
||||
normalize_sha256,
|
||||
select_primary_model_file,
|
||||
)
|
||||
|
||||
@@ -678,10 +679,78 @@ def search_lora_manager_archive_for_file(
|
||||
exact_only: bool = False,
|
||||
limit: int = 10,
|
||||
progress_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
|
||||
sha256: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Search the archive for the best model/version match for a filename.
|
||||
"""
|
||||
requested_sha256 = normalize_sha256(sha256)
|
||||
if requested_sha256:
|
||||
conn = _connect_readonly()
|
||||
if not conn:
|
||||
return None
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT
|
||||
m.id AS model_id,
|
||||
m.name AS model_name,
|
||||
m.type AS model_type,
|
||||
v.id AS version_id,
|
||||
v.name AS version_name,
|
||||
v.base_model AS base_model,
|
||||
v.position AS position
|
||||
FROM models m
|
||||
JOIN model_versions v ON v.model_id = m.id
|
||||
ORDER BY m.id ASC, COALESCE(v.position, 999999) ASC
|
||||
"""
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
files = _load_version_files(conn, row["version_id"])
|
||||
matching_file = next(
|
||||
(
|
||||
file_info
|
||||
for file_info in files
|
||||
if normalize_sha256(
|
||||
file_info.get("sha256")
|
||||
or (file_info.get("hashes") or {}).get("SHA256")
|
||||
or (file_info.get("hashes") or {}).get("sha256")
|
||||
)
|
||||
== requested_sha256
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not matching_file:
|
||||
continue
|
||||
|
||||
result = _build_result_from_row(conn, row)
|
||||
result.update(
|
||||
{
|
||||
"filename": matching_file.get("name") or result.get("filename", ""),
|
||||
"download_url": matching_file.get("downloadUrl") or result.get("download_url"),
|
||||
"size": (
|
||||
int((matching_file.get("sizeKB") or 0) * 1024)
|
||||
if matching_file.get("sizeKB") is not None
|
||||
else result.get("size")
|
||||
),
|
||||
"sha256": requested_sha256,
|
||||
"hashes": matching_file.get("hashes") or {"SHA256": requested_sha256},
|
||||
"match_type": "hash",
|
||||
"confidence": 100.0,
|
||||
}
|
||||
)
|
||||
_report_progress(
|
||||
progress_callback,
|
||||
"found",
|
||||
"Found LoRA archive SHA-256 match",
|
||||
92,
|
||||
confidence=100.0,
|
||||
)
|
||||
return result
|
||||
finally:
|
||||
conn.close()
|
||||
return None
|
||||
|
||||
search_query = os.path.splitext(get_filename_from_path(filename))[0]
|
||||
if not search_query:
|
||||
return None
|
||||
|
||||
+1795
-1704
File diff suppressed because it is too large
Load Diff
+20
-14
@@ -2471,7 +2471,9 @@
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: var(--mr-missing-control-bg-hover, rgba(255, 255, 255, 0.06));
|
||||
box-sizing: border-box;
|
||||
background-color: var(--mr-missing-control-bg, rgba(255, 255, 255, 0.035));
|
||||
border: 1px solid var(--mr-missing-control-border, rgba(255, 255, 255, 0.105));
|
||||
border-radius: var(--mr-missing-control-radius, 7px);
|
||||
box-shadow: var(--mr-missing-control-shadow, none);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
@@ -2479,7 +2481,8 @@
|
||||
}
|
||||
|
||||
.mr-link-name-switch:hover .mr-switch-knob {
|
||||
background-color: var(--mr-missing-control-border-hover, rgba(255, 255, 255, 0.17));
|
||||
background-color: var(--mr-missing-control-bg-hover, rgba(255, 255, 255, 0.06));
|
||||
border-color: var(--mr-missing-control-border-hover, rgba(255, 255, 255, 0.17));
|
||||
}
|
||||
|
||||
.mr-link-name-switch .mr-switch-labels {
|
||||
@@ -2589,12 +2592,13 @@
|
||||
.mr-link-name-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--mr-spacing-7);
|
||||
justify-content: center;
|
||||
gap: var(--mr-spacing-8);
|
||||
height: var(--mr-download-control-height);
|
||||
min-height: var(--mr-download-control-height);
|
||||
min-width: 108px;
|
||||
padding: 0 var(--mr-spacing-12);
|
||||
width: 100%;
|
||||
min-width: 82px;
|
||||
padding: 0 var(--mr-spacing-10);
|
||||
color: var(--mr-text);
|
||||
background: var(--mr-missing-control-bg, rgba(255, 255, 255, 0.035));
|
||||
border: 1px solid var(--mr-missing-control-border, rgba(255, 255, 255, 0.105));
|
||||
@@ -2613,7 +2617,8 @@
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: var(--mr-spacing-6);
|
||||
min-width: 108px;
|
||||
min-width: 82px;
|
||||
max-width: 82px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -2627,15 +2632,15 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 var(--mr-font-base);
|
||||
width: var(--mr-font-base);
|
||||
height: var(--mr-font-base);
|
||||
}
|
||||
|
||||
.mr-link-name-action-icon svg {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: var(--mr-font-base);
|
||||
height: var(--mr-font-base);
|
||||
}
|
||||
|
||||
.mr-link-name-action:hover:not(:disabled) {
|
||||
@@ -9572,10 +9577,11 @@ a.mr-image-preview-resource-name:hover {
|
||||
}
|
||||
|
||||
#model-resolver-modal .mr-missing-detail-pane .mr-download-section .mr-link-name-action {
|
||||
min-width: 108px;
|
||||
min-width: 82px;
|
||||
width: 82px;
|
||||
min-height: var(--mr-missing-field-height, 38px);
|
||||
height: var(--mr-missing-field-height, 38px);
|
||||
padding: 0 var(--mr-spacing-12);
|
||||
padding: 0 var(--mr-spacing-10);
|
||||
color: var(--mr-text);
|
||||
background: var(--mr-missing-control-bg, rgba(255, 255, 255, 0.035));
|
||||
border-color: var(--mr-missing-control-border, rgba(255, 255, 255, 0.105));
|
||||
|
||||
@@ -2265,13 +2265,15 @@ export const resolveDownloadMethods = {
|
||||
*/
|
||||
async searchOnline(missing, { workflowKey = this.getWorkflowScopedQueueKey(), forceSearch = false, source = null, searchQuery = '' } = {}) {
|
||||
const providedSearchQuery = String(searchQuery || '').trim();
|
||||
const searchSha256 = normalizeSha256(providedSearchQuery);
|
||||
const state = this.getSearchStateForWorkflow(workflowKey, missing);
|
||||
const requestedSearchQuery = providedSearchQuery;
|
||||
const requestedSearchQuery = searchSha256 ? '' : providedSearchQuery;
|
||||
let filename = requestedSearchQuery || this.getFilenameFromPath(missing.original_path);
|
||||
let category = missing.category || this.getNodeTypeDownloadCategory?.(missing.node_type) || '';
|
||||
if (providedSearchQuery) {
|
||||
state.inputMode = 'name';
|
||||
state.manualSearchQuery = providedSearchQuery;
|
||||
state.lastSearchMode = searchSha256 ? 'sha256' : 'name';
|
||||
this.persistSearchStateForWorkflow(workflowKey, missing, state);
|
||||
}
|
||||
const missingSearchKey = this.getMissingSearchKey(missing);
|
||||
@@ -2297,7 +2299,7 @@ export const resolveDownloadMethods = {
|
||||
|
||||
// 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)
|
||||
if (missing.is_urn && !requestedSearchQuery) {
|
||||
if (missing.is_urn && !requestedSearchQuery && !searchSha256) {
|
||||
const urnSearchName = missing.civitai_info?.expected_filename
|
||||
|| missing.download_source?.filename
|
||||
|| missing.civitai_info?.model_name;
|
||||
@@ -2324,7 +2326,7 @@ export const resolveDownloadMethods = {
|
||||
}
|
||||
}
|
||||
|
||||
const isUrn = Boolean(missing.is_urn && !requestedSearchQuery);
|
||||
const isUrn = Boolean(missing.is_urn && !requestedSearchQuery && !searchSha256);
|
||||
const resultsId = `search-results-${missing.node_id}-${missing.widget_index}`;
|
||||
const canUpdateCurrentWorkflow = workflowKey === this.getWorkflowScopedQueueKey();
|
||||
const resultsDiv = canUpdateCurrentWorkflow
|
||||
@@ -2404,6 +2406,8 @@ export const resolveDownloadMethods = {
|
||||
const tokens = this.getStoredTokens();
|
||||
const baseSearchData = {
|
||||
filename,
|
||||
sha256: searchSha256,
|
||||
search_mode: searchSha256 ? 'sha256' : 'name',
|
||||
category,
|
||||
is_urn: isUrn,
|
||||
base_model_context: baseModelContext,
|
||||
|
||||
@@ -412,7 +412,7 @@ export const searchPanelMethods = {
|
||||
? this.getSearchButtonLabelLines(buttonText).join(' ')
|
||||
: String(buttonText || 'Add');
|
||||
const icon = mode === 'name' ? getSvgIcon('search') : getSvgIcon('link');
|
||||
return `<span class="mr-link-name-action-text">${this.escapeHtml(label)}</span><span class="mr-link-name-action-icon" aria-hidden="true">${icon}</span>`;
|
||||
return `<span class="mr-btn-icon mr-link-name-action-icon" aria-hidden="true">${icon}</span><span class="mr-link-name-action-text">${this.escapeHtml(label)}</span>`;
|
||||
},
|
||||
|
||||
renderLinkNameSwitchIcon(mode = 'link') {
|
||||
@@ -454,11 +454,11 @@ export const searchPanelMethods = {
|
||||
const actionHeading = control.querySelector('.mr-link-name-action-heading');
|
||||
if (actionHeading) actionHeading.textContent = mode === 'name' ? 'Search' : 'Link';
|
||||
if (input) {
|
||||
input.placeholder = mode === 'name' ? 'Search by model name...' : 'https://...';
|
||||
input.placeholder = mode === 'name' ? 'Model name or SHA-256...' : 'https://...';
|
||||
if (mode === 'name' && document.activeElement !== input) {
|
||||
input.value = String(currentState.manualSearchQuery || '');
|
||||
}
|
||||
input.setAttribute('aria-label', mode === 'name' ? 'Model name' : 'Model URL');
|
||||
input.setAttribute('aria-label', mode === 'name' ? 'Model name or SHA-256 hash' : 'Model URL');
|
||||
}
|
||||
if (actionButton) {
|
||||
const buttonText = mode === 'link'
|
||||
|
||||
Reference in New Issue
Block a user