207 lines
5.8 KiB
Python
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
|