"""Valida un workflow ComfyUI contra el catalogo /object_info del servidor. Cruza los class_type del workflow contra los nodos disponibles y los nombres de modelos (checkpoints, loras, vae, controlnet, upscale) contra los combos enumerados de cada nodo. Asi se detectan nodos o modelos faltantes ANTES de encolar (POST /prompt), evitando un HTTP 400 del servidor. Compone comfyui_object_info (no reimplementa la consulta HTTP). Impura: red (HTTP GET via comfyui_object_info). Solo stdlib. """ import os import sys _THIS_DIR = os.path.dirname(os.path.abspath(__file__)) if _THIS_DIR not in sys.path: sys.path.insert(0, _THIS_DIR) from comfyui_object_info import comfyui_object_info # noqa: E402 # inputs cuyo valor es el nombre (string) de un asset/modelo en disco _MODEL_INPUTS = { "ckpt_name", "lora_name", "vae_name", "control_net_name", "model_name", "unet_name", "clip_name", "style_model_name", "gligen_name", } def comfyui_validate_workflow( workflow: dict, server: str = "127.0.0.1:8188", timeout: float = 30.0, ) -> dict: """Valida un workflow API format contra un servidor ComfyUI vivo. Args: workflow: dict en API format ({node_id: {class_type, inputs}}). server: host:port del servidor ComfyUI (sin esquema). timeout: timeout de la consulta HTTP en segundos. Returns: dict {ok, valid, missing_nodes, missing_models, error}: - ok: la validacion se pudo ejecutar (el servidor respondio). - valid: el workflow no tiene nodos ni modelos faltantes. - missing_nodes: lista de class_type ausentes en el servidor. - missing_models: lista de {node, input, value} con valores de modelo no presentes en el combo enumerado correspondiente. - error: mensaje si no se pudo consultar el servidor (ok=False). """ try: obj_info = comfyui_object_info(server=server, timeout=timeout) except RuntimeError as exc: return { "ok": False, "valid": False, "missing_nodes": [], "missing_models": [], "error": str(exc), } missing_nodes = [] missing_models = [] for nid, node in workflow.items(): if not isinstance(node, dict): continue ctype = node.get("class_type") if ctype not in obj_info: missing_nodes.append(ctype) continue spec = obj_info[ctype].get("input", {}) allowed = {} for section in ("required", "optional"): for name, decl in (spec.get(section) or {}).items(): if isinstance(decl, list) and decl and isinstance(decl[0], list): allowed[name] = set(decl[0]) for in_name, val in node.get("inputs", {}).items(): if in_name in _MODEL_INPUTS and isinstance(val, str): opts = allowed.get(in_name) if opts is not None and val not in opts: missing_models.append( {"node": nid, "input": in_name, "value": val} ) # dedup missing_nodes preservando orden seen = set() missing_nodes = [c for c in missing_nodes if not (c in seen or seen.add(c))] valid = not missing_nodes and not missing_models return { "ok": True, "valid": valid, "missing_nodes": missing_nodes, "missing_models": missing_models, "error": "", } if __name__ == "__main__": import json sys.path.insert(0, _THIS_DIR) from comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow wf = comfyui_build_txt2img_workflow("dreamshaper_8.safetensors", "a cat") print(json.dumps(comfyui_validate_workflow(wf), indent=2))