From e178ab8d2d1b52375492245c4a6b0042d82bd26a Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 27 Jun 2026 20:35:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(ml):=20comfyui=5Flist=5Ftemplates=20+=20co?= =?UTF-8?q?mfyui=5Fextract=5Ftemplate=20=E2=80=94=20extraer=20grafos=20de?= =?UTF-8?q?=20los=20templates=20oficiales=20de=20ComfyUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capitaliza el descubrimiento y extraccion de los workflow templates oficiales que trae el paquete pip comfyui-workflow-templates 0.10.3 (los del menu Browse Templates del frontend de ComfyUI). Hasta ahora no habia forma programatica de listarlos ni extraer su grafo de nodos. - comfyui_list_templates: lista los 451 templates reales (nombre, bundle/categoria, path, n_nodes, node_types). Filtra las ~16 entradas index* no-workflow. - comfyui_extract_template: extrae el grafo + class_types de un template por nombre; to_api convierte a API format reusando comfyui_import_workflow_json. Desde la 0.10.x el paquete es multi-bundle y ya no expone una carpeta templates/ unica; ambas funciones usan la API oficial comfyui_workflow_templates_core via el interprete de ComfyUI. node_types aplana subgrafos y descarta los UUID de instancia. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../functions/ml/comfyui_extract_template.md | 79 +++++ .../functions/ml/comfyui_extract_template.py | 302 ++++++++++++++++++ python/functions/ml/comfyui_list_templates.md | 82 +++++ python/functions/ml/comfyui_list_templates.py | 284 ++++++++++++++++ 4 files changed, 747 insertions(+) create mode 100644 python/functions/ml/comfyui_extract_template.md create mode 100644 python/functions/ml/comfyui_extract_template.py create mode 100644 python/functions/ml/comfyui_list_templates.md create mode 100644 python/functions/ml/comfyui_list_templates.py diff --git a/python/functions/ml/comfyui_extract_template.md b/python/functions/ml/comfyui_extract_template.md new file mode 100644 index 00000000..e88351eb --- /dev/null +++ b/python/functions/ml/comfyui_extract_template.md @@ -0,0 +1,79 @@ +--- +name: comfyui_extract_template +kind: function +lang: py +domain: ml +version: "1.0.0" +purity: impure +signature: "def comfyui_extract_template(name: str, comfyui_python: str | None = None, to_api: bool = False, server: str = \"127.0.0.1:8188\") -> dict" +description: "Extrae el grafo de nodos de un workflow template oficial de ComfyUI por su template_id. Devuelve el grafo completo (formato UI: nodes/links), la lista de class_types que usa (aplanando subgrafos y descartando UUID de instancia), el formato, el bundle y los assets en disco. Opcionalmente (to_api=True) convierte el grafo UI a API format reutilizando comfyui_import_workflow_json (requiere un servidor ComfyUI vivo). Nombre inexistente -> error legible con sugerencias, sin traceback. Localiza el interprete de ComfyUI y usa su API oficial via subprocess. Impura: lee disco (+ red opcional si to_api)." +tags: [comfyui, ml, templates, workflow, extract] +uses_functions: ["comfyui_import_workflow_json_py_ml"] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: name + desc: "template_id exacto del template (p.ej. 'sdxl_simple_example', 'image_sdxl'). Usa comfyui_list_templates para ver los nombres disponibles." + - name: comfyui_python + desc: "Ruta al interprete python de ComfyUI con el paquete comfyui-workflow-templates. None autodetecta (env COMFYUI_PYTHON, ~/ComfyUI/.venv/bin/python)." + - name: to_api + desc: "True intenta convertir el grafo UI a API format via comfyui_import_workflow_json (requiere servidor ComfyUI vivo en `server`). Si falla, el grafo UI se devuelve igualmente y el motivo va en api_error." + - name: server + desc: "host:port del servidor ComfyUI usado para la conversion to_api (default '127.0.0.1:8188')." +output: "dict {ok, name, format, class_types, has_subgraphs, n_nodes, graph, api_workflow, api_error, bundle, version, assets, error}. graph = dict del template (formato UI o API). class_types = lista ordenada de tipos de nodo reales. api_workflow = dict API si to_api tuvo exito, si no {}. Nunca lanza: nombre inexistente -> ok=False con error + sugerencias." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/ml/comfyui_extract_template.py" +--- + +## Ejemplo + +```bash +# Lanzable directo (grafo slim + class_types de un template concreto): +python/.venv/bin/python3 python/functions/ml/comfyui_extract_template.py sdxl_simple_example + +# Con conversion a API format (necesita ComfyUI corriendo en 127.0.0.1:8188): +python/.venv/bin/python3 python/functions/ml/comfyui_extract_template.py sdxl_simple_example --to-api +``` + +```python +import sys, os +sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions")) +from ml.comfyui_extract_template import comfyui_extract_template + +res = comfyui_extract_template("sdxl_simple_example") +print(res["format"], res["n_nodes"], "nodos") # ui_graph 25 nodos +print(res["class_types"]) # ['CheckpointLoaderSimple', 'KSamplerAdvanced', ...] +graph = res["graph"] # dict cargable en la UI tal cual +``` + +## Cuando usarla + +Cuando quieras reutilizar la estructura de nodos de un template oficial: cargar su +grafo en tu UI, usarlo de base para un workflow propio, o saber exactamente que +class_types encadena. Segundo paso del flujo listar (`comfyui_list_templates`) -> +extraer. Para encolar el resultado en `/prompt` usa `to_api=True` (o pasa el grafo por +`comfyui_import_workflow_json`). + +## Gotchas + +- El grafo viene en **formato UI** (nodes/links con posiciones), no en API format. La + UI de ComfyUI lo entiende tal cual (cargalo o copia el dict); para `/prompt` hay que + convertirlo a API format con `to_api=True`. +- `to_api=True` reutiliza `comfyui_import_workflow_json`, que necesita un **servidor + ComfyUI vivo** para mapear los widgets a sus claves de input. Sin servidor, la + extraccion del grafo UI sigue funcionando (ok=True) y el motivo del fallo de + conversion va en `api_error` (no rompe). KISS: no se fuerza la conversion. +- Templates **subgraphed** (con `definitions.subgraphs`, `has_subgraphs=True`): la + conversion a API NO expande el subgraph (limitacion de la normalizacion UI->API + estandar), asi que `api_workflow` puede quedar con solo los nodos top-level. Para + esos, cargar el grafo UI en la UI es lo fiable. `class_types` sí incluye los nodos + reales de dentro del subgraph. +- Nombre inexistente -> `ok=False` con `error` legible y sugerencias por substring (o + difflib). No lanza traceback. +- El paquete vive en el venv de ComfyUI; si no se encuentra el interprete o el paquete, + `ok=False` indicando `pip install comfyui-workflow-templates`. diff --git a/python/functions/ml/comfyui_extract_template.py b/python/functions/ml/comfyui_extract_template.py new file mode 100644 index 00000000..6a132604 --- /dev/null +++ b/python/functions/ml/comfyui_extract_template.py @@ -0,0 +1,302 @@ +"""Extrae el grafo de nodos de un workflow template oficial de ComfyUI por su nombre. + +Funcion impura: lee disco (el .json del template instalado) ejecutando la API oficial +del paquete comfyui-workflow-templates dentro del interprete de ComfyUI. + +Dado el nombre de un template (su template_id, p.ej. "image_sdxl" o +"api_bfl_flux2_max_sofa_swap"), devuelve: + - graph: el dict completo del .json (formato UI: nodes/links con posiciones). + - class_types: la lista de tipos de nodo (class_type) que usa, aplanando los + subgrafos de `definitions` si los hay. + - format: "ui_graph" (lo normal en los templates) o "api". + - assets: rutas en disco de los ficheros del template (json + previews .webp). + +Opcionalmente (to_api=True) intenta convertir el grafo UI a API format reutilizando +comfyui_import_workflow_json del registry. Esa conversion necesita un servidor ComfyUI +vivo para mapear los widgets a sus claves de input; si no lo hay, se devuelve el grafo +UI + class_types igualmente y se reporta el motivo en api_error (KISS: no se fuerza la +conversion de grafos complejos). + +El paquete vive en el venv de ComfyUI (no en el del registry), por eso esta funcion no +lo importa: localiza el interprete de ComfyUI y le pasa un script que usa la API oficial. +""" +import json +import os +import subprocess +import sys +import tempfile + +_THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +if _THIS_DIR not in sys.path: + sys.path.insert(0, _THIS_DIR) + + +# Script que corre DENTRO del python de ComfyUI. Resuelve un template por id, vuelca su +# grafo + metadata como JSON. Si no existe, devuelve sugerencias cercanas. +_EXTRACT_SCRIPT = r""" +import json, sys, difflib, re +try: + import comfyui_workflow_templates_core as core +except Exception as exc: + print(json.dumps({"__err__": "import", "msg": str(exc)})) + sys.exit(0) + +_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") + +TID = json.loads({tid_json!r}) +m = core.load_manifest() +if TID not in m.templates: + near = [k for k in m.templates if TID.lower() in k.lower()][:8] + if not near: + near = difflib.get_close_matches(TID, list(m.templates.keys()), n=8, cutoff=0.6) + print(json.dumps({"__err__": "not_found", "suggestions": near})) + sys.exit(0) + +entry = m.templates[TID] +json_asset = next((a.filename for a in entry.assets if a.filename.endswith(".json")), None) +if not json_asset: + print(json.dumps({"__err__": "no_json"})) + sys.exit(0) + +path = core.get_asset_path(TID, json_asset) +with open(path, encoding="utf-8") as fh: + graph = json.load(fh) + +# Detecta formato y extrae class_types. +fmt = "unknown" +class_types = set() +has_subgraphs = False +if isinstance(graph, dict) and isinstance(graph.get("nodes"), list): + fmt = "ui_graph" + for n in graph["nodes"]: + t = n.get("type") if isinstance(n, dict) else None + if t and not _UUID_RE.match(str(t)): + class_types.add(t) + defs = graph.get("definitions") + if isinstance(defs, dict) and isinstance(defs.get("subgraphs"), list): + for sg in defs["subgraphs"]: + for n in (sg.get("nodes") or []) if isinstance(sg, dict) else []: + if isinstance(n, dict) and n.get("type"): + has_subgraphs = True + if not _UUID_RE.match(str(n["type"])): + class_types.add(n["type"]) +elif isinstance(graph, dict): + fmt = "api" + for v in graph.values(): + if isinstance(v, dict) and v.get("class_type"): + class_types.add(v["class_type"]) + +print(json.dumps({ + "graph": graph, + "class_types": sorted(class_types), + "format": fmt, + "has_subgraphs": has_subgraphs, + "bundle": entry.bundle, + "version": entry.version, + "assets": core.resolve_all_assets(TID), + "json_path": path, +})) +""" + + +def _find_comfyui_python(explicit: str | None) -> str | None: + """Localiza un interprete de ComfyUI con el paquete instalado (ver list_templates).""" + candidates = [] + if explicit: + candidates.append(os.path.expanduser(explicit)) + env = os.environ.get("COMFYUI_PYTHON") + if env: + candidates.append(os.path.expanduser(env)) + candidates += [ + os.path.expanduser("~/ComfyUI/.venv/bin/python"), + os.path.expanduser("~/ComfyUI/venv/bin/python"), + os.path.expanduser("~/comfyui/.venv/bin/python"), + sys.executable, + ] + for c in candidates: + if c and os.path.isfile(c): + return c + return None + + +def comfyui_extract_template( + name: str, + comfyui_python: str | None = None, + to_api: bool = False, + server: str = "127.0.0.1:8188", +) -> dict: + """Extrae el grafo y los class_types de un template oficial de ComfyUI por nombre. + + Args: + name: template_id exacto del template (p.ej. "image_sdxl"). Usa + comfyui_list_templates para ver los nombres disponibles. + comfyui_python: ruta al interprete python de ComfyUI con el paquete + comfyui-workflow-templates. Si None, se autodetecta. + to_api: si True, intenta convertir el grafo UI a API format reutilizando + comfyui_import_workflow_json (requiere un servidor ComfyUI vivo en + `server`). Si la conversion falla, se devuelve el grafo UI igualmente y + el motivo va en api_error. + server: host:port del servidor ComfyUI para la conversion to_api. + + Returns: + dict {ok, name, format, class_types, has_subgraphs, n_nodes, graph, + api_workflow, api_error, bundle, version, assets, error}: + - graph: el dict del template en formato UI (o API si ya lo estaba). + - class_types: lista ordenada de tipos de nodo del grafo (incluye los de + subgrafos de `definitions`). + - api_workflow: dict en API format si to_api tuvo exito, si no {}. + Nunca lanza. Nombre inexistente -> ok=False con error legible + sugerencias. + """ + py = _find_comfyui_python(comfyui_python) + base = { + "ok": False, + "name": name, + "format": "", + "class_types": [], + "has_subgraphs": False, + "n_nodes": 0, + "graph": {}, + "api_workflow": {}, + "api_error": "", + "bundle": "", + "version": "", + "assets": [], + "error": "", + } + if not py: + base["error"] = ( + "no se encontro un interprete de ComfyUI. Pasa comfyui_python=... o " + "define COMFYUI_PYTHON. Instala el paquete con: " + "pip install comfyui-workflow-templates" + ) + return base + + script = _EXTRACT_SCRIPT.replace("{tid_json!r}", repr(json.dumps(name))) + try: + proc = subprocess.run( + [py, "-c", script], + capture_output=True, + text=True, + timeout=60, + ) + except Exception as exc: # noqa: BLE001 + base["error"] = f"fallo al ejecutar el interprete de ComfyUI ({py}): {exc}" + return base + + if proc.returncode != 0: + base["error"] = f"el interprete de ComfyUI fallo: {proc.stderr.strip()[:500]}" + return base + + try: + data = json.loads(proc.stdout.strip().splitlines()[-1]) + except Exception as exc: # noqa: BLE001 + base["error"] = f"salida no parseable del interprete de ComfyUI: {exc}" + return base + + err = data.get("__err__") + if err == "import": + base["error"] = ( + f"el paquete comfyui-workflow-templates no esta instalado en {py} " + f"({data.get('msg', '')}). Instalalo con: " + "pip install comfyui-workflow-templates" + ) + return base + if err == "not_found": + sug = data.get("suggestions", []) + hint = f" ¿Quizas: {', '.join(sug)}?" if sug else "" + base["error"] = f"template '{name}' no existe en el paquete.{hint}" + return base + if err == "no_json": + base["error"] = f"el template '{name}' no tiene asset .json." + return base + + graph = data.get("graph", {}) + fmt = data.get("format", "") + nodes = graph.get("nodes") if isinstance(graph, dict) else None + n_nodes = len(nodes) if isinstance(nodes, list) else ( + len(graph) if fmt == "api" and isinstance(graph, dict) else 0 + ) + + out = { + "ok": True, + "name": name, + "format": fmt, + "class_types": data.get("class_types", []), + "has_subgraphs": data.get("has_subgraphs", False), + "n_nodes": n_nodes, + "graph": graph, + "api_workflow": {}, + "api_error": "", + "bundle": data.get("bundle", ""), + "version": data.get("version", ""), + "assets": data.get("assets", []), + "error": "", + } + + if to_api: + if fmt == "api": + out["api_workflow"] = graph + else: + out["api_workflow"], out["api_error"] = _convert_to_api(graph, server) + + return out + + +def _convert_to_api(graph: dict, server: str) -> tuple[dict, str]: + """Convierte un grafo UI a API format via comfyui_import_workflow_json del registry. + + Requiere un servidor ComfyUI vivo para mapear widgets. Devuelve (workflow, "") + si tuvo exito o ({}, motivo) si fallo. No lanza. + """ + try: + from comfyui_import_workflow_json import comfyui_import_workflow_json + except Exception as exc: # noqa: BLE001 + return {}, f"no se pudo importar comfyui_import_workflow_json: {exc}" + + tmp = None + try: + with tempfile.NamedTemporaryFile( + "w", suffix=".json", delete=False, encoding="utf-8" + ) as fh: + json.dump(graph, fh) + tmp = fh.name + res = comfyui_import_workflow_json(tmp, server=server) + if res.get("ok"): + return res.get("workflow", {}), "" + return {}, ( + res.get("error", "conversion fallida") + + f" (requiere un servidor ComfyUI vivo en {server})" + ) + except Exception as exc: # noqa: BLE001 + return {}, f"conversion to_api fallida: {exc}" + finally: + if tmp and os.path.exists(tmp): + try: + os.unlink(tmp) + except OSError: + pass + + +if __name__ == "__main__": + import argparse + + ap = argparse.ArgumentParser(description="Extrae el grafo de un template ComfyUI") + ap.add_argument("name", help="template_id (ver comfyui_list_templates)") + ap.add_argument("--comfyui-python", default=None) + ap.add_argument("--to-api", action="store_true") + ap.add_argument("--server", default="127.0.0.1:8188") + ap.add_argument("--full", action="store_true", help="incluye el grafo entero") + args = ap.parse_args() + + res = comfyui_extract_template( + args.name, + args.comfyui_python, + to_api=args.to_api, + server=args.server, + ) + if args.full or not res["ok"]: + print(json.dumps(res, indent=2, ensure_ascii=False)) + else: + slim = {k: v for k, v in res.items() if k != "graph"} + slim["graph_keys"] = list(res["graph"].keys()) if isinstance(res["graph"], dict) else [] + print(json.dumps(slim, indent=2, ensure_ascii=False)) diff --git a/python/functions/ml/comfyui_list_templates.md b/python/functions/ml/comfyui_list_templates.md new file mode 100644 index 00000000..1155eb05 --- /dev/null +++ b/python/functions/ml/comfyui_list_templates.md @@ -0,0 +1,82 @@ +--- +name: comfyui_list_templates +kind: function +lang: py +domain: ml +version: "1.0.0" +purity: impure +signature: "def comfyui_list_templates(comfyui_python: str | None = None, bundle: str | None = None, name_filter: str | None = None, with_nodes: bool = True, workflows_only: bool = True, limit: int = 0) -> dict" +description: "Lista los workflow templates oficiales del paquete pip comfyui-workflow-templates (los del menu 'Browse Templates' del frontend de ComfyUI). Devuelve nombre, bundle/categoria, path en disco, n_nodes y node_types (class_types reales, aplanando subgrafos y descartando los UUID de instancia). Localiza el interprete de ComfyUI y usa su API oficial via subprocess (el paquete vive en el venv de ComfyUI, no en el del registry). Impura: lee disco. Filtra entradas no-workflow (index*/localizacion) por defecto." +tags: [comfyui, ml, templates, workflow, discovery] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: comfyui_python + desc: "Ruta al interprete python de ComfyUI con el paquete comfyui-workflow-templates instalado. None autodetecta (env COMFYUI_PYTHON, ~/ComfyUI/.venv/bin/python, ~/ComfyUI/venv/bin/python)." + - name: bundle + desc: "Filtra por bundle exacto: 'media-api', 'media-image', 'media-video' o 'media-other'. None = todos." + - name: name_filter + desc: "Subcadena (case-insensitive) que debe contener el nombre del template. None = sin filtro." + - name: with_nodes + desc: "True (default) incluye node_types en cada registro; False los omite (registros mas ligeros)." + - name: workflows_only + desc: "True (default) excluye entradas que no son grafos de workflow (ficheros index*/localizacion del paquete)." + - name: limit + desc: "Si > 0, trunca a los primeros N templates tras filtrar y ordenar por nombre." +output: "dict {ok: bool, count: int, package_version: str, templates: list, error: str}. Cada template: {name, category, bundle, version, path, n_nodes, node_types, is_workflow}. Nunca lanza: paquete ausente o interprete no hallado -> ok=False con error legible que indica como instalar (pip install comfyui-workflow-templates)." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/ml/comfyui_list_templates.py" +--- + +## Ejemplo + +```bash +# Lanzable directo (muestra version del paquete + 15 primeros con sus node_types): +./fn run comfyui_list_templates + +# Filtrado por bundle de imagen, sin abrir node_types, primeros 20: +python/.venv/bin/python3 python/functions/ml/comfyui_list_templates.py \ + --bundle media-image --no-nodes --limit 20 +``` + +```python +import sys, os +sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions")) +from ml.comfyui_list_templates import comfyui_list_templates + +res = comfyui_list_templates(name_filter="sdxl") +print(res["count"], "templates SDXL") # p.ej. 4 +for t in res["templates"]: + print(t["name"], t["n_nodes"], t["node_types"][:3]) +``` + +## Cuando usarla + +Para descubrir que workflow templates oficiales trae ComfyUI sin abrir la UI: +explorar el catalogo, filtrar por bundle/nombre, o saber que `node_types` usa cada +template antes de extraerlo con `comfyui_extract_template`. Primer paso del flujo +listar -> extraer -> (cargar en UI / convertir a API). + +## Gotchas + +- El paquete `comfyui-workflow-templates` vive en el venv de ComfyUI, NO en el del + registry. La funcion no lo importa: localiza el python de ComfyUI y corre su API + oficial en un subprocess. Si no encuentra ese interprete (o el paquete no esta + instalado) devuelve `ok=False` con un error que dice como instalarlo. No lanza. +- Desde la 0.10.x el paquete es multi-bundle y ya NO expone una carpeta `templates/` + unica (la API antigua `get_templates_path()` lanza a proposito). Por eso se usa + `comfyui_workflow_templates_core` (`load_manifest`/`get_asset_path`). +- `node_types` aplana los subgrafos de `definitions` y descarta los `type` que son + UUID (instancias de subgraph), para mostrar class_types reales (KSampler, CLIPLoader, + …) en vez de identificadores opacos. `n_nodes` cuenta solo los nodos top-level. +- `workflows_only=True` (default) excluye ~16 entradas `index*` que son metadata de + localizacion del frontend, no grafos. Pasa `workflows_only=False` (o `--all` en CLI) + para verlas. +- Impura: abre cada `.json` en disco (≈451 ficheros pequeños, ~0.2s). No toca red ni + arranca GPU. diff --git a/python/functions/ml/comfyui_list_templates.py b/python/functions/ml/comfyui_list_templates.py new file mode 100644 index 00000000..37511eb2 --- /dev/null +++ b/python/functions/ml/comfyui_list_templates.py @@ -0,0 +1,284 @@ +"""Lista los workflow templates oficiales que trae el paquete comfyui-workflow-templates. + +Funcion impura: lee disco (los .json de los templates instalados) ejecutando la +API oficial del paquete dentro del interprete de ComfyUI. + +ComfyUI 0.26+ distribuye los templates oficiales (los del menu "Browse Templates" +del frontend) en el paquete pip `comfyui-workflow-templates`, que desde la 0.10.x es +un meta-paquete multi-bundle: ya NO expone una carpeta `templates/` unica, sino una +API en `comfyui_workflow_templates_core` (`load_manifest`, `iter_templates`, +`get_asset_path`). Cada template es un grafo de nodos en formato UI (nodes/links con +posiciones), agrupado en uno de cuatro bundles: media-api, media-image, media-video, +media-other. + +Como el paquete vive en el venv de ComfyUI (no en el del registry), esta funcion no +lo importa directamente: localiza el interprete de ComfyUI y le pasa un script que usa +la API oficial y vuelca el catalogo como JSON. Asi es robusta ante cambios de la +estructura interna del paquete. +""" +import json +import os +import subprocess +import sys + + +# Script que corre DENTRO del python de ComfyUI. Usa la API oficial del paquete y +# vuelca el catalogo (metadata + node_types por template) como una linea JSON. +_DUMP_SCRIPT = r""" +import json, sys, re +try: + import comfyui_workflow_templates_core as core +except Exception as exc: + print(json.dumps({"__err__": "import", "msg": str(exc)})) + sys.exit(0) + +_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") + +def _collect_types(graph): + # Recoge class_types reales: aplana los subgrafos de definitions y descarta los + # type que son UUID (instancias de subgraph, cuyo contenido real ya se incluye). + types = set() + if isinstance(graph, dict) and isinstance(graph.get("nodes"), list): + for n in graph["nodes"]: + if isinstance(n, dict) and n.get("type") and not _UUID_RE.match(str(n["type"])): + types.add(n["type"]) + defs = graph.get("definitions") + if isinstance(defs, dict) and isinstance(defs.get("subgraphs"), list): + for sg in defs["subgraphs"]: + for n in (sg.get("nodes") or []) if isinstance(sg, dict) else []: + if isinstance(n, dict) and n.get("type") and not _UUID_RE.match(str(n["type"])): + types.add(n["type"]) + return len(graph["nodes"]), sorted(types) + if isinstance(graph, dict): # API format + for v in graph.values(): + if isinstance(v, dict) and v.get("class_type"): + types.add(v["class_type"]) + if types: + return len(graph), sorted(types) + return 0, [] + +WITH_NODES = {with_nodes} +m = core.load_manifest() +try: + import importlib.metadata as _md + pkg_version = _md.version("comfyui-workflow-templates") +except Exception: + pkg_version = "" + +out = [] +for tid, entry in m.templates.items(): + json_asset = next( + (a.filename for a in entry.assets if a.filename.endswith(".json")), None + ) + path = core.get_asset_path(tid, json_asset) if json_asset else "" + rec = { + "name": tid, + "bundle": entry.bundle, + "category": entry.bundle, + "version": entry.version, + "path": path, + "n_nodes": 0, + "node_types": [], + } + rec["is_workflow"] = False + if path: + try: + with open(path, encoding="utf-8") as fh: + graph = json.load(fh) + n_nodes, node_types = _collect_types(graph) + is_api = isinstance(graph, dict) and any( + isinstance(v, dict) and v.get("class_type") for v in graph.values() + ) + rec["is_workflow"] = bool( + (isinstance(graph, dict) and isinstance(graph.get("nodes"), list) and graph["nodes"]) + or is_api + ) + rec["n_nodes"] = n_nodes + if WITH_NODES: + rec["node_types"] = node_types + except Exception: + pass + out.append(rec) + +print(json.dumps({"package_version": pkg_version, "templates": out})) +""" + + +def _find_comfyui_python(explicit: str | None) -> str | None: + """Devuelve la ruta a un interprete de ComfyUI que tenga el paquete instalado. + + Orden de busqueda: argumento explicito -> env COMFYUI_PYTHON -> candidatos + habituales (~/ComfyUI/.venv, ~/ComfyUI/venv) -> el python actual. Devuelve None + si ninguno existe en disco. + """ + candidates = [] + if explicit: + candidates.append(os.path.expanduser(explicit)) + env = os.environ.get("COMFYUI_PYTHON") + if env: + candidates.append(os.path.expanduser(env)) + candidates += [ + os.path.expanduser("~/ComfyUI/.venv/bin/python"), + os.path.expanduser("~/ComfyUI/venv/bin/python"), + os.path.expanduser("~/comfyui/.venv/bin/python"), + sys.executable, + ] + for c in candidates: + if c and os.path.isfile(c): + return c + return None + + +def comfyui_list_templates( + comfyui_python: str | None = None, + bundle: str | None = None, + name_filter: str | None = None, + with_nodes: bool = True, + workflows_only: bool = True, + limit: int = 0, +) -> dict: + """Lista los templates oficiales de ComfyUI con su grafo de nodos. + + Args: + comfyui_python: ruta al interprete python de ComfyUI que tiene instalado + el paquete comfyui-workflow-templates. Si None, se autodetecta (env + COMFYUI_PYTHON o ~/ComfyUI/.venv/bin/python). + bundle: si se da, filtra por bundle exacto ("media-api", "media-image", + "media-video", "media-other"). + name_filter: si se da, filtra a templates cuyo nombre contenga esta + subcadena (case-insensitive). + with_nodes: si True (default) incluye node_types en cada registro. Si + False los omite (registros mas ligeros). + workflows_only: si True (default) excluye entradas que no son grafos de + workflow (ficheros index*/localizacion del paquete). + limit: si > 0, trunca la lista a los primeros N tras filtrar. + + Returns: + dict {ok, count, package_version, templates, error}: + - templates: lista de {name, category, bundle, version, path, n_nodes, + node_types} ordenada por name. + - count: numero de templates devueltos (tras filtros y limit). + Nunca lanza: cualquier fallo (paquete ausente, interprete no hallado) + devuelve ok=False con un error legible. + """ + py = _find_comfyui_python(comfyui_python) + if not py: + return { + "ok": False, + "count": 0, + "package_version": "", + "templates": [], + "error": ( + "no se encontro un interprete de ComfyUI. Pasa comfyui_python=... " + "o define COMFYUI_PYTHON. El paquete se instala con: " + "pip install comfyui-workflow-templates" + ), + } + + script = _DUMP_SCRIPT.replace("{with_nodes}", "True" if with_nodes else "False") + try: + proc = subprocess.run( + [py, "-c", script], + capture_output=True, + text=True, + timeout=120, + ) + except Exception as exc: # noqa: BLE001 + return { + "ok": False, + "count": 0, + "package_version": "", + "templates": [], + "error": f"fallo al ejecutar el interprete de ComfyUI ({py}): {exc}", + } + + if proc.returncode != 0: + return { + "ok": False, + "count": 0, + "package_version": "", + "templates": [], + "error": f"el interprete de ComfyUI fallo: {proc.stderr.strip()[:500]}", + } + + try: + data = json.loads(proc.stdout.strip().splitlines()[-1]) + except Exception as exc: # noqa: BLE001 + return { + "ok": False, + "count": 0, + "package_version": "", + "templates": [], + "error": f"salida no parseable del interprete de ComfyUI: {exc}", + } + + if data.get("__err__") == "import": + return { + "ok": False, + "count": 0, + "package_version": "", + "templates": [], + "error": ( + "el paquete comfyui-workflow-templates no esta instalado en " + f"{py} ({data.get('msg', '')}). Instalalo con: " + "pip install comfyui-workflow-templates" + ), + } + + templates = data.get("templates", []) + if workflows_only: + templates = [t for t in templates if t.get("is_workflow")] + if bundle: + templates = [t for t in templates if t.get("bundle") == bundle] + if name_filter: + nf = name_filter.lower() + templates = [t for t in templates if nf in t.get("name", "").lower()] + templates.sort(key=lambda t: t.get("name", "")) + if limit and limit > 0: + templates = templates[:limit] + + return { + "ok": True, + "count": len(templates), + "package_version": data.get("package_version", ""), + "templates": templates, + "error": "", + } + + +if __name__ == "__main__": + import argparse + + ap = argparse.ArgumentParser(description="Lista templates oficiales de ComfyUI") + ap.add_argument("--comfyui-python", default=None) + ap.add_argument("--bundle", default=None) + ap.add_argument("--name-filter", default=None) + ap.add_argument("--no-nodes", action="store_true", help="omite node_types") + ap.add_argument("--all", action="store_true", help="incluye entradas no-workflow (index*)") + ap.add_argument("--limit", type=int, default=0) + ap.add_argument("--full", action="store_true", help="dump completo (todos los node_types)") + args = ap.parse_args() + + res = comfyui_list_templates( + args.comfyui_python, + bundle=args.bundle, + name_filter=args.name_filter, + with_nodes=not args.no_nodes, + workflows_only=not args.all, + limit=args.limit, + ) + if args.full or not res["ok"]: + print(json.dumps(res, indent=2, ensure_ascii=False)) + else: + print( + json.dumps( + { + "ok": res["ok"], + "count": res["count"], + "package_version": res["package_version"], + "sample": res["templates"][:15], + }, + indent=2, + ensure_ascii=False, + ) + )