This commit is contained in:
puke
2025-05-14 18:43:03 +08:00
commit 26a8afd0dd
7 changed files with 878 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
/test
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Cursor
.cursorrules
+87
View File
@@ -0,0 +1,87 @@
# ComfyUI-OneAPI
ComfyUI-OneAPI is a plugin that provides simple REST API interfaces for ComfyUI, allowing you to execute ComfyUI workflows through a single API request.
[中文文档](README_CN.md)
## Features
- **Simple Interface**: Execute complex ComfyUI workflows through a single API endpoint
- **Parameter Mapping**: Support for dynamically replacing parameters in workflows
- **Asynchronous Execution**: Support for both synchronous and asynchronous execution modes
- **No WebSocket Dependency**: Uses HTTP polling to get results, no need to handle WebSocket connections
## Installation
1. Download or clone this repository to the `custom_nodes` directory of ComfyUI
2. Restart ComfyUI
## API Usage
### Execute Workflow
```
POST /oneapi/v1/execute
Request Body:
{
"workflow": {Workflow JSON},
"params": {
"paramName": "paramValue"
},
"wait_for_result": true,
"timeout": 300
}
```
## Workflow Parameter Mapping
To use dynamic parameters in your workflow, you need to use special markers in the node titles:
1. LoadImage node: Use `$image_param` format
2. Other nodes: Use `$param.field_name` format
Examples:
- A LoadImage node marked with `$input_image` will use the input_image parameter from params as the image filename
- A CLIPTextEncode node marked with `$prompt.text` will replace the text field with the prompt parameter from params
## Examples
### curl Examples
#### Text-to-Image Example
```bash
curl -X POST "http://localhost:8188/oneapi/v1/execute" \
-H "Content-Type: application/json" \
-d '{
"workflow": '"$(cat workflows/example_workflow.json)"',
"params": {
"prompt": "a cute dog with a red hat"
},
"wait_for_result": true
}'
```
#### Image-to-Image Example
```bash
# Execute image-to-image workflow
curl -X POST "http://localhost:8188/oneapi/v1/execute" \
-H "Content-Type: application/json" \
-d '{
"workflow": '"$(cat workflows/example_img2img_workflow.json)"',
"params": {
"prompt": "a cute dog with a red hat",
"image": "https://example.com/input.jpg"
},
"wait_for_result": true
}'
```
## Notes
- This plugin is designed to simplify API calls, and does not provide WebSocket real-time progress information
- Long-running workflows may cause request timeouts, consider setting appropriate timeout values
- Parameter mapping in workflows depends on special markers in node titles
+87
View File
@@ -0,0 +1,87 @@
# ComfyUI-OneAPI
ComfyUI-OneAPI 是一个为 ComfyUI 提供简单REST API接口的插件,使您能够通过一个简单的API请求执行ComfyUI工作流。
[English Documentation](README.md)
## 特点
- **简单接口**:通过单个API接口执行复杂的ComfyUI工作流
- **参数映射**:支持动态替换工作流中的参数
- **异步执行**:支持同步或异步执行模式
- **无WebSocket依赖**:使用HTTP轮询获取结果,不需要处理WebSocket连接
## 安装
1. 下载或克隆此仓库到ComfyUI的`custom_nodes`目录
2. 重启ComfyUI
## API使用
### 执行工作流
```
POST /oneapi/v1/execute
请求体:
{
"workflow": {工作流JSON},
"params": {
"参数名": "参数值"
},
"wait_for_result": true,
"timeout": 300
}
```
## 工作流参数映射
要在工作流中使用动态参数,需要在节点标题中使用特殊标记:
1. LoadImage节点:使用 `$image_param` 格式
2. 其他节点:使用 `$param.field_name` 格式
例如:
- 标记为 `$input_image` 的LoadImage节点,将使用params中的input_image参数作为图片文件名
- 标记为 `$prompt.text` 的CLIPTextEncode节点,将使用params中的prompt参数替换text字段
## 示例
### curl示例
#### 文生图示例
```bash
curl -X POST "http://localhost:8188/oneapi/v1/execute" \
-H "Content-Type: application/json" \
-d '{
"workflow": '"$(cat workflows/example_workflow.json)"',
"params": {
"prompt": "a cute dog with a red hat"
},
"wait_for_result": true
}'
```
#### 图生图示例
```bash
# 执行图生图工作流
curl -X POST "http://localhost:8188/oneapi/v1/execute" \
-H "Content-Type: application/json" \
-d '{
"workflow": '"$(cat workflows/example_img2img_workflow.json)"',
"params": {
"prompt": "a cute dog with a red hat",
"image": "https://example.com/input.jpg"
},
"wait_for_result": true
}'
```
## 注意事项
- 此插件设计为简化API调用,不提供WebSocket实时进度信息
- 超长时间的工作流可能导致请求超时,建议设置合理的超时时间
- 工作流中的参数映射依赖于节点标题中的特殊标记
+24
View File
@@ -0,0 +1,24 @@
import os
import sys
import importlib.util
# Make sure oneapi.py can be properly loaded
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir)
# Import oneapi module, register routes
def init_oneapi():
try:
spec = importlib.util.spec_from_file_location("oneapi", os.path.join(current_dir, "oneapi.py"))
oneapi = importlib.util.module_from_spec(spec)
spec.loader.exec_module(oneapi)
print("ComfyUI-OneAPI loaded")
except Exception as e:
print(f"ComfyUI-OneAPI failed to load: {str(e)}")
# This function will be automatically called when ComfyUI loads the plugin
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# Initialize API
init_oneapi()
+397
View File
@@ -0,0 +1,397 @@
import os
import json
import uuid
import time
import copy
import asyncio
import aiohttp
import tempfile
import mimetypes
from urllib.parse import urlparse
from aiohttp import web
from server import PromptServer
import execution
# Get routes
routes = PromptServer.instance.routes
@routes.post('/oneapi/v1/execute')
async def execute_workflow(request):
"""
Execute workflow API
Parameters:
- workflow: Workflow JSON
- params: Parameter mapping dictionary
- wait_for_result: Whether to wait for results (default True)
- timeout: Timeout in seconds (default 300)
Returns:
- Workflow execution results
"""
try:
# Get request data
data = await request.json()
# Extract parameters
workflow = data.get('workflow')
params = data.get('params', {})
wait_for_result = data.get('wait_for_result', True)
timeout = data.get('timeout', 300)
if not workflow:
return web.json_response({"error": "Workflow data is missing"}, status=400)
# Process workflow parameters
if params:
workflow = await _apply_params_to_workflow(workflow, params)
# Generate client ID
client_id = str(uuid.uuid4())
# Submit workflow to ComfyUI queue
prompt_id = await _queue_prompt(workflow, client_id)
if not prompt_id:
return web.json_response({"error": "Failed to submit workflow"}, status=500)
result = {
"status": "queued",
"prompt_id": prompt_id,
"message": "Workflow submitted"
}
# If not waiting for results, return immediately
if not wait_for_result:
return web.json_response(result)
# Poll for results
result = await _wait_for_results(prompt_id, timeout, request)
return web.json_response(result)
except Exception as e:
print(f"Error executing workflow: {str(e)}")
return web.json_response({"error": str(e)}, status=500)
async def _apply_params_to_workflow(workflow, params):
"""
Apply parameters to the workflow
Handles two types of parameter mappings:
1. LoadImage node: $image_param
2. Regular node: $param.field
"""
workflow = copy.deepcopy(workflow)
for node_id, node_data in workflow.items():
# Skip nodes that don't meet criteria
if not _is_valid_node(node_data):
continue
# Process parameter markers in the node
await _process_node_params(node_data, params)
return workflow
def _is_valid_node(node_data):
"""Check if node is valid and contains a title"""
return (isinstance(node_data, dict) and
"_meta" in node_data and
"title" in node_data["_meta"])
async def _process_node_params(node_data, params):
"""Process parameter markers in the node"""
title = node_data["_meta"]["title"]
# Split title and look for parameter markers
parts = title.split(',')
for part in parts:
part = part.strip()
if not part.startswith('$'):
continue
# Process parameter marker
await _process_param_marker(node_data, part[1:], params)
async def _process_param_marker(node_data, var_spec, params):
"""
Process individual parameter marker
Format: param_name.field_name
- param_name: Parameter name, corresponding to key in params
- field_name: Node input field name
Special handling for LoadImage node's image field
"""
# Must have field separator
if '.' not in var_spec:
print(f"Parameter marker format error, should be '$param.field': {var_spec}")
return
# Parse parameter name and field name
var_name, input_field = var_spec.split('.', 1)
# Check if parameter exists
if var_name not in params:
return
# Get parameter value
param_value = params[var_name]
# Special handling for LoadImage node's image field
if node_data.get('class_type') == 'LoadImage' and input_field == 'image':
await _handle_load_image(node_data, param_value)
else:
# Regular parameter setting
await _set_node_param(node_data, input_field, param_value)
async def _handle_load_image(node_data, image_path_or_url):
"""
Handle LoadImage node's image parameter
Args:
node_data: Node data
image_path_or_url: Image path or URL
"""
# Ensure inputs exists
if "inputs" not in node_data:
node_data["inputs"] = {}
# If parameter value is a URL starting with http, upload the image first
if isinstance(image_path_or_url, str) and image_path_or_url.startswith(('http://', 'https://')):
try:
# Upload image and get uploaded image name
image_value = await _upload_image_from_source(image_path_or_url)
# Use uploaded image name as LoadImage node's image value
await _set_node_param(node_data, "image", image_value)
print(f"Image uploaded: {image_value}")
except Exception as e:
print(f"Failed to upload image: {str(e)}")
# Throw exception on upload failure
raise Exception(f"Image upload failed: {str(e)}")
else:
# Use parameter value directly as image name
await _set_node_param(node_data, "image", image_path_or_url)
async def _set_node_param(node_data, input_field, param_value):
"""
Set node parameter
Args:
node_data: Node data
input_field: Input field name
param_value: Parameter value
"""
# Ensure inputs exists
if "inputs" not in node_data:
node_data["inputs"] = {}
# Set parameter value
node_data["inputs"][input_field] = param_value
async def _upload_image_from_source(image_url) -> str:
"""
Upload image from URL
Args:
image_url: Image URL
Returns:
Upload image file name
"""
# Download image from URL
async with aiohttp.ClientSession() as session:
async with session.get(image_url) as response:
if response.status != 200:
raise Exception(f"Failed to download image: HTTP {response.status}")
# Extract filename from URL
parsed_url = urlparse(image_url)
filename = os.path.basename(parsed_url.path)
if not filename:
filename = f"temp_image_{hash(image_url)}.jpg"
# Get image data
image_data = await response.read()
# Save to temporary file
suffix = os.path.splitext(filename)[1] or ".jpg"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp.write(image_data)
temp_path = tmp.name
try:
# Upload temporary file to ComfyUI
return await _upload_image(temp_path)
finally:
# Delete temporary file
os.unlink(temp_path)
async def _upload_image(image_path) -> str:
"""
Upload image to ComfyUI
Args:
image_path: Image path
Returns:
Upload image file name
"""
# Read image data
with open(image_path, 'rb') as f:
image_data = f.read()
# Extract filename
filename = os.path.basename(image_path)
# Auto-detect file MIME type
mime_type = mimetypes.guess_type(filename)[0]
if mime_type is None:
# Default to generic image type
mime_type = 'application/octet-stream'
# Prepare form data
data = aiohttp.FormData()
data.add_field('image', image_data,
filename=filename,
content_type=mime_type)
# Upload image (internal ComfyUI API call still uses 127.0.0.1)
async with aiohttp.ClientSession() as session:
async with session.post("http://127.0.0.1:8188/upload/image", data=data) as response:
if response.status != 200:
raise Exception(f"Failed to upload image: HTTP {response.status}")
# Get upload result
result = await response.json()
return result.get('name', '')
async def _queue_prompt(workflow, client_id):
"""Submit workflow to queue using HTTP API"""
try:
prompt_data = {
"prompt": workflow,
"client_id": client_id
}
json_data = json.dumps(prompt_data)
# Use aiohttp to send request
async with aiohttp.ClientSession() as session:
async with session.post("http://127.0.0.1:8188/prompt",
data=json_data,
headers={"Content-Type": "application/json"}) as response:
if response.status != 200:
print(f"Failed to submit workflow: {response.status}")
return None
result = await response.json()
# Return prompt_id
return result.get("prompt_id")
except Exception as e:
print(f"Error submitting workflow: {str(e)}")
return None
async def _get_base_url(request):
"""
Get base URL for building image URLs
Args:
request: HTTP request object
Returns:
Base URL string
"""
# Default base URL for local access
base_url = "http://127.0.0.1:8188"
if request:
host = request.headers.get('Host')
if host:
# Try multiple methods to get request protocol
scheme = request.headers.get('X-Forwarded-Proto') or \
request.headers.get('X-Scheme') or \
request.headers.get('X-Forwarded-Scheme') or \
(request.headers.get('X-Forwarded-Ssl') == 'on' and 'https') or \
(request.headers.get('X-Forwarded-Protocol') == 'https' and 'https') or \
('https' if request.url.scheme == 'https' else 'http')
# Build base URL
base_url = f"{scheme}://{host}"
return base_url
async def _wait_for_results(prompt_id, timeout=300, request=None):
"""Wait for workflow execution results, get history using HTTP API"""
start_time = time.time()
result = {
"status": "processing",
"prompt_id": prompt_id,
"images": []
}
# Get base URL for image URLs
base_url = await _get_base_url(request)
while True:
# Check timeout
if timeout > 0 and (time.time() - start_time) > timeout:
result["status"] = "timeout"
return result
# Get history using HTTP API
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:8188/history") as response:
if response.status != 200:
# API call failed, retry after waiting
await asyncio.sleep(1.0)
continue
# Get entire history
history_data = await response.json()
# Check if specified prompt_id is in history
if prompt_id not in history_data:
# Workflow might not be completed yet, retry after waiting
await asyncio.sleep(1.0)
continue
# Get history for specific prompt_id
prompt_history = history_data[prompt_id]
# Check if completed
if "outputs" in prompt_history:
result["status"] = "completed"
# Store original outputs
if 'outputs' in result:
del result['outputs'] # Remove outputs field
# Process outputs, especially focusing on SaveImage nodes
for node_id, node_output in prompt_history["outputs"].items():
if "images" in node_output:
for img_data in node_output["images"]:
filename = img_data.get("filename")
subfolder = img_data.get("subfolder", "")
img_type = img_data.get("type", "output")
# Build image URL
img_url = f"{base_url}/view?filename={filename}"
if subfolder:
img_url += f"&subfolder={subfolder}"
if img_type:
img_url += f"&type={img_type}"
# Simplified: directly add URL string to results
result["images"].append(img_url)
# Return complete results
return result
except Exception as e:
print(f"Error getting history: {str(e)}")
# Continue trying after error
# Wait before checking again
await asyncio.sleep(1.0)
print("ComfyUI-OneAPI routes registered")
+139
View File
@@ -0,0 +1,139 @@
{
"3": {
"inputs": {
"seed": 212,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 0.9,
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"14",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"4": {
"inputs": {
"ckpt_name": "SDXL/dreamshaperXL_lightningDPMSDE.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"6": {
"inputs": {
"text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt),$prompt.text"
}
},
"7": {
"inputs": {
"text": "text, watermark",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"10": {
"inputs": {
"image": "03.JPG"
},
"class_type": "LoadImage",
"_meta": {
"title": "Load Image,$image.image"
}
},
"11": {
"inputs": {
"width": 512,
"height": 512,
"interpolation": "nearest",
"method": "fill / crop",
"condition": "always",
"multiple_of": 0,
"image": [
"10",
0
]
},
"class_type": "ImageResize+",
"_meta": {
"title": "🔧 Image Resize"
}
},
"14": {
"inputs": {
"pixels": [
"11",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
}
}
+108
View File
@@ -0,0 +1,108 @@
{
"3": {
"inputs": {
"seed": 278586368967862,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"5",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"4": {
"inputs": {
"ckpt_name": "SDXL/dreamshaperXL_lightningDPMSDE.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"5": {
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"6": {
"inputs": {
"text": "beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt),$prompt.text"
}
},
"7": {
"inputs": {
"text": "text, watermark",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}