"""Validate the API workflow connections against the ComfyUI UI workflow. ComfyUI API exports collapse some UI-only SetNode/GetNode routing pairs. This validator resolves those pairs before comparing links so the check reflects the actual graph semantics instead of the visual routing helpers. """ from __future__ import annotations import argparse import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_API = ROOT / "workflows" / "voicegate_api.json" DEFAULT_UI = ROOT / "VoiceGate" / "workflows" / "VoiceGate-Workflow.json" def load_json(path: Path) -> Any: with path.open("r", encoding="utf-8") as file: return json.load(file) def is_connection(value: Any) -> bool: return ( isinstance(value, list) and len(value) == 2 and isinstance(value[0], str) and isinstance(value[1], int) ) def build_expected_connections(ui_workflow: dict[str, Any]) -> dict[str, dict[str, Any]]: nodes = ui_workflow["nodes"] links = ui_workflow["links"] nodes_by_id = {str(node["id"]): node for node in nodes} links_by_id = { link[0]: { "from": str(link[1]), "slot": link[2], "to": str(link[3]), "to_slot": link[4], "type": link[5], } for link in links } set_by_name: dict[str, dict[str, Any]] = {} for node in nodes: if node.get("type") != "SetNode": continue values = node.get("widgets_values") or [] inputs = node.get("inputs") or [] if not values or not inputs: continue link_id = inputs[0].get("link") link = links_by_id.get(link_id) if link: set_by_name[values[0]] = { "from": link["from"], "slot": link["slot"], "via_set": str(node["id"]), } def resolve_source(from_node: str, slot: int) -> dict[str, Any]: node = nodes_by_id.get(from_node) if node and node.get("type") == "GetNode": values = node.get("widgets_values") or [] name = values[0] if values else None if name in set_by_name: resolved = dict(set_by_name[name]) resolved["via_get"] = from_node resolved["name"] = name return resolved return {"from": from_node, "slot": slot} expected: dict[str, dict[str, Any]] = {} for node in nodes: for input_def in node.get("inputs") or []: link_id = input_def.get("link") if link_id is None: continue link = links_by_id.get(link_id) if not link: continue key = f"{node['id']}.{input_def['name']}" expected[key] = { **resolve_source(link["from"], link["slot"]), "type": link["type"], } return expected def validate(api_workflow: dict[str, Any], ui_workflow: dict[str, Any]) -> list[dict[str, Any]]: expected = build_expected_connections(ui_workflow) mismatches: list[dict[str, Any]] = [] for node_id, node in api_workflow.items(): for input_name, value in (node.get("inputs") or {}).items(): if not is_connection(value): continue actual = {"from": value[0], "slot": value[1]} expected_connection = expected.get(f"{node_id}.{input_name}") if ( not expected_connection or expected_connection["from"] != actual["from"] or expected_connection["slot"] != actual["slot"] ): mismatches.append( { "node": node_id, "class_type": node.get("class_type"), "input": input_name, "actual": actual, "expected": expected_connection, } ) return mismatches def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--api", type=Path, default=DEFAULT_API) parser.add_argument("--ui", type=Path, default=DEFAULT_UI) return parser.parse_args() def main() -> None: args = parse_args() api_workflow = load_json(args.api) ui_workflow = load_json(args.ui) mismatches = validate(api_workflow, ui_workflow) checked = sum( 1 for node in api_workflow.values() for value in (node.get("inputs") or {}).values() if is_connection(value) ) print(f"checked_connections={checked}") print(f"mismatches={len(mismatches)}") if mismatches: print(json.dumps(mismatches, ensure_ascii=False, indent=2)) raise SystemExit(1) if __name__ == "__main__": main()