Files
fn_registry/python/functions/ml/comfyui_install_3d_model.py
T
egutierrez cda36408d0 feat(ml): modelos con prefijo de categoría (IMG_/VIDEO_/3D_) + refs actualizadas
Renombra los 13 checkpoints/diffusion models de ComfyUI prefijando la
categoría al inicio del nombre, para que en el dropdown de carga el usuario
distinga de inmediato imagen/vídeo/3D y no cargue un modelo en el nodo
equivocado. Misma operación que se hizo con los LoRAs (report 0197) pero
sobre los modelos.

Clasificación:
- IMG_: dreamshaper_8, juggernaut_xl_v11, v1-5-pruned-emaonly-fp16,
  flux1-dev-fp8-e4m3fn, flux1-schnell-fp8-e4m3fn
- VIDEO_: svd, ltx-video-2b-v0.9.5, wan2.1_t2v_1.3B_fp16
- 3D_: stable_zero123, sv3d_p, hunyuan3d-dit-v2-mini, hunyuan3d-dit-v2-mv,
  hy3dgen/hunyuan3d-dit-v2-0-fp16 (mantiene subcarpeta)

A diferencia de los LoRAs aquí solo se PREFIJA la categoría conservando el
nombre completo (versión/arquitectura). Archivos físicos renombrados en
~/ComfyUI/models/checkpoints, /mnt/2tb/comfyui_models/{checkpoints,
diffusion_models} y la subcarpeta hy3dgen/. Mapa de reversión en
~/ComfyUI/models/checkpoints/_ckpt_rename_map.json.

Actualiza todas las refs (ckpt_name/unet_name + defaults + prosa) en los
builders gamedev/vídeo/3D, style presets, pipelines, tests y los workflows
de ComfyUI. Arregla de paso el default roto de comfyui_text_to_3d_oneshot
(apuntaba a v1-5-pruned-emaonly.safetensors inexistente; ahora al real
IMG_v1-5-pruned-emaonly-fp16.safetensors).

No tocados (justificado): repo-paths de HuggingFace en comfyui_install_3d_model
(<repo>/model.fp16.safetensors son rutas de descarga, no nombres de dropdown)
y el mock de stable-diffusion.cpp en test_genconfig_to_sdcpp_args.

Verificado: dropdowns CheckpointLoaderSimple + UNETLoader listan los nombres
con prefijo; 1 generación real con IMG_juggernaut_xl_v11 (node_errors vacío,
pixelart_00003_.png); 327 tests comfyui verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:24:52 +02:00

190 lines
7.6 KiB
Python

"""Instala un checkpoint Hunyuan3D-2 en la carpeta checkpoints/ de ComfyUI.
ComfyUI 0.26.0 reconstruye mallas 3D con los nodos nativos de Hunyuan3D-2, que
cargan un checkpoint self-contained (DiT de forma + VAE 3D + encoder de imagen en
un solo .safetensors) via ImageOnlyCheckpointLoader. Esta funcion resuelve el repo
de HuggingFace de la variante pedida, REUTILIZA la cache de HF si ya esta bajado
(sin re-descargar), y copia el .safetensors a la carpeta checkpoints/ (la ruta real
que declara extra_model_paths.yaml) con el nombre que espera el loader nativo.
Cascada: (1) si el destino ya existe -> reutiliza; (2) si esta en la cache de HF
-> copia desde la cache; (3) si no -> descarga con huggingface_hub (token de
`pass` si la variante fuera gated).
Impura: lectura de YAML, escritura en disco, posible red (HTTP) y subprocess (pass).
"""
import os
import shutil
import subprocess
# variant -> (repo_id HF, ruta del archivo dentro del repo, nombre destino en checkpoints/)
_VARIANTS = {
"mini": (
"tencent/Hunyuan3D-2mini",
"hunyuan3d-dit-v2-mini/model.fp16.safetensors",
"3D_hunyuan3d-dit-v2-mini.safetensors",
),
"standard": (
"tencent/Hunyuan3D-2",
"hunyuan3d-dit-v2-0/model.fp16.safetensors",
"3D_hunyuan3d-dit-v2-0.safetensors",
),
"mv": (
"tencent/Hunyuan3D-2mv",
"hunyuan3d-dit-v2-mv/model.fp16.safetensors",
"3D_hunyuan3d-dit-v2-mv.safetensors",
),
}
_MIN_BYTES = 1_000_000 # un .safetensors real pesa GBs; descarta restos/HTML.
def _checkpoints_dir(comfyui_dir: str) -> str:
"""Resuelve el directorio real de checkpoints de ComfyUI.
Lee extra_model_paths.yaml (prefiere la seccion con is_default) para devolver
`<base_path>/<checkpoints_subdir>`. Si el YAML no existe o no se puede parsear,
cae a la ruta nativa `<comfyui_dir>/models/checkpoints`.
"""
base = os.path.expanduser(comfyui_dir)
native = os.path.join(base, "models", "checkpoints")
yml = os.path.join(base, "extra_model_paths.yaml")
if not os.path.isfile(yml):
return native
try:
import yaml
with open(yml, encoding="utf-8") as fh:
data = yaml.safe_load(fh) or {}
except Exception: # noqa: BLE001 — YAML/PyYAML no disponible: usar nativa.
return native
if not isinstance(data, dict):
return native
fallback = None
for section in data.values():
if not isinstance(section, dict):
continue
sub = section.get("checkpoints")
if not sub:
continue
bp = os.path.expanduser(str(section.get("base_path", "")))
first_line = str(sub).splitlines()[0].strip()
resolved = os.path.join(bp, first_line)
if section.get("is_default"):
return resolved
if fallback is None:
fallback = resolved
return fallback or native
def _find_in_hf_cache(repo_id: str, repo_filename: str) -> str | None:
"""Busca el archivo en la cache local de HuggingFace, sin red.
Layout: ~/.cache/huggingface/hub/models--<org>--<name>/snapshots/<hash>/...
Resuelve el symlink al blob real y verifica un tamano minimo. Devuelve la ruta
real o None.
"""
org_name = repo_id.replace("/", "--")
hub = os.path.expanduser("~/.cache/huggingface/hub")
cache_root = os.path.join(hub, f"models--{org_name}", "snapshots")
if not os.path.isdir(cache_root):
return None
target = os.path.basename(repo_filename)
for snap in os.listdir(cache_root):
snap_dir = os.path.join(cache_root, snap)
if not os.path.isdir(snap_dir):
continue
for root, _dirs, files in os.walk(snap_dir):
if target in files:
real = os.path.realpath(os.path.join(root, target))
if os.path.isfile(real) and os.path.getsize(real) >= _MIN_BYTES:
return real
return None
def _pass_hf_token() -> str | None:
"""Lee el token de HuggingFace de `pass API_TOKEN_huggingFace`, o None."""
try:
out = subprocess.run(
["pass", "show", "API_TOKEN_huggingFace"],
capture_output=True, text=True, timeout=10,
)
if out.returncode == 0:
tok = out.stdout.splitlines()[0].strip() if out.stdout.strip() else ""
return tok or None
except (OSError, subprocess.SubprocessError):
pass
return None
def comfyui_install_3d_model(
variant: str = "mini",
*,
hf_token: str | None = None,
comfyui_dir: str = "~/ComfyUI",
) -> dict:
"""Instala el checkpoint Hunyuan3D-2 de la variante pedida en checkpoints/.
Args:
variant: "mini" (≈5 GB VRAM, default), "standard" (dit-v2-0, ≈6 GB) o
"mv" (multiview). Determina el repo de HF y el nombre destino.
hf_token: token de HuggingFace si la variante fuera gated. Si None y hace
falta descargar, se intenta leer de `pass show API_TOKEN_huggingFace`.
keyword-only.
comfyui_dir: raiz de la instalacion de ComfyUI (se expande ~). La carpeta
real de checkpoints se resuelve via extra_model_paths.yaml. keyword-only.
Returns:
dict {ok, path, bytes, reused_cache, error}. path = ruta del checkpoint en
checkpoints/; reused_cache=True si ya estaba instalado o se copio de la
cache de HF (sin descarga de red). Si falla, ok=False y error explica.
"""
if variant not in _VARIANTS:
return {"ok": False, "path": "", "bytes": 0, "reused_cache": False,
"error": f"variant {variant!r} no valida; usa {sorted(_VARIANTS)}"}
repo_id, repo_filename, dest_name = _VARIANTS[variant]
ckpt_dir = _checkpoints_dir(comfyui_dir)
dest = os.path.join(ckpt_dir, dest_name)
# 1. Ya instalado.
if os.path.isfile(dest) and os.path.getsize(dest) >= _MIN_BYTES:
return {"ok": True, "path": dest, "bytes": os.path.getsize(dest),
"reused_cache": True, "error": ""}
# 2. En la cache de HF -> copiar (sin red).
cached = _find_in_hf_cache(repo_id, repo_filename)
if cached:
try:
os.makedirs(ckpt_dir, exist_ok=True)
shutil.copy2(cached, dest)
except OSError as exc:
return {"ok": False, "path": "", "bytes": 0, "reused_cache": False,
"error": f"no se pudo copiar de la cache HF a {dest}: {exc}"}
return {"ok": True, "path": dest, "bytes": os.path.getsize(dest),
"reused_cache": True, "error": ""}
# 3. Descargar con huggingface_hub (lazy; usa su propia cache).
token = hf_token or _pass_hf_token()
try:
from huggingface_hub import hf_hub_download
except ImportError:
return {"ok": False, "path": "", "bytes": 0, "reused_cache": False,
"error": ("no esta en la cache de HF y huggingface_hub no esta "
"instalado en este venv. Instala huggingface_hub o baja "
f"el archivo {repo_filename!r} de {repo_id!r} a mano (o con "
"comfyui_download_model usando la URL de resolve de HF).")}
try:
local = hf_hub_download(repo_id=repo_id, filename=repo_filename, token=token)
os.makedirs(ckpt_dir, exist_ok=True)
shutil.copy2(local, dest)
except Exception as exc: # noqa: BLE001 — red/auth/gated/escritura.
return {"ok": False, "path": "", "bytes": 0, "reused_cache": False,
"error": f"fallo descargando {repo_filename} de {repo_id}: {exc}"}
return {"ok": True, "path": dest, "bytes": os.path.getsize(dest),
"reused_cache": False, "error": ""}
if __name__ == "__main__":
import json
print(json.dumps(comfyui_install_3d_model("mini"), ensure_ascii=False, indent=2))