fix: isolate manual/HuggingFace metadata from stale CivitAI provenance
This commit is contained in:
+46
-29
@@ -570,26 +570,39 @@ def build_lora_manager_metadata(
|
||||
source.get("version_description"),
|
||||
)
|
||||
|
||||
civitai_payload = dict(_as_dict(source.get("civitai")))
|
||||
if model_id and "modelId" not in civitai_payload:
|
||||
civitai_payload["modelId"] = _coerce_int_or_value(model_id)
|
||||
if version_id and "id" not in civitai_payload:
|
||||
civitai_payload["id"] = _coerce_int_or_value(version_id)
|
||||
if version_name and "name" not in civitai_payload:
|
||||
civitai_payload["name"] = str(version_name)
|
||||
if base_model and "baseModel" not in civitai_payload:
|
||||
civitai_payload["baseModel"] = str(base_model)
|
||||
if trained_words and "trainedWords" not in civitai_payload:
|
||||
civitai_payload["trainedWords"] = trained_words
|
||||
if images and "images" not in civitai_payload:
|
||||
civitai_payload["images"] = images
|
||||
if direct_url and "downloadUrl" not in civitai_payload:
|
||||
civitai_payload["downloadUrl"] = _strip_sensitive_url_params(str(direct_url))
|
||||
if version_description and "description" not in civitai_payload:
|
||||
civitai_payload["description"] = str(version_description)
|
||||
is_civitai_source = source_name in {
|
||||
"civitai",
|
||||
"civarchive",
|
||||
"lora_manager_archive",
|
||||
} or (
|
||||
not source_name
|
||||
and bool(source.get("civitai") or source.get("civitai_details"))
|
||||
)
|
||||
civitai_payload = (
|
||||
dict(_as_dict(source.get("civitai"))) if is_civitai_source else {}
|
||||
)
|
||||
if is_civitai_source:
|
||||
if model_id and "modelId" not in civitai_payload:
|
||||
civitai_payload["modelId"] = _coerce_int_or_value(model_id)
|
||||
if version_id and "id" not in civitai_payload:
|
||||
civitai_payload["id"] = _coerce_int_or_value(version_id)
|
||||
if version_name and "name" not in civitai_payload:
|
||||
civitai_payload["name"] = str(version_name)
|
||||
if base_model and "baseModel" not in civitai_payload:
|
||||
civitai_payload["baseModel"] = str(base_model)
|
||||
if trained_words and "trainedWords" not in civitai_payload:
|
||||
civitai_payload["trainedWords"] = trained_words
|
||||
if images and "images" not in civitai_payload:
|
||||
civitai_payload["images"] = images
|
||||
if direct_url and "downloadUrl" not in civitai_payload:
|
||||
civitai_payload["downloadUrl"] = _strip_sensitive_url_params(
|
||||
str(direct_url)
|
||||
)
|
||||
if version_description and "description" not in civitai_payload:
|
||||
civitai_payload["description"] = str(version_description)
|
||||
|
||||
files = _as_list(_first_present(selected_version.get("files"), source.get("files")))
|
||||
if files and "files" not in civitai_payload:
|
||||
if is_civitai_source and files and "files" not in civitai_payload:
|
||||
civitai_payload["files"] = files
|
||||
|
||||
model_payload = dict(_as_dict(civitai_payload.get("model")))
|
||||
@@ -602,9 +615,9 @@ def build_lora_manager_metadata(
|
||||
model_payload["description"] = str(model_description)
|
||||
if tags and "tags" not in model_payload:
|
||||
model_payload["tags"] = tags
|
||||
if model_payload:
|
||||
if is_civitai_source and model_payload:
|
||||
civitai_payload["model"] = model_payload
|
||||
if creator and "creator" not in civitai_payload:
|
||||
if is_civitai_source and creator and "creator" not in civitai_payload:
|
||||
civitai_payload["creator"] = creator
|
||||
|
||||
metadata_source = _metadata_source_value(source_name, source.get("metadata_source"))
|
||||
@@ -624,7 +637,10 @@ def build_lora_manager_metadata(
|
||||
"preview_url": str(preview_url or ""),
|
||||
"preview_nsfw_level": 0,
|
||||
"notes": "",
|
||||
"from_civitai": bool(metadata_source or model_id or version_id),
|
||||
"from_civitai": bool(
|
||||
is_civitai_source
|
||||
and (metadata_source or model_id or version_id or civitai_payload)
|
||||
),
|
||||
"civitai": civitai_payload,
|
||||
"tags": tags,
|
||||
"modelDescription": str(model_description or ""),
|
||||
@@ -2298,14 +2314,15 @@ def download_model(
|
||||
|
||||
if existing_sha256 == expected_sha256:
|
||||
message = "This model is already downloaded and matches the source hash."
|
||||
metadata_path = get_metadata_sidecar_path(dest_path)
|
||||
if not os.path.exists(metadata_path):
|
||||
metadata_path = write_lora_manager_metadata(
|
||||
dest_path,
|
||||
metadata or {},
|
||||
category,
|
||||
url,
|
||||
) or ""
|
||||
# Refresh the sidecar even when it already exists. The selected
|
||||
# source may be more authoritative than metadata left by an
|
||||
# earlier fuzzy search or manual download attempt.
|
||||
metadata_path = write_lora_manager_metadata(
|
||||
dest_path,
|
||||
metadata or {},
|
||||
category,
|
||||
url,
|
||||
) or ""
|
||||
size = os.path.getsize(dest_path)
|
||||
with download_lock:
|
||||
if download_id in download_progress:
|
||||
|
||||
+42
-16
@@ -26,6 +26,7 @@ from ..type_utils import (
|
||||
extract_file_size,
|
||||
fetch_remote_file_size_cached,
|
||||
looks_like_model_file,
|
||||
normalize_sha256,
|
||||
prepare_remote_size_probe_url,
|
||||
)
|
||||
from .common import build_unified_search_result
|
||||
@@ -124,8 +125,8 @@ def _author_index_cache_key(author: str, headers: Dict[str, str]) -> str:
|
||||
return f"{author}::{auth_key}"
|
||||
|
||||
|
||||
def _is_author_index_fresh(index: Optional[Dict[str, Any]]) -> bool:
|
||||
if not index:
|
||||
def _is_author_index_fresh(index: Any) -> bool:
|
||||
if not isinstance(index, dict) or not index:
|
||||
return False
|
||||
|
||||
updated_at = index.get("updated_at")
|
||||
@@ -142,6 +143,15 @@ def _read_persistent_author_indexes() -> Dict[str, Any]:
|
||||
return {"version": HF_AUTHOR_INDEX_CACHE_VERSION, "authors": {}}
|
||||
if not isinstance(data.get("authors"), dict):
|
||||
data["authors"] = {}
|
||||
else:
|
||||
# A short-lived regression stored the raw HuggingFace response list in
|
||||
# place of the built author index. Ignore malformed entries so they are
|
||||
# fetched and rebuilt normally.
|
||||
data["authors"] = {
|
||||
author: index
|
||||
for author, index in data["authors"].items()
|
||||
if isinstance(index, dict)
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
@@ -206,8 +216,6 @@ def _fetch_author_index(author: str, headers: Dict[str, str], limit: int = 200):
|
||||
if not isinstance(repos, list):
|
||||
return None
|
||||
|
||||
return repos
|
||||
|
||||
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']}"
|
||||
@@ -356,13 +364,7 @@ def _build_huggingface_result(
|
||||
match_type: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
lfs_info = file_info.get("lfs") if isinstance(file_info.get("lfs"), dict) else {}
|
||||
sha256 = (
|
||||
file_info.get("sha256")
|
||||
or file_info.get("hash")
|
||||
or lfs_info.get("sha256")
|
||||
or lfs_info.get("oid")
|
||||
)
|
||||
sha256 = _extract_huggingface_file_sha256(file_info)
|
||||
download_url = get_huggingface_download_url(repo_id, file_path)
|
||||
size = extract_file_size(file_info)
|
||||
if not size:
|
||||
@@ -382,6 +384,16 @@ def _build_huggingface_result(
|
||||
)
|
||||
|
||||
|
||||
def _extract_huggingface_file_sha256(file_info: Dict[str, Any]) -> str:
|
||||
lfs_info = file_info.get("lfs") if isinstance(file_info.get("lfs"), dict) else {}
|
||||
return normalize_sha256(
|
||||
file_info.get("sha256")
|
||||
or file_info.get("hash")
|
||||
or lfs_info.get("sha256")
|
||||
or lfs_info.get("oid")
|
||||
)
|
||||
|
||||
|
||||
def _get_repo_tree(
|
||||
repo_id: str, headers: Dict[str, str], branch: str = "main"
|
||||
) -> Optional[List[Dict[str, Any]]]:
|
||||
@@ -1025,11 +1037,23 @@ def build_huggingface_custom_result(
|
||||
headers = {}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
size = fetch_remote_file_size_cached(
|
||||
download_url,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
file_info: Dict[str, Any] = {}
|
||||
repo_tree = _get_repo_tree(repo_id, headers, branch)
|
||||
normalized_file_path = file_path.replace("\\", "/").strip("/")
|
||||
for candidate in repo_tree or []:
|
||||
candidate_path = str(candidate.get("path") or "").replace("\\", "/").strip("/")
|
||||
if candidate_path == normalized_file_path:
|
||||
file_info = candidate
|
||||
break
|
||||
|
||||
size = extract_file_size(file_info)
|
||||
if not size:
|
||||
size = fetch_remote_file_size_cached(
|
||||
download_url,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
sha256 = _extract_huggingface_file_sha256(file_info)
|
||||
|
||||
def quote_url_path(val):
|
||||
return quote(str(val or "").replace("\\", "/"), safe="/")
|
||||
@@ -1046,6 +1070,8 @@ def build_huggingface_custom_result(
|
||||
url=download_url,
|
||||
download_url=download_url,
|
||||
size=size,
|
||||
sha256=sha256,
|
||||
hashes={"SHA256": sha256} if sha256 else {},
|
||||
match_type="custom_url",
|
||||
details_source="huggingface",
|
||||
repo_id=repo_id,
|
||||
|
||||
@@ -7,6 +7,7 @@ from unittest.mock import patch
|
||||
|
||||
from core import downloader
|
||||
from core.downloader import (
|
||||
build_lora_manager_metadata,
|
||||
download_model,
|
||||
get_metadata_sidecar_path,
|
||||
get_progress,
|
||||
@@ -15,6 +16,34 @@ from core.downloader import (
|
||||
|
||||
|
||||
class DownloaderMetadataSidecarTests(unittest.TestCase):
|
||||
def test_huggingface_metadata_does_not_create_civitai_identity(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
model_path = os.path.join(tmpdir, "model.safetensors")
|
||||
with open(model_path, "wb") as handle:
|
||||
handle.write(b"abc")
|
||||
|
||||
payload = build_lora_manager_metadata(
|
||||
model_path,
|
||||
{
|
||||
"details_source": "huggingface",
|
||||
"source": "huggingface",
|
||||
"model_id": "DreamFast/Qwen3-VL-4b-Heretic-ComfyUI",
|
||||
"filename": "model.safetensors",
|
||||
"sha256": "a" * 64,
|
||||
"download_url": (
|
||||
"https://huggingface.co/DreamFast/repo/resolve/main/"
|
||||
"model.safetensors"
|
||||
),
|
||||
},
|
||||
category="text_encoders",
|
||||
)
|
||||
|
||||
self.assertEqual("huggingface", payload["source"])
|
||||
self.assertEqual("a" * 64, payload["sha256"])
|
||||
self.assertFalse(payload["from_civitai"])
|
||||
self.assertEqual({}, payload["civitai"])
|
||||
self.assertIsNone(payload["metadata_source"])
|
||||
|
||||
def test_writes_lora_manager_sidecar_and_sanitizes_secrets(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
model_path = os.path.join(tmpdir, "example.safetensors")
|
||||
@@ -105,6 +134,53 @@ class DownloaderMetadataSidecarTests(unittest.TestCase):
|
||||
self.assertTrue(progress["already_exists"])
|
||||
self.assertIn("already downloaded", progress["message"])
|
||||
|
||||
def test_existing_matching_file_replaces_stale_sidecar_provenance(self):
|
||||
content = b"existing HuggingFace model"
|
||||
expected_sha256 = hashlib.sha256(content).hexdigest()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
model_path = os.path.join(tmpdir, "existing.safetensors")
|
||||
with open(model_path, "wb") as handle:
|
||||
handle.write(content)
|
||||
metadata_path = get_metadata_sidecar_path(model_path)
|
||||
with open(metadata_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(
|
||||
{
|
||||
"source": "civarchive",
|
||||
"details_source": "civarchive",
|
||||
"sha256": "0" * 64,
|
||||
"hash_status": "completed",
|
||||
"from_civitai": True,
|
||||
"civitai": {"modelId": 123},
|
||||
},
|
||||
handle,
|
||||
)
|
||||
|
||||
with patch("core.downloader.get_download_directory", return_value=tmpdir):
|
||||
result = download_model(
|
||||
"https://huggingface.co/DreamFast/repo/resolve/main/"
|
||||
"existing.safetensors",
|
||||
"existing.safetensors",
|
||||
"text_encoders",
|
||||
download_id="refreshstalesidecar",
|
||||
metadata={
|
||||
"source": "huggingface",
|
||||
"details_source": "huggingface",
|
||||
"model_id": "DreamFast/repo",
|
||||
"filename": "existing.safetensors",
|
||||
"sha256": expected_sha256,
|
||||
},
|
||||
)
|
||||
|
||||
with open(metadata_path, "r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
|
||||
self.assertTrue(result["success"])
|
||||
self.assertTrue(result["already_exists"])
|
||||
self.assertEqual("huggingface", payload["source"])
|
||||
self.assertEqual(expected_sha256, payload["sha256"])
|
||||
self.assertFalse(payload["from_civitai"])
|
||||
self.assertEqual({}, payload["civitai"])
|
||||
|
||||
def test_existing_file_uses_completed_metadata_hash_before_hashing_file(self):
|
||||
expected_sha256 = "a" * 64
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
|
||||
@@ -567,6 +567,128 @@ test('search suggestion metadata prefers exact matching base model over weaker a
|
||||
assert.equal(merged.filename, 'snofs_krea_v1.safetensors');
|
||||
});
|
||||
|
||||
test('manual URL download metadata never inherits provider identity or hash from search results', () => {
|
||||
const getDownloadMetadata = eval(`(${extractMethod(downloadTargetMethodsSource, 'getDownloadMetadata')})`);
|
||||
const staleHash = 'a'.repeat(64);
|
||||
const exactHash = 'b'.repeat(64);
|
||||
const missing = {
|
||||
original_path: 'QWEN\\qwen3-vl-4b-heretic_int8.safetensors',
|
||||
category: 'text_encoders',
|
||||
civitai_info: {
|
||||
source: 'civarchive',
|
||||
model_id: 100,
|
||||
version_id: 200,
|
||||
sha256: staleHash,
|
||||
hashes: { SHA256: staleHash },
|
||||
civitai: { modelId: 100, id: 200 }
|
||||
},
|
||||
download_source: {
|
||||
source: 'civarchive',
|
||||
model_id: 100,
|
||||
version_id: 200,
|
||||
sha256: staleHash
|
||||
}
|
||||
};
|
||||
const source = {
|
||||
source: 'huggingface',
|
||||
details_source: 'huggingface',
|
||||
model_id: 'DreamFast/Qwen3-VL-4b-Heretic-ComfyUI',
|
||||
filename: 'qwen3-vl-4b-heretic_int8.safetensors',
|
||||
download_url: 'https://huggingface.co/DreamFast/repo/resolve/main/model.safetensors',
|
||||
version_url: 'https://huggingface.co/DreamFast/repo/blob/main/model.safetensors',
|
||||
provided_url: 'https://huggingface.co/DreamFast/repo/blob/main/model.safetensors',
|
||||
custom_url: true,
|
||||
url_source: 'custom',
|
||||
match_type: 'custom_url',
|
||||
sha256: exactHash,
|
||||
hashes: { SHA256: exactHash }
|
||||
};
|
||||
const dialog = {
|
||||
getCachedSearchSuggestionData() {
|
||||
throw new Error('manual URL must not read cached search metadata');
|
||||
},
|
||||
getCompatibleCivitaiSearchResult() {
|
||||
throw new Error('manual URL must not read cached CivitAI metadata');
|
||||
},
|
||||
getFilenameFromPath(value = '') {
|
||||
return String(value).split(/[\\/]/).at(-1) || '';
|
||||
}
|
||||
};
|
||||
|
||||
const metadata = getDownloadMetadata.call(dialog, missing, source, {
|
||||
filename: source.filename,
|
||||
category: 'text_encoders',
|
||||
url: source.download_url,
|
||||
openUrl: source.version_url,
|
||||
pathMetadata: {
|
||||
filename: source.filename,
|
||||
category: 'text_encoders'
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(metadata.source, 'huggingface');
|
||||
assert.equal(metadata.model_id, 'DreamFast/Qwen3-VL-4b-Heretic-ComfyUI');
|
||||
assert.equal(metadata.version_id, '');
|
||||
assert.equal(metadata.sha256, exactHash);
|
||||
assert.deepEqual(metadata.hashes, { SHA256: exactHash });
|
||||
assert.equal(metadata.custom_url, true);
|
||||
assert.equal(metadata.url_source, 'custom');
|
||||
assert.equal(metadata.provided_url, source.provided_url);
|
||||
assert.equal('civitai' in metadata, false);
|
||||
});
|
||||
|
||||
test('manual URL table row preserves the explicit provenance boundary', () => {
|
||||
const getCustomUrlResultTableRow = eval(`(${extractMethod(searchPanelMethodsSource, 'getCustomUrlResultTableRow')})`);
|
||||
const result = {
|
||||
source: 'huggingface',
|
||||
model_id: 'DreamFast/Qwen3-VL-4b-Heretic-ComfyUI',
|
||||
filename: 'qwen3-vl-4b-heretic_int8.safetensors',
|
||||
download_url: 'https://huggingface.co/DreamFast/repo/resolve/main/model.safetensors',
|
||||
page_url: 'https://huggingface.co/DreamFast/repo/blob/main/model.safetensors',
|
||||
provided_url: 'https://huggingface.co/DreamFast/repo/blob/main/model.safetensors',
|
||||
custom_url: true
|
||||
};
|
||||
const dialog = {
|
||||
getFilenameFromPath(value = '') {
|
||||
return String(value).split(/[\\/]/).at(-1) || '';
|
||||
},
|
||||
getMissingDownloadCategory() {
|
||||
return 'text_encoders';
|
||||
},
|
||||
getSourceResultDownloadCategory() {
|
||||
return 'text_encoders';
|
||||
},
|
||||
getMissingModelKey() {
|
||||
return 'missing-key';
|
||||
},
|
||||
getLocalHashMatchIdentitiesForResult() {
|
||||
return [];
|
||||
},
|
||||
getHashMatchLabelForSearchResult() {
|
||||
return '';
|
||||
},
|
||||
getSearchResultMatchDisplay() {
|
||||
return { label: 'Provided', className: 'strong' };
|
||||
},
|
||||
formatSearchResultSize() {
|
||||
return '';
|
||||
},
|
||||
getSearchResultTimestamp() {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const row = getCustomUrlResultTableRow.call(dialog, {
|
||||
original_path: result.filename,
|
||||
category: 'text_encoders'
|
||||
}, result);
|
||||
|
||||
assert.equal(row.detailsContext.custom_url, true);
|
||||
assert.equal(row.detailsContext.url_source, 'custom');
|
||||
assert.equal(row.detailsContext.provided_url, result.provided_url);
|
||||
assert.equal(row.detailsContext.source, 'huggingface');
|
||||
});
|
||||
|
||||
test('search suggestion does not let a delayed incompatible result replace workflow path context', () => {
|
||||
const getCachedSearchSuggestionData = eval(`(${extractMethod(downloadTargetMethodsSource, 'getCachedSearchSuggestionData')})`);
|
||||
const getFirstSearchResult = eval(`(${extractMethod(downloadTargetMethodsSource, 'getFirstSearchResult')})`);
|
||||
|
||||
@@ -5,6 +5,10 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from core.sources.huggingface import (
|
||||
_fetch_author_index,
|
||||
_is_author_index_fresh,
|
||||
_read_persistent_author_indexes,
|
||||
build_huggingface_custom_result,
|
||||
parse_huggingface_url,
|
||||
get_huggingface_download_url,
|
||||
_normalize_huggingface_size_probe_url,
|
||||
@@ -15,6 +19,66 @@ from core.type_utils import extract_file_size
|
||||
|
||||
class HuggingFaceSourceTests(unittest.TestCase):
|
||||
|
||||
def test_fetch_author_index_builds_index_from_provider_repo_list(self):
|
||||
with patch(
|
||||
"core.sources.huggingface.execute_provider_json_request",
|
||||
return_value=[
|
||||
{
|
||||
"id": "Comfy-Org/example",
|
||||
"siblings": [
|
||||
{
|
||||
"rfilename": "text_encoders/model.safetensors",
|
||||
"size": 123,
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
):
|
||||
index = _fetch_author_index("Comfy-Org", headers={})
|
||||
|
||||
self.assertIsInstance(index, dict)
|
||||
self.assertEqual("Comfy-Org", index["author"])
|
||||
self.assertEqual(1, index["repo_count"])
|
||||
self.assertEqual(1, index["file_count"])
|
||||
self.assertEqual(
|
||||
"text_encoders/model.safetensors",
|
||||
index["files"][0]["path"],
|
||||
)
|
||||
|
||||
def test_malformed_list_author_index_is_not_treated_as_fresh(self):
|
||||
self.assertFalse(_is_author_index_fresh([{"id": "Comfy-Org/example"}]))
|
||||
|
||||
def test_persistent_cache_discards_raw_provider_list_entries(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
index_path = Path(tmpdir) / "huggingface-author-index.json"
|
||||
index_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"authors": {
|
||||
"Comfy-Org": [{"id": "Comfy-Org/example"}],
|
||||
"Valid": {
|
||||
"author": "Valid",
|
||||
"updated_at": 1,
|
||||
"repo_count": 0,
|
||||
"file_count": 0,
|
||||
"repos": [],
|
||||
"files": [],
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with patch(
|
||||
"core.sources.huggingface.HF_AUTHOR_INDEX_CACHE_PATH",
|
||||
str(index_path),
|
||||
):
|
||||
data = _read_persistent_author_indexes()
|
||||
|
||||
self.assertNotIn("Comfy-Org", data["authors"])
|
||||
self.assertIn("Valid", data["authors"])
|
||||
|
||||
def test_persistent_author_index_is_pretty_printed(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
index_path = Path(tmpdir) / "huggingface-author-index.json"
|
||||
@@ -85,6 +149,45 @@ class HuggingFaceSourceTests(unittest.TestCase):
|
||||
normalized = _normalize_huggingface_size_probe_url(url)
|
||||
self.assertEqual(normalized, "https://huggingface.co/user/repo/resolve/main/model.safetensors")
|
||||
|
||||
def test_custom_result_uses_sha256_from_exact_huggingface_file(self):
|
||||
sha256 = "a" * 64
|
||||
url = (
|
||||
"https://huggingface.co/DreamFast/Qwen3-VL-4b-Heretic-ComfyUI/"
|
||||
"blob/main/qwen3-vl-4b-heretic_int8.safetensors"
|
||||
)
|
||||
with patch(
|
||||
"core.sources.huggingface._get_repo_tree",
|
||||
return_value=[
|
||||
{
|
||||
"path": "qwen3-vl-4b-heretic_int8.safetensors",
|
||||
"size": 123,
|
||||
"lfs": {"oid": f"sha256:{sha256}", "size": 123},
|
||||
}
|
||||
],
|
||||
):
|
||||
result = build_huggingface_custom_result(url)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(sha256, result["sha256"])
|
||||
self.assertEqual({"SHA256": sha256}, result["hashes"])
|
||||
self.assertEqual(123, result["size"])
|
||||
self.assertEqual("huggingface", result["source"])
|
||||
self.assertTrue(result["custom_url"])
|
||||
|
||||
def test_custom_result_leaves_hash_empty_when_huggingface_does_not_provide_it(self):
|
||||
url = "https://huggingface.co/user/repo/blob/main/model.safetensors"
|
||||
with patch("core.sources.huggingface._get_repo_tree", return_value=None):
|
||||
with patch(
|
||||
"core.sources.huggingface.fetch_remote_file_size_cached",
|
||||
return_value=456,
|
||||
):
|
||||
result = build_huggingface_custom_result(url)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual("", result["sha256"])
|
||||
self.assertEqual({}, result["hashes"])
|
||||
self.assertEqual(456, result["size"])
|
||||
|
||||
def test_build_huggingface_search_queries(self):
|
||||
queries = build_filename_search_queries("some_model_bf16.safetensors")
|
||||
self.assertIn("some_model", queries)
|
||||
|
||||
@@ -1341,12 +1341,25 @@ export const downloadTargetMethods = {
|
||||
|
||||
getDownloadMetadata(missing = {}, source = {}, options = {}) {
|
||||
const sourceData = source && typeof source === 'object' ? source : {};
|
||||
const searchSuggestion = this.getCachedSearchSuggestionData(missing);
|
||||
const compatibleCivitaiSearch = this.getCompatibleCivitaiSearchResult?.(missing) || {};
|
||||
const isProvidedUrl = Boolean(
|
||||
sourceData.custom_url
|
||||
|| sourceData.url_source === 'custom'
|
||||
|| sourceData.match_type === 'custom_url'
|
||||
|| sourceData.provided_url
|
||||
);
|
||||
// A URL explicitly selected by the user is an authoritative provenance
|
||||
// boundary. Never fill its hash, provider IDs, or file details from an
|
||||
// earlier fuzzy search result belonging to another provider.
|
||||
const searchSuggestion = isProvidedUrl ? null : this.getCachedSearchSuggestionData(missing);
|
||||
const compatibleCivitaiSearch = isProvidedUrl
|
||||
? {}
|
||||
: (this.getCompatibleCivitaiSearchResult?.(missing) || {});
|
||||
const inheritedDownloadSource = isProvidedUrl ? {} : (missing?.download_source || {});
|
||||
const inheritedCivitaiInfo = isProvidedUrl ? {} : (missing?.civitai_info || {});
|
||||
const merged = {
|
||||
...(missing?.civitai_info || {}),
|
||||
...inheritedCivitaiInfo,
|
||||
...compatibleCivitaiSearch,
|
||||
...(missing?.download_source || {}),
|
||||
...inheritedDownloadSource,
|
||||
...(searchSuggestion || {}),
|
||||
...sourceData
|
||||
};
|
||||
@@ -1359,7 +1372,7 @@ export const downloadTargetMethods = {
|
||||
|| sourceData.source
|
||||
|| sourceData.sourceKey
|
||||
);
|
||||
const idSource = sourceHasIdentity ? sourceData : (missing?.download_source || {});
|
||||
const idSource = sourceHasIdentity ? sourceData : inheritedDownloadSource;
|
||||
const selectedVersion = merged.selected_version || merged.selectedVersion || null;
|
||||
const pathMetadata = options.pathMetadata || this.getDownloadPathMetadata(missing, sourceData);
|
||||
const toList = (value) => {
|
||||
@@ -1437,6 +1450,15 @@ export const downloadTargetMethods = {
|
||||
if (merged.metadata_source) metadata.metadata_source = merged.metadata_source;
|
||||
if (merged.is_deleted !== undefined) metadata.is_deleted = Boolean(merged.is_deleted);
|
||||
if (merged.civitai_deleted !== undefined) metadata.civitai_deleted = Boolean(merged.civitai_deleted);
|
||||
if (isProvidedUrl) {
|
||||
metadata.custom_url = true;
|
||||
metadata.url_source = 'custom';
|
||||
metadata.provided_url = sourceData.provided_url
|
||||
|| sourceData.version_url
|
||||
|| sourceData.page_url
|
||||
|| sourceData.url
|
||||
|| '';
|
||||
}
|
||||
|
||||
return metadata;
|
||||
},
|
||||
|
||||
@@ -2086,19 +2086,19 @@ export const searchPanelMethods = {
|
||||
result.type || '',
|
||||
result.base_model || ''
|
||||
].filter(Boolean).join(' / ');
|
||||
const detailsContext = ['civitai', 'civarchive'].includes(source) && (result.model_id || result.modelId)
|
||||
? {
|
||||
...result,
|
||||
source,
|
||||
details_source: source,
|
||||
model_id: result.model_id || result.modelId,
|
||||
version_id: result.version_id || result.versionId,
|
||||
name: modelName,
|
||||
filename,
|
||||
missing_key: this.getMissingModelKey(missing),
|
||||
category
|
||||
}
|
||||
: null;
|
||||
const detailsContext = {
|
||||
...result,
|
||||
source,
|
||||
details_source: source,
|
||||
custom_url: true,
|
||||
url_source: 'custom',
|
||||
model_id: result.model_id || result.modelId,
|
||||
version_id: result.version_id || result.versionId,
|
||||
name: modelName,
|
||||
filename,
|
||||
missing_key: this.getMissingModelKey(missing),
|
||||
category
|
||||
};
|
||||
const localHashMatchIdentities = this.getLocalHashMatchIdentitiesForResult?.(
|
||||
localHashMatches,
|
||||
source,
|
||||
|
||||
Reference in New Issue
Block a user