Files
PozzettiAndrea-ComfyUI-Geom…/tests/workflow_tests/workflow_executor.py
T
2026-02-12 21:19:40 +01:00

207 lines
5.8 KiB
Python

# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2025 ComfyUI-GeometryPack Contributors
"""
Workflow executor for running ComfyUI workflows via API.
Executes workflow JSON files through the ComfyUI server API and
retrieves results.
"""
import json
import urllib.request
import urllib.parse
import uuid
import time
from typing import Dict, Any, Optional
from pathlib import Path
try:
import websocket
WEBSOCKET_AVAILABLE = True
except ImportError:
WEBSOCKET_AVAILABLE = False
class WorkflowExecutor:
"""Executes ComfyUI workflows via the server API."""
def __init__(self, server_address="127.0.0.1:8188"):
"""
Initialize workflow executor.
Args:
server_address: ComfyUI server address (host:port)
"""
self.server_address = server_address
self.client_id = str(uuid.uuid4())
self.ws = None
def connect(self):
"""Connect to ComfyUI websocket for real-time updates."""
if not WEBSOCKET_AVAILABLE:
raise ImportError("websocket-client package required for workflow execution")
ws_url = f"ws://{self.server_address}/ws?clientId={self.client_id}"
self.ws = websocket.WebSocket()
self.ws.connect(ws_url)
def disconnect(self):
"""Disconnect from ComfyUI websocket."""
if self.ws:
self.ws.close()
self.ws = None
def queue_prompt(self, prompt: Dict[str, Any]) -> str:
"""
Queue a prompt for execution.
Args:
prompt: API format prompt
Returns:
Prompt ID for tracking
"""
p = {"prompt": prompt, "client_id": self.client_id}
data = json.dumps(p).encode('utf-8')
req = urllib.request.Request(
f"http://{self.server_address}/prompt",
data=data,
headers={'Content-Type': 'application/json'}
)
response = urllib.request.urlopen(req)
result = json.loads(response.read())
if 'prompt_id' not in result:
raise RuntimeError(f"Failed to queue prompt: {result}")
return result['prompt_id']
def wait_for_completion(self, prompt_id: str, timeout: int = 300) -> bool:
"""
Wait for prompt execution to complete.
Args:
prompt_id: Prompt ID to wait for
timeout: Maximum wait time in seconds
Returns:
True if completed successfully
"""
if not self.ws:
raise RuntimeError("Not connected to websocket")
start_time = time.time()
while True:
if time.time() - start_time > timeout:
raise TimeoutError(f"Workflow execution timed out after {timeout}s")
out = self.ws.recv()
if isinstance(out, str):
message = json.loads(out)
if message['type'] == 'executing':
data = message['data']
# When node is None and prompt_id matches, execution is done
if data['node'] is None and data['prompt_id'] == prompt_id:
return True
elif message['type'] == 'execution_error':
error_data = message['data']
raise RuntimeError(f"Execution error: {error_data}")
def get_history(self, prompt_id: str) -> Dict[str, Any]:
"""
Get execution history for a prompt.
Args:
prompt_id: Prompt ID
Returns:
History data including outputs
"""
url = f"http://{self.server_address}/history/{prompt_id}"
with urllib.request.urlopen(url) as response:
history = json.loads(response.read())
if prompt_id not in history:
raise RuntimeError(f"Prompt {prompt_id} not found in history")
return history[prompt_id]
def get_outputs(self, prompt_id: str) -> Dict[str, Any]:
"""
Get outputs from executed workflow.
Args:
prompt_id: Prompt ID
Returns:
Dictionary of node outputs
"""
history = self.get_history(prompt_id)
return history.get('outputs', {})
def execute_workflow(self, workflow: Dict[str, Any], timeout: int = 300) -> Dict[str, Any]:
"""
Execute a workflow and return outputs.
Args:
workflow: Workflow in API format
timeout: Maximum execution time in seconds
Returns:
Dictionary of outputs by node ID
"""
# Queue the workflow
prompt_id = self.queue_prompt(workflow)
# Wait for completion
self.wait_for_completion(prompt_id, timeout)
# Get and return outputs
return self.get_outputs(prompt_id)
def execute_workflow_file(self, workflow_path: str, timeout: int = 300) -> Dict[str, Any]:
"""
Load and execute a workflow JSON file.
Args:
workflow_path: Path to workflow JSON (UI or API format)
timeout: Maximum execution time in seconds
Returns:
Dictionary of outputs by node ID
"""
from .workflow_converter import WorkflowConverter
# Load workflow
with open(workflow_path, 'r') as f:
workflow_json = json.load(f)
# Check if it's UI format (has 'nodes' array) or API format
if 'nodes' in workflow_json:
# Convert from UI format to API format
workflow = WorkflowConverter.convert_to_api_format(workflow_json)
else:
# Already in API format
workflow = workflow_json
return self.execute_workflow(workflow, timeout)
def __enter__(self):
"""Context manager entry."""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.disconnect()
return False