Skip to content

Commit eee0388

Browse files
authored
feat(run): auto-convert UI-format workflows via server endpoint (#448)
Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent 8864b51 commit eee0388

2 files changed

Lines changed: 316 additions & 41 deletions

File tree

comfy_cli/command/run.py

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,73 @@
1919
workspace_manager = WorkspaceManager()
2020

2121

22-
def load_api_workflow(file: str):
23-
with open(file, encoding="utf-8") as f:
24-
workflow = json.load(f)
25-
# Check for litegraph properties to ensure this isnt a UI workflow file
26-
if "nodes" in workflow and "links" in workflow:
27-
return None
22+
def is_ui_workflow(workflow) -> bool:
23+
return (
24+
isinstance(workflow, dict)
25+
and isinstance(workflow.get("nodes"), list)
26+
and isinstance(workflow.get("links"), list)
27+
)
2828

29-
# Try validating the first entry to ensure it has a node class property
30-
node_id = next(iter(workflow))
31-
node = workflow[node_id]
32-
if "class_type" not in node:
33-
return None
3429

35-
return workflow
30+
def _validate_api_workflow(workflow):
31+
"""Return the workflow dict if it has the shape of API format, else None."""
32+
if not isinstance(workflow, dict) or not workflow:
33+
return None
34+
node = workflow[next(iter(workflow))]
35+
if not isinstance(node, dict) or "class_type" not in node:
36+
return None
37+
return workflow
38+
39+
40+
class WorkflowConverterUnavailable(Exception):
41+
"""The running ComfyUI server doesn't expose /workflow/convert."""
42+
43+
44+
def convert_ui_workflow_via_server(workflow: dict, host: str, port: int, timeout: int) -> dict:
45+
"""POST a UI-format workflow to the server's /workflow/convert and return API-format JSON.
46+
47+
Raises WorkflowConverterUnavailable if the server doesn't expose the endpoint.
48+
Raises typer.Exit on other conversion failures.
49+
"""
50+
url = f"http://{host}:{port}/workflow/convert"
51+
req = request.Request(url, json.dumps(workflow).encode("utf-8"))
52+
req.add_header("Content-Type", "application/json")
53+
try:
54+
resp = request.urlopen(req, timeout=timeout)
55+
except urllib.error.HTTPError as e:
56+
if e.code in (404, 405):
57+
raise WorkflowConverterUnavailable() from e
58+
body = e.read().decode("utf-8", errors="replace").strip()
59+
pprint(f"[bold red]Workflow conversion failed (HTTP {e.code}): {body[:500]}[/bold red]")
60+
raise typer.Exit(code=1) from e
61+
except urllib.error.URLError as e:
62+
pprint(f"[bold red]Workflow conversion failed: {e.reason}[/bold red]")
63+
raise typer.Exit(code=1) from e
64+
try:
65+
converted = json.loads(resp.read())
66+
except json.JSONDecodeError as e:
67+
pprint("[bold red]Workflow conversion failed: server returned invalid JSON[/bold red]")
68+
raise typer.Exit(code=1) from e
69+
if not isinstance(converted, dict) or not converted:
70+
pprint("[bold red]Workflow conversion failed: expected a non-empty JSON object[/bold red]")
71+
raise typer.Exit(code=1)
72+
first = converted[next(iter(converted))]
73+
if not isinstance(first, dict) or "class_type" not in first:
74+
pprint("[bold red]Workflow conversion failed: returned data is not API workflow format[/bold red]")
75+
raise typer.Exit(code=1)
76+
return converted
77+
78+
79+
def _print_converter_unavailable_help() -> None:
80+
pprint(
81+
"[bold red]This ComfyUI server doesn't expose a /workflow/convert endpoint[/bold red]\n"
82+
"[bold red]to convert it to API format.[/bold red]\n"
83+
"\n"
84+
"[yellow]Workarounds:[/yellow]\n"
85+
"[yellow] * Install a custom node that adds /workflow/convert on the server[/yellow]\n"
86+
"[yellow] * Or, in the ComfyUI frontend, use 'File > Export (API)' to save[/yellow]\n"
87+
"[yellow] your workflow as API format[/yellow]"
88+
)
3689

3790

3891
def execute(workflow: str, host, port, wait=True, verbose=False, local_paths=False, timeout=30):
@@ -44,16 +97,33 @@ def execute(workflow: str, host, port, wait=True, verbose=False, local_paths=Fal
4497
)
4598
raise typer.Exit(code=1)
4699

47-
workflow = load_api_workflow(workflow)
48-
49-
if not workflow:
50-
pprint("[bold red]Specified workflow does not appear to be an API workflow json file[/bold red]")
51-
raise typer.Exit(code=1)
52-
53100
if not check_comfy_server_running(port, host):
54101
pprint(f"[bold red]ComfyUI not running on specified address ({host}:{port})[/bold red]")
55102
raise typer.Exit(code=1)
56103

104+
try:
105+
with open(workflow_name, encoding="utf-8") as f:
106+
raw_workflow = json.load(f)
107+
except OSError as e:
108+
pprint(f"[bold red]Unable to read workflow file: {e}[/bold red]")
109+
raise typer.Exit(code=1) from e
110+
except json.JSONDecodeError as e:
111+
pprint(f"[bold red]Specified workflow file is not valid JSON: {e}[/bold red]")
112+
raise typer.Exit(code=1) from e
113+
114+
if is_ui_workflow(raw_workflow):
115+
pprint("[yellow]Detected UI-format workflow, converting via server's /workflow/convert...[/yellow]")
116+
try:
117+
workflow = convert_ui_workflow_via_server(raw_workflow, host, port, timeout)
118+
except WorkflowConverterUnavailable:
119+
_print_converter_unavailable_help()
120+
raise typer.Exit(code=1)
121+
else:
122+
workflow = _validate_api_workflow(raw_workflow)
123+
if not workflow:
124+
pprint("[bold red]Specified workflow does not appear to be an API workflow json file[/bold red]")
125+
raise typer.Exit(code=1)
126+
57127
progress = None
58128
start = time.time()
59129
if wait:

0 commit comments

Comments
 (0)