Skip to content

Commit 486a561

Browse files
authored
feat(run): convert UI workflows to API format client-side via /object_info (#450)
Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent eee0388 commit 486a561

7 files changed

Lines changed: 4855 additions & 124 deletions

File tree

comfy_cli/command/run.py

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from websocket import WebSocket, WebSocketException, WebSocketTimeoutException
1515

1616
from comfy_cli.env_checker import check_comfy_server_running
17+
from comfy_cli.workflow_to_api import WorkflowConversionError, convert_ui_to_api
1718
from comfy_cli.workspace_manager import WorkspaceManager
1819

1920
workspace_manager = WorkspaceManager()
@@ -37,55 +38,31 @@ def _validate_api_workflow(workflow):
3738
return workflow
3839

3940

40-
class WorkflowConverterUnavailable(Exception):
41-
"""The running ComfyUI server doesn't expose /workflow/convert."""
41+
def fetch_object_info(host: str, port: int, timeout: int) -> dict:
42+
"""GET ``/object_info`` from the running ComfyUI server.
4243
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.
44+
The response describes every loaded node class's input schema and is what
45+
the converter uses to map widget values to input names, fill defaults, etc.
4946
"""
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")
47+
url = f"http://{host}:{port}/object_info"
5348
try:
54-
resp = request.urlopen(req, timeout=timeout)
49+
with request.urlopen(url, timeout=timeout) as resp:
50+
body = resp.read()
5551
except urllib.error.HTTPError as e:
56-
if e.code in (404, 405):
57-
raise WorkflowConverterUnavailable() from e
5852
body = e.read().decode("utf-8", errors="replace").strip()
59-
pprint(f"[bold red]Workflow conversion failed (HTTP {e.code}): {body[:500]}[/bold red]")
53+
pprint(f"[bold red]Failed to fetch /object_info (HTTP {e.code}): {body[:500]}[/bold red]")
6054
raise typer.Exit(code=1) from e
6155
except urllib.error.URLError as e:
62-
pprint(f"[bold red]Workflow conversion failed: {e.reason}[/bold red]")
56+
pprint(f"[bold red]Failed to fetch /object_info: {e.reason}[/bold red]")
57+
raise typer.Exit(code=1) from e
58+
except TimeoutError as e:
59+
pprint(f"[bold red]Failed to fetch /object_info: timed out after {timeout}s[/bold red]")
6360
raise typer.Exit(code=1) from e
6461
try:
65-
converted = json.loads(resp.read())
62+
return json.loads(body)
6663
except json.JSONDecodeError as e:
67-
pprint("[bold red]Workflow conversion failed: server returned invalid JSON[/bold red]")
64+
pprint("[bold red]Failed to fetch /object_info: server returned invalid JSON[/bold red]")
6865
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-
)
8966

9067

9168
def execute(workflow: str, host, port, wait=True, verbose=False, local_paths=False, timeout=30):
@@ -112,11 +89,29 @@ def execute(workflow: str, host, port, wait=True, verbose=False, local_paths=Fal
11289
raise typer.Exit(code=1) from e
11390

11491
if is_ui_workflow(raw_workflow):
115-
pprint("[yellow]Detected UI-format workflow, converting via server's /workflow/convert...[/yellow]")
92+
pprint("[yellow]Detected UI-format workflow, converting to API format...[/yellow]")
93+
object_info = fetch_object_info(host, port, timeout)
11694
try:
117-
workflow = convert_ui_workflow_via_server(raw_workflow, host, port, timeout)
118-
except WorkflowConverterUnavailable:
119-
_print_converter_unavailable_help()
95+
workflow = convert_ui_to_api(raw_workflow, object_info)
96+
except WorkflowConversionError as e:
97+
pprint(f"[bold red]Workflow conversion failed: {e}[/bold red]")
98+
raise typer.Exit(code=1) from e
99+
except Exception as e:
100+
# The converter is experimental; an unexpected crash here is a bug
101+
# in our code, not user error. Show a clean message and a pointer.
102+
pprint(
103+
f"[bold red]Workflow conversion crashed unexpectedly: {type(e).__name__}: {e}[/bold red]\n"
104+
"[yellow]The UI-to-API converter is experimental. Please report this at[/yellow]\n"
105+
"[yellow] https://github.com/Comfy-Org/comfy-cli/issues[/yellow]\n"
106+
"[yellow]and attach the workflow file if possible.[/yellow]"
107+
)
108+
if verbose:
109+
import traceback
110+
111+
traceback.print_exc()
112+
raise typer.Exit(code=1) from e
113+
if not workflow:
114+
pprint("[bold red]Workflow conversion produced no executable nodes[/bold red]")
120115
raise typer.Exit(code=1)
121116
else:
122117
workflow = _validate_api_workflow(raw_workflow)

0 commit comments

Comments
 (0)