Skip to content

Commit f8a6e8b

Browse files
committed
feat: add HuggingFace model details, variant table, and download context
1 parent 9aa7077 commit f8a6e8b

10 files changed

Lines changed: 1657 additions & 899 deletions

File tree

__init__.py

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ def setup_routes(self):
496496
check_huggingface_token,
497497
get_author_fallback_index_status,
498498
get_huggingface_download_url,
499+
get_huggingface_model_details,
499500
parse_huggingface_url,
500501
refresh_author_fallback_index,
501502
search_huggingface_for_file,
@@ -3332,37 +3333,46 @@ async def model_details(request):
33323333
model_id = data.get("model_id")
33333334
version_id = data.get("version_id")
33343335
civitai_key = data.get("civitai_key", "")
3336+
hf_token = data.get("hf_token", "")
3337+
file_path = data.get("file_path", "")
3338+
branch = data.get("branch", "")
33353339

33363340
if not download_available:
33373341
return web.json_response(
33383342
{"error": "Download providers are not available"}, status=503
33393343
)
33403344

3341-
try:
3342-
model_id = (
3343-
int(model_id)
3344-
if model_id is not None and str(model_id).strip()
3345-
else None
3346-
)
3347-
except (TypeError, ValueError):
3348-
model_id = None
3349-
3350-
try:
3351-
version_id = (
3352-
int(version_id)
3353-
if version_id is not None and str(version_id).strip()
3354-
else None
3355-
)
3356-
except (TypeError, ValueError):
3357-
version_id = None
3358-
33593345
if source == "lora_manager_archive":
33603346
source = "civitai"
33613347

3362-
if source not in {"civitai", "civarchive"}:
3348+
if source not in {"civitai", "civarchive", "huggingface"}:
33633349
return web.json_response(
33643350
{"error": "Unsupported model details source"}, status=400
33653351
)
3352+
3353+
if source == "huggingface":
3354+
model_id = str(model_id or "").strip()
3355+
branch = str(branch or version_id or "main").strip() or "main"
3356+
version_id = branch
3357+
else:
3358+
try:
3359+
model_id = (
3360+
int(model_id)
3361+
if model_id is not None and str(model_id).strip()
3362+
else None
3363+
)
3364+
except (TypeError, ValueError):
3365+
model_id = None
3366+
3367+
try:
3368+
version_id = (
3369+
int(version_id)
3370+
if version_id is not None and str(version_id).strip()
3371+
else None
3372+
)
3373+
except (TypeError, ValueError):
3374+
version_id = None
3375+
33663376
if not model_id:
33673377
return web.json_response(
33683378
{"error": "model_id is required"}, status=400
@@ -3375,12 +3385,20 @@ async def model_details(request):
33753385
version_id,
33763386
civitai_key or None,
33773387
)
3378-
else:
3388+
elif source == "civarchive":
33793389
details = await asyncio.to_thread(
33803390
get_civarchive_model_details,
33813391
model_id,
33823392
version_id,
33833393
)
3394+
else:
3395+
details = await asyncio.to_thread(
3396+
get_huggingface_model_details,
3397+
model_id,
3398+
file_path,
3399+
branch,
3400+
hf_token or None,
3401+
)
33843402

33853403
if not details:
33863404
return web.json_response(

core/sources/huggingface.py

Lines changed: 211 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import os
8+
import posixpath
89
import re
910
import threading
1011
import time
@@ -331,7 +332,214 @@ def parse_huggingface_url(url: str) -> Optional[Dict[str, str]]:
331332

332333
def get_huggingface_download_url(repo: str, filename: str, branch: str = "main") -> str:
333334
"""Generate a direct download URL for a HuggingFace file."""
334-
return f"https://huggingface.co/{repo}/resolve/{branch}/{quote(filename)}"
335+
return (
336+
f"https://huggingface.co/{repo}/resolve/"
337+
f"{quote(branch, safe='')}/{quote(filename)}"
338+
)
339+
340+
341+
def _normalize_huggingface_path(value: Any) -> str:
342+
return str(value or "").replace("\\", "/").strip("/")
343+
344+
345+
def _get_huggingface_variant_label(filename: str) -> str:
346+
"""Return a concise precision/quantization label inferred from a filename."""
347+
stem = os.path.splitext(get_filename_from_path(filename))[0].lower()
348+
patterns = (
349+
("nvfp4_mixed", "NVFP4 mixed"),
350+
("fp8_scaled", "FP8 scaled"),
351+
("int8_convrot", "INT8 convrot"),
352+
("nvfp4", "NVFP4"),
353+
("fp8", "FP8"),
354+
("bf16", "BF16"),
355+
("fp16", "FP16"),
356+
("int8", "INT8"),
357+
("int4", "INT4"),
358+
)
359+
for token, label in patterns:
360+
if re.search(rf"(?:^|[_.-]){re.escape(token)}(?:$|[_.-])", stem):
361+
return label
362+
return ""
363+
364+
365+
def _normalize_huggingface_details_file(
366+
repo_id: str,
367+
branch: str,
368+
file_info: Dict[str, Any],
369+
target_path: str,
370+
) -> Optional[Dict[str, Any]]:
371+
file_path = _normalize_huggingface_path(file_info.get("path"))
372+
if not file_path:
373+
return None
374+
375+
download_url = get_huggingface_download_url(repo_id, file_path, branch)
376+
if not looks_like_model_file(download_url):
377+
return None
378+
379+
filename = get_filename_from_path(file_path)
380+
sha256 = _extract_huggingface_file_sha256(file_info)
381+
extension = os.path.splitext(filename)[1].lstrip(".")
382+
metadata = {
383+
"format": extension.upper() if extension else "",
384+
"fp": _get_huggingface_variant_label(filename),
385+
}
386+
metadata = {key: value for key, value in metadata.items() if value}
387+
page_url = (
388+
f"https://huggingface.co/{repo_id}/blob/"
389+
f"{quote(branch, safe='')}/{quote(file_path, safe='/')}"
390+
)
391+
392+
return {
393+
"name": filename,
394+
"filename": filename,
395+
"path": file_path,
396+
"size": extract_file_size(file_info),
397+
"sha256": sha256,
398+
"hash": sha256,
399+
"hashes": {"SHA256": sha256} if sha256 else {},
400+
"download_url": download_url,
401+
"url": page_url,
402+
"primary": bool(target_path and file_path == target_path),
403+
"metadata": metadata,
404+
}
405+
406+
407+
def get_huggingface_model_details(
408+
repo_id: str,
409+
file_path: str = "",
410+
branch: str = "main",
411+
token: Optional[str] = None,
412+
) -> Optional[Dict[str, Any]]:
413+
"""Fetch model-file variants from the folder containing a matched HF file."""
414+
normalized_repo_id = str(repo_id or "").strip().strip("/")
415+
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", normalized_repo_id):
416+
return None
417+
418+
normalized_branch = str(branch or "main").strip() or "main"
419+
if len(normalized_branch) > 200 or any(
420+
character in normalized_branch for character in "\r\n?#"
421+
):
422+
return None
423+
424+
target_path = _normalize_huggingface_path(file_path)
425+
target_directory = posixpath.dirname(target_path)
426+
headers = {"Authorization": f"Bearer {token.strip()}"} if token and token.strip() else {}
427+
repo_tree = _get_repo_tree(
428+
normalized_repo_id,
429+
headers=headers,
430+
branch=normalized_branch,
431+
)
432+
if not repo_tree:
433+
return None
434+
435+
normalized_files = []
436+
for file_info in repo_tree:
437+
candidate_path = _normalize_huggingface_path(file_info.get("path"))
438+
if not candidate_path:
439+
continue
440+
if target_path and posixpath.dirname(candidate_path) != target_directory:
441+
continue
442+
443+
normalized_file = _normalize_huggingface_details_file(
444+
normalized_repo_id,
445+
normalized_branch,
446+
file_info,
447+
target_path,
448+
)
449+
if normalized_file:
450+
normalized_files.append(normalized_file)
451+
452+
if not normalized_files:
453+
return None
454+
455+
normalized_files.sort(
456+
key=lambda item: (
457+
not item.get("primary", False),
458+
str(item.get("name") or "").lower(),
459+
)
460+
)
461+
462+
model_data: Dict[str, Any] = {}
463+
try:
464+
response_data = execute_provider_json_request(
465+
"HuggingFace model details",
466+
f"{HF_API_URL}/models/{normalized_repo_id}",
467+
headers=headers,
468+
timeout=15,
469+
)
470+
if isinstance(response_data, dict):
471+
model_data = response_data
472+
except Exception as e:
473+
log.debug(
474+
f"Error getting HuggingFace model metadata for {normalized_repo_id}: {e}"
475+
)
476+
477+
card_data = (
478+
model_data.get("cardData")
479+
if isinstance(model_data.get("cardData"), dict)
480+
else {}
481+
)
482+
tags = model_data.get("tags") if isinstance(model_data.get("tags"), list) else []
483+
repo_url = f"https://huggingface.co/{normalized_repo_id}"
484+
tree_url = (
485+
f"{repo_url}/tree/{quote(normalized_branch, safe='')}"
486+
+ (
487+
f"/{quote(target_directory, safe='/')}"
488+
if target_directory
489+
else ""
490+
)
491+
)
492+
version = {
493+
"id": normalized_branch,
494+
"name": normalized_branch,
495+
"published_at": model_data.get("createdAt") or "",
496+
"updated_at": model_data.get("lastModified") or "",
497+
"description": "",
498+
"trained_words": [],
499+
"stats": {
500+
"downloads": model_data.get("downloads", 0),
501+
"thumbsUpCount": model_data.get("likes", 0),
502+
},
503+
"files": normalized_files,
504+
"images": [],
505+
"url": tree_url,
506+
}
507+
owner = normalized_repo_id.split("/", 1)[0]
508+
description = (
509+
model_data.get("description")
510+
or card_data.get("description")
511+
or (
512+
f"Select a model file from `{target_directory}` in this Hugging Face "
513+
"repository."
514+
if target_directory
515+
else "Select a model file from this Hugging Face repository."
516+
)
517+
)
518+
519+
return {
520+
"source": "huggingface",
521+
"details_source": "huggingface",
522+
"model_id": normalized_repo_id,
523+
"version_id": normalized_branch,
524+
"name": model_data.get("modelId") or normalized_repo_id,
525+
"type": model_data.get("pipeline_tag")
526+
or model_data.get("library_name")
527+
or "Model repository",
528+
"description": description,
529+
"creator": {"username": model_data.get("author") or owner},
530+
"stats": {
531+
"downloads": model_data.get("downloads", 0),
532+
"thumbsUpCount": model_data.get("likes", 0),
533+
},
534+
"tags": tags,
535+
"url": repo_url,
536+
"version_url": tree_url,
537+
"folder": target_directory,
538+
"branch": normalized_branch,
539+
"versions": [version],
540+
"selected_version": version,
541+
"images": [],
542+
}
335543

336544

337545

@@ -381,6 +589,7 @@ def _build_huggingface_result(
381589
sha256=sha256,
382590
repo_id=repo_id,
383591
path=file_path,
592+
branch="main",
384593
)
385594

386595

@@ -1077,6 +1286,7 @@ def quote_url_path(val):
10771286
repo_id=repo_id,
10781287
repo=repo_id,
10791288
path=file_path,
1289+
branch=branch,
10801290
page_url=page_url,
10811291
version_url=page_url,
10821292
custom_url=True,

0 commit comments

Comments
 (0)