merge(ml): comfyui_list_templates + comfyui_extract_template (extraer grafos de templates oficiales)
This commit is contained in:
@@ -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`.
|
||||||
@@ -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))
|
||||||
@@ -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.
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user