chore: auto-commit (5 archivos)

- docs/capabilities/comfyui.md
- python/functions/ml/comfyui_import_workflow_json.md
- python/functions/ml/comfyui_import_workflow_json.py
- python/functions/pipelines/comfyui_text_to_3d_oneshot.md
- python/functions/pipelines/comfyui_text_to_3d_oneshot.py

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 01:52:46 +02:00
parent d3f05a19a5
commit 337f75b527
5 changed files with 417 additions and 9 deletions
@@ -3,10 +3,10 @@ name: comfyui_import_workflow_json
kind: function
lang: py
domain: ml
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "def comfyui_import_workflow_json(source: str, *, server: str = \"127.0.0.1:8188\", timeout: float = 15.0) -> dict"
description: "Lee un workflow ComfyUI desde una URL (http/https) o un path local y lo normaliza a API format. Si viene en formato UI graph ({nodes, links}) lo convierte a API format usando /object_info para mapear los widgets; si ya es API format lo devuelve tal cual. Compone comfyui_object_info. Impura: HTTP GET / lectura de disco."
description: "Lee un workflow ComfyUI desde una URL (http/https) o un path local y lo normaliza a API format. Si viene en formato UI graph ({nodes, links}) lo convierte a API format usando /object_info para mapear los widgets; si ya es API format lo devuelve tal cual. Omite los nodos virtuales del editor (Note, MarkdownNote, PrimitiveNode, Reroute) tal como hace ComfyUI al pasar UI->API: resuelve los Reroute reconectando la conexion directa origen->destino e inyecta los PrimitiveNode como valor de widget en el consumidor. Compone comfyui_object_info. Impura: HTTP GET / lectura de disco."
tags: [comfyui, ml, import, workflow, stable-diffusion]
uses_functions: [comfyui_object_info_py_ml]
uses_types: []
@@ -66,3 +66,20 @@ en un PNG usa `comfyui_import_workflow_png`.
- API format se detecta porque todos los valores top-level son dicts con
`class_type`; UI graph por la clave `nodes`. Otros JSON dan
"formato no reconocido".
- Los nodos virtuales del editor (`Note`, `MarkdownNote`, `PrimitiveNode`,
`Reroute` y variantes `Reroute*`) NO aparecen en el API format resultante —
igual que cuando ComfyUI exporta UI->API. Los `Reroute` se resuelven saltando
el passthrough y reconectando el origen real al consumidor; una cadena de
Reroutes rota (entrada sin link) o con ciclo deja el input sin conexion en
lugar de apuntar a un nodo inexistente. Los `PrimitiveNode` se inyectan como
valor literal de widget en el consumidor (su `widgets_values[0]`).
- El filtrado es idempotente: un workflow ya en API format (sin nodos virtuales)
pasa intacto; un UI graph sin virtuales conserva todas sus conexiones.
## Capability growth log
- v1.1.0 (2026-06-24) — la conversion UI->API omite los nodos virtuales del
editor (Note/MarkdownNote/PrimitiveNode/Reroute), resuelve los Reroute
reconectando origen->destino e inyecta los PrimitiveNode como valor de widget.
Antes esos nodos viajaban al API format y `comfyui_validate_workflow` los
marcaba como `missing_nodes` (falsos positivos). Gap del report 0086.
@@ -79,8 +79,57 @@ def comfyui_import_workflow_json(
"error": "formato de workflow no reconocido (ni API ni UI graph)"}
# Node types virtuales del editor de ComfyUI: solo existen en el UI graph y se
# descartan al pasar UI -> API (ComfyUI hace lo mismo). Note/MarkdownNote son
# anotaciones; PrimitiveNode inyecta un valor de widget; Reroute es un passthrough
# de una conexion (se resuelve reconectando origen real -> destino).
_NOTE_TYPES = {"Note", "MarkdownNote"}
def _is_reroute(ctype) -> bool:
"""True si el node type es un Reroute (nativo 'Reroute' o variantes custom)."""
return isinstance(ctype, str) and ctype.startswith("Reroute")
def _is_virtual(ctype) -> bool:
"""True si el node type es virtual del editor (no va al API format)."""
return ctype in _NOTE_TYPES or ctype == "PrimitiveNode" or _is_reroute(ctype)
def _resolve_source(src_node, src_slot, node_by_id, link_src, _depth=0):
"""Resuelve el origen real de una conexion saltando los nodos Reroute.
Un Reroute en el UI graph es un passthrough: su salida solo reenvia lo que
llega a su unica entrada. Para producir API format hay que reconectar el
consumidor directamente al origen real (origen -> destino, sin el Reroute).
Devuelve (node_id, slot) del nodo no-Reroute al que se conecta, o None si la
cadena de Reroutes esta rota (entrada sin link) o forma un ciclo.
"""
if _depth > 64:
return None # ciclo de Reroutes: aborta la resolucion.
node = node_by_id.get(src_node)
if node is None or not _is_reroute(node.get("type")):
return (src_node, src_slot)
link = None
for inp in node.get("inputs", []) or []:
if inp.get("link") is not None:
link = inp["link"]
break
if link is None or link not in link_src:
return None # Reroute sin entrada conectada: link muerto.
nxt_node, nxt_slot = link_src[link]
return _resolve_source(nxt_node, nxt_slot, node_by_id, link_src, _depth + 1)
def _ui_graph_to_api(graph: dict, obj_info) -> dict:
"""Convierte un UI graph de ComfyUI a API format (best-effort)."""
"""Convierte un UI graph de ComfyUI a API format (best-effort).
Omite los nodos virtuales del editor (Note, MarkdownNote, PrimitiveNode,
Reroute) tal como hace ComfyUI al pasar de UI a API: las anotaciones se
descartan, los Reroute se resuelven reconectando la conexion directa
origen->destino, y los PrimitiveNode se inyectan como valor de widget en el
consumidor.
"""
nodes = graph.get("nodes", []) or []
links = graph.get("links", []) or []
# link_id -> (src_node_id, src_slot)
@@ -88,22 +137,38 @@ def _ui_graph_to_api(graph: dict, obj_info) -> dict:
for lk in links:
if isinstance(lk, list) and len(lk) >= 5:
link_src[lk[0]] = (str(lk[1]), lk[2])
# node_id (str) -> node dict, para TODOS los nodos (incluidos los virtuales),
# necesario para resolver Reroutes e inyectar valores de PrimitiveNode.
node_by_id = {str(n.get("id")): n for n in nodes if n.get("id") is not None}
api = {}
for node in nodes:
ctype = node.get("type")
if ctype is None:
continue
if ctype is None or _is_virtual(ctype):
continue # los virtuales no existen en API format.
nid = str(node.get("id"))
inputs = {}
connected = set()
for inp in node.get("inputs", []) or []:
name = inp.get("name")
link = inp.get("link")
if name is not None and link is not None and link in link_src:
src_node, src_slot = link_src[link]
inputs[name] = [src_node, src_slot]
connected.add(name)
if name is None or link is None or link not in link_src:
continue
src_node, src_slot = link_src[link]
resolved = _resolve_source(src_node, src_slot, node_by_id, link_src)
if resolved is None:
continue # cadena de Reroutes rota: el input queda sin conexion.
rnode, rslot = resolved
src = node_by_id.get(rnode)
if src is not None and src.get("type") == "PrimitiveNode":
# PrimitiveNode: inyecta su valor constante como widget, no como link.
wv = src.get("widgets_values")
if isinstance(wv, list) and wv:
inputs[name] = wv[0]
connected.add(name)
continue
inputs[name] = [rnode, rslot]
connected.add(name)
widgets = node.get("widgets_values")
if isinstance(widgets, dict):
inputs.update(widgets)