diff --git a/README.md b/README.md index 307af6f..a40ad42 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,119 @@ -# ComfyUI-OneAPI +# 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. +ComfyUI-OneAPI is a plugin that provides simple REST API interfaces for ComfyUI, allowing you to execute complex ComfyUI workflows through a single API request. [中文文档](README_CN.md) -## Features +## ⚡️ Quick Start -- **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 +### 🚀 Execute Workflow with Just One Request -## Installation +```bash +curl -X POST "http://localhost:8188/oneapi/v1/execute" \ + -H "Content-Type: application/json" \ + -d '{ + "workflow": {...} # Replace with your workflow JSON + }' +``` + +### 📝 Simplest Request Format + +```json +{ + "workflow": {...} // API version of workflow JSON +} +``` + +### 📤 Common Response Format + +```json +{ + "status": "completed", + "images": ["http://server/image1.png", "http://server/image2.png"] +} +``` + +## 🔥 Advanced Usage + +### 1️⃣ Dynamic Parameter Replacement - No More Workflow Edits 🔄 + +Add markers in node titles to easily replace parameters: + +```json +// Request +{ + "workflow": {...}, + "params": { + "prompt": "cute cat", + "input_image": "https://example.com/image.jpg" + } +} +``` + +**✨ How to Mark Nodes:** +- 📝 Text Prompt: Add `$prompt.text` to CLIPTextEncode node title +- 🖼️ Input Image: Add `$input_image` to LoadImage node title + +### 2️⃣ Distinguish Multiple Outputs - Handle Complex Workflows 🧩 + +When your workflow has multiple SaveImage nodes, easily distinguish different outputs: + +```json +// Response +{ + "status": "completed", + "images": ["http://server/image1.png", "http://server/image2.png"], + "images_by_var": { + "background": ["http://server/image1.png"], + "character": ["http://server/image2.png"] + } +} +``` + +**✨ How to Mark Output Nodes:** +- 💾 Add `$output.background` or `$output.character` to SaveImage node titles + +## 📋 Advanced Features + +### 📦 Installation 1. Download or clone this repository to the `custom_nodes` directory of ComfyUI 2. Restart ComfyUI -## API Usage - -### Execute Workflow +### 🔌 API Parameters ``` POST /oneapi/v1/execute Request Body: { - "workflow": {Workflow JSON}, - "params": { - "paramName": "paramValue" - } + "workflow": {...}, // API version of workflow JSON + "params": {...}, // Optional: Parameter mapping + "wait_for_result": true/false, // Optional: Wait for results (default true) + "timeout": 300 // Optional: Timeout in seconds } ``` -## Workflow Parameter Mapping +### 🏷️ Node Title Marker Rules -To use dynamic parameters in your workflow, you need to use special markers in the node titles: +#### ⬇️ Input Parameter Markers -1. LoadImage node: Use `$image_param` format -2. Other nodes: Use `$param.field_name` format +1. 🖼️ LoadImage node: Use `$image_param` format +2. 🔄 Other nodes: Use `$param.field_name` format Examples: +- `$input_image` - LoadImage node uses params.input_image as the image +- `$prompt.text` - Replaces text field with params.prompt -- A LoadImage node marked with `$input_image` will use the input_image parameter from params as the image param -- A CLIPTextEncode node marked with `$prompt.text` will replace the text field with the prompt parameter from params +#### ⬆️ Output Markers -## Examples +Add markers to SaveImage node titles: +- Format: `$output.name` (e.g., `$output.background`) +- Without markers, node ID is used as the variable name -### curl Examples +## 🔍 Examples -#### Text-to-Image Example +### 📝 Text-to-Image Example ```bash curl -X POST "http://localhost:8188/oneapi/v1/execute" \ @@ -61,10 +126,9 @@ curl -X POST "http://localhost:8188/oneapi/v1/execute" \ }' ``` -#### Image-to-Image Example +### 🖼️ 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 '{ @@ -76,8 +140,8 @@ curl -X POST "http://localhost:8188/oneapi/v1/execute" \ }' ``` -## Notes +## ⚠️ 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 \ No newline at end of file +- 🔄 This plugin uses HTTP polling to get results, does not provide WebSocket real-time progress +- ⏱️ Long-running workflows may cause request timeouts, consider setting appropriate timeout values +- 🏷️ Parameter mapping and output marking depend on special markers in node titles \ No newline at end of file diff --git a/README_CN.md b/README_CN.md index 65ca52a..480954b 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,54 +1,119 @@ -# ComfyUI-OneAPI +# ComfyUI-OneAPI ✨ -ComfyUI-OneAPI 是一个为 ComfyUI 提供简单REST API接口的插件,使您能够通过一个简单的API请求执行ComfyUI工作流。 +ComfyUI-OneAPI 是一个为 ComfyUI 提供简单REST API接口的插件,只需一个API请求即可执行复杂的ComfyUI工作流。 [English Documentation](README.md) -## 特点 +## ⚡️ 快速开始 -- **简单接口**:通过单个API接口执行复杂的ComfyUI工作流 -- **参数映射**:支持动态替换工作流中的参数 -- **异步执行**:支持同步或异步执行模式 -- **无WebSocket依赖**:使用HTTP轮询获取结果,不需要处理WebSocket连接 +### 🚀 仅需一个请求即可执行工作流 -## 安装 +```bash +curl -X POST "http://localhost:8188/oneapi/v1/execute" \ + -H "Content-Type: application/json" \ + -d '{ + "workflow": {...} # 替换为您的工作流JSON + }' +``` + +### 📝 最简单的请求格式 + +```json +{ + "workflow": {...} // 工作流的API版JSON +} +``` + +### 📤 常见响应格式 + +```json +{ + "status": "completed", + "images": ["http://server/image1.png", "http://server/image2.png"] +} +``` + +## 🔥 进阶用法 + +### 1️⃣ 动态替换参数 - 告别工作流修改 🔄 + +在节点标题中添加标记,轻松替换参数: + +```json +// 请求 +{ + "workflow": {...}, + "params": { + "prompt": "cute cat", + "input_image": "https://example.com/image.jpg" + } +} +``` + +**✨ 如何标记节点:** +- 📝 文本提示:在CLIPTextEncode节点标题添加 `$prompt.text` +- 🖼️ 输入图像:在LoadImage节点标题添加 `$input_image` + +### 2️⃣ 区分多个输出 - 处理复杂工作流 🧩 + +当工作流有多个SaveImage节点时,轻松区分不同输出: + +```json +// 响应 +{ + "status": "completed", + "images": ["http://server/image1.png", "http://server/image2.png"], + "images_by_var": { + "background": ["http://server/image1.png"], + "character": ["http://server/image2.png"] + } +} +``` + +**✨ 如何标记输出节点:** +- 💾 在SaveImage节点标题添加 `$output.background` 或 `$output.character` + +## 📋 高级功能 + +### 📦 安装 1. 下载或克隆此仓库到ComfyUI的`custom_nodes`目录 2. 重启ComfyUI -## API使用 - -### 执行工作流 +### 🔌 API参数详解 ``` POST /oneapi/v1/execute 请求体: { - "workflow": {工作流JSON}, - "params": { - "参数名": "参数值" - } + "workflow": {...}, // 工作流的API版JSON + "params": {...}, // 可选:参数映射 + "wait_for_result": true/false, // 可选:是否等待结果(默认true) + "timeout": 300 // 可选:超时时间(秒) } ``` -## 工作流参数映射 +### 🏷️ 节点标题标记规则 -要在工作流中使用动态参数,需要在节点标题中使用特殊标记: +#### ⬇️ 输入参数标记 -1. LoadImage节点:使用 `$image_param` 格式 -2. 其他节点:使用 `$param.field_name` 格式 +1. 🖼️ LoadImage节点:使用 `$image_param` 格式 +2. 🔄 其他节点:使用 `$param.field_name` 格式 -例如: +示例: +- `$input_image` - LoadImage节点使用params.input_image作为图片 +- `$prompt.text` - 使用params.prompt替换text字段 -- 标记为 `$input_image` 的LoadImage节点,将使用params中的input_image参数作为图片参数 -- 标记为 `$prompt.text` 的CLIPTextEncode节点,将使用params中的prompt参数替换text字段 +#### ⬆️ 输出标记 -## 示例 +在SaveImage节点标题中添加标记: +- 格式:`$output.name`(如:`$output.background`) +- 无标记时使用节点ID作为标识 -### curl示例 +## 🔍 示例 -#### 文生图示例 +### 📝 文生图示例 ```bash curl -X POST "http://localhost:8188/oneapi/v1/execute" \ @@ -61,10 +126,9 @@ curl -X POST "http://localhost:8188/oneapi/v1/execute" \ }' ``` -#### 图生图示例 +### 🖼️ 图生图示例 ```bash -# 执行图生图工作流 curl -X POST "http://localhost:8188/oneapi/v1/execute" \ -H "Content-Type: application/json" \ -d '{ @@ -76,8 +140,8 @@ curl -X POST "http://localhost:8188/oneapi/v1/execute" \ }' ``` -## 注意事项 +## ⚠️ 注意事项 -- 此插件设计为简化API调用,不提供WebSocket实时进度信息 -- 超长时间的工作流可能导致请求超时,建议设置合理的超时时间 -- 工作流中的参数映射依赖于节点标题中的特殊标记 \ No newline at end of file +- 🔄 此插件使用HTTP轮询获取结果,不提供WebSocket实时进度 +- ⏱️ 长时间运行的工作流可能导致超时,请设置合适的timeout值 +- 🏷️ 参数映射和输出标记依赖于节点标题中的特殊标记 \ No newline at end of file diff --git a/oneapi.py b/oneapi.py index dd417e5..8b4575e 100644 --- a/oneapi.py +++ b/oneapi.py @@ -27,7 +27,19 @@ async def execute_workflow(request): - timeout: Timeout in seconds (default 300) Returns: - - Workflow execution results + - Return values: + - status: Status (queued, processing, completed, timeout) + - prompt_id: Prompt ID + - images: List of image URLs [string, ...] + - images_by_var: Mapped image URLs by variable name {var_name: [string, ...], ...} + + Node title markers: + - Input: Use "$param.field" in node title to map parameter values + - Output: Use "$output" or "$output.name" in SaveImage node title to specify outputs + - "$output.name" - Marks a SaveImage node with a custom output name (added to "images_by_var[name]") + - If no explicit output marker is set, the node_id is used as the variable name + - Only SaveImage nodes are considered for output + - Both images and images_by_var fields are always included in the response """ try: # Get request data @@ -46,6 +58,9 @@ async def execute_workflow(request): if params: workflow = await _apply_params_to_workflow(workflow, params) + # Extract and save output node information + output_id_2_var = await _extract_output_nodes(workflow) + # Generate client ID client_id = str(uuid.uuid4()) @@ -68,7 +83,7 @@ async def execute_workflow(request): return web.json_response(result) # Poll for results - result = await _wait_for_results(prompt_id, timeout, request) + result = await _wait_for_results(prompt_id, timeout, request, output_id_2_var) return web.json_response(result) except Exception as e: @@ -115,6 +130,52 @@ async def _process_node_params(node_data, params): # Process parameter marker await _process_param_marker(node_data, part[1:], params) +async def _extract_output_nodes(workflow): + """ + Extract SaveImage nodes and their output variable names from workflow + + Args: + workflow: Workflow JSON object + + Returns: + Dictionary mapping node_id to output variable name + """ + output_id_2_var = {} + + for node_id, node_data in workflow.items(): + # Skip nodes that don't meet criteria + if not _is_valid_node(node_data): + continue + + # Only process SaveImage nodes + if node_data.get('class_type') != 'SaveImage': + continue + + # Get node title + title = node_data["_meta"]["title"] + + # Check for $output marker in the title + output_var = None + parts = title.split(',') + for part in parts: + part = part.strip() + if part.startswith('$output'): + # Parse output marker + if '.' in part: + # Format: $output.name - Specify output name + output_var = part.split('.', 1)[1] + if not output_var: + raise Exception(f"Invalid output marker format (empty name): {part}") + else: + # Simple $output without variable name is not valid + raise Exception(f"Invalid output marker format (missing name): {part}. Use $output.name format.") + + # For SaveImage nodes, always register in output_id_2_var + # If no explicit marker, use node_id as the variable name + output_id_2_var[node_id] = output_var if output_var else str(node_id) + + return output_id_2_var + async def _process_param_marker(node_data, var_spec, params): """ Process individual parameter marker @@ -322,13 +383,14 @@ async def _get_base_url(request): return base_url -async def _wait_for_results(prompt_id, timeout=None, request=None): +async def _wait_for_results(prompt_id, timeout=None, request=None, output_id_2_var=None): """Wait for workflow execution results, get history using HTTP API""" start_time = time.time() result = { "status": "processing", "prompt_id": prompt_id, - "images": [] + "images": [], + "images_by_var": {} } # Get base URL for image URLs @@ -365,13 +427,14 @@ async def _wait_for_results(prompt_id, timeout=None, request=None): if "outputs" in prompt_history: result["status"] = "completed" - # Store original outputs - if 'outputs' in result: - del result['outputs'] # Remove outputs field + # Store all SaveImage node outputs + output_id_2_images = {} - # Process outputs, especially focusing on SaveImage nodes + # Process outputs, focusing on nodes with images for node_id, node_output in prompt_history["outputs"].items(): if "images" in node_output: + # Store image URLs for this node + node_images = [] for img_data in node_output["images"]: filename = img_data.get("filename") subfolder = img_data.get("subfolder", "") @@ -384,8 +447,25 @@ async def _wait_for_results(prompt_id, timeout=None, request=None): if img_type: img_url += f"&type={img_type}" - # Simplified: directly add URL string to results - result["images"].append(img_url) + node_images.append(img_url) + + output_id_2_images[node_id] = node_images + + # Process according to output mapping + if output_id_2_var and output_id_2_images: + # First pass: handle nodes with explicit mapping + for node_id, output_var in output_id_2_var.items(): + if node_id in output_id_2_images: + # Add to images_by_var using the variable name + result["images_by_var"][output_var] = output_id_2_images[node_id] + + # For backward compatibility: add all output images to the images array as well + for images in output_id_2_images.values(): + result["images"].extend(images) + else: + # No mapping or no image outputs, fallback to including all images in the images array + for node_images in output_id_2_images.values(): + result["images"].extend(node_images) # Return complete results return result