623 lines
22 KiB
Python
623 lines
22 KiB
Python
"""
|
|
Utilities for loading and processing ComfyUI workflows.
|
|
Supports both UI format (saved normally via Ctrl+S) and API format.
|
|
"""
|
|
import json
|
|
import logging
|
|
import os
|
|
import random
|
|
import re
|
|
from comfy_execution.graph_utils import GraphBuilder
|
|
|
|
log = logging.getLogger("ComfyUI-Subworkflow")
|
|
|
|
_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}$", re.I
|
|
)
|
|
|
|
SWF_SUBWORKFLOW_INPUT = "SWF_SubworkflowInput"
|
|
SWF_SUBWORKFLOW_OUTPUT = "SWF_SubworkflowOutput"
|
|
MAX_SLOTS = 8
|
|
PLACEHOLDER = "[select workflow]"
|
|
|
|
|
|
def _workflows_dir() -> str:
|
|
import folder_paths
|
|
return os.path.join(folder_paths.base_path, "user", "default", "workflows")
|
|
|
|
|
|
def list_workflow_files() -> list[str]:
|
|
d = _workflows_dir()
|
|
files = []
|
|
if os.path.isdir(d):
|
|
for root, _, fnames in os.walk(d):
|
|
for f in fnames:
|
|
if f.endswith(".json"):
|
|
rel = os.path.relpath(os.path.join(root, f), d)
|
|
files.append(rel.replace(os.sep, "/"))
|
|
return [PLACEHOLDER] + sorted(files)
|
|
|
|
|
|
def load_workflow(filename: str) -> dict:
|
|
path = os.path.join(_workflows_dir(), filename)
|
|
with open(path, encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def is_ui_format(data: dict) -> bool:
|
|
return "nodes" in data
|
|
|
|
|
|
def _sort_key(node_id: str):
|
|
try:
|
|
return (0, int(node_id))
|
|
except (ValueError, TypeError):
|
|
return (1, node_id)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# I/O discovery
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def get_workflow_io(data: dict) -> tuple[list[dict], list[dict]]:
|
|
"""
|
|
Return (inputs, outputs) sorted by node id.
|
|
Each entry: {"node_id": str, "slot_name": str}
|
|
"""
|
|
if is_ui_format(data):
|
|
return _get_workflow_io_ui(data)
|
|
return _get_workflow_io_api(data)
|
|
|
|
|
|
def _get_workflow_io_ui(data: dict) -> tuple[list[dict], list[dict]]:
|
|
inputs, outputs = [], []
|
|
for node in data.get("nodes", []):
|
|
ntype = _node_class_type(node)
|
|
nid = str(node.get("id"))
|
|
widgets = node.get("widgets_values") or []
|
|
slot_name = widgets[0] if widgets else nid
|
|
if ntype == SWF_SUBWORKFLOW_INPUT:
|
|
inputs.append({"node_id": nid, "slot_name": slot_name})
|
|
elif ntype == SWF_SUBWORKFLOW_OUTPUT:
|
|
outputs.append({"node_id": nid, "slot_name": slot_name})
|
|
inputs.sort(key=lambda x: _sort_key(x["node_id"]))
|
|
outputs.sort(key=lambda x: _sort_key(x["node_id"]))
|
|
return inputs, outputs
|
|
|
|
|
|
def _get_workflow_io_api(data: dict) -> tuple[list[dict], list[dict]]:
|
|
inputs, outputs = [], []
|
|
for nid, node in data.items():
|
|
if nid.startswith("_"):
|
|
continue
|
|
ct = node.get("class_type", "")
|
|
slot = node.get("inputs", {}).get("slot_name", nid)
|
|
if ct == SWF_SUBWORKFLOW_INPUT:
|
|
inputs.append({"node_id": nid, "slot_name": slot})
|
|
elif ct == SWF_SUBWORKFLOW_OUTPUT:
|
|
outputs.append({"node_id": nid, "slot_name": slot})
|
|
inputs.sort(key=lambda x: _sort_key(x["node_id"]))
|
|
outputs.sort(key=lambda x: _sort_key(x["node_id"]))
|
|
return inputs, outputs
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Widget value resolution (UI format only)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _node_class_type(node: dict) -> str | None:
|
|
"""
|
|
Resolve the ComfyUI class type for a UI-format node.
|
|
|
|
In standard LiteGraph format the class type lives in node["type"].
|
|
In newer ComfyUI builds the type field holds a UUID. For regular nodes
|
|
the real class name is in node["properties"]["Node name for S&R"].
|
|
For group nodes the UUID itself IS the registered class key in
|
|
NODE_CLASS_MAPPINGS, so we fall back to the UUID when the property is absent.
|
|
"""
|
|
ct = node.get("type")
|
|
if not ct:
|
|
return None
|
|
if not _UUID_RE.match(str(ct)):
|
|
return ct # plain class name, use as-is
|
|
# UUID: prefer the human-readable S&R name, fall back to UUID (group nodes)
|
|
return (node.get("properties") or {}).get("Node name for S&R") or ct
|
|
|
|
|
|
def _parse_link(lnk) -> tuple[str, str, int, str, int]:
|
|
"""
|
|
Normalise a link entry to (link_id, src_node_id, src_slot, dst_node_id, dst_slot).
|
|
Handles both the classic array format [id, src, src_slot, dst, dst_slot, type]
|
|
and the newer object format {"id":…, "origin_id":…, "origin_slot":…, …}.
|
|
"""
|
|
if isinstance(lnk, dict):
|
|
return (
|
|
str(lnk.get("id", "")),
|
|
str(lnk.get("origin_id", "")),
|
|
int(lnk.get("origin_slot", 0)),
|
|
str(lnk.get("target_id", "")),
|
|
int(lnk.get("target_slot", 0)),
|
|
)
|
|
return (str(lnk[0]), str(lnk[1]), int(lnk[2]), str(lnk[3]), int(lnk[4]))
|
|
|
|
|
|
def _iter_widget_specs(class_type: str, linked_names: set):
|
|
import nodes as _comfy_nodes
|
|
cls = _comfy_nodes.NODE_CLASS_MAPPINGS.get(class_type)
|
|
if cls is None:
|
|
return
|
|
try:
|
|
input_types = cls.INPUT_TYPES()
|
|
except Exception:
|
|
return
|
|
|
|
widget_index = 0
|
|
for category in ("required", "optional"):
|
|
for name, type_def in input_types.get(category, {}).items():
|
|
if not isinstance(type_def, (list, tuple)):
|
|
continue
|
|
input_type = type_def[0]
|
|
opts = type_def[1] if len(type_def) > 1 and isinstance(type_def[1], dict) else {}
|
|
if opts.get("forceInput"):
|
|
continue
|
|
if not (isinstance(input_type, list) or input_type in ("INT", "FLOAT", "STRING", "BOOLEAN")):
|
|
continue
|
|
|
|
skip = name in linked_names
|
|
control_index = None
|
|
if opts.get("control_after_generate"):
|
|
control_index = widget_index + 1
|
|
yield {
|
|
"name": name,
|
|
"input_type": input_type,
|
|
"opts": opts,
|
|
"index": widget_index,
|
|
"skip": skip,
|
|
"control_index": control_index,
|
|
}
|
|
widget_index += 2 if control_index is not None else 1
|
|
|
|
|
|
def _get_widget_values(class_type: str, linked_names: set, widgets_values: list) -> dict:
|
|
"""
|
|
Return {widget_name: value} by aligning widgets_values with the full ordered
|
|
widget list for the node class, including hidden control-after-generate
|
|
widgets that ComfyUI stores next to controlled numeric widgets.
|
|
"""
|
|
result = {}
|
|
for spec in _iter_widget_specs(class_type, linked_names):
|
|
index = spec["index"]
|
|
if index >= len(widgets_values):
|
|
break
|
|
if not spec["skip"]:
|
|
result[spec["name"]] = widgets_values[index]
|
|
return result
|
|
|
|
|
|
def _next_controlled_value(value, input_type: str, opts: dict, mode: str):
|
|
mode = str(mode).lower()
|
|
if mode in ("fixed", "none", ""):
|
|
return value
|
|
|
|
min_value = opts.get("min", 0)
|
|
max_value = opts.get("max", 0xffffffffffffffff if input_type == "INT" else 1.0)
|
|
step = opts.get("step", 1)
|
|
|
|
if input_type == "INT":
|
|
min_value = int(min_value)
|
|
max_value = int(max_value)
|
|
step = int(step) if step else 1
|
|
value = int(value)
|
|
if mode == "increment":
|
|
return min_value if value + step > max_value else value + step
|
|
if mode == "decrement":
|
|
return max_value if value - step < min_value else value - step
|
|
if mode == "randomize":
|
|
return random.randint(min_value, max_value)
|
|
elif input_type == "FLOAT":
|
|
min_value = float(min_value)
|
|
max_value = float(max_value)
|
|
step = float(step) if step else 1.0
|
|
value = float(value)
|
|
if mode == "increment":
|
|
return min_value if value + step > max_value else value + step
|
|
if mode == "decrement":
|
|
return max_value if value - step < min_value else value - step
|
|
if mode == "randomize":
|
|
return random.uniform(min_value, max_value)
|
|
|
|
return value
|
|
|
|
|
|
def _apply_control_after_generate_to_nodes(nodes_list: list, location: str) -> tuple[int, int]:
|
|
candidates = 0
|
|
changed = 0
|
|
for node in nodes_list:
|
|
class_type = _node_class_type(node)
|
|
widgets_values = node.get("widgets_values")
|
|
if not class_type or not isinstance(widgets_values, list):
|
|
continue
|
|
|
|
linked_names = {
|
|
inp.get("name")
|
|
for inp in (node.get("inputs") or [])
|
|
if inp.get("name") and inp.get("link") is not None
|
|
}
|
|
for spec in _iter_widget_specs(class_type, linked_names):
|
|
value_index = spec["index"]
|
|
control_index = spec["control_index"]
|
|
if control_index is None or control_index >= len(widgets_values):
|
|
continue
|
|
if value_index >= len(widgets_values):
|
|
continue
|
|
|
|
candidates += 1
|
|
old_value = widgets_values[value_index]
|
|
mode = widgets_values[control_index]
|
|
try:
|
|
new_value = _next_controlled_value(old_value, spec["input_type"], spec["opts"], mode)
|
|
except (TypeError, ValueError) as e:
|
|
log.warning(
|
|
"Subworkflow: control-after-generate skipped %s node %s %s value=%r mode=%r: %s",
|
|
location,
|
|
node.get("id"),
|
|
spec["name"],
|
|
old_value,
|
|
mode,
|
|
e,
|
|
)
|
|
continue
|
|
if new_value == old_value:
|
|
continue
|
|
widgets_values[value_index] = new_value
|
|
changed += 1
|
|
log.info(
|
|
"Subworkflow: control-after-generate updated %s node %s %s from %r to %r (%s)",
|
|
location,
|
|
node.get("id"),
|
|
spec["name"],
|
|
old_value,
|
|
new_value,
|
|
mode,
|
|
)
|
|
|
|
return candidates, changed
|
|
|
|
|
|
def apply_control_after_generate(data: dict) -> int:
|
|
"""
|
|
Mutate cached UI-format workflow widget values the same way ComfyUI's
|
|
frontend advances control-after-generate widgets between queued runs.
|
|
"""
|
|
if not is_ui_format(data):
|
|
return 0
|
|
|
|
candidates, changed = _apply_control_after_generate_to_nodes(data.get("nodes", []), "workflow")
|
|
|
|
for subgraph in ((data.get("definitions") or {}).get("subgraphs") or []):
|
|
if not isinstance(subgraph, dict):
|
|
continue
|
|
sg_candidates, sg_changed = _apply_control_after_generate_to_nodes(
|
|
subgraph.get("nodes") or [],
|
|
f"subgraph {subgraph.get('id')}",
|
|
)
|
|
candidates += sg_candidates
|
|
changed += sg_changed
|
|
|
|
if changed:
|
|
log.info("Subworkflow: updated %d cached control-after-generate widget(s)", changed)
|
|
return changed
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subgraph expansion
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def build_expansion(data: dict, outer_inputs: dict):
|
|
"""
|
|
Build a GraphBuilder subgraph from an inner workflow.
|
|
Accepts both UI format and API format.
|
|
|
|
outer_inputs: {"swf_in_0": value, "swf_in_1": value, ...}
|
|
Returns (output_refs, graph).
|
|
"""
|
|
if is_ui_format(data):
|
|
return _build_expansion_ui(data, outer_inputs)
|
|
return _build_expansion_api(data, outer_inputs)
|
|
|
|
|
|
def _build_subgraph_defs(data: dict) -> dict:
|
|
"""Extract subgraph (group node) definitions from workflow data, keyed by UUID."""
|
|
result = {}
|
|
for sg in ((data.get("definitions") or {}).get("subgraphs") or []):
|
|
if isinstance(sg, dict) and sg.get("id"):
|
|
result[sg["id"]] = sg
|
|
return result
|
|
|
|
|
|
def _expand_subgraph(outer_node: dict, sg_def: dict, outer_link_map: dict,
|
|
outer_resolve_fn, graph, id_prefix: str) -> list:
|
|
"""
|
|
Expand a subgraph (group node) definition inline into the GraphBuilder.
|
|
|
|
The subgraph uses two virtual boundary nodes:
|
|
inputNode (id = inputNode.id, e.g. -10): outputs carry each outer input value
|
|
outputNode (id = outputNode.id, e.g. -20): inputs receive the subgraph outputs
|
|
|
|
Returns a list of NodeRefs corresponding to sg_def['outputs'].
|
|
"""
|
|
import nodes as _comfy_nodes
|
|
|
|
sg_nodes = sg_def.get("nodes") or []
|
|
sg_links = sg_def.get("links") or []
|
|
sg_inputs_def = sg_def.get("inputs") or []
|
|
sg_outputs_def = sg_def.get("outputs") or []
|
|
input_node_id = str((sg_def.get("inputNode") or {}).get("id", -10))
|
|
|
|
# Parse inner links.
|
|
parsed = [_parse_link(lnk) for lnk in sg_links]
|
|
sg_link_map = {lid: (src, ss) for lid, src, ss, _, _ in parsed}
|
|
|
|
# Build outer_values_by_slot: {subgraph_input_slot_index → resolved value}
|
|
sg_input_name_to_slot = {inp.get("name"): i for i, inp in enumerate(sg_inputs_def)}
|
|
outer_values_by_slot: dict[int, object] = {}
|
|
for outer_inp in (outer_node.get("inputs") or []):
|
|
name = outer_inp.get("name")
|
|
link_id = outer_inp.get("link")
|
|
if name and link_id is not None:
|
|
slot_idx = sg_input_name_to_slot.get(name)
|
|
if slot_idx is not None:
|
|
src = outer_link_map.get(str(link_id))
|
|
if src:
|
|
outer_values_by_slot[slot_idx] = outer_resolve_fn(src[0], src[1])
|
|
|
|
# Pass 1: register inner nodes (with prefixed IDs to avoid conflicts).
|
|
sg_refs: dict = {}
|
|
for node in sg_nodes:
|
|
nid = str(node.get("id"))
|
|
ct = _node_class_type(node)
|
|
if not ct:
|
|
log.warning("SWF sg: node id=%s has no class_type, skipping", nid)
|
|
continue
|
|
if ct not in _comfy_nodes.NODE_CLASS_MAPPINGS:
|
|
log.warning("SWF sg: node id=%s type=%r not in NODE_CLASS_MAPPINGS, skipping", nid, ct)
|
|
continue
|
|
sg_refs[nid] = graph.node(ct, id=f"{id_prefix}_{nid}")
|
|
|
|
def resolve_sg_link(link_id: str):
|
|
src = sg_link_map.get(str(link_id))
|
|
if src is None:
|
|
return None
|
|
src_id, src_slot = str(src[0]), int(src[1])
|
|
if src_id == input_node_id:
|
|
return outer_values_by_slot.get(src_slot)
|
|
ref = sg_refs.get(src_id)
|
|
if ref is None:
|
|
log.warning("SWF sg: link %s src node %s not in sg_refs", link_id, src_id)
|
|
return None
|
|
return ref.out(src_slot)
|
|
|
|
# Pass 2: wire inner nodes.
|
|
for node in sg_nodes:
|
|
nid = str(node.get("id"))
|
|
gb_node = sg_refs.get(nid)
|
|
if gb_node is None:
|
|
continue
|
|
ct = _node_class_type(node)
|
|
node_inputs = node.get("inputs") or []
|
|
widgets_values = node.get("widgets_values") or []
|
|
|
|
linked_names: set = set()
|
|
for inp in node_inputs:
|
|
link_id = inp.get("link")
|
|
name = inp.get("name")
|
|
if link_id is not None and name:
|
|
resolved = resolve_sg_link(str(link_id))
|
|
if resolved is not None:
|
|
linked_names.add(name)
|
|
gb_node.set_input(name, resolved)
|
|
else:
|
|
src = sg_link_map.get(str(link_id))
|
|
if src and str(src[0]) == input_node_id:
|
|
pass # outer not wired — fall through to inner widget default
|
|
else:
|
|
linked_names.add(name)
|
|
log.warning("SWF sg: node %s (%s) input %r UNRESOLVED", nid, ct, name)
|
|
|
|
for wname, val in _get_widget_values(ct, linked_names, widgets_values).items():
|
|
gb_node.set_input(wname, val)
|
|
|
|
# Collect output refs from the subgraph's virtual outputNode.
|
|
output_refs = []
|
|
for out_def in sg_outputs_def:
|
|
link_ids = out_def.get("linkIds") or []
|
|
if not link_ids:
|
|
log.warning("SWF sg: output %r has no linkIds", out_def.get("name"))
|
|
output_refs.append(None)
|
|
continue
|
|
src = sg_link_map.get(str(link_ids[0]))
|
|
if src is None:
|
|
log.warning("SWF sg: output link %s not in sg_link_map", link_ids[0])
|
|
output_refs.append(None)
|
|
continue
|
|
src_id, src_slot = str(src[0]), int(src[1])
|
|
ref = sg_refs.get(src_id)
|
|
if ref is None:
|
|
log.warning("SWF sg: output src node %s not in sg_refs", src_id)
|
|
output_refs.append(None)
|
|
continue
|
|
output_refs.append(ref.out(src_slot))
|
|
|
|
return output_refs
|
|
|
|
|
|
def _build_expansion_ui(data: dict, outer_inputs: dict):
|
|
nodes_list = data.get("nodes", [])
|
|
links_list = data.get("links", [])
|
|
|
|
subgraph_defs = _build_subgraph_defs(data)
|
|
|
|
parsed_links = [_parse_link(lnk) for lnk in links_list]
|
|
link_map = {lid: (src, ss) for lid, src, ss, _, _ in parsed_links}
|
|
dst_to_src: dict[str, tuple[str, int]] = {}
|
|
for _, src, ss, dst, _ in parsed_links:
|
|
dst_to_src.setdefault(dst, (src, ss))
|
|
|
|
inputs_info, outputs_info = _get_workflow_io_ui(data)
|
|
|
|
fi_node_ids = {inp["node_id"] for inp in inputs_info}
|
|
fo_node_ids = {out["node_id"] for out in outputs_info}
|
|
func_node_ids = fi_node_ids | fo_node_ids
|
|
|
|
fi_value = {inp["node_id"]: outer_inputs.get(f"swf_in_{i}") for i, inp in enumerate(inputs_info)}
|
|
|
|
graph = GraphBuilder()
|
|
node_refs: dict = {}
|
|
subgraph_outputs: dict = {} # nid → [output_ref, ...]
|
|
|
|
def resolve_link(src_node_id: str, src_slot: int):
|
|
if src_node_id in fi_value:
|
|
return fi_value[src_node_id]
|
|
if src_node_id in subgraph_outputs:
|
|
refs = subgraph_outputs[src_node_id]
|
|
return refs[src_slot] if src_slot < len(refs) else None
|
|
ref = node_refs.get(src_node_id)
|
|
if ref is None:
|
|
log.warning("SWF: node %s slot %d unresolvable", src_node_id, src_slot)
|
|
return ref.out(src_slot) if ref is not None else None
|
|
|
|
import nodes as _comfy_nodes
|
|
|
|
# Pass 1: register regular inner nodes (skip SWF boundary and subgraph nodes).
|
|
for node in nodes_list:
|
|
nid = str(node["id"])
|
|
if nid in func_node_ids:
|
|
continue
|
|
ct = _node_class_type(node)
|
|
if not ct:
|
|
log.warning("SWF: node id=%s has no resolvable class_type, skipping", nid)
|
|
continue
|
|
if ct in subgraph_defs:
|
|
continue # expanded separately below
|
|
if ct not in _comfy_nodes.NODE_CLASS_MAPPINGS:
|
|
log.warning("SWF: node id=%s type=%r not in NODE_CLASS_MAPPINGS, skipping", nid, ct)
|
|
continue
|
|
node_refs[nid] = graph.node(ct, id=nid)
|
|
|
|
# Subgraph expansion pass (runs after pass 1 so node_refs is populated).
|
|
for node in nodes_list:
|
|
nid = str(node["id"])
|
|
if nid in func_node_ids:
|
|
continue
|
|
ct = _node_class_type(node)
|
|
if not ct or ct not in subgraph_defs:
|
|
continue
|
|
refs = _expand_subgraph(
|
|
outer_node=node,
|
|
sg_def=subgraph_defs[ct],
|
|
outer_link_map=link_map,
|
|
outer_resolve_fn=resolve_link,
|
|
graph=graph,
|
|
id_prefix=nid,
|
|
)
|
|
subgraph_outputs[nid] = refs
|
|
|
|
# Pass 2: wire inputs for regular inner nodes.
|
|
for node in nodes_list:
|
|
nid = str(node["id"])
|
|
if nid in func_node_ids:
|
|
continue
|
|
gb_node = node_refs.get(nid)
|
|
if gb_node is None:
|
|
continue # subgraph nodes are not in node_refs
|
|
|
|
ct = _node_class_type(node)
|
|
node_inputs = node.get("inputs") or []
|
|
widgets_values = node.get("widgets_values") or []
|
|
|
|
linked_names: set = set()
|
|
for inp in node_inputs:
|
|
link_id = inp.get("link")
|
|
name = inp.get("name")
|
|
if link_id is not None and name:
|
|
linked_names.add(name)
|
|
src = link_map.get(str(link_id))
|
|
if src:
|
|
resolved = resolve_link(src[0], src[1])
|
|
if resolved is not None:
|
|
gb_node.set_input(name, resolved)
|
|
else:
|
|
log.warning("SWF: node %s (%s) input %r unresolved", nid, ct, name)
|
|
else:
|
|
log.warning("SWF: node %s (%s) input %r: link %s not in link_map",
|
|
nid, ct, name, link_id)
|
|
|
|
for wname, val in _get_widget_values(ct, linked_names, widgets_values).items():
|
|
gb_node.set_input(wname, val)
|
|
|
|
# Collect output refs from Subworkflow Output nodes.
|
|
output_refs = []
|
|
for out in outputs_info:
|
|
src = dst_to_src.get(out["node_id"])
|
|
if src is None:
|
|
log.warning("SWF: Subworkflow Output node=%s has no incoming link", out["node_id"])
|
|
ref = resolve_link(src[0], src[1]) if src else None
|
|
output_refs.append(ref)
|
|
|
|
return output_refs, graph
|
|
|
|
|
|
def _build_expansion_api(data: dict, outer_inputs: dict):
|
|
inputs_info, outputs_info = _get_workflow_io_api(data)
|
|
|
|
fi_value = {inp["node_id"]: outer_inputs.get(f"swf_in_{i}") for i, inp in enumerate(inputs_info)}
|
|
func_node_ids = {inp["node_id"] for inp in inputs_info} | {out["node_id"] for out in outputs_info}
|
|
|
|
graph = GraphBuilder()
|
|
node_refs: dict = {}
|
|
|
|
def resolve_link(link_val):
|
|
src_id = str(link_val[0])
|
|
src_slot = int(link_val[1])
|
|
if src_id in fi_value:
|
|
return fi_value[src_id]
|
|
ref = node_refs.get(src_id)
|
|
if ref is None:
|
|
log.warning("SWF api: node %s slot %d unresolvable", src_id, src_slot)
|
|
return ref.out(src_slot) if ref is not None else None
|
|
|
|
for nid, node in data.items():
|
|
if nid.startswith("_") or nid in func_node_ids:
|
|
continue
|
|
ct = node.get("class_type")
|
|
if ct is None:
|
|
continue
|
|
node_refs[nid] = graph.node(ct, id=nid)
|
|
|
|
for nid, node in data.items():
|
|
if nid.startswith("_") or nid in func_node_ids:
|
|
continue
|
|
gb_node = node_refs.get(nid)
|
|
if gb_node is None:
|
|
continue
|
|
for inp_name, inp_val in node.get("inputs", {}).items():
|
|
if isinstance(inp_val, list) and len(inp_val) == 2 and isinstance(inp_val[0], (str, int)):
|
|
resolved = resolve_link(inp_val)
|
|
if resolved is not None:
|
|
gb_node.set_input(inp_name, resolved)
|
|
else:
|
|
log.warning("SWF api: node %s input %r unresolved link %s", nid, inp_name, inp_val)
|
|
else:
|
|
gb_node.set_input(inp_name, inp_val)
|
|
|
|
output_refs = []
|
|
for out in outputs_info:
|
|
inp_dict = data[out["node_id"]].get("inputs", {})
|
|
val = inp_dict.get("value")
|
|
if isinstance(val, list) and len(val) == 2 and isinstance(val[0], (str, int)):
|
|
output_refs.append(resolve_link(val))
|
|
else:
|
|
output_refs.append(val)
|
|
|
|
return output_refs, graph
|