feat: add HuggingFace model details, variant table, and download context
This commit is contained in:
+38
-20
@@ -496,6 +496,7 @@ class ModelResolverExtension:
|
||||
check_huggingface_token,
|
||||
get_author_fallback_index_status,
|
||||
get_huggingface_download_url,
|
||||
get_huggingface_model_details,
|
||||
parse_huggingface_url,
|
||||
refresh_author_fallback_index,
|
||||
search_huggingface_for_file,
|
||||
@@ -3332,37 +3333,46 @@ class ModelResolverExtension:
|
||||
model_id = data.get("model_id")
|
||||
version_id = data.get("version_id")
|
||||
civitai_key = data.get("civitai_key", "")
|
||||
hf_token = data.get("hf_token", "")
|
||||
file_path = data.get("file_path", "")
|
||||
branch = data.get("branch", "")
|
||||
|
||||
if not download_available:
|
||||
return web.json_response(
|
||||
{"error": "Download providers are not available"}, status=503
|
||||
)
|
||||
|
||||
try:
|
||||
model_id = (
|
||||
int(model_id)
|
||||
if model_id is not None and str(model_id).strip()
|
||||
else None
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
model_id = None
|
||||
|
||||
try:
|
||||
version_id = (
|
||||
int(version_id)
|
||||
if version_id is not None and str(version_id).strip()
|
||||
else None
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
version_id = None
|
||||
|
||||
if source == "lora_manager_archive":
|
||||
source = "civitai"
|
||||
|
||||
if source not in {"civitai", "civarchive"}:
|
||||
if source not in {"civitai", "civarchive", "huggingface"}:
|
||||
return web.json_response(
|
||||
{"error": "Unsupported model details source"}, status=400
|
||||
)
|
||||
|
||||
if source == "huggingface":
|
||||
model_id = str(model_id or "").strip()
|
||||
branch = str(branch or version_id or "main").strip() or "main"
|
||||
version_id = branch
|
||||
else:
|
||||
try:
|
||||
model_id = (
|
||||
int(model_id)
|
||||
if model_id is not None and str(model_id).strip()
|
||||
else None
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
model_id = None
|
||||
|
||||
try:
|
||||
version_id = (
|
||||
int(version_id)
|
||||
if version_id is not None and str(version_id).strip()
|
||||
else None
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
version_id = None
|
||||
|
||||
if not model_id:
|
||||
return web.json_response(
|
||||
{"error": "model_id is required"}, status=400
|
||||
@@ -3375,12 +3385,20 @@ class ModelResolverExtension:
|
||||
version_id,
|
||||
civitai_key or None,
|
||||
)
|
||||
else:
|
||||
elif source == "civarchive":
|
||||
details = await asyncio.to_thread(
|
||||
get_civarchive_model_details,
|
||||
model_id,
|
||||
version_id,
|
||||
)
|
||||
else:
|
||||
details = await asyncio.to_thread(
|
||||
get_huggingface_model_details,
|
||||
model_id,
|
||||
file_path,
|
||||
branch,
|
||||
hf_token or None,
|
||||
)
|
||||
|
||||
if not details:
|
||||
return web.json_response(
|
||||
|
||||
+211
-1
@@ -5,6 +5,7 @@ Search and download models from HuggingFace Hub.
|
||||
"""
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
@@ -331,7 +332,214 @@ def parse_huggingface_url(url: str) -> Optional[Dict[str, str]]:
|
||||
|
||||
def get_huggingface_download_url(repo: str, filename: str, branch: str = "main") -> str:
|
||||
"""Generate a direct download URL for a HuggingFace file."""
|
||||
return f"https://huggingface.co/{repo}/resolve/{branch}/{quote(filename)}"
|
||||
return (
|
||||
f"https://huggingface.co/{repo}/resolve/"
|
||||
f"{quote(branch, safe='')}/{quote(filename)}"
|
||||
)
|
||||
|
||||
|
||||
def _normalize_huggingface_path(value: Any) -> str:
|
||||
return str(value or "").replace("\\", "/").strip("/")
|
||||
|
||||
|
||||
def _get_huggingface_variant_label(filename: str) -> str:
|
||||
"""Return a concise precision/quantization label inferred from a filename."""
|
||||
stem = os.path.splitext(get_filename_from_path(filename))[0].lower()
|
||||
patterns = (
|
||||
("nvfp4_mixed", "NVFP4 mixed"),
|
||||
("fp8_scaled", "FP8 scaled"),
|
||||
("int8_convrot", "INT8 convrot"),
|
||||
("nvfp4", "NVFP4"),
|
||||
("fp8", "FP8"),
|
||||
("bf16", "BF16"),
|
||||
("fp16", "FP16"),
|
||||
("int8", "INT8"),
|
||||
("int4", "INT4"),
|
||||
)
|
||||
for token, label in patterns:
|
||||
if re.search(rf"(?:^|[_.-]){re.escape(token)}(?:$|[_.-])", stem):
|
||||
return label
|
||||
return ""
|
||||
|
||||
|
||||
def _normalize_huggingface_details_file(
|
||||
repo_id: str,
|
||||
branch: str,
|
||||
file_info: Dict[str, Any],
|
||||
target_path: str,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
file_path = _normalize_huggingface_path(file_info.get("path"))
|
||||
if not file_path:
|
||||
return None
|
||||
|
||||
download_url = get_huggingface_download_url(repo_id, file_path, branch)
|
||||
if not looks_like_model_file(download_url):
|
||||
return None
|
||||
|
||||
filename = get_filename_from_path(file_path)
|
||||
sha256 = _extract_huggingface_file_sha256(file_info)
|
||||
extension = os.path.splitext(filename)[1].lstrip(".")
|
||||
metadata = {
|
||||
"format": extension.upper() if extension else "",
|
||||
"fp": _get_huggingface_variant_label(filename),
|
||||
}
|
||||
metadata = {key: value for key, value in metadata.items() if value}
|
||||
page_url = (
|
||||
f"https://huggingface.co/{repo_id}/blob/"
|
||||
f"{quote(branch, safe='')}/{quote(file_path, safe='/')}"
|
||||
)
|
||||
|
||||
return {
|
||||
"name": filename,
|
||||
"filename": filename,
|
||||
"path": file_path,
|
||||
"size": extract_file_size(file_info),
|
||||
"sha256": sha256,
|
||||
"hash": sha256,
|
||||
"hashes": {"SHA256": sha256} if sha256 else {},
|
||||
"download_url": download_url,
|
||||
"url": page_url,
|
||||
"primary": bool(target_path and file_path == target_path),
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
|
||||
def get_huggingface_model_details(
|
||||
repo_id: str,
|
||||
file_path: str = "",
|
||||
branch: str = "main",
|
||||
token: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch model-file variants from the folder containing a matched HF file."""
|
||||
normalized_repo_id = str(repo_id or "").strip().strip("/")
|
||||
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", normalized_repo_id):
|
||||
return None
|
||||
|
||||
normalized_branch = str(branch or "main").strip() or "main"
|
||||
if len(normalized_branch) > 200 or any(
|
||||
character in normalized_branch for character in "\r\n?#"
|
||||
):
|
||||
return None
|
||||
|
||||
target_path = _normalize_huggingface_path(file_path)
|
||||
target_directory = posixpath.dirname(target_path)
|
||||
headers = {"Authorization": f"Bearer {token.strip()}"} if token and token.strip() else {}
|
||||
repo_tree = _get_repo_tree(
|
||||
normalized_repo_id,
|
||||
headers=headers,
|
||||
branch=normalized_branch,
|
||||
)
|
||||
if not repo_tree:
|
||||
return None
|
||||
|
||||
normalized_files = []
|
||||
for file_info in repo_tree:
|
||||
candidate_path = _normalize_huggingface_path(file_info.get("path"))
|
||||
if not candidate_path:
|
||||
continue
|
||||
if target_path and posixpath.dirname(candidate_path) != target_directory:
|
||||
continue
|
||||
|
||||
normalized_file = _normalize_huggingface_details_file(
|
||||
normalized_repo_id,
|
||||
normalized_branch,
|
||||
file_info,
|
||||
target_path,
|
||||
)
|
||||
if normalized_file:
|
||||
normalized_files.append(normalized_file)
|
||||
|
||||
if not normalized_files:
|
||||
return None
|
||||
|
||||
normalized_files.sort(
|
||||
key=lambda item: (
|
||||
not item.get("primary", False),
|
||||
str(item.get("name") or "").lower(),
|
||||
)
|
||||
)
|
||||
|
||||
model_data: Dict[str, Any] = {}
|
||||
try:
|
||||
response_data = execute_provider_json_request(
|
||||
"HuggingFace model details",
|
||||
f"{HF_API_URL}/models/{normalized_repo_id}",
|
||||
headers=headers,
|
||||
timeout=15,
|
||||
)
|
||||
if isinstance(response_data, dict):
|
||||
model_data = response_data
|
||||
except Exception as e:
|
||||
log.debug(
|
||||
f"Error getting HuggingFace model metadata for {normalized_repo_id}: {e}"
|
||||
)
|
||||
|
||||
card_data = (
|
||||
model_data.get("cardData")
|
||||
if isinstance(model_data.get("cardData"), dict)
|
||||
else {}
|
||||
)
|
||||
tags = model_data.get("tags") if isinstance(model_data.get("tags"), list) else []
|
||||
repo_url = f"https://huggingface.co/{normalized_repo_id}"
|
||||
tree_url = (
|
||||
f"{repo_url}/tree/{quote(normalized_branch, safe='')}"
|
||||
+ (
|
||||
f"/{quote(target_directory, safe='/')}"
|
||||
if target_directory
|
||||
else ""
|
||||
)
|
||||
)
|
||||
version = {
|
||||
"id": normalized_branch,
|
||||
"name": normalized_branch,
|
||||
"published_at": model_data.get("createdAt") or "",
|
||||
"updated_at": model_data.get("lastModified") or "",
|
||||
"description": "",
|
||||
"trained_words": [],
|
||||
"stats": {
|
||||
"downloads": model_data.get("downloads", 0),
|
||||
"thumbsUpCount": model_data.get("likes", 0),
|
||||
},
|
||||
"files": normalized_files,
|
||||
"images": [],
|
||||
"url": tree_url,
|
||||
}
|
||||
owner = normalized_repo_id.split("/", 1)[0]
|
||||
description = (
|
||||
model_data.get("description")
|
||||
or card_data.get("description")
|
||||
or (
|
||||
f"Select a model file from `{target_directory}` in this Hugging Face "
|
||||
"repository."
|
||||
if target_directory
|
||||
else "Select a model file from this Hugging Face repository."
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"source": "huggingface",
|
||||
"details_source": "huggingface",
|
||||
"model_id": normalized_repo_id,
|
||||
"version_id": normalized_branch,
|
||||
"name": model_data.get("modelId") or normalized_repo_id,
|
||||
"type": model_data.get("pipeline_tag")
|
||||
or model_data.get("library_name")
|
||||
or "Model repository",
|
||||
"description": description,
|
||||
"creator": {"username": model_data.get("author") or owner},
|
||||
"stats": {
|
||||
"downloads": model_data.get("downloads", 0),
|
||||
"thumbsUpCount": model_data.get("likes", 0),
|
||||
},
|
||||
"tags": tags,
|
||||
"url": repo_url,
|
||||
"version_url": tree_url,
|
||||
"folder": target_directory,
|
||||
"branch": normalized_branch,
|
||||
"versions": [version],
|
||||
"selected_version": version,
|
||||
"images": [],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -381,6 +589,7 @@ def _build_huggingface_result(
|
||||
sha256=sha256,
|
||||
repo_id=repo_id,
|
||||
path=file_path,
|
||||
branch="main",
|
||||
)
|
||||
|
||||
|
||||
@@ -1077,6 +1286,7 @@ def build_huggingface_custom_result(
|
||||
repo_id=repo_id,
|
||||
repo=repo_id,
|
||||
path=file_path,
|
||||
branch=branch,
|
||||
page_url=page_url,
|
||||
version_url=page_url,
|
||||
custom_url=True,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ from core.sources.huggingface import (
|
||||
_is_author_index_fresh,
|
||||
_read_persistent_author_indexes,
|
||||
build_huggingface_custom_result,
|
||||
get_huggingface_model_details,
|
||||
parse_huggingface_url,
|
||||
get_huggingface_download_url,
|
||||
_normalize_huggingface_size_probe_url,
|
||||
@@ -192,3 +193,78 @@ class HuggingFaceSourceTests(unittest.TestCase):
|
||||
queries = build_filename_search_queries("some_model_bf16.safetensors")
|
||||
self.assertIn("some_model", queries)
|
||||
self.assertIn("some_model_bf16", queries)
|
||||
|
||||
def test_model_details_lists_only_variants_from_matched_folder(self):
|
||||
sha256 = "b" * 64
|
||||
tree = [
|
||||
{
|
||||
"path": "diffusion_models/model_fp8_scaled.safetensors",
|
||||
"size": 100,
|
||||
"lfs": {"oid": f"sha256:{sha256}", "size": 100},
|
||||
},
|
||||
{
|
||||
"path": "diffusion_models/model_int8_convrot.safetensors",
|
||||
"size": 80,
|
||||
},
|
||||
{
|
||||
"path": "text_encoders/encoder_fp8.safetensors",
|
||||
"size": 50,
|
||||
},
|
||||
{"path": "diffusion_models/README.md", "size": 10},
|
||||
]
|
||||
with patch("core.sources.huggingface._get_repo_tree", return_value=tree):
|
||||
with patch(
|
||||
"core.sources.huggingface.execute_provider_json_request",
|
||||
return_value={
|
||||
"modelId": "Comfy-Org/example",
|
||||
"author": "Comfy-Org",
|
||||
"downloads": 123,
|
||||
"likes": 4,
|
||||
"tags": ["comfyui"],
|
||||
},
|
||||
):
|
||||
details = get_huggingface_model_details(
|
||||
"Comfy-Org/example",
|
||||
"diffusion_models/model_fp8_scaled.safetensors",
|
||||
)
|
||||
|
||||
self.assertIsNotNone(details)
|
||||
self.assertEqual("huggingface", details["source"])
|
||||
self.assertEqual("diffusion_models", details["folder"])
|
||||
files = details["selected_version"]["files"]
|
||||
self.assertEqual(
|
||||
[
|
||||
"model_fp8_scaled.safetensors",
|
||||
"model_int8_convrot.safetensors",
|
||||
],
|
||||
[file_info["name"] for file_info in files],
|
||||
)
|
||||
self.assertTrue(files[0]["primary"])
|
||||
self.assertEqual("FP8 scaled", files[0]["metadata"]["fp"])
|
||||
self.assertEqual(sha256, files[0]["sha256"])
|
||||
self.assertEqual("INT8 convrot", files[1]["metadata"]["fp"])
|
||||
|
||||
def test_model_details_passes_huggingface_token_to_tree_and_metadata(self):
|
||||
tree = [{"path": "model.safetensors", "size": 100}]
|
||||
with patch(
|
||||
"core.sources.huggingface._get_repo_tree",
|
||||
return_value=tree,
|
||||
) as mock_tree:
|
||||
with patch(
|
||||
"core.sources.huggingface.execute_provider_json_request",
|
||||
return_value={},
|
||||
) as mock_request:
|
||||
details = get_huggingface_model_details(
|
||||
"private/repo",
|
||||
"model.safetensors",
|
||||
token="hf_secret",
|
||||
)
|
||||
|
||||
self.assertIsNotNone(details)
|
||||
expected_headers = {"Authorization": "Bearer hf_secret"}
|
||||
mock_tree.assert_called_once_with(
|
||||
"private/repo",
|
||||
headers=expected_headers,
|
||||
branch="main",
|
||||
)
|
||||
self.assertEqual(expected_headers, mock_request.call_args.kwargs["headers"])
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
getModelCardUrl,
|
||||
parseHuggingFaceFileUrl,
|
||||
} from '../web/resolver/utils/url_utils.js';
|
||||
|
||||
const searchPanelSource = await readFile(
|
||||
new URL('../web/resolver/search/search_panel.js', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const modelInfoSource = await readFile(
|
||||
new URL('../web/resolver/views/model_info_methods.js', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const resolveDownloadSource = await readFile(
|
||||
new URL('../web/resolver/actions/resolve_download_methods.js', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const resolverCssSource = await readFile(
|
||||
new URL('../web/css/resolver-main.css', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
test('HuggingFace download rows expose the Show More action', () => {
|
||||
assert.match(
|
||||
searchPanelSource,
|
||||
/\['civitai', 'civarchive', 'huggingface'\]\.includes\(normalizedSource\)/
|
||||
);
|
||||
assert.match(
|
||||
modelInfoSource,
|
||||
/\['civitai', 'civarchive', 'huggingface', 'lora_manager_archive'\]\.includes\(source\)/
|
||||
);
|
||||
assert.match(
|
||||
resolveDownloadSource,
|
||||
/if \(hfResult && hfResult\.url\)[\s\S]*?detailsContext:\s*\{[\s\S]*?\.\.\.hfResult,[\s\S]*?details_source:\s*'huggingface'/
|
||||
);
|
||||
});
|
||||
|
||||
test('HuggingFace details requests keep folder, branch, and token context', () => {
|
||||
assert.match(modelInfoSource, /file_path: model\.path/);
|
||||
assert.match(modelInfoSource, /branch: model\.branch \|\| ''/);
|
||||
assert.match(modelInfoSource, /hf_token: tokens\.hf_token \|\| ''/);
|
||||
});
|
||||
|
||||
test('a selected HuggingFace variant keeps its repository path', () => {
|
||||
assert.match(
|
||||
modelInfoSource,
|
||||
/path: selection\.path \|\| selectionFile\?\.path \|\| ''/
|
||||
);
|
||||
assert.match(
|
||||
modelInfoSource,
|
||||
/branch: selection\.branch \|\| contextModel\.branch \|\| ''/
|
||||
);
|
||||
});
|
||||
|
||||
test('HuggingFace blob URLs expose repository, branch, and containing path', () => {
|
||||
const url = (
|
||||
'https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/blob/main/'
|
||||
+ 'split_files/text_encoders/llava_llama3_fp8_scaled.safetensors'
|
||||
);
|
||||
|
||||
assert.deepEqual(parseHuggingFaceFileUrl(url), {
|
||||
repo: 'Comfy-Org/HunyuanVideo_repackaged',
|
||||
revision: 'main',
|
||||
path: 'split_files/text_encoders/llava_llama3_fp8_scaled.safetensors',
|
||||
filename: 'llava_llama3_fp8_scaled.safetensors',
|
||||
});
|
||||
assert.equal(getModelCardUrl(url), url);
|
||||
});
|
||||
|
||||
test('Local Database adds HuggingFace details only for direct HF file URLs', () => {
|
||||
assert.match(
|
||||
resolveDownloadSource,
|
||||
/const huggingFaceFile = parseHuggingFaceFileUrl\(modelListResult\.url\)/
|
||||
);
|
||||
assert.match(
|
||||
resolveDownloadSource,
|
||||
/const modelListDetailsContext = huggingFaceFile[\s\S]*?details_source:\s*'huggingface'[\s\S]*?model_id:\s*huggingFaceFile\.repo[\s\S]*?branch:\s*huggingFaceFile\.revision[\s\S]*?path:\s*huggingFaceFile\.path/
|
||||
);
|
||||
assert.match(
|
||||
resolveDownloadSource,
|
||||
/detailsContext:\s*modelListDetailsContext/
|
||||
);
|
||||
});
|
||||
|
||||
test('Workflow URL derives HuggingFace details from a direct file URL', () => {
|
||||
assert.match(
|
||||
searchPanelSource,
|
||||
/const huggingFaceFile = normalizedSource === 'huggingface'[\s\S]*?parseHuggingFaceFileUrl\([\s\S]*?downloadSource\.download_url \|\| downloadSource\.url \|\| rawModelUrl[\s\S]*?\)/
|
||||
);
|
||||
assert.match(
|
||||
searchPanelSource,
|
||||
/model_id: downloadSource\.model_id \|\| huggingFaceFile\?\.repo/
|
||||
);
|
||||
assert.match(
|
||||
searchPanelSource,
|
||||
/branch: downloadSource\.branch \|\| huggingFaceFile\?\.revision \|\| ''/
|
||||
);
|
||||
assert.match(
|
||||
searchPanelSource,
|
||||
/path: downloadSource\.path \|\| huggingFaceFile\?\.path \|\| ''/
|
||||
);
|
||||
});
|
||||
|
||||
test('HuggingFace details use a full-width compact variant table', () => {
|
||||
assert.match(
|
||||
modelInfoSource,
|
||||
/mr-model-details-main \$\{isHuggingFace \? 'is-huggingface' : ''\}/
|
||||
);
|
||||
assert.match(modelInfoSource, /mr-model-details-file-table-header/);
|
||||
assert.match(modelInfoSource, /mr-model-details-file-table-row/);
|
||||
assert.match(modelInfoSource, /mr-model-details-file-table-use/);
|
||||
assert.match(
|
||||
resolverCssSource,
|
||||
/\.mr-model-details-main\.is-huggingface\s*\{[^}]*display:\s*block/s
|
||||
);
|
||||
assert.match(
|
||||
resolverCssSource,
|
||||
/\.mr-model-details-main\.is-huggingface \.mr-model-details-content\s*\{[^}]*display:\s*none/s
|
||||
);
|
||||
assert.match(
|
||||
resolverCssSource,
|
||||
/\.mr-model-details-file-table-row\s*\{[^}]*min-height:\s*52px/s
|
||||
);
|
||||
});
|
||||
@@ -5393,6 +5393,51 @@ a.mr-image-preview-resource-name:hover {
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.mr-model-details-main.is-huggingface {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mr-model-details-main.is-huggingface .mr-model-details-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mr-model-details-side.is-huggingface {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
border-left: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mr-model-details-side.is-huggingface .mr-model-details-summary-panel {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.mr-model-details-side.is-huggingface .mr-model-details-kv {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
.mr-model-details-side.is-huggingface .mr-model-details-kv div {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
padding: 7px 9px;
|
||||
background: #25262b;
|
||||
}
|
||||
|
||||
.mr-model-details-side.is-huggingface .mr-model-details-tags {
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.mr-model-details-side.is-huggingface .mr-model-details-variants-panel {
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.mr-model-details-version-tabs {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
@@ -5651,6 +5696,126 @@ a.mr-image-preview-resource-name:hover {
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.mr-model-details-files.is-table {
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-header,
|
||||
.mr-model-details-file-table-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(220px, 1.45fr)
|
||||
minmax(240px, 1.35fr)
|
||||
minmax(180px, 0.9fr)
|
||||
minmax(132px, 0.55fr);
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-header {
|
||||
min-height: 30px;
|
||||
padding: 0 11px;
|
||||
background: rgba(255, 255, 255, 0.055);
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
font-size: 10px;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-row {
|
||||
min-height: 52px;
|
||||
padding: 7px 11px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.07);
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-row:nth-child(odd) {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-row.is-primary {
|
||||
background: rgba(114, 211, 138, 0.055);
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-row.is-target {
|
||||
background: rgba(114, 211, 138, 0.09);
|
||||
box-shadow: inset 3px 0 0 rgba(114, 211, 138, 0.78);
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-name,
|
||||
.mr-model-details-file-table-summary {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-name strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: #f3f3f5;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-summary {
|
||||
overflow: hidden;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-badges span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 20px;
|
||||
padding: 2px 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-row.is-target .mr-model-details-file-table-badges span:first-child {
|
||||
background: rgba(114, 211, 138, 0.18);
|
||||
color: #9cf2b2;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-badges .is-empty {
|
||||
color: rgba(255, 255, 255, 0.42);
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-use {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-mirrors {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.mr-model-details-file {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
@@ -6008,6 +6173,16 @@ a.mr-image-preview-resource-name:hover {
|
||||
border-left: 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-header,
|
||||
.mr-model-details-file-table-row {
|
||||
grid-template-columns:
|
||||
minmax(190px, 1.2fr)
|
||||
minmax(210px, 1.2fr)
|
||||
minmax(150px, 0.8fr)
|
||||
minmax(120px, 0.55fr);
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -6050,6 +6225,29 @@ a.mr-image-preview-resource-name:hover {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 5px 10px;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-name,
|
||||
.mr-model-details-file-table-summary,
|
||||
.mr-model-details-file-table-badges {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.mr-model-details-file-table-use {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / span 3;
|
||||
width: auto;
|
||||
min-width: 112px;
|
||||
}
|
||||
|
||||
.mr-model-details-mirror {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { createModuleLogger } from "../../log_system/log_funcs.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
import { getModelCardUrl, parseHuggingFaceFileUrl } from "../utils/url_utils.js";
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
import { normalizePathIdentity } from "../utils/html_utils.js";
|
||||
const log = createModuleLogger('resolve_download_methods');
|
||||
@@ -3060,6 +3060,22 @@ export const resolveDownloadMethods = {
|
||||
if (modelListResult && modelListResult.url) {
|
||||
const modelListHash = getHashMatchDisplay('model_list', modelListResult);
|
||||
const missingCategory = this.getMissingDownloadCategory?.(missing, 'checkpoints') || missing.category || 'checkpoints';
|
||||
const huggingFaceFile = parseHuggingFaceFileUrl(modelListResult.url);
|
||||
const modelListDetailsContext = huggingFaceFile
|
||||
? {
|
||||
...modelListResult,
|
||||
source: modelListResult.source || 'model_list',
|
||||
details_source: 'huggingface',
|
||||
model_id: huggingFaceFile.repo,
|
||||
repo_id: huggingFaceFile.repo,
|
||||
version_id: huggingFaceFile.revision,
|
||||
branch: huggingFaceFile.revision,
|
||||
path: huggingFaceFile.path,
|
||||
filename: modelListResult.filename || huggingFaceFile.filename,
|
||||
missing_key: this.getMissingModelKey(missing),
|
||||
category: modelListResult.directory || missingCategory
|
||||
}
|
||||
: null;
|
||||
addRow({
|
||||
sourceKey: 'model-list',
|
||||
sourceLabel: 'Local Database',
|
||||
@@ -3073,7 +3089,8 @@ export const resolveDownloadMethods = {
|
||||
category: modelListResult.directory || missingCategory,
|
||||
openUrl: getModelCardUrl(modelListResult.url),
|
||||
searchedAt: this.getSearchResultTimestamp(modelListResult),
|
||||
localHashMatchIdentities: modelListHash.identities
|
||||
localHashMatchIdentities: modelListHash.identities,
|
||||
detailsContext: modelListDetailsContext
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3095,7 +3112,15 @@ export const resolveDownloadMethods = {
|
||||
category: missingCategory,
|
||||
openUrl: hfModelUrl,
|
||||
searchedAt: this.getSearchResultTimestamp(hfResult),
|
||||
localHashMatchIdentities: hfHash.identities
|
||||
localHashMatchIdentities: hfHash.identities,
|
||||
detailsContext: {
|
||||
...hfResult,
|
||||
name: hfRepo || hfResult.filename,
|
||||
filename: hfResult.filename,
|
||||
details_source: 'huggingface',
|
||||
missing_key: this.getMissingModelKey(missing),
|
||||
category: missingCategory
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ 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";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
import { getModelCardUrl, parseHuggingFaceFileUrl } from "../utils/url_utils.js";
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
|
||||
const localStorage = safeStorage;
|
||||
@@ -2045,6 +2045,12 @@ export const searchPanelMethods = {
|
||||
|| downloadSource.workflow_model_url
|
||||
|| downloadSource.url;
|
||||
const modelUrl = getModelCardUrl(rawModelUrl) || rawModelUrl;
|
||||
const normalizedSource = String(source).toLowerCase();
|
||||
const huggingFaceFile = normalizedSource === 'huggingface'
|
||||
? parseHuggingFaceFileUrl(
|
||||
downloadSource.download_url || downloadSource.url || rawModelUrl
|
||||
)
|
||||
: null;
|
||||
const versionName = downloadSource.version_name || missing.civitai_info?.version_name || '';
|
||||
const modelParts = this.getModelVersionParts(
|
||||
downloadSource.name || missing.civitai_info?.model_name || '',
|
||||
@@ -2070,13 +2076,16 @@ export const searchPanelMethods = {
|
||||
version: modelParts.version,
|
||||
category: rowCategory
|
||||
});
|
||||
const rowDetailsContext = ['civitai', 'civarchive'].includes(String(source).toLowerCase())
|
||||
const rowDetailsContext = ['civitai', 'civarchive', 'huggingface'].includes(normalizedSource)
|
||||
? {
|
||||
...downloadSource,
|
||||
source,
|
||||
details_source: String(source).toLowerCase(),
|
||||
model_id: downloadSource.model_id,
|
||||
version_id: downloadSource.version_id,
|
||||
details_source: normalizedSource,
|
||||
model_id: downloadSource.model_id || huggingFaceFile?.repo,
|
||||
repo_id: downloadSource.repo_id || downloadSource.repo || huggingFaceFile?.repo,
|
||||
version_id: downloadSource.version_id || huggingFaceFile?.revision,
|
||||
branch: downloadSource.branch || huggingFaceFile?.revision || '',
|
||||
path: downloadSource.path || huggingFaceFile?.path || '',
|
||||
name: modelName,
|
||||
filename: downloadFilename,
|
||||
missing_key: this.getMissingModelKey(missing),
|
||||
|
||||
@@ -2,6 +2,41 @@
|
||||
* URL Utilities for Model Resolver
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse a Hugging Face blob/resolve file URL into repository coordinates.
|
||||
* @param {string} value
|
||||
* @returns {{repo: string, revision: string, path: string, filename: string}|null}
|
||||
*/
|
||||
export function parseHuggingFaceFileUrl(value) {
|
||||
if (!value) return null;
|
||||
|
||||
try {
|
||||
const url = new URL(String(value));
|
||||
const host = url.hostname.toLowerCase().replace(/^www\./, '');
|
||||
if (host !== 'huggingface.co') return null;
|
||||
|
||||
const parts = url.pathname.split('/').filter(Boolean);
|
||||
if (parts.length < 5 || !['resolve', 'blob'].includes(parts[2])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decodePart = part => decodeURIComponent(part);
|
||||
const repo = `${decodePart(parts[0])}/${decodePart(parts[1])}`;
|
||||
const revision = decodePart(parts[3]);
|
||||
const path = parts.slice(4).map(decodePart).join('/');
|
||||
if (!repo || !revision || !path) return null;
|
||||
|
||||
return {
|
||||
repo,
|
||||
revision,
|
||||
path,
|
||||
filename: path.split('/').pop() || ''
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the download URL and returns the model card URL.
|
||||
* Supports HuggingFace and CivitAI platforms.
|
||||
@@ -14,12 +49,14 @@ export function getModelCardUrl(downloadUrl) {
|
||||
try {
|
||||
// HuggingFace URLs
|
||||
if (downloadUrl.includes('huggingface.co')) {
|
||||
const fileMatch = downloadUrl.match(/huggingface\.co\/([^\/]+\/[^\/]+)\/(?:resolve|blob)\/([^\/]+)\/(.+)$/);
|
||||
if (fileMatch) {
|
||||
const repo = fileMatch[1];
|
||||
const revision = fileMatch[2];
|
||||
const filePath = fileMatch[3].split(/[?#]/)[0];
|
||||
return `https://huggingface.co/${repo}/blob/${revision}/${filePath}`;
|
||||
const file = parseHuggingFaceFileUrl(downloadUrl);
|
||||
if (file) {
|
||||
const encodedRevision = encodeURIComponent(file.revision);
|
||||
const encodedPath = file.path
|
||||
.split('/')
|
||||
.map(part => encodeURIComponent(part))
|
||||
.join('/');
|
||||
return `https://huggingface.co/${file.repo}/blob/${encodedRevision}/${encodedPath}`;
|
||||
}
|
||||
|
||||
const match = downloadUrl.match(/huggingface\.co\/([^\/]+\/[^\/]+)/);
|
||||
|
||||
@@ -563,7 +563,7 @@ export const modelInfoMethods = {
|
||||
canShowSourceDetails(model = {}) {
|
||||
const source = String(model.details_source || model.source || '').toLowerCase();
|
||||
return model?.context_scope === 'download_table'
|
||||
&& ['civitai', 'civarchive', 'lora_manager_archive'].includes(source)
|
||||
&& ['civitai', 'civarchive', 'huggingface', 'lora_manager_archive'].includes(source)
|
||||
&& Boolean(model.model_id || model.modelId);
|
||||
},
|
||||
|
||||
@@ -2766,7 +2766,15 @@ export const modelInfoMethods = {
|
||||
source: model.details_source || model.source,
|
||||
model_id: model.model_id || model.modelId,
|
||||
version_id: model.version_id || model.versionId,
|
||||
civitai_key: tokens.civitai_key || ''
|
||||
file_path: model.path
|
||||
|| model.file_path
|
||||
|| model.selected_file?.path
|
||||
|| model.file_info?.path
|
||||
|| model.file?.path
|
||||
|| '',
|
||||
branch: model.branch || '',
|
||||
civitai_key: tokens.civitai_key || '',
|
||||
hf_token: tokens.hf_token || ''
|
||||
})
|
||||
}, 'Fetch model details');
|
||||
details._detailsData = data;
|
||||
@@ -2830,7 +2838,15 @@ export const modelInfoMethods = {
|
||||
source: data.source || details._sourceModel?.details_source || details._sourceModel?.source,
|
||||
model_id: data.model_id || details._sourceModel?.model_id || details._sourceModel?.modelId,
|
||||
version_id: versionId,
|
||||
civitai_key: tokens.civitai_key || ''
|
||||
file_path: details._sourceModel?.path
|
||||
|| details._sourceModel?.file_path
|
||||
|| details._sourceModel?.selected_file?.path
|
||||
|| details._sourceModel?.file_info?.path
|
||||
|| details._sourceModel?.file?.path
|
||||
|| '',
|
||||
branch: data.branch || details._sourceModel?.branch || '',
|
||||
civitai_key: tokens.civitai_key || '',
|
||||
hf_token: tokens.hf_token || ''
|
||||
})
|
||||
}, 'Fetch model version details');
|
||||
const freshVersion = fresh.selected_version;
|
||||
@@ -2864,6 +2880,7 @@ export const modelInfoMethods = {
|
||||
const description = data.description || selectedVersion.description || '';
|
||||
const sanitizedDescription = description ? this.sanitizeDescriptionHtml(description) : '';
|
||||
const source = data.source || contextModel.details_source || contextModel.source || '';
|
||||
const isHuggingFace = String(source).toLowerCase() === 'huggingface';
|
||||
const creator = data.creator || {};
|
||||
const creatorName = creator.username || creator.name || '';
|
||||
const pageUrl = data.version_url || data.url || '';
|
||||
@@ -2878,7 +2895,7 @@ export const modelInfoMethods = {
|
||||
].filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== '');
|
||||
|
||||
return `
|
||||
<div class="mr-model-details-shell">
|
||||
<div class="mr-model-details-shell ${isHuggingFace ? 'is-huggingface' : ''}">
|
||||
<div class="mr-model-details-topbar">
|
||||
<button type="button" class="mr-model-details-icon-btn" data-action="close" aria-label="Close">${getSvgIcon('x')}</button>
|
||||
<div class="mr-model-details-title">
|
||||
@@ -2890,7 +2907,7 @@ export const modelInfoMethods = {
|
||||
${pageUrl ? `<a class="mr-model-details-action" href="${this.escapeHtml(pageUrl)}" target="_blank" rel="noopener noreferrer">${getSvgIcon('externalLink')} Open page</a>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-model-details-main">
|
||||
<div class="mr-model-details-main ${isHuggingFace ? 'is-huggingface' : ''}">
|
||||
<section class="mr-model-details-content">
|
||||
<div class="mr-model-details-version-tabs">
|
||||
${versions.map(version => `
|
||||
@@ -2917,8 +2934,8 @@ export const modelInfoMethods = {
|
||||
${sanitizedDescription ? sanitizedDescription : '<p>No description available.</p>'}
|
||||
</div>
|
||||
</section>
|
||||
<aside class="mr-model-details-side">
|
||||
<section class="mr-model-details-panel">
|
||||
<aside class="mr-model-details-side ${isHuggingFace ? 'is-huggingface' : ''}">
|
||||
<section class="mr-model-details-panel mr-model-details-summary-panel">
|
||||
<h3>Details</h3>
|
||||
<div class="mr-model-details-kv">
|
||||
<div><span>Type</span><strong>${this.escapeHtml(data.type || '-')}</strong></div>
|
||||
@@ -2934,9 +2951,17 @@ export const modelInfoMethods = {
|
||||
<div class="mr-model-details-tags">${trainedWords.map(word => `<span>${this.escapeHtml(word)}</span>`).join('')}</div>
|
||||
</section>
|
||||
` : ''}
|
||||
<section class="mr-model-details-panel">
|
||||
<section class="mr-model-details-panel mr-model-details-variants-panel">
|
||||
<h3>Download Variants</h3>
|
||||
<div class="mr-model-details-files">
|
||||
<div class="mr-model-details-files ${isHuggingFace ? 'is-table' : ''}">
|
||||
${isHuggingFace ? `
|
||||
<div class="mr-model-details-file-table-header" aria-hidden="true">
|
||||
<span>File</span>
|
||||
<span>Size / Hash</span>
|
||||
<span>Variant</span>
|
||||
<span>Action</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${this.renderSourceModelDetailsFiles(selectedVersion, data, contextModel)}
|
||||
</div>
|
||||
</section>
|
||||
@@ -2988,6 +3013,8 @@ export const modelInfoMethods = {
|
||||
|
||||
renderSourceModelDetailsFiles(version = {}, data = {}, contextModel = {}) {
|
||||
const files = Array.isArray(version.files) ? version.files.filter(file => file?.download_url) : [];
|
||||
const source = String(data.source || contextModel.details_source || contextModel.source || '').toLowerCase();
|
||||
const isHuggingFace = source === 'huggingface';
|
||||
if (!files.length) {
|
||||
return '<div class="mr-model-details-empty">No downloadable files available for this version.</div>';
|
||||
}
|
||||
@@ -3048,6 +3075,8 @@ export const modelInfoMethods = {
|
||||
selected_version: selectedVersion,
|
||||
base_model: version.base_model || contextModel.base_model,
|
||||
tags: data.tags || [],
|
||||
path: file.path || '',
|
||||
branch: data.branch || contextModel.branch || '',
|
||||
match_type: 'selected',
|
||||
confidence: 100
|
||||
};
|
||||
@@ -3059,6 +3088,33 @@ export const modelInfoMethods = {
|
||||
file.primary ? 'Primary' : '',
|
||||
mirrors.length ? `${mirrors.length} mirrors` : ''
|
||||
].filter(Boolean);
|
||||
const compactBadges = Array.from(new Set([
|
||||
...summaryBadges,
|
||||
...fileMeta.badges
|
||||
]));
|
||||
const selectionData = this.escapeHtml(encodeURIComponent(JSON.stringify(payload)));
|
||||
const actionLabel = isTarget ? 'Use matched file' : 'Use this file';
|
||||
if (isHuggingFace) {
|
||||
return `
|
||||
<div class="mr-model-details-file-table-row ${file.primary ? 'is-primary' : ''} ${isTarget ? 'is-target' : ''}">
|
||||
<div class="mr-model-details-file-table-name">
|
||||
<strong>${this.escapeHtml(file.name || `File ${index + 1}`)}</strong>
|
||||
</div>
|
||||
<div class="mr-model-details-file-table-summary" data-tooltip="${this.escapeHtml(fileMeta.summary)}">
|
||||
${this.escapeHtml(fileMeta.summary)}
|
||||
</div>
|
||||
<div class="mr-model-details-file-table-badges">
|
||||
${compactBadges.length
|
||||
? compactBadges.map(badge => `<span>${this.escapeHtml(badge)}</span>`).join('')
|
||||
: '<span class="is-empty">Model file</span>'}
|
||||
</div>
|
||||
<button type="button" class="mr-btn mr-btn-primary mr-model-details-use mr-model-details-file-table-use" data-selection="${selectionData}">
|
||||
${actionLabel}
|
||||
</button>
|
||||
${mirrorList ? `<div class="mr-model-details-file-table-mirrors">${mirrorList}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const openAttr = index === 0 ? ' open' : '';
|
||||
return `
|
||||
<details class="mr-model-details-file ${file.primary ? 'is-primary' : ''} ${isTarget ? 'is-target' : ''}"${openAttr}>
|
||||
@@ -3072,8 +3128,8 @@ export const modelInfoMethods = {
|
||||
</summary>
|
||||
<div class="mr-model-details-file-body">
|
||||
${fileMeta.badges.length ? `<div class="mr-model-details-file-badges">${fileMeta.badges.map(badge => `<span>${this.escapeHtml(badge)}</span>`).join('')}</div>` : ''}
|
||||
<button type="button" class="mr-btn mr-btn-primary mr-model-details-use" data-selection="${this.escapeHtml(encodeURIComponent(JSON.stringify(payload)))}">
|
||||
${isTarget ? 'Use matched file' : 'Use this file'}
|
||||
<button type="button" class="mr-btn mr-btn-primary mr-model-details-use" data-selection="${selectionData}">
|
||||
${actionLabel}
|
||||
</button>
|
||||
${mirrorList}
|
||||
</div>
|
||||
@@ -3546,6 +3602,8 @@ export const modelInfoMethods = {
|
||||
sha256: selectionSha256,
|
||||
hash: selectionSha256,
|
||||
tags: selection.tags || [],
|
||||
path: selection.path || selectionFile?.path || '',
|
||||
branch: selection.branch || contextModel.branch || '',
|
||||
match_type: selection.match_type || 'selected',
|
||||
confidence: selection.confidence || 100,
|
||||
searchedAt: new Date().toISOString()
|
||||
|
||||
Reference in New Issue
Block a user