fffiloni's picture
Upload 14 files
30f60d0 verified
Raw
History Blame
28.2 kB
from __future__ import annotations
import json
import re
import tempfile
from pathlib import Path
from typing import Any
from huggingface_hub import HfApi, hf_hub_download
MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$")
SMALL_CONFIG_MAX_BYTES = 2_000_000
WEIGHT_EXTENSIONS = (".safetensors", ".bin", ".pt", ".pth", ".ckpt", ".onnx", ".gguf")
RISKY_SERIALIZATION_EXTENSIONS = (".bin", ".pt", ".pth", ".ckpt", ".pkl", ".pickle", ".joblib")
PYTHON_EXTENSIONS = (".py",)
DIFFUSERS_EXAMPLE_RE = re.compile(r"(from\s+diffusers\s+import|DiffusionPipeline\.from_pretrained|AutoPipelineFor(?:Text2Image|Image2Image|Inpainting)\.from_pretrained|StableDiffusion(?:XL)?Pipeline\.from_pretrained|FluxPipeline\.from_pretrained|ZImagePipeline)", re.IGNORECASE)
GENERIC_INFERENCE_EXAMPLE_RE = re.compile(r"(from_pretrained\s*\(|gradio_client|pipeline\s*\(|pipe\s*\(|predict\s*\()", re.IGNORECASE)
DIFFUSERS_PIPELINE_CLASSES = {
"DiffusionPipeline",
"StableDiffusionPipeline",
"StableDiffusionXLPipeline",
"StableDiffusionImg2ImgPipeline",
"StableDiffusionInpaintPipeline",
"AutoPipelineForText2Image",
"AutoPipelineForImage2Image",
"AutoPipelineForInpainting",
"FluxPipeline",
"ZImagePipeline",
}
TASK_OUTPUT_TYPE_MAP = {
"text-to-image": "image",
"image-to-image": "image",
"image-to-video": "video",
"text-to-video": "video",
"text-to-audio": "audio",
"text-to-speech": "audio",
"automatic-speech-recognition": "text",
"audio-classification": "text",
"text-generation": "text",
"text2text-generation": "text",
"summarization": "text",
"translation": "text",
"question-answering": "text",
"fill-mask": "text",
"sentence-similarity": "text",
"token-classification": "text",
"zero-shot-classification": "text",
"image-classification": "text",
"object-detection": "image",
"image-segmentation": "image",
}
IMAGE_PIPELINE_HINTS = (
"text2image",
"texttoimage",
"image2image",
"inpaint",
"stableDiffusion".lower(),
"fluxpipeline",
"zimagepipeline",
)
def infer_expected_output_type(
*,
pipeline_tag: str | None = None,
library_name: str | None = None,
tags: list[str] | None = None,
pipeline_class: str | None = None,
readme: str | None = None,
) -> str | None:
task = (pipeline_tag or "").strip().lower()
if task in TASK_OUTPUT_TYPE_MAP:
return TASK_OUTPUT_TYPE_MAP[task]
tag_set = {str(t).lower() for t in (tags or [])}
if {"text-to-image", "image-to-image", "diffusers"} & tag_set:
if "text-to-video" not in tag_set and "image-to-video" not in tag_set:
return "image"
if {"text-to-video", "image-to-video"} & tag_set:
return "video"
if {"text-to-audio", "text-to-speech", "audio"} & tag_set and "automatic-speech-recognition" not in tag_set:
return "audio"
cls = (pipeline_class or "").strip().lower()
if any(hint in cls for hint in IMAGE_PIPELINE_HINTS):
return "image"
if "video" in cls:
return "video"
if "audio" in cls or "speech" in cls:
return "audio"
text = (readme or "").lower()
if re.search(r"\.images\s*\[|generated image|text-to-image|image = pipe\(", text):
return "image"
if re.search(r"\.frames\s*\[|generated video|text-to-video|export_to_video", text):
return "video"
if re.search(r"\.audios?\s*\[|generated audio|text-to-speech|soundfile|\.wav", text):
return "audio"
if re.search(r"generated_text|tokenizer\.decode|text-generation|response\s*=", text):
return "text"
return None
def normalize_model_id(value: str | None) -> str:
cleaned = (value or "").strip()
cleaned = cleaned.replace("https://huggingface.co/", "")
cleaned = cleaned.split("?", 1)[0].split("#", 1)[0].strip("/")
if not MODEL_ID_RE.match(cleaned):
raise ValueError("Model ID must look like owner/name or a Hugging Face model URL.")
return cleaned
def _get(obj: Any, name: str, default: Any = None) -> Any:
if isinstance(obj, dict):
return obj.get(name, default)
return getattr(obj, name, default)
def _sibling_name(sibling: Any) -> str:
return str(_get(sibling, "rfilename", _get(sibling, "path", _get(sibling, "name", ""))) or "")
def _sibling_size(sibling: Any) -> int | None:
value = _get(sibling, "size", None)
try:
return int(value) if value is not None else None
except Exception:
return None
def _read_small_json(repo_id: str, filename: str, *, token: str | None, max_bytes: int = SMALL_CONFIG_MAX_BYTES) -> dict[str, Any]:
try:
with tempfile.TemporaryDirectory(prefix="asf-model-scan-") as tmp:
path = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model", token=token, local_dir=tmp)
p = Path(path)
if p.stat().st_size > max_bytes:
return {}
return json.loads(p.read_text(encoding="utf-8"))
except Exception:
return {}
def _read_small_text(repo_id: str, filename: str, *, token: str | None, max_bytes: int = SMALL_CONFIG_MAX_BYTES) -> str:
try:
with tempfile.TemporaryDirectory(prefix="asf-model-scan-") as tmp:
path = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="model", token=token, local_dir=tmp)
p = Path(path)
if p.stat().st_size > max_bytes:
return ""
return p.read_text(encoding="utf-8", errors="replace")
except Exception:
return ""
def _model_index_pipeline_class(model_index: dict[str, Any]) -> str:
value = model_index.get("_class_name") or model_index.get("pipeline_class") or ""
return str(value or "")
def _extract_model_card_signals(readme: str, model_index: dict[str, Any]) -> dict[str, Any]:
text = readme or ""
diffusers_example = bool(DIFFUSERS_EXAMPLE_RE.search(text))
inference_example = diffusers_example or bool(GENERIC_INFERENCE_EXAMPLE_RE.search(text))
pipeline_class = _model_index_pipeline_class(model_index)
if not pipeline_class and text:
for name in sorted(DIFFUSERS_PIPELINE_CLASSES, key=len, reverse=True):
if name in text:
pipeline_class = name
break
runtime_hints = []
lower = text.lower()
for label, patterns in [
("bfloat16", ("bfloat16", "bf16")),
("float16", ("float16", "fp16")),
("device_map", ("device_map",)),
("cuda", ("cuda", ".to(\"cuda\")", ".to('cuda')")),
("offload", ("offload", "enable_model_cpu_offload")),
("num_inference_steps", ("num_inference_steps", "inference steps")),
]:
if any(p in lower for p in patterns):
runtime_hints.append(label)
return {
"has_inference_example": inference_example,
"has_diffusers_example": diffusers_example,
"pipeline_class": pipeline_class,
"runtime_hints": runtime_hints[:8],
}
NATIVE_KERNEL_PATTERNS: tuple[tuple[str, tuple[str, ...], str], ...] = (
("flash_attn", ("flash-attn", "flash_attn", "flash attention", "flashattention", "flash-attention", "enable_flashattn", "flash_attention_2"), "Flash Attention dependency or runtime flag"),
("xformers", ("xformers", "memory_efficient_attention"), "xFormers attention dependency"),
("triton", ("triton", "triton kernel", "@triton", "triton.jit"), "Triton/fused kernel dependency"),
("custom_cuda", ("cuda extension", "custom cuda", "cpp_extension", "setup.py build_ext", "fused kernel", "fused ops", "custom kernel"), "Custom native/CUDA extension"),
("attention_interface", ("attentioninterface", "attention interface", "attn_implementation"), "Transformers attention backend hook"),
("hf_kernels", ("hf kernels", "kernel hub", "hugging face kernels", "kernels-community", "from kernels import", "pip install kernels"), "HF Kernels / Kernel Hub mention"),
)
def build_kernel_strategy(*, readme: str | None = None, files: list[str] | None = None) -> dict[str, Any]:
"""Return a visibility-only mitigation plan for native kernel dependencies.
This is metadata/readme based. It must not inject packages or mark a model
terminally blocked by itself; it gives Pi and the UI a safer strategy than
blindly adding source-built CUDA packages to requirements.txt.
"""
text = (readme or "").lower()
file_list = [str(f).lower() for f in (files or [])]
detected: list[str] = []
signals: list[str] = []
for key, patterns, label in NATIVE_KERNEL_PATTERNS:
if any(pattern in text for pattern in patterns):
detected.append(key)
signals.append(label)
if any(f.endswith((".cu", ".cuh")) or "/csrc/" in f or f.startswith("csrc/") for f in file_list):
if "custom_cuda" not in detected:
detected.append("custom_cuda")
signals.append("Native CUDA/C++ source files in repository")
if any(f.endswith((".cpp", ".cc")) and ("cuda" in f or "/csrc/" in f or f.startswith("csrc/")) for f in file_list):
if "custom_cuda" not in detected:
detected.append("custom_cuda")
signals.append("Native C++ extension source files in repository")
native_risk = any(k in detected for k in {"flash_attn", "xformers", "triton", "custom_cuda"})
candidate_backends: list[str] = []
if native_risk:
candidate_backends.extend(["torch_sdpa", "hf_kernels", "xformers_wheel"])
if "attention_interface" in detected:
candidate_backends.append("transformers_attention_interface")
if "hf_kernels" in detected and "hf_kernels" not in candidate_backends:
candidate_backends.append("hf_kernels")
rejected_actions = []
if native_risk:
rejected_actions.append({
"action": "blind_pip_install_native_cuda_package",
"reason": "Source-built CUDA/native packages are fragile in Spaces and can mismatch the managed PyTorch/CUDA runtime.",
})
if "flash_attn" in detected:
rejected_actions.append({
"action": "pip_install_flash_attn_without_fallback",
"reason": "Prefer PyTorch SDPA, Transformers AttentionInterface, HF Kernels/Kernel Hub, or a compatible wheel before forcing flash-attn source builds.",
})
selected = "none"
if native_risk:
selected = "prefer_runtime_backends_before_source_builds"
elif "hf_kernels" in detected:
selected = "hf_kernels_available_if_model_uses_supported_kernel"
return {
"schema_version": "kernel_strategy.v1",
"native_kernel_risk": bool(native_risk),
"detected_dependencies": detected,
"signals": signals[:10],
"selected_strategy": selected,
"candidate_backends": candidate_backends[:8],
"rejected_actions": rejected_actions,
"requires_manual_review": bool("custom_cuda" in detected),
"pi_instruction": (
"Do not compile native CUDA packages blindly. Prefer PyTorch SDPA, compatible wheels, HF Kernels/Kernel Hub, "
"Transformers AttentionInterface, or Diffusers attention processors when they match the required operation. "
"Declare a technical blocker if a strict native extension has no plausible fallback."
if native_risk else
"No native kernel dependency was detected by the metadata scan."
),
"visibility_only": True,
}
def _has_any(text: str, patterns: tuple[str, ...]) -> bool:
lower = text.lower()
return any(pattern in lower for pattern in patterns)
def _build_complexity_assessment(
*,
pipeline_tag: str | None,
library_name: str | None,
tags: list[str],
files: list[str],
readme: str,
expected_output_type: str | None,
custom_code: bool,
gated: Any,
) -> dict[str, Any]:
"""Estimate build/runtime risk without changing the existing pre-scan verdict.
This is deliberately a lightweight metadata/model-card heuristic. It helps
the UI warn users before long Jobs; it is not a hard launch gate.
"""
lower_readme = (readme or "").lower()
lower_files = [str(f).lower() for f in files]
tag_set = {str(t).lower() for t in tags}
task = (pipeline_tag or "").lower()
library = (library_name or "").lower()
points = 0
signals: list[str] = []
mitigations: list[str] = []
def add(points_delta: int, signal: str, mitigation: str | None = None) -> None:
nonlocal points
points += points_delta
if signal not in signals:
signals.append(signal)
if mitigation and mitigation not in mitigations:
mitigations.append(mitigation)
if expected_output_type == "video" or "video" in task or {"text-to-video", "image-to-video"} & tag_set:
add(3, "video or image-to-video output", "Expect a long build/repair cycle and prefer strong fallback hardware.")
if any(word in lower_readme for word in ("avatar", "audio-driven", "audio driven", "talking head", "lip sync", "lip-sync")):
add(2, "audio/video avatar workflow", "Refresh sign-in before launch and expect larger validation payloads.")
if custom_code or "trust_remote_code" in tag_set or "custom_code" in tag_set:
add(2, "custom code or trust_remote_code required", "Review generated code and blockers before paid hardware attempts.")
if gated in {True, "auto", "manual"} or str(gated).lower() in {"true", "auto", "manual"}:
add(1, "gated model access", "Confirm the same HF account has accepted access terms.")
if _has_any(lower_readme, ("torchrun", "nproc_per_node", "distributed", "init_process_group", "nccl", "context_parallel", "tensor parallel", "pipeline parallel", "multi-gpu", "multi gpu")):
add(4, "multi-GPU / distributed runtime hints", "Treat ZeroGPU as unlikely unless Pi can prove a single-GPU refactor.")
if _has_any(lower_readme, ("flash-attn", "flash_attn", "flash attention", "flashattention", "flash-attention")):
add(2, "flash-attn or custom attention dependency", "Prefer PyTorch SDPA, xformers wheels, or HF Kernels before source builds.")
if _has_any(lower_readme, ("xformers", "triton", "cuda extension", "fused kernel", "fused ops", "custom kernel")) or any(f.endswith((".cu", ".cpp")) or "/csrc/" in f for f in lower_files):
add(2, "native CUDA/kernel dependency risk", "Prefer runtime-compatible wheels or HF Kernels over compiling during Space build.")
if "ffmpeg" in lower_readme or any("ffmpeg" in f for f in lower_files):
add(1, "ffmpeg or system media dependency", "Ensure generated app declares media/system requirements clearly.")
if _has_any(lower_readme, ("conda ", "mamba ", "apt-get", "sudo apt", "pip install -e", "git clone")):
add(1, "non-standard install instructions", "Pi should vendor required code or simplify requirements for Spaces.")
weight_files = [f for f in lower_files if f.endswith(WEIGHT_EXTENSIONS)]
safetensor_shards = [f for f in lower_files if f.endswith(".safetensors")]
if len(weight_files) >= 20 or len(safetensor_shards) >= 12:
add(2, "many weight shards", "Expect longer cold start and validation windows.")
elif len(weight_files) >= 8 or len(safetensor_shards) >= 6:
add(1, "multiple weight shards", "Allow extra boot time before validation.")
vram_match = re.search(r"\b(?:vram|gpu memory|memory)\D{0,16}([3-9]\d|1\d{2})\s*(?:gb|gib)\b|\b([3-9]\d|1\d{2})\s*(?:gb|gib)\s*(?:vram|gpu)", lower_readme)
if vram_match:
add(3, "high VRAM mentioned in model card", "Prefer fixed GPU fallback and refresh sign-in before launch.")
# Keep the assessment independent from the existing pass/fail scan score.
if points >= 9:
level = "very_high"
label = "Very high risk"
recommended_seconds = 180 * 60
elif points >= 6:
level = "high"
label = "High risk"
recommended_seconds = 120 * 60
elif points >= 3:
level = "medium"
label = "Medium risk"
recommended_seconds = 60 * 60
else:
level = "low"
label = "Low risk"
recommended_seconds = 30 * 60
return {
"schema_version": "model_build_risk.v1",
"level": level,
"label": label,
"score": max(0, points),
"recommended_session_seconds": recommended_seconds,
"recommended_session_minutes": recommended_seconds // 60,
"signals": signals[:10],
"mitigations": mitigations[:6],
"summary": f"{label}; recommended HF session remaining: {recommended_seconds // 60}m+.",
"visibility_only": True,
}
def _classify(score: int, *, blocking: bool = False) -> str:
if blocking:
return "unsupported"
if score >= 75:
return "safe"
if score >= 45:
return "caution"
return "risky"
def analyze_model_metadata(
*,
model_id: str,
pipeline_tag: str | None = None,
library_name: str | None = None,
tags: list[str] | None = None,
siblings: list[Any] | None = None,
gated: Any = None,
private: Any = None,
card_data: dict[str, Any] | None = None,
config: dict[str, Any] | None = None,
model_index: dict[str, Any] | None = None,
readme: str | None = None,
) -> dict[str, Any]:
"""Pure heuristic model-card scan used by the API and tests.
It intentionally does not claim to be a security scanner. It summarizes
common signals that predict whether an autonomous Space build is likely to
be safe, supported, and worth spending compute on.
"""
tags = [str(t) for t in (tags or []) if t]
files = [_sibling_name(s) for s in (siblings or []) if _sibling_name(s)]
lower_files = [f.lower() for f in files]
config = config or {}
model_index = model_index or {}
readme = readme or ""
card_signals = _extract_model_card_signals(readme, model_index)
has_inference_example = bool(card_signals["has_inference_example"])
has_diffusers_example = bool(card_signals["has_diffusers_example"])
pipeline_class = str(card_signals["pipeline_class"] or "")
runtime_hints = list(card_signals["runtime_hints"] or [])
expected_output_type = infer_expected_output_type(
pipeline_tag=pipeline_tag,
library_name=library_name,
tags=tags,
pipeline_class=pipeline_class,
readme=readme,
)
score = 50
good: list[str] = []
risk: list[str] = []
recommendations: list[str] = []
has_safetensors = any(f.endswith(".safetensors") for f in lower_files)
has_risky_serialization = any(f.endswith(RISKY_SERIALIZATION_EXTENSIONS) for f in lower_files)
has_weight_file = any(f.endswith(WEIGHT_EXTENSIONS) for f in lower_files)
has_python = any(f.endswith(PYTHON_EXTENSIONS) for f in lower_files)
has_config = "config.json" in lower_files
has_model_index = "model_index.json" in lower_files
has_readme = bool(readme.strip()) or any(f in {"readme.md", "README.md".lower()} for f in lower_files)
custom_code = bool(config.get("auto_map")) or "custom_code" in tags or "trust_remote_code" in tags or has_python
if pipeline_tag:
score += 12
good.append(f"Pipeline tag detected: {pipeline_tag}.")
else:
score -= 12
risk.append("No pipeline tag detected; task type may be ambiguous for an autonomous builder.")
if expected_output_type:
good.append(f"Expected output type inferred: {expected_output_type}.")
if library_name:
score += 10
good.append(f"Library detected: {library_name}.")
elif model_index:
score += 6
good.append("Diffusers-style model_index.json detected.")
else:
score -= 8
risk.append("No library metadata detected.")
if has_safetensors:
score += 18
good.append("Safetensors weights are available.")
elif has_weight_file:
score -= 10
risk.append("No safetensors file detected; weights may use less safe or less portable formats.")
else:
score -= 18
risk.append("No obvious model weight file detected in the repository listing.")
if has_risky_serialization:
score -= 14
risk.append("Repository includes pickle-like or PyTorch pickle serialization files (.bin/.pt/.pth/.ckpt/.pkl).")
if custom_code:
score -= 22
risk.append("Custom code or trust_remote_code is likely required.")
recommendations.append("Review code manually before using Strict inference or paid hardware.")
if gated in {True, "auto", "manual"} or str(gated).lower() in {"true", "auto", "manual"}:
score -= 18
risk.append("Model appears gated; the Job may fail unless the signed-in account has accepted access terms.")
recommendations.append("Confirm model access with the same Hugging Face account before launching.")
if private:
score -= 8
risk.append("Model is private; ensure the OAuth token has access.")
if has_config or has_model_index:
score += 8
good.append("Standard config metadata is present.")
else:
score -= 8
risk.append("No config.json or model_index.json detected.")
is_diffusers = (library_name or "").lower() == "diffusers" or "diffusers" in {t.lower() for t in tags} or bool(model_index)
if is_diffusers and has_model_index:
score += 8
good.append("Diffusers repository structure detected.")
if pipeline_class:
score += 5
good.append(f"Pipeline class detected: {pipeline_class}.")
if has_diffusers_example:
score += 12
good.append("Model card includes a runnable Diffusers example.")
elif has_inference_example:
score += 7
good.append("Model card includes an inference example.")
elif has_readme:
score -= 5
risk.append("No clear runnable inference example found in the model card.")
if runtime_hints:
score += 4
good.append("Model card provides runtime hints: " + ", ".join(runtime_hints) + ".")
if has_readme and len(readme.strip()) > 400:
score += 6
good.append("Model card documentation is present.")
elif not has_readme:
score -= 8
risk.append("README/model card appears missing.")
else:
score -= 4
risk.append("Model card documentation appears very short.")
task = (pipeline_tag or "").lower()
library = (library_name or "").lower()
unsupported = False
if task in {"reinforcement-learning", "robotics"}:
unsupported = True
risk.append(f"Task '{pipeline_tag}' is not a good fit for automatic Gradio Space generation.")
if "gguf" in tags or any(f.endswith(".gguf") for f in lower_files):
score -= 10
risk.append("GGUF assets may need a custom llama.cpp runtime rather than the standard Transformers/Diffusers path.")
if library in {"adapter-transformers"}:
score -= 8
risk.append(f"Library '{library_name}' may require custom integration.")
# A clear Diffusers model card with safetensors and a runnable example is a strong
# positive signal. Do not downgrade it just because the model may be large; size
# affects hardware planning, not whether the card is healthy for autonomous build.
if is_diffusers and has_safetensors and has_diffusers_example and has_model_index and not custom_code and not unsupported:
score = max(score, 82)
score = max(0, min(100, score))
verdict = _classify(score, blocking=unsupported)
build_risk = _build_complexity_assessment(
pipeline_tag=pipeline_tag,
library_name=library_name,
tags=tags,
files=files,
readme=readme,
expected_output_type=expected_output_type,
custom_code=custom_code,
gated=gated,
)
kernel_strategy = build_kernel_strategy(readme=readme, files=files)
if kernel_strategy.get("native_kernel_risk"):
recommendations.append("Native kernel risk detected; prefer PyTorch SDPA, HF Kernels/Kernel Hub, compatible wheels, or model-specific fallbacks before source builds.")
if build_risk["level"] in {"high", "very_high"}:
recommendations.append(
f"Refresh HF sign-in before launch; this scan recommends {build_risk['recommended_session_minutes']}m+ remaining for this model."
)
if not recommendations:
if verdict == "safe":
recommendations.append("Good candidate for Strict inference.")
elif verdict == "caution":
recommendations.append("Use Best effort inference if the model card lacks a clear runnable example.")
elif verdict == "risky":
recommendations.append("Prefer Best effort or Demo scaffold until the risky signals are reviewed.")
else:
recommendations.append("Do not launch automatically; inspect manually first.")
return {
"ok": True,
"model_id": model_id,
"verdict": verdict,
"score": score,
"summary": {
"safe": "Looks like a good candidate for an autonomous build.",
"caution": "Usable, but review the caution signals before spending compute.",
"risky": "Risky for an autonomous paid build; review before launching.",
"unsupported": "Not a good fit for the automated builder.",
}[verdict],
"good_signals": good[:8],
"risk_signals": risk[:10],
"recommendations": recommendations[:6],
"expected_output_type": expected_output_type or "",
"build_risk": build_risk,
"kernel_strategy": kernel_strategy,
"metadata": {
"pipeline_tag": pipeline_tag or "",
"library_name": library_name or "",
"tags": tags[:30],
"gated": gated,
"private": bool(private),
"file_count": len(files),
"has_safetensors": has_safetensors,
"has_risky_serialization": has_risky_serialization,
"has_custom_code_signal": custom_code,
"has_inference_example": has_inference_example,
"has_diffusers_example": has_diffusers_example,
"pipeline_class": pipeline_class,
"runtime_hints": runtime_hints,
"expected_output_type": expected_output_type or "",
"diffusers_standard": bool(is_diffusers and has_model_index and has_safetensors),
"build_risk_level": build_risk["level"],
"recommended_session_minutes": build_risk["recommended_session_minutes"],
"native_kernel_risk": bool(kernel_strategy.get("native_kernel_risk")),
"kernel_strategy": kernel_strategy,
},
}
def scan_model_card(model_id_or_url: str, *, token: str | None = None) -> dict[str, Any]:
model_id = normalize_model_id(model_id_or_url)
api = HfApi(token=token)
try:
info = api.model_info(model_id, files_metadata=True, token=token)
except TypeError:
info = api.model_info(model_id, token=token)
siblings = list(_get(info, "siblings", []) or [])
files = {_sibling_name(s).lower(): s for s in siblings if _sibling_name(s)}
config = _read_small_json(model_id, "config.json", token=token) if "config.json" in files else {}
model_index = _read_small_json(model_id, "model_index.json", token=token) if "model_index.json" in files else {}
readme = _read_small_text(model_id, "README.md", token=token) if "readme.md" in files else ""
card_data = _get(info, "card_data", _get(info, "cardData", {})) or {}
if not isinstance(card_data, dict):
try:
card_data = dict(card_data)
except Exception:
card_data = {}
return analyze_model_metadata(
model_id=model_id,
pipeline_tag=_get(info, "pipeline_tag", None),
library_name=_get(info, "library_name", None),
tags=list(_get(info, "tags", []) or []),
siblings=siblings,
gated=_get(info, "gated", None),
private=_get(info, "private", False),
card_data=card_data,
config=config,
model_index=model_index,
readme=readme,
)