Files
fn_registry/python/functions/ml/comfyui_inject_ipadapter.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

219 lines
8.7 KiB
Python

"""Inyecta una rama IPAdapter en un workflow ComfyUI ya construido (API format).
Toma un workflow en API format (dict, p.ej. salida de
comfyui_build_txt2img_workflow, ya con LoRAs encadenadas si las hay) y le injerta
la rama IPAdapter del custom node ComfyUI_IPAdapter_plus (cubiq), repuntando el
KSampler para que su MODEL venga condicionado por una imagen de referencia:
- mode='style': IPAdapterUnifiedLoader + IPAdapter. La imagen de referencia
transfiere estilo/composicion al resultado (image prompt clasico).
- mode='faceid': IPAdapterUnifiedLoaderFaceID + IPAdapterFaceID. Usa insightface
para extraer el embedding de la cara de la referencia e imponer un rostro
consistente en el personaje generado.
La fuente del MODEL es la que HOY alimenta el KSampler.model (tras las LoRAs, no
el checkpoint crudo): asi el IPAdapter se aplica sobre el modelo ya modificado
por los LoRAs, en el orden correcto del mixer.
Es la version ENCADENABLE-sobre-dict del builder
comfyui_build_ipadapter_workflow, que construye el grafo entero desde cero y NO
encadena. Reusa sus constantes de preset/weight_type por defecto. Pensada para
componerse con inject_lora / inject_controlnet / inject_hires_fix sobre un mismo
dict base (ver comfyui_compose_capabilities).
Funcion pura: sin red, sin I/O. No muta el dict de entrada (copia profunda). Los
class_type/inputs estan verificados contra /object_info del servidor (IPAdapter
plus), reutilizando exactamente los del builder.
"""
from __future__ import annotations
import copy
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# Reutiliza los defaults de preset/weight_type del builder para no duplicarlos.
from ml.comfyui_build_ipadapter_workflow import ( # noqa: E402
_DEFAULT_PRESET,
_DEFAULT_WEIGHT_TYPE,
)
def _is_link(v) -> bool:
"""True si v es una conexion ComfyUI [node_id(str), output_index(int)]."""
return (
isinstance(v, list)
and len(v) == 2
and isinstance(v[0], str)
and isinstance(v[1], int)
)
def comfyui_inject_ipadapter(
workflow: dict,
ref_image: str,
*,
mode: str = "style",
weight: float = 0.8,
preset: str | None = None,
weight_type: str | None = None,
start_at: float = 0.0,
end_at: float = 1.0,
weight_faceidv2: float = 1.0,
lora_strength: float = 0.6,
combine_embeds: str = "concat",
embeds_scaling: str = "V only",
provider: str = "CPU",
model_node: str | None = None,
) -> dict:
"""Devuelve una copia del workflow con una rama IPAdapter inyectada.
Args:
workflow: dict en API format (ej. salida de
comfyui_build_txt2img_workflow, posiblemente con LoRAs). No se muta;
se devuelve una copia.
ref_image: nombre del archivo de imagen de referencia en el directorio
input/ del servidor ComfyUI (lo carga un nodo LoadImage). En faceid
debe contener una cara nitida; en style es la imagen de estilo. No
puede estar vacio.
mode: 'style' (transfiere estilo/composicion) o 'faceid' (rostro
consistente via insightface + FaceID). keyword-only.
weight: peso de la influencia IPAdapter (0..1+). 0.8 es un buen punto de
partida; sube para mas parecido, baja para mas libertad del prompt.
preset: preset del UnifiedLoader. Si None usa el default del modo
('STANDARD (medium strength)' para style, 'FACEID PLUS V2' para faceid).
weight_type: tipo de ponderacion del nodo IPAdapter/FaceID. Si None usa el
default del modo ('standard' para style, 'linear' para faceid).
start_at: fraccion del sampling donde empieza a aplicar IPAdapter (0..1).
end_at: fraccion del sampling donde deja de aplicar (0..1).
weight_faceidv2: peso del embedding FaceID v2 (solo mode='faceid').
lora_strength: fuerza de la LoRA FaceID que carga el UnifiedLoaderFaceID
(solo mode='faceid').
combine_embeds: como combinar embeddings si hay varias caras
('concat'|'add'|'subtract'|'average'|'norm average'). Solo faceid.
embeds_scaling: escalado de embeddings ('V only'|'K+V'|...). Solo faceid.
provider: backend de insightface ('CPU'|'CUDA'|...). CPU por defecto para
no competir por VRAM con el modelo de difusion. Solo faceid.
model_node: node_id cuya salida MODEL (slot 0) alimentara la rama
IPAdapter. Si None, se detecta la fuente que hoy alimenta el
KSampler.model (con el CheckpointLoader como fallback). keyword-only.
Returns:
copia del workflow con LoadImage + (UnifiedLoader|UnifiedLoaderFaceID) +
(IPAdapter|IPAdapterFaceID) anadidos (node_ids = max id numerico + 1/2/3)
y el KSampler.model repuntado a la salida MODEL de la rama IPAdapter.
Raises:
ValueError: si mode no es 'style' ni 'faceid', si ref_image esta vacio, si
no se encuentra un KSampler, o si no se puede determinar la fuente del
MODEL (y no se pasa model_node explicito).
"""
if mode not in ("style", "faceid"):
raise ValueError(
f"comfyui_inject_ipadapter: mode debe ser 'style' o 'faceid', no {mode!r}"
)
if not ref_image:
raise ValueError("comfyui_inject_ipadapter: ref_image no puede estar vacio")
wf = copy.deepcopy(workflow)
ksampler_id = next(
(nid for nid, n in wf.items() if str(n.get("class_type", "")).endswith("KSampler")),
None,
)
if ksampler_id is None:
raise ValueError(
"comfyui_inject_ipadapter: no se encontro ningun KSampler en el workflow."
)
ks_inputs = wf[ksampler_id].get("inputs", {})
if model_node is not None:
model_src = [model_node, 0]
elif _is_link(ks_inputs.get("model")):
model_src = list(ks_inputs["model"])
else:
ckpt = next(
(nid for nid, n in wf.items()
if str(n.get("class_type", "")).startswith("CheckpointLoader")),
None,
)
if ckpt is None:
raise ValueError(
"comfyui_inject_ipadapter: no se pudo determinar la fuente del "
"MODEL; pasa model_node explicito."
)
model_src = [ckpt, 0]
numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
base = (max(numeric) + 1) if numeric else len(wf) + 1
load_id = str(base)
loader_id = str(base + 1)
apply_id = str(base + 2)
used_preset = preset if preset is not None else _DEFAULT_PRESET[mode]
used_wtype = weight_type if weight_type is not None else _DEFAULT_WEIGHT_TYPE[mode]
wf[load_id] = {
"class_type": "LoadImage",
"inputs": {"image": ref_image},
}
if mode == "style":
wf[loader_id] = {
"class_type": "IPAdapterUnifiedLoader",
"inputs": {"model": list(model_src), "preset": used_preset},
}
wf[apply_id] = {
"class_type": "IPAdapter",
"inputs": {
"model": [loader_id, 0],
"ipadapter": [loader_id, 1],
"image": [load_id, 0],
"weight": weight,
"start_at": start_at,
"end_at": end_at,
"weight_type": used_wtype,
},
}
else: # faceid
wf[loader_id] = {
"class_type": "IPAdapterUnifiedLoaderFaceID",
"inputs": {
"model": list(model_src),
"preset": used_preset,
"lora_strength": lora_strength,
"provider": provider,
},
}
wf[apply_id] = {
"class_type": "IPAdapterFaceID",
"inputs": {
"model": [loader_id, 0],
"ipadapter": [loader_id, 1],
"image": [load_id, 0],
"weight": weight,
"weight_faceidv2": weight_faceidv2,
"weight_type": used_wtype,
"combine_embeds": combine_embeds,
"start_at": start_at,
"end_at": end_at,
"embeds_scaling": embeds_scaling,
},
}
# Repunta el KSampler para que tome el MODEL condicionado por IPAdapter.
wf[ksampler_id]["inputs"]["model"] = [apply_id, 0]
return wf
if __name__ == "__main__":
import json
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
base = comfyui_build_txt2img_workflow("IMG_dreamshaper_8.safetensors", "a knight, cinematic")
wf_style = comfyui_inject_ipadapter(base, "style_ref.png", mode="style", weight=0.8)
wf_face = comfyui_inject_ipadapter(base, "face_ref.png", mode="faceid", weight=0.9)
print(json.dumps({"style_nodes": list(wf_style), "faceid_nodes": list(wf_face)}, indent=2))