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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user