"""Busca modelos / LoRAs en Civitai via su API publica GET /api/v1/models. Normaliza cada resultado a un dict pequeno y reutilizable ({name, type, base_model, version_id, download_url, nsfw}) tomando la primera version del modelo. La busqueda publica funciona sin token; pasar un token solo sube el rate limit y desbloquea modelos restringidos. Impura: red (HTTP GET a civitai.com). Solo stdlib (urllib, json). """ import json import urllib.error import urllib.parse import urllib.request _API = "https://civitai.com/api/v1/models" _TIMEOUT = 30.0 def comfyui_search_civitai_models( query: str, *, types: str = "Checkpoint", base_model: str | None = None, sort: str = "Highest Rated", limit: int = 20, token: str | None = None, ) -> dict: """Busca modelos en Civitai y devuelve resultados normalizados. Args: query: texto de busqueda (nombre del modelo, ej. "dreamshaper"). types: tipo(s) de modelo, CSV. Valores Civitai: Checkpoint, LORA, TextualInversion, Controlnet, VAE, Upscaler, ... keyword-only. base_model: filtra por modelo base (ej. "SD 1.5", "SDXL 1.0"). None no filtra. keyword-only. sort: orden Civitai ("Highest Rated", "Most Downloaded", "Newest"). keyword-only. limit: numero maximo de resultados (1-100). keyword-only. token: API token de Civitai (header Authorization Bearer). Opcional: la busqueda publica funciona sin el. No hardcodear: pasar desde pass/vault. keyword-only. Returns: dict {ok, items, count, error}. items es una lista de {name, type, base_model, version_id, download_url, nsfw} (la primera version de cada modelo). ok=False con error si la peticion falla; una busqueda sin resultados devuelve ok=True con items=[] (no es error). """ params = [ ("query", query), ("limit", str(max(1, min(int(limit), 100)))), ("sort", sort), ] for t in str(types).split(","): t = t.strip() if t: params.append(("types", t)) if base_model: for bm in (base_model if isinstance(base_model, (list, tuple)) else [base_model]): params.append(("baseModels", str(bm))) url = f"{_API}?{urllib.parse.urlencode(params)}" headers = {"User-Agent": "fn-registry/comfyui_search_civitai_models"} if token: headers["Authorization"] = f"Bearer {token}" try: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: data = json.loads(resp.read()) except urllib.error.HTTPError as exc: body = exc.read().decode(errors="replace")[:300] return {"ok": False, "items": [], "count": 0, "error": f"HTTP {exc.code} en {url}: {body}"} except urllib.error.URLError as exc: return {"ok": False, "items": [], "count": 0, "error": f"no se pudo conectar a civitai.com: {exc.reason}"} except json.JSONDecodeError as exc: return {"ok": False, "items": [], "count": 0, "error": f"respuesta no es JSON valido: {exc}"} items = [] for model in data.get("items", []) or []: versions = model.get("modelVersions") or [] v0 = versions[0] if versions else {} items.append({ "name": model.get("name"), "type": model.get("type"), "base_model": v0.get("baseModel"), "version_id": v0.get("id"), "download_url": v0.get("downloadUrl"), "nsfw": bool(model.get("nsfw", False)), }) return {"ok": True, "items": items, "count": len(items), "error": ""} if __name__ == "__main__": out = comfyui_search_civitai_models("dreamshaper", limit=5) print(out["ok"], out["count"]) for it in out["items"]: print(f" {it['name']} [{it['base_model']}] v{it['version_id']} -> {it['download_url']}")