""" utils/hf_utils.py ───────────────── Utilities for validating and resolving HuggingFace model IDs or external URLs. Used by the UI's "Custom model" text input before attempting to load. """ from __future__ import annotations import logging import re from typing import Literal import requests from huggingface_hub import HfApi, model_info from huggingface_hub.utils import RepositoryNotFoundError, HFValidationError logger = logging.getLogger(__name__) HF_REPO_RE = re.compile(r"^[A-Za-z0-9_\-\.]+/[A-Za-z0-9_\-\.]+$") URL_RE = re.compile(r"^https?://") ModelSource = Literal["hf_repo", "url", "invalid"] def classify_model_id(model_id: str) -> ModelSource: """ Decide whether a raw string is: - a valid HuggingFace repo slug ("org/model-name") - an external URL ("https://…") - invalid """ model_id = model_id.strip() if URL_RE.match(model_id): return "url" if HF_REPO_RE.match(model_id): return "hf_repo" return "invalid" def validate_hf_repo(repo_id: str, token: str | None = None) -> tuple[bool, str]: """ Check whether a HuggingFace repo exists and is publicly accessible. Returns ------- (ok, message) """ try: info = model_info(repo_id, token=token) pipeline_tag = getattr(info, "pipeline_tag", "unknown") return True, f"✅ Found: `{repo_id}` (pipeline: {pipeline_tag})" except RepositoryNotFoundError: return False, f"❌ Repository not found: `{repo_id}`" except HFValidationError as e: return False, f"❌ Validation error: {e}" except Exception as e: return False, f"❌ Unexpected error: {e}" def validate_url(url: str, timeout: int = 5) -> tuple[bool, str]: """HEAD-request an external model URL to check reachability.""" try: r = requests.head(url, timeout=timeout, allow_redirects=True) if r.status_code < 400: return True, f"✅ URL reachable (HTTP {r.status_code})" return False, f"❌ URL returned HTTP {r.status_code}" except requests.ConnectionError: return False, "❌ Cannot reach URL — check your connection" except requests.Timeout: return False, "❌ URL timed out" except Exception as e: return False, f"❌ {e}" def validate_custom_model(model_id: str) -> tuple[bool, str]: """ Top-level validator: routes to HF or URL check depending on input shape. Safe to call from Gradio event handlers. """ model_id = model_id.strip() if not model_id: return False, "⚠️ No model ID entered" source = classify_model_id(model_id) if source == "hf_repo": return validate_hf_repo(model_id) if source == "url": return validate_url(model_id) return False, f"❌ `{model_id}` does not look like a HF repo ID or a URL" def search_hf_models(query: str, pipeline_tag: str | None = None, limit: int = 10) -> list[dict]: """ Search HuggingFace Hub for models matching a query. Returns a list of dicts with keys: model_id, pipeline_tag, downloads, likes. """ try: api = HfApi() results = api.list_models( search=query, filter=pipeline_tag, sort="downloads", direction=-1, limit=limit, ) out = [] for m in results: out.append({ "model_id": m.id, "pipeline_tag": getattr(m, "pipeline_tag", "—"), "downloads": getattr(m, "downloads", 0), "likes": getattr(m, "likes", 0), }) return out except Exception as e: logger.warning("HF search failed: %s", e) return []