"""Inyecta una rama ControlNet en un workflow ComfyUI ya construido (API format). Toma un workflow en API format (dict, p.ej. salida de comfyui_build_txt2img_workflow) que tiene un KSampler cuyo condicionamiento positivo viene de un CLIPTextEncode, y le encadena la rama de ControlNet: LoadImage (imagen de control) ---+ ControlNetLoader (modelo CN) ----+--> ControlNetApply --> KSampler.positive CLIPTextEncode (positivo) -------+ ControlNetApply re-condiciona el positivo con la imagen de control (canny, depth, pose, scribble, ...) y el KSampler se repunta para tomar ese condicionamiento. Es la version ENCADENABLE-sobre-dict del builder comfyui_build_controlnet_workflow, que construye el grafo entero desde cero y NO encadena. Reusa los mismos class_types/inputs (LoadImage, ControlNetLoader, ControlNetApply). Pensada para componerse con inject_lora / inject_ipadapter / 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). """ import copy 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_controlnet( workflow: dict, control_image: str, cn_name: str, *, strength: float = 1.0, positive_node: str | None = None, ) -> dict: """Devuelve una copia del workflow con una rama ControlNet inyectada. Localiza el condicionamiento positivo actual del KSampler (lo que hoy alimenta su input `positive`), inserta LoadImage + ControlNetLoader + ControlNetApply, y repunta el KSampler para que tome el positivo ya condicionado por el ControlNet. Args: workflow: dict en API format (ej. salida de comfyui_build_txt2img_workflow). No se muta; se devuelve una copia. control_image: nombre del archivo de la imagen de control dentro de la carpeta input/ del servidor ComfyUI (lo carga el nodo LoadImage). Suele ser un mapa preprocesado (canny/depth/openpose). No puede estar vacio. cn_name: nombre del modelo ControlNet en models/controlnet/ tal como lo lista /object_info para ControlNetLoader (control_net_name). strength: fuerza con la que el ControlNet condiciona la generacion (0.0 = nula, 1.0 = plena). keyword-only. positive_node: node_id cuya salida CONDITIONING (slot 0) se usara como positivo de entrada del ControlNetApply. Si None, se detecta la fuente que hoy alimenta el KSampler.positive. keyword-only. Returns: copia del workflow con LoadImage + ControlNetLoader + ControlNetApply anadidos (node_ids = max id numerico existente + 1, + 2, + 3) y el KSampler.positive repuntado a la salida del ControlNetApply. Raises: ValueError: si control_image esta vacio, si no se encuentra un KSampler, o si no se puede determinar la fuente del condicionamiento positivo (y no se pasa positive_node explicito). """ if not control_image: raise ValueError( "comfyui_inject_controlnet: control_image no puede estar vacio " "(ControlNet necesita una imagen de control en input/)." ) 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_controlnet: no se encontro ningun KSampler en el workflow." ) ks_inputs = wf[ksampler_id].get("inputs", {}) if positive_node is not None: pos_src = [positive_node, 0] elif _is_link(ks_inputs.get("positive")): pos_src = list(ks_inputs["positive"]) else: raise ValueError( "comfyui_inject_controlnet: no se pudo determinar la fuente del " "condicionamiento positivo; pasa positive_node explicito." ) 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) wf[load_id] = { "class_type": "LoadImage", "inputs": {"image": control_image}, } wf[loader_id] = { "class_type": "ControlNetLoader", "inputs": {"control_net_name": cn_name}, } wf[apply_id] = { "class_type": "ControlNetApply", "inputs": { "conditioning": list(pos_src), "control_net": [loader_id, 0], "image": [load_id, 0], "strength": strength, }, } # Repunta el KSampler para que tome el positivo condicionado por el ControlNet. wf[ksampler_id]["inputs"]["positive"] = [apply_id, 0] return wf if __name__ == "__main__": import json import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow base = comfyui_build_txt2img_workflow("dreamshaper_8.safetensors", "a knight, dramatic") wf = comfyui_inject_controlnet( base, "pose_canny.png", "control_v11p_sd15_canny.pth", strength=0.8 ) print(json.dumps(wf, indent=2))