"""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("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))