raubatz's picture
download
raw
7.99 kB
#!/usr/bin/env python3
"""Minimal ComfyUI workflow runner for CI smoke tests."""
import argparse
import json
import os
import sys
import time
import uuid
from pathlib import Path
from typing import Iterable, Optional
import requests
import websocket
DEFAULT_HOST = os.environ.get("COMFYUI_HOST", "127.0.0.1")
DEFAULT_PORT = int(os.environ.get("COMFYUI_PORT", "8188"))
DEFAULT_CONNECT_TIMEOUT = int(os.environ.get("COMFYUI_CONNECT_TIMEOUT", "60"))
DEFAULT_WORKFLOW_TIMEOUT = int(os.environ.get("COMFYUI_WORKFLOW_TIMEOUT", "900"))
class ComfyWorkflowRunner:
def __init__(self, host: str, port: int, connect_timeout: int, workflow_timeout: int, secure: bool = False) -> None:
self.host = host
self.port = port
protocol_http = "https" if secure else "http"
protocol_ws = "wss" if secure else "ws"
self.base_http = f"{protocol_http}://{host}:{port}"
self.base_ws = f"{protocol_ws}://{host}:{port}/ws"
self.connect_timeout = connect_timeout
self.workflow_timeout = workflow_timeout
self.client_id = str(uuid.uuid4())
self.session = requests.Session()
self.websocket: Optional[websocket.WebSocket] = None
def wait_for_server(self) -> None:
deadline = time.monotonic() + self.connect_timeout
while time.monotonic() < deadline:
try:
response = self.session.get(f"{self.base_http}/system_stats", timeout=5)
if response.status_code == 200:
return
except requests.RequestException:
time.sleep(1)
raise TimeoutError(f"ComfyUI server not reachable at {self.base_http}")
def open_websocket(self) -> None:
ws = websocket.WebSocket()
ws.settimeout(5)
ws.connect(f"{self.base_ws}?clientId={self.client_id}")
self.websocket = ws
def close_websocket(self) -> None:
if self.websocket:
try:
self.websocket.close()
finally:
self.websocket = None
def queue_prompt(self, prompt: dict) -> str:
payload = {"prompt": prompt, "client_id": self.client_id}
response = self.session.post(f"{self.base_http}/prompt", json=payload, timeout=15)
response.raise_for_status()
data = response.json()
prompt_id = data.get("prompt_id")
if not prompt_id:
raise RuntimeError("No prompt_id returned from ComfyUI")
return prompt_id
def wait_for_completion(self, prompt_id: str) -> bool:
if not self.websocket:
raise RuntimeError("WebSocket connection not established")
deadline = time.monotonic() + self.workflow_timeout
ws = self.websocket
while time.monotonic() < deadline:
try:
message = ws.recv()
except websocket.WebSocketTimeoutException:
continue
except Exception as exc: # noqa: BLE001
print(f"WebSocket error: {exc}", file=sys.stderr, flush=True)
return False
if isinstance(message, bytes):
continue
try:
payload = json.loads(message)
except json.JSONDecodeError:
continue
message_type = payload.get("type")
data = payload.get("data", {})
if message_type == "execution_error":
if data.get("prompt_id") == prompt_id:
print(f"Execution error: {payload}", file=sys.stderr, flush=True)
return False
elif message_type == "status" and data.get("status") == "error":
if data.get("prompt_id") == prompt_id:
print(f"Status error: {payload}", file=sys.stderr, flush=True)
return False
elif message_type == "executing":
if data.get("prompt_id") == prompt_id and data.get("node") is None:
return True
print("Workflow timed out", file=sys.stderr, flush=True)
return False
def run_workflow(self, workflow_path: Path) -> bool:
previous_workflow = os.environ.get("MGPU_JSON_WORKFLOW")
previous_prompt = os.environ.get("MGPU_JSON_PROMPT")
def restore_env() -> None:
if previous_workflow is None:
os.environ.pop("MGPU_JSON_WORKFLOW", None)
else:
os.environ["MGPU_JSON_WORKFLOW"] = previous_workflow
if previous_prompt is None:
os.environ.pop("MGPU_JSON_PROMPT", None)
else:
os.environ["MGPU_JSON_PROMPT"] = previous_prompt
if workflow_path:
os.environ["MGPU_JSON_WORKFLOW"] = workflow_path.name
try:
with workflow_path.open("r", encoding="utf-8") as handle:
workflow = json.load(handle)
except (OSError, json.JSONDecodeError) as exc:
print(f"Failed to load workflow {workflow_path}: {exc}", file=sys.stderr, flush=True)
restore_env()
return False
print(f"Running workflow {workflow_path}", flush=True)
start = time.monotonic()
try:
prompt_id = self.queue_prompt(workflow)
os.environ["MGPU_JSON_PROMPT"] = prompt_id
except requests.HTTPError as exc:
print(f"HTTP error while queueing workflow: {exc}", file=sys.stderr, flush=True)
restore_env()
return False
except requests.RequestException as exc:
print(f"Request error while queueing workflow: {exc}", file=sys.stderr, flush=True)
restore_env()
return False
except RuntimeError as exc:
print(str(exc), file=sys.stderr, flush=True)
restore_env()
return False
try:
if not self.wait_for_completion(prompt_id):
return False
duration = time.monotonic() - start
print(f"Workflow {workflow_path} completed in {duration:.2f}s", flush=True)
return True
finally:
restore_env()
def run_suite(self, workflows: Iterable[Path], fail_fast: bool) -> bool:
self.wait_for_server()
self.open_websocket()
try:
overall = True
for workflow in workflows:
ok = self.run_workflow(workflow)
if not ok:
overall = False
if fail_fast:
break
return overall
finally:
self.close_websocket()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run ComfyUI workflows via the HTTP/WebSocket API")
parser.add_argument("workflows", nargs="+", type=Path, help="Workflow files in ComfyUI API JSON format")
parser.add_argument("--host", default=DEFAULT_HOST, help="ComfyUI HTTP host")
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="ComfyUI HTTP port")
parser.add_argument("--connect-timeout", type=int, default=DEFAULT_CONNECT_TIMEOUT, help="Seconds to wait for the server to come online")
parser.add_argument("--workflow-timeout", type=int, default=DEFAULT_WORKFLOW_TIMEOUT, help="Seconds to wait for each workflow to finish")
parser.add_argument("--fail-fast", action="store_true", help="Stop on first workflow failure")
parser.add_argument("--secure", action="store_true", help="Use secure HTTPS/WSS connections (default: insecure for localhost)")
return parser.parse_args()
def main() -> int:
args = parse_args()
runner = ComfyWorkflowRunner(
host=args.host,
port=args.port,
connect_timeout=args.connect_timeout,
workflow_timeout=args.workflow_timeout,
secure=args.secure,
)
success = runner.run_suite(args.workflows, fail_fast=args.fail_fast)
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())

Xet Storage Details

Size:
7.99 kB
·
Xet hash:
0cda393623b2edfccbc1d848929c6a05b108dcd4df25dcd34f481c1a8d38dc8a

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.