remove sweetalert2; add ui logic;
This commit is contained in:
@@ -80,6 +80,39 @@ When your workflow has multiple SaveImage nodes, easily distinguish different ou
|
||||
1. Download or clone this repository to the `custom_nodes` directory of ComfyUI
|
||||
2. Restart ComfyUI
|
||||
|
||||
### 🖥️ UI Features
|
||||
|
||||
This plugin adds convenient UI features to ComfyUI's interface:
|
||||
|
||||
#### 📝 Save Workflow as API
|
||||
|
||||
**How to use:**
|
||||
1. Right-click on the canvas (empty area)
|
||||
2. Select "🚀 Save Workflow as API"
|
||||
3. Enter a workflow name in the dialog
|
||||
4. Choose whether to overwrite if the file exists
|
||||
5. Click "Save"
|
||||
|
||||
The workflow will be saved to `user/default/api_workflows/` directory as a JSON file that can be used with the API.
|
||||
|
||||
#### 🏷️ Set Node Input Parameters
|
||||
|
||||
**How to use:**
|
||||
1. Select a single node in the workflow
|
||||
2. Right-click on the node
|
||||
3. Select "🚀 Set Node Input"
|
||||
4. Choose which field you want to parameterize from the list
|
||||
5. Enter a variable name for the parameter
|
||||
6. The node's title will be automatically updated with the parameter marker
|
||||
|
||||
**Example:**
|
||||
- Select a CLIPTextEncode node
|
||||
- Choose "text" field
|
||||
- Enter "prompt" as variable name
|
||||
- Node title will be updated to include `$prompt.text`
|
||||
|
||||
This feature makes it easy to mark nodes for parameter replacement without manually editing node titles.
|
||||
|
||||
### 🔌 API Parameters
|
||||
|
||||
```
|
||||
@@ -151,7 +184,7 @@ curl -X POST "http://localhost:8188/oneapi/v1/execute" \
|
||||
### Supported forms for the workflow parameter
|
||||
|
||||
- 1. Pass workflow as a JSON object (original logic).
|
||||
- 2. Pass a local workflow filename (e.g. `1.json`), which will be loaded from `user/default/workflows/1.json`.
|
||||
- 2. Pass a local workflow filename (e.g. `1.json`), which will be loaded from `user/default/api_workflows/1.json`.
|
||||
- 3. Pass a workflow URL (e.g. `http://xxx/1.json`), which will be downloaded and parsed automatically.
|
||||
|
||||
How to distinguish:
|
||||
@@ -165,7 +198,7 @@ How to distinguish:
|
||||
{"workflow": {"node1": {...}, ...}}
|
||||
|
||||
// 2. Pass local filename
|
||||
// 1.json corresponds to <ComfyUI root>/user/default/workflows/1.json
|
||||
// 1.json corresponds to <ComfyUI root>/user/default/api_workflows/1.json
|
||||
{"workflow": "1.json"}
|
||||
|
||||
// 3. Pass URL
|
||||
|
||||
+35
-2
@@ -80,6 +80,39 @@ curl -X POST "http://localhost:8188/oneapi/v1/execute" \
|
||||
1. 下载或克隆此仓库到ComfyUI的`custom_nodes`目录
|
||||
2. 重启ComfyUI
|
||||
|
||||
### 🖥️ 界面功能
|
||||
|
||||
此插件为 ComfyUI 界面添加了便捷的功能:
|
||||
|
||||
#### 📝 保存工作流为API
|
||||
|
||||
**使用方法:**
|
||||
1. 在画布空白处右键
|
||||
2. 选择"🚀 Save Workflow as API"
|
||||
3. 在对话框中输入工作流名称
|
||||
4. 选择是否覆盖已存在的文件
|
||||
5. 点击"保存"
|
||||
|
||||
工作流将保存到 `user/default/api_workflows/` 目录下,生成可用于API调用的JSON文件。
|
||||
|
||||
#### 🏷️ 设置节点输入参数
|
||||
|
||||
**使用方法:**
|
||||
1. 选择工作流中的单个节点
|
||||
2. 右键点击节点
|
||||
3. 选择"🚀 Set Node Input"
|
||||
4. 从列表中选择要参数化的字段
|
||||
5. 输入参数的变量名
|
||||
6. 节点标题将自动更新参数标记
|
||||
|
||||
**示例:**
|
||||
- 选择 CLIPTextEncode 节点
|
||||
- 选择"text"字段
|
||||
- 输入"prompt"作为变量名
|
||||
- 节点标题将更新为包含 `$prompt.text`
|
||||
|
||||
此功能让您无需手动编辑节点标题,即可轻松标记节点以进行参数替换。
|
||||
|
||||
### 🔌 API参数详解
|
||||
|
||||
```
|
||||
@@ -151,7 +184,7 @@ curl -X POST "http://localhost:8188/oneapi/v1/execute" \
|
||||
### workflow 参数支持三种形式
|
||||
|
||||
- 1. 直接传递 workflow 的 JSON 对象(原有逻辑)。
|
||||
- 2. 传递本地 workflow 文件名(如 `1.json`),会自动从 `user/default/workflows/1.json` 读取。
|
||||
- 2. 传递本地 workflow 文件名(如 `1.json`),会自动从 `user/default/api_workflows/1.json` 读取。
|
||||
- 3. 传递 workflow 的 URL(如 `http://xxx/1.json`),会自动下载并解析。
|
||||
|
||||
区分方式:
|
||||
@@ -165,7 +198,7 @@ curl -X POST "http://localhost:8188/oneapi/v1/execute" \
|
||||
{"workflow": {"node1": {...}, ...}}
|
||||
|
||||
// 2. 传本地文件名
|
||||
// 1.json 对应的是 <ComfyUI根目录>/user/default/workflows/1.json
|
||||
// 1.json 对应的是 <ComfyUI根目录>/user/default/api_workflows/1.json
|
||||
{"workflow": "1.json"}
|
||||
|
||||
// 3. 传 URL
|
||||
|
||||
@@ -11,19 +11,55 @@ import mimetypes
|
||||
from urllib.parse import urlparse
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
import folder_paths
|
||||
import execution
|
||||
from workflow_ui_2_api import convert_ui_to_api, adjust_workflow_format
|
||||
from workflow_format import adjust_workflow_format
|
||||
|
||||
# Constants
|
||||
API_WORKFLOWS_DIR = 'api_workflows'
|
||||
|
||||
# Get routes
|
||||
routes = PromptServer.instance.routes
|
||||
prompt_server = PromptServer.instance
|
||||
routes = prompt_server.routes
|
||||
|
||||
# Get workflow paths
|
||||
path_custom_nodes = os.path.dirname(os.path.dirname(__file__))
|
||||
path_comfyui_root = os.path.dirname(path_custom_nodes)
|
||||
path_workflows = os.path.join(path_comfyui_root, 'user/default/workflows')
|
||||
|
||||
# Output node types for workflow result extraction
|
||||
OUTPUT_NODE_TYPES = ["SaveImage", "SaveVideo", "VHS_VideoCombine"]
|
||||
@routes.post('/oneapi/v1/save-api-workflow')
|
||||
async def save_api_workflow(request):
|
||||
"""
|
||||
Save API workflow to user's workflow directory.
|
||||
"""
|
||||
data = await request.json()
|
||||
name = data.get('name')
|
||||
workflow = data.get('workflow')
|
||||
overwrite = data.get('overwrite', False)
|
||||
|
||||
if not name:
|
||||
return web.json_response({"error": "Name is required"}, status=400)
|
||||
if not workflow:
|
||||
return web.json_response({"error": "Workflow is required"}, status=400)
|
||||
|
||||
fmt = adjust_workflow_format(workflow)
|
||||
if fmt == 'invalid':
|
||||
return web.json_response({"error": "Invalid workflow format"}, status=400)
|
||||
if fmt == 'ui':
|
||||
return web.json_response({"error": "UI format workflow is not supported. Please convert to API format and try again."}, status=400)
|
||||
|
||||
name_with_json = name if name.endswith('.json') else f'{name}.json'
|
||||
relative_path = f'{API_WORKFLOWS_DIR}/{name_with_json}'
|
||||
save_path = prompt_server.user_manager.get_request_user_filepath(request, relative_path, create_dir=True)
|
||||
if not save_path:
|
||||
return web.json_response({"error": "Failed to get save path"}, status=500)
|
||||
if os.path.exists(save_path) and not overwrite:
|
||||
return web.json_response({"error": "File already exists. Use overwrite=true to overwrite."}, status=400)
|
||||
|
||||
with open(save_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(workflow, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return web.json_response({"message": "Workflow saved successfully", "filename": name_with_json})
|
||||
|
||||
@routes.post('/oneapi/v1/execute')
|
||||
async def execute_workflow(request):
|
||||
@@ -31,10 +67,11 @@ async def execute_workflow(request):
|
||||
Execute workflow API
|
||||
|
||||
Parameters:
|
||||
- workflow: Workflow JSON, filename (under user/default/workflows/), or URL
|
||||
- workflow: Workflow JSON, filename (under user/default/api_workflows/), or URL
|
||||
- params: Parameter mapping dictionary
|
||||
- wait_for_result: Whether to wait for results (default True)
|
||||
- timeout: Timeout in seconds (default 300)
|
||||
- timeout: Timeout in seconds (default None)
|
||||
- prompt_ext_params: Extra parameters for prompt request (optional)
|
||||
|
||||
Returns:
|
||||
- Return values:
|
||||
@@ -44,14 +81,16 @@ async def execute_workflow(request):
|
||||
- images_by_var: Mapped image URLs by variable name {var_name: [string, ...], ...}, only present if there are image results
|
||||
- videos: List of video URLs [string, ...], only present if there are video results
|
||||
- videos_by_var: Mapped video URLs by variable name {var_name: [string, ...], ...}, only present if there are video results
|
||||
- texts: List of text outputs [string, ...], only present if there are text results
|
||||
- texts_by_var: Mapped text outputs by variable name {var_name: [string, ...], ...}, only present if there are text results
|
||||
|
||||
Node title markers:
|
||||
- Input: Use "$param.field" in node title to map parameter values
|
||||
- Output: Use "$output" or "$output.name" in output node title (see OUTPUT_NODE_TYPES) to specify outputs
|
||||
- "$output.name" - Marks an output node with a custom output name (added to "images_by_var[name]" or "videos_by_var[name]")
|
||||
- Output: Use "$output.name" in output node title to specify outputs
|
||||
- "$output.name" - Marks an output node with a custom output name (added to "images_by_var[name]" or "videos_by_var[name]" or "texts_by_var[name]")
|
||||
- If no explicit output marker is set, the node_id is used as the variable name
|
||||
- Only nodes in OUTPUT_NODE_TYPES are considered for output
|
||||
- images/images_by_var/videos/videos_by_var fields are only included if there are corresponding results
|
||||
- Any node that produces outputs (images, videos, texts) will be included in results
|
||||
- images/images_by_var/videos/videos_by_var/texts/texts_by_var fields are only included if there are corresponding results
|
||||
"""
|
||||
try:
|
||||
# Get request data
|
||||
@@ -62,6 +101,7 @@ async def execute_workflow(request):
|
||||
params = data.get('params', {})
|
||||
wait_for_result = data.get('wait_for_result', True)
|
||||
timeout = data.get('timeout', None)
|
||||
prompt_ext_params = data.get('prompt_ext_params', {})
|
||||
|
||||
# Support workflow as local path or URL
|
||||
if isinstance(workflow, dict):
|
||||
@@ -70,7 +110,7 @@ async def execute_workflow(request):
|
||||
if workflow.startswith('http://') or workflow.startswith('https://'):
|
||||
workflow = await _load_workflow_from_url(workflow)
|
||||
else:
|
||||
workflow = _load_workflow_from_local(workflow)
|
||||
workflow = _load_workflow_from_local(workflow, request)
|
||||
else:
|
||||
return web.json_response({"error": "Invalid workflow parameter"}, status=400)
|
||||
|
||||
@@ -82,7 +122,7 @@ async def execute_workflow(request):
|
||||
if fmt == 'invalid':
|
||||
return web.json_response({"error": "Invalid workflow format"}, status=400)
|
||||
if fmt == 'ui':
|
||||
workflow = await convert_ui_to_api(workflow)
|
||||
return web.json_response({"error": "UI format workflow is not supported. Please convert to API format and try again."}, status=400)
|
||||
|
||||
# Process workflow parameters
|
||||
if params:
|
||||
@@ -96,7 +136,7 @@ async def execute_workflow(request):
|
||||
|
||||
# Submit workflow to ComfyUI queue
|
||||
try:
|
||||
prompt_id = await _queue_prompt(workflow, client_id)
|
||||
prompt_id = await _queue_prompt(workflow, client_id, prompt_ext_params)
|
||||
except Exception as e:
|
||||
error_message = f"Failed to submit workflow: [{type(e)}] {str(e)}"
|
||||
print(error_message)
|
||||
@@ -163,17 +203,16 @@ async def _process_node_params(node_data, params):
|
||||
async def _extract_output_nodes(workflow):
|
||||
"""
|
||||
Extract output nodes and their output variable names from workflow
|
||||
(output node types are defined in OUTPUT_NODE_TYPES)
|
||||
|
||||
Args:
|
||||
workflow: Workflow JSON object
|
||||
|
||||
Returns:
|
||||
Dictionary mapping node_id to output variable name
|
||||
Dictionary mapping node_id to output variable name (only for nodes with explicit $output.name markers)
|
||||
|
||||
Note:
|
||||
Only nodes in OUTPUT_NODE_TYPES are considered as output nodes.
|
||||
Output fields (images, videos, *_by_var) are only included in the response if there are corresponding results.
|
||||
Any node that produces outputs will be included in results.
|
||||
Output fields (images, videos, texts, *_by_var) are only included in the response if there are corresponding results.
|
||||
"""
|
||||
output_id_2_var = {}
|
||||
|
||||
@@ -181,10 +220,6 @@ async def _extract_output_nodes(workflow):
|
||||
# Skip nodes that don't meet criteria
|
||||
if not _is_valid_node(node_data):
|
||||
continue
|
||||
|
||||
# Only process output nodes (see OUTPUT_NODE_TYPES)
|
||||
if node_data.get('class_type') not in OUTPUT_NODE_TYPES:
|
||||
continue
|
||||
|
||||
# Get node title
|
||||
title = node_data["_meta"]["title"]
|
||||
@@ -205,9 +240,9 @@ async def _extract_output_nodes(workflow):
|
||||
# Simple $output without variable name is not valid
|
||||
raise Exception(f"Invalid output marker format (missing name): {part}. Use $output.name format.")
|
||||
|
||||
# For output 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)
|
||||
# Only register in output_id_2_var if there's an explicit output marker
|
||||
if output_var:
|
||||
output_id_2_var[node_id] = output_var
|
||||
|
||||
return output_id_2_var
|
||||
|
||||
@@ -363,13 +398,17 @@ async def _upload_image(image_path) -> str:
|
||||
result = await response.json()
|
||||
return result.get('name', '')
|
||||
|
||||
async def _queue_prompt(workflow, client_id):
|
||||
async def _queue_prompt(workflow, client_id, prompt_ext_params=None):
|
||||
"""Submit workflow to queue using HTTP API"""
|
||||
prompt_data = {
|
||||
"prompt": workflow,
|
||||
"client_id": client_id
|
||||
}
|
||||
|
||||
# Update prompt_data with all parameters from prompt_ext_params
|
||||
if prompt_ext_params:
|
||||
prompt_data.update(prompt_ext_params)
|
||||
|
||||
json_data = json.dumps(prompt_data)
|
||||
|
||||
# Use aiohttp to send request
|
||||
@@ -418,62 +457,21 @@ async def _get_base_url(request):
|
||||
|
||||
return base_url
|
||||
|
||||
# Helper: extract URLs for a specific media type from a node output
|
||||
def _extract_node_media_urls(node_output, base_url, media_key):
|
||||
"""
|
||||
Extract URLs for a specific media type (e.g., 'images', 'gifs') from a node output.
|
||||
Args:
|
||||
node_output: Output dict for a node
|
||||
base_url: Base URL for constructing file URLs
|
||||
media_key: Key in node_output ('images' or 'gifs')
|
||||
Returns:
|
||||
List of URLs for the specified media type
|
||||
"""
|
||||
urls = []
|
||||
for media_data in node_output.get(media_key, []):
|
||||
filename = media_data.get("filename")
|
||||
subfolder = media_data.get("subfolder", "")
|
||||
media_type = media_data.get("type", "output")
|
||||
url = f"{base_url}/view?filename={filename}"
|
||||
if subfolder:
|
||||
url += f"&subfolder={subfolder}"
|
||||
if media_type:
|
||||
url += f"&type={media_type}"
|
||||
urls.append(url)
|
||||
return urls
|
||||
|
||||
# Helper: collect all outputs of a given media type from all nodes
|
||||
def _collect_outputs_by_type(outputs, base_url, media_key):
|
||||
"""
|
||||
Collect all outputs of a given media type from all nodes.
|
||||
Args:
|
||||
outputs: All outputs dict
|
||||
base_url: Base URL for constructing file URLs
|
||||
media_key: Key in node_output ('images' or 'gifs')
|
||||
Returns:
|
||||
Dict mapping node_id to list of URLs
|
||||
"""
|
||||
result = {}
|
||||
for node_id, node_output in outputs.items():
|
||||
urls = _extract_node_media_urls(node_output, base_url, media_key)
|
||||
if urls:
|
||||
result[node_id] = urls
|
||||
return result
|
||||
|
||||
# Helper: map outputs by variable name
|
||||
def _map_outputs_by_var(output_id_2_var, output_id_2_media):
|
||||
"""
|
||||
Map outputs by variable name using output_id_2_var mapping.
|
||||
Args:
|
||||
output_id_2_var: Dict mapping node_id to variable name
|
||||
output_id_2_media: Dict mapping node_id to list of URLs
|
||||
output_id_2_var: Dict mapping node_id to variable name (for nodes with explicit markers)
|
||||
output_id_2_media: Dict mapping node_id to list of URLs/data
|
||||
Returns:
|
||||
Dict mapping variable name to list of URLs
|
||||
Dict mapping variable name to list of URLs/data
|
||||
"""
|
||||
result = {}
|
||||
for node_id, var_name in output_id_2_var.items():
|
||||
if node_id in output_id_2_media:
|
||||
result[var_name] = output_id_2_media[node_id]
|
||||
for node_id, media_data in output_id_2_media.items():
|
||||
# Use explicit variable name if available, otherwise use node_id
|
||||
var_name = output_id_2_var.get(node_id, str(node_id))
|
||||
result[var_name] = media_data
|
||||
return result
|
||||
|
||||
# Helper: flatten all lists in a dict into a single list
|
||||
@@ -529,7 +527,9 @@ async def _wait_for_results(prompt_id, timeout=None, request=None, output_id_2_v
|
||||
"images": [],
|
||||
"images_by_var": {},
|
||||
"videos": [],
|
||||
"videos_by_var": {}
|
||||
"videos_by_var": {},
|
||||
"texts": [],
|
||||
"texts_by_var": {}
|
||||
}
|
||||
|
||||
# Get base URL for image/video URLs
|
||||
@@ -560,35 +560,50 @@ async def _wait_for_results(prompt_id, timeout=None, request=None, output_id_2_v
|
||||
# Collect all image and video outputs by file extension
|
||||
output_id_2_images = {}
|
||||
output_id_2_videos = {}
|
||||
output_id_2_texts = {}
|
||||
for node_id, node_output in prompt_history["outputs"].items():
|
||||
images, videos = _split_media_by_suffix(node_output, base_url)
|
||||
if images:
|
||||
output_id_2_images[node_id] = images
|
||||
if videos:
|
||||
output_id_2_videos[node_id] = videos
|
||||
# Collect text outputs
|
||||
if "text" in node_output:
|
||||
# Handle text field as string or list
|
||||
texts = node_output["text"]
|
||||
if isinstance(texts, str):
|
||||
texts = [texts]
|
||||
elif not isinstance(texts, list):
|
||||
texts = [str(texts)]
|
||||
output_id_2_texts[node_id] = texts
|
||||
|
||||
# Map by variable name if mapping is available
|
||||
if output_id_2_var and output_id_2_images:
|
||||
if output_id_2_images:
|
||||
result["images_by_var"] = _map_outputs_by_var(output_id_2_var, output_id_2_images)
|
||||
result["images"] = _extend_flat_list_from_dict(result["images_by_var"])
|
||||
elif output_id_2_images:
|
||||
result["images"] = _extend_flat_list_from_dict(output_id_2_images)
|
||||
|
||||
if output_id_2_var and output_id_2_videos:
|
||||
if output_id_2_videos:
|
||||
result["videos_by_var"] = _map_outputs_by_var(output_id_2_var, output_id_2_videos)
|
||||
result["videos"] = _extend_flat_list_from_dict(result["videos_by_var"])
|
||||
elif output_id_2_videos:
|
||||
result["videos"] = _extend_flat_list_from_dict(output_id_2_videos)
|
||||
|
||||
# Remove empty fields for images/videos
|
||||
if not result["images"]:
|
||||
result.pop("images")
|
||||
if not result["images_by_var"]:
|
||||
result.pop("images_by_var")
|
||||
if "videos" in result and not result["videos"]:
|
||||
result.pop("videos")
|
||||
if "videos_by_var" in result and not result["videos_by_var"]:
|
||||
result.pop("videos_by_var")
|
||||
# Handle texts/texts_by_var
|
||||
if output_id_2_texts:
|
||||
result["texts_by_var"] = _map_outputs_by_var(output_id_2_var, output_id_2_texts)
|
||||
result["texts"] = _extend_flat_list_from_dict(result["texts_by_var"])
|
||||
|
||||
# Remove empty fields for images/videos/texts
|
||||
if not result.get("images"):
|
||||
result.pop("images", None)
|
||||
if not result.get("images_by_var"):
|
||||
result.pop("images_by_var", None)
|
||||
if not result.get("videos"):
|
||||
result.pop("videos", None)
|
||||
if not result.get("videos_by_var"):
|
||||
result.pop("videos_by_var", None)
|
||||
if not result.get("texts"):
|
||||
result.pop("texts", None)
|
||||
if not result.get("texts_by_var"):
|
||||
result.pop("texts_by_var", None)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -596,14 +611,21 @@ async def _wait_for_results(prompt_id, timeout=None, request=None, output_id_2_v
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
# New: Load workflow from local file
|
||||
def _load_workflow_from_local(filename):
|
||||
def _load_workflow_from_local(filename, request=None):
|
||||
"""
|
||||
Load workflow JSON from user/default/workflows directory
|
||||
Load workflow JSON from user's workflow directory
|
||||
"""
|
||||
file_path = os.path.join(path_workflows, filename)
|
||||
if not os.path.isfile(file_path):
|
||||
raise Exception(f"Workflow file not found: {file_path}")
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
if not request or not prompt_server.user_manager:
|
||||
raise Exception("User context is required to load workflow from user directory")
|
||||
|
||||
name_with_json = filename if filename.endswith('.json') else f'{filename}.json'
|
||||
relative_path = f'{API_WORKFLOWS_DIR}/{name_with_json}'
|
||||
api_workflow_path = prompt_server.user_manager.get_request_user_filepath(request, relative_path, create_dir=False)
|
||||
|
||||
if not api_workflow_path or not os.path.isfile(api_workflow_path):
|
||||
raise Exception(f"Workflow file not found in user directory: {filename}")
|
||||
|
||||
with open(api_workflow_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
# New: Load workflow from URL
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
const Swal = window.Sweetalert2;
|
||||
|
||||
function addCanvasMenuOptions() {
|
||||
const original_getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||
// get the basic options
|
||||
const originOptions = original_getCanvasMenuOptions.apply(this, arguments);
|
||||
|
||||
const addedOptions = [
|
||||
{
|
||||
content: "T Load Default Workflow",
|
||||
callback: async () => {
|
||||
Swal.fire({
|
||||
title: 'Hello',
|
||||
text: 'Hello',
|
||||
icon: 'success',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
},
|
||||
null
|
||||
]
|
||||
return [...addedOptions, ...originOptions];
|
||||
// return originOptions;
|
||||
}
|
||||
}
|
||||
|
||||
function addExtraMenuOptions(nodeType, nodeData, app) {
|
||||
const nodeTypeClass = nodeType?.comfyClass;
|
||||
const original_getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||
original_getExtraMenuOptions?.apply(this, arguments);
|
||||
options.splice(0, 0, {
|
||||
content: "Set Node Input",
|
||||
callback: async () => {
|
||||
const selectedNodes = app.canvas.selected_nodes;
|
||||
const selectedNodeIds = Object.keys(selectedNodes);
|
||||
if (selectedNodeIds.length > 1) {
|
||||
Swal.fire({
|
||||
title: 'Error',
|
||||
text: 'Please select only one node',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeId = selectedNodeIds[0];
|
||||
const node = selectedNodes[nodeId];
|
||||
const fieldNames = node.widgets.map(e => e.name);
|
||||
const selectedFieldName = await Swal.fire({
|
||||
title: 'Select Input Field',
|
||||
input: 'select',
|
||||
inputOptions: fieldNames,
|
||||
inputPlaceholder: 'Select Input Field',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
const fieldIndex = selectedFieldName.value;
|
||||
if (fieldIndex !== "") {
|
||||
const fieldName = fieldNames[fieldIndex];
|
||||
const varName = await Swal.fire({
|
||||
title: `Select Variable Name for ${fieldName}`,
|
||||
input: 'text',
|
||||
inputPlaceholder: 'Select Variable Name',
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return 'Please enter a variable name';
|
||||
}
|
||||
},
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
addVar2Node(node, varName.value, fieldName);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function addVar2Node(node, varName, fieldName) {
|
||||
const newPart = `$${varName}.${fieldName}`;
|
||||
// Split and remove existing items with the same fieldName
|
||||
let parts = node.title.split(',').map(item => item.trim());
|
||||
parts = parts.filter(item => {
|
||||
// Match $xxx.fieldName
|
||||
const match = item.match(/^\$[^.]+\.(.+)$/);
|
||||
// Keep items that are not the current fieldName, or not in $xxx.fieldName format
|
||||
return !(match && match[1] === fieldName);
|
||||
});
|
||||
// Add the new one
|
||||
parts.push(newPart);
|
||||
// Rejoin
|
||||
node.title = parts.filter(Boolean).join(',');
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyUI-OneAPI-ConvertUIToAPI",
|
||||
async setup() {
|
||||
// Custom canvas background right-click menu
|
||||
// addCanvasMenuOptions();
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
// Custom node right-click menu
|
||||
addExtraMenuOptions(nodeType, nodeData, app);
|
||||
},
|
||||
})
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { $el } from "../../scripts/ui.js";
|
||||
|
||||
function formatTitle(text) {
|
||||
return `🚀 ${text}`;
|
||||
}
|
||||
|
||||
// Show save workflow dialog using ComfyUI native dialog
|
||||
async function showSaveWorkflowDialog() {
|
||||
return new Promise((resolve) => {
|
||||
const html = '<h3 style="margin: 0 0 15px 0; color: var(--content-fg);">Save API Workflow</h3>' +
|
||||
'<div style="margin-bottom: 15px;">' +
|
||||
'<label style="display: block; margin-bottom: 5px; color: var(--content-fg);">Workflow name:</label>' +
|
||||
'<input type="text" id="workflow-name" placeholder="Enter workflow name" style="width: 100%; padding: 8px; box-sizing: border-box;" required>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label style="display: flex; align-items: center; color: var(--content-fg);">' +
|
||||
'<input type="checkbox" id="overwrite-checkbox" style="margin-right: 8px;">' +
|
||||
'Overwrite if exists' +
|
||||
'</label>' +
|
||||
'</div>';
|
||||
|
||||
// Use existing dialog instance
|
||||
const dialog = app.ui.dialog;
|
||||
|
||||
// Show the dialog with HTML content
|
||||
dialog.show(html);
|
||||
|
||||
// Replace the default buttons with our custom buttons
|
||||
const modalContent = dialog.element.querySelector('.comfy-modal-content');
|
||||
const defaultButtons = modalContent.querySelectorAll('button');
|
||||
defaultButtons.forEach(btn => btn.remove());
|
||||
|
||||
// Handle cancel
|
||||
const handleCancel = () => {
|
||||
dialog.close();
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
// Handle save
|
||||
const handleSave = () => {
|
||||
const nameInput = dialog.element.querySelector('#workflow-name');
|
||||
const overwriteCheckbox = dialog.element.querySelector('#overwrite-checkbox');
|
||||
const name = nameInput.value.trim();
|
||||
if (!name) {
|
||||
alert('Please enter a workflow name');
|
||||
nameInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
dialog.close();
|
||||
resolve({
|
||||
name: name,
|
||||
overwrite: overwriteCheckbox.checked
|
||||
});
|
||||
};
|
||||
|
||||
// Create our custom button container using $el
|
||||
const buttonContainer = $el('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
justifyContent: 'flex-end'
|
||||
}
|
||||
}, [
|
||||
$el('button', {
|
||||
textContent: 'Cancel',
|
||||
style: { padding: '8px 16px' },
|
||||
onclick: handleCancel
|
||||
}),
|
||||
$el('button', {
|
||||
textContent: 'Save',
|
||||
style: { padding: '8px 16px' },
|
||||
onclick: handleSave
|
||||
})
|
||||
]);
|
||||
|
||||
modalContent.appendChild(buttonContainer);
|
||||
|
||||
// Get elements from the dialog
|
||||
const nameInput = dialog.element.querySelector('#workflow-name');
|
||||
|
||||
// Focus on name input
|
||||
setTimeout(() => nameInput?.focus(), 100);
|
||||
|
||||
// Handle Enter key in name input
|
||||
nameInput?.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancel();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Call save API
|
||||
async function saveWorkflowApi(name, workflow, overwrite) {
|
||||
const response = await api.fetchApi('/oneapi/v1/save-api-workflow', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
workflow: workflow,
|
||||
overwrite: overwrite
|
||||
})
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
return { response, responseData };
|
||||
}
|
||||
|
||||
// Show result message using ComfyUI native dialog
|
||||
function showResultMessage(success, message) {
|
||||
const title = success ? 'Save Successful' : 'Save Failed';
|
||||
const color = success ? '#28a745' : '#dc3545';
|
||||
const html = '<h3 style="margin: 0 0 15px 0; color: ' + color + '; text-align: center;">' + title + '</h3>' +
|
||||
'<p style="margin: 0; text-align: center; color: var(--content-fg);">' + message + '</p>';
|
||||
|
||||
const dialog = app.ui.dialog;
|
||||
|
||||
// Show the dialog
|
||||
dialog.show(html);
|
||||
|
||||
// Replace the default buttons with our custom button
|
||||
const modalContent = dialog.element.querySelector('.comfy-modal-content');
|
||||
const defaultButtons = modalContent.querySelectorAll('button');
|
||||
defaultButtons.forEach(btn => btn.remove());
|
||||
|
||||
// Create our custom button container using $el
|
||||
const buttonContainer = $el('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
}, [
|
||||
$el('button', {
|
||||
textContent: 'OK',
|
||||
style: { padding: '8px 16px' },
|
||||
onclick: () => dialog.close(),
|
||||
$: (btn) => setTimeout(() => btn.focus(), 100)
|
||||
})
|
||||
]);
|
||||
|
||||
modalContent.appendChild(buttonContainer);
|
||||
}
|
||||
|
||||
// Main function to save workflow as API
|
||||
async function saveWorkflowAsAPI() {
|
||||
try {
|
||||
// Get workflow data
|
||||
const result = await app.graphToPrompt();
|
||||
const workflow = result.output;
|
||||
|
||||
// Show save dialog
|
||||
const formValues = await showSaveWorkflowDialog();
|
||||
if (!formValues) return;
|
||||
|
||||
const { name, overwrite } = formValues;
|
||||
|
||||
// Call save API
|
||||
const { response, responseData } = await saveWorkflowApi(name, workflow, overwrite);
|
||||
|
||||
// Show result
|
||||
if (response.ok) {
|
||||
showResultMessage(true, `Workflow saved as: ${responseData.filename}`);
|
||||
} else {
|
||||
showResultMessage(false, responseData.error || 'Unknown error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving workflow:', error);
|
||||
showResultMessage(false, 'Error occurred while saving workflow: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function addCanvasMenuOptions() {
|
||||
const original_getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions;
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||
// get the basic options
|
||||
const originOptions = original_getCanvasMenuOptions.apply(this, arguments);
|
||||
|
||||
const addedOptions = [
|
||||
{
|
||||
content: formatTitle("Save Workflow as API"),
|
||||
callback: saveWorkflowAsAPI
|
||||
},
|
||||
null
|
||||
]
|
||||
return [...addedOptions, ...originOptions];
|
||||
// return originOptions;
|
||||
}
|
||||
}
|
||||
|
||||
function addExtraMenuOptions(nodeType, nodeData, app) {
|
||||
const nodeTypeClass = nodeType?.comfyClass;
|
||||
const original_getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||
original_getExtraMenuOptions?.apply(this, arguments);
|
||||
options.splice(0, 0, {
|
||||
content: formatTitle("Set Node Input"),
|
||||
callback: async () => {
|
||||
const selectedNodes = app.canvas.selected_nodes;
|
||||
const selectedNodeIds = Object.keys(selectedNodes);
|
||||
if (selectedNodeIds.length > 1) {
|
||||
alert('Please select only one node');
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeId = selectedNodeIds[0];
|
||||
const node = selectedNodes[nodeId];
|
||||
const fieldNames = node.widgets.map(e => e.name);
|
||||
|
||||
// Show field selection dialog
|
||||
const fieldName = await showFieldSelectionDialog(fieldNames);
|
||||
if (!fieldName) return;
|
||||
|
||||
// Show variable name input dialog
|
||||
const varName = await showVariableNameDialog(fieldName);
|
||||
if (!varName) return;
|
||||
|
||||
addVar2Node(node, varName, fieldName);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Show field selection dialog
|
||||
async function showFieldSelectionDialog(fieldNames) {
|
||||
return new Promise((resolve) => {
|
||||
const options = fieldNames.map((name, index) =>
|
||||
'<div style="margin-bottom: 8px;">' +
|
||||
'<label style="display: flex; align-items: center; color: var(--content-fg); cursor: pointer;">' +
|
||||
'<input type="radio" name="field-selection" value="' + name + '" style="margin-right: 8px;">' +
|
||||
name +
|
||||
'</label>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
|
||||
const html = '<h3 style="margin: 0 0 15px 0; color: var(--content-fg);">Select Input Field</h3>' +
|
||||
'<div id="radio-container">' +
|
||||
options +
|
||||
'</div>';
|
||||
|
||||
const dialog = app.ui.dialog;
|
||||
|
||||
// Show the dialog
|
||||
dialog.show(html);
|
||||
|
||||
// Replace the default buttons with our custom buttons
|
||||
const modalContent = dialog.element.querySelector('.comfy-modal-content');
|
||||
const defaultButtons = modalContent.querySelectorAll('button');
|
||||
defaultButtons.forEach(btn => btn.remove());
|
||||
|
||||
const handleCancel = () => {
|
||||
dialog.close();
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
const handleOK = () => {
|
||||
const selectedRadio = dialog.element.querySelector('input[name="field-selection"]:checked');
|
||||
if (!selectedRadio) {
|
||||
alert('Please select a field');
|
||||
return;
|
||||
}
|
||||
dialog.close();
|
||||
resolve(selectedRadio.value);
|
||||
};
|
||||
|
||||
// Create our custom button container using $el
|
||||
const buttonContainer = $el('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
justifyContent: 'flex-end'
|
||||
}
|
||||
}, [
|
||||
$el('button', {
|
||||
textContent: 'Cancel',
|
||||
style: { padding: '8px 16px' },
|
||||
onclick: handleCancel
|
||||
}),
|
||||
$el('button', {
|
||||
textContent: 'OK',
|
||||
style: { padding: '8px 16px' },
|
||||
onclick: handleOK
|
||||
})
|
||||
]);
|
||||
|
||||
modalContent.appendChild(buttonContainer);
|
||||
|
||||
// Auto-select first option and focus on it
|
||||
setTimeout(() => {
|
||||
const firstRadio = dialog.element.querySelector('input[name="field-selection"]');
|
||||
if (firstRadio) {
|
||||
firstRadio.checked = true;
|
||||
firstRadio.focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 基于 radio 按钮父控件监听键盘事件
|
||||
const radioContainer = dialog.element.querySelector('#radio-container');
|
||||
radioContainer?.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleOK();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancel();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Show variable name input dialog
|
||||
async function showVariableNameDialog(fieldName) {
|
||||
return new Promise((resolve) => {
|
||||
const html = '<h3 style="margin: 0 0 15px 0; color: var(--content-fg);">Set Variable Name for `' + fieldName + '`</h3>' +
|
||||
'<div>' +
|
||||
'<input type="text" id="var-name" placeholder="Enter variable name" style="width: 100%; padding: 8px; box-sizing: border-box;" required>' +
|
||||
'</div>';
|
||||
|
||||
const dialog = app.ui.dialog;
|
||||
|
||||
// Show the dialog
|
||||
dialog.show(html);
|
||||
|
||||
// Replace the default buttons with our custom buttons
|
||||
const modalContent = dialog.element.querySelector('.comfy-modal-content');
|
||||
const defaultButtons = modalContent.querySelectorAll('button');
|
||||
defaultButtons.forEach(btn => btn.remove());
|
||||
|
||||
const handleCancel = () => {
|
||||
dialog.close();
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
const handleOK = () => {
|
||||
const varNameInput = dialog.element.querySelector('#var-name');
|
||||
const varName = varNameInput.value.trim();
|
||||
if (!varName) {
|
||||
alert('Please enter a variable name');
|
||||
varNameInput?.focus();
|
||||
return;
|
||||
}
|
||||
dialog.close();
|
||||
resolve(varName);
|
||||
};
|
||||
|
||||
// Create our custom button container using $el
|
||||
const buttonContainer = $el('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
justifyContent: 'flex-end'
|
||||
}
|
||||
}, [
|
||||
$el('button', {
|
||||
textContent: 'Cancel',
|
||||
style: { padding: '8px 16px' },
|
||||
onclick: handleCancel
|
||||
}),
|
||||
$el('button', {
|
||||
textContent: 'OK',
|
||||
style: { padding: '8px 16px' },
|
||||
onclick: handleOK
|
||||
})
|
||||
]);
|
||||
|
||||
modalContent.appendChild(buttonContainer);
|
||||
|
||||
// Get elements
|
||||
const varNameInput = dialog.element.querySelector('#var-name');
|
||||
|
||||
setTimeout(() => varNameInput?.focus(), 100);
|
||||
|
||||
// 基于文本输入框监听键盘事件
|
||||
varNameInput?.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleOK();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancel();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addVar2Node(node, varName, fieldName) {
|
||||
const newPart = '$' + varName + '.' + fieldName;
|
||||
// Split and remove existing items with the same fieldName
|
||||
let parts = node.title.split(',').map(item => item.trim());
|
||||
parts = parts.filter(item => {
|
||||
// Match $xxx.fieldName
|
||||
const match = item.match(/^\$[^.]+\.(.+)$/);
|
||||
// Keep items that are not the current fieldName, or not in $xxx.fieldName format
|
||||
return !(match && match[1] === fieldName);
|
||||
});
|
||||
// Add the new one
|
||||
parts.push(newPart);
|
||||
// Rejoin
|
||||
node.title = parts.filter(Boolean).join(',');
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyUI-OneAPI-ConvertUIToAPI",
|
||||
async setup() {
|
||||
// Custom canvas background right-click menu
|
||||
addCanvasMenuOptions();
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
// Custom node right-click menu
|
||||
addExtraMenuOptions(nodeType, nodeData, app);
|
||||
},
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
from typing import Literal
|
||||
|
||||
def adjust_workflow_format(workflow: dict) -> Literal['ui', 'api', 'invalid']:
|
||||
"""
|
||||
Determine if workflow is UI or API format.
|
||||
Args:
|
||||
workflow (dict): workflow object
|
||||
Returns:
|
||||
Literal['ui', 'api', 'invalid']: 'ui' if UI format, 'api' if API format, 'invalid' if neither
|
||||
"""
|
||||
if isinstance(workflow, dict):
|
||||
if 'nodes' in workflow and isinstance(workflow['nodes'], list):
|
||||
return 'ui'
|
||||
# Heuristic: API format top-level keys are all digit strings
|
||||
if all(isinstance(k, str) and k.isdigit() for k in workflow.keys()) and len(workflow) > 0:
|
||||
return 'api'
|
||||
return 'invalid'
|
||||
@@ -1,145 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from typing import Literal
|
||||
|
||||
async def get_object_info(base_url):
|
||||
"""
|
||||
Fetch object_info from ComfyUI server.
|
||||
Args:
|
||||
base_url (str): ComfyUI server base url
|
||||
Returns:
|
||||
dict: object_info json
|
||||
"""
|
||||
url = base_url.rstrip('/') + '/api/object_info'
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
raise Exception(f"Failed to fetch object_info: {resp.status}")
|
||||
return await resp.json()
|
||||
|
||||
def adjust_workflow_format(workflow: dict) -> Literal['ui', 'api', 'invalid']:
|
||||
"""
|
||||
Determine if workflow is UI or API format.
|
||||
Args:
|
||||
workflow (dict): workflow object
|
||||
Returns:
|
||||
Literal['ui', 'api', 'invalid']: 'ui' if UI format, 'api' if API format, 'invalid' if neither
|
||||
"""
|
||||
if not isinstance(workflow, dict):
|
||||
return 'invalid'
|
||||
if 'nodes' in workflow and isinstance(workflow['nodes'], list):
|
||||
return 'ui'
|
||||
# Heuristic: API format top-level keys are all digit strings
|
||||
if all(isinstance(k, str) and k.isdigit() for k in workflow.keys()) and len(workflow) > 0:
|
||||
return 'api'
|
||||
return 'invalid'
|
||||
|
||||
async def convert_ui_to_api(ui_json, base_url='http://127.0.0.1:8188'):
|
||||
"""
|
||||
Convert ComfyUI UI workflow format to API workflow format using live object_info.
|
||||
Args:
|
||||
ui_json (dict): UI format workflow JSON
|
||||
base_url (str): ComfyUI server base url
|
||||
Returns:
|
||||
dict: API format workflow JSON
|
||||
"""
|
||||
object_info = await get_object_info(base_url)
|
||||
api = {}
|
||||
node_id_map = {}
|
||||
for node in ui_json.get('nodes', []):
|
||||
node_id_map[node['id']] = str(node['id'])
|
||||
for node in ui_json.get('nodes', []):
|
||||
node_id = str(node['id'])
|
||||
node_type = node['type']
|
||||
title = node.get('title')
|
||||
if not title:
|
||||
title = node.get('properties', {}).get('Node name for S&R', node_type)
|
||||
meta = {"title": title}
|
||||
inputs = {}
|
||||
widgets = node.get('widgets_values', [])
|
||||
# 1. Process all links and write to inputs
|
||||
for inp in node.get('inputs', []):
|
||||
# Check if this input has an actual link (not null)
|
||||
if inp.get('link') is not None:
|
||||
link_id = inp['link']
|
||||
for link in ui_json.get('links', []):
|
||||
if link[0] == link_id:
|
||||
from_node = str(link[1])
|
||||
from_slot = link[2]
|
||||
inputs[inp['name']] = [from_node, from_slot]
|
||||
break
|
||||
# 2. Find all fields covered by links (those with non-null link values)
|
||||
linked_fields = set(inp['name'] for inp in node.get('inputs', []) if inp.get('link') is not None)
|
||||
node_info = object_info.get(node_type, {})
|
||||
input_order = node_info.get('input_order', {}).get('required', [])
|
||||
input_required = node_info.get('input', {}).get('required', {})
|
||||
# 3. Assign widgets_values to fields not covered by links
|
||||
widget_idx = 0
|
||||
|
||||
for field in input_order:
|
||||
if field in linked_fields:
|
||||
# Skip fields that have links
|
||||
continue
|
||||
|
||||
# Check if we have more widget values to process
|
||||
if widget_idx >= len(widgets):
|
||||
break
|
||||
|
||||
value = widgets[widget_idx]
|
||||
|
||||
# Check if this field has control_after_generate property
|
||||
field_info = input_required.get(field)
|
||||
has_control_after_generate = False
|
||||
if isinstance(field_info, list) and len(field_info) > 1:
|
||||
meta_info = field_info[1]
|
||||
if isinstance(meta_info, dict) and meta_info.get('control_after_generate'):
|
||||
has_control_after_generate = True
|
||||
|
||||
# Assign the widget value to the current field
|
||||
inputs[field] = value
|
||||
widget_idx += 1
|
||||
|
||||
# Special handling: if this field has control_after_generate, check if next value is a control value
|
||||
# Fields with control_after_generate (like seed) may have control values (like "randomize")
|
||||
# following their actual value in widgets_values
|
||||
if has_control_after_generate and widget_idx < len(widgets):
|
||||
next_value = widgets[widget_idx]
|
||||
control_values = {"randomize", "increment", "decrement", "fixed"}
|
||||
if next_value in control_values:
|
||||
widget_idx += 1
|
||||
# 4. Fill default values for missing required fields
|
||||
for field in input_order:
|
||||
if field not in inputs:
|
||||
field_info = input_required.get(field)
|
||||
if isinstance(field_info, list) and len(field_info) > 1:
|
||||
meta_info = field_info[1]
|
||||
if isinstance(meta_info, dict) and 'default' in meta_info:
|
||||
inputs[field] = meta_info['default']
|
||||
api[node_id] = {
|
||||
'inputs': inputs,
|
||||
'class_type': node_type,
|
||||
'_meta': meta
|
||||
}
|
||||
return api
|
||||
|
||||
async def main():
|
||||
"""
|
||||
Main entry for CLI usage. Usage: python workflow_ui_2_api.py [base_url]
|
||||
Converts test/ui.json to test/ui_to_api.json using live object_info.
|
||||
"""
|
||||
base_url = 'http://127.0.0.1:8188'
|
||||
if len(sys.argv) > 1:
|
||||
base_url = sys.argv[1]
|
||||
ui_path = 'test/ui.json'
|
||||
api_path = 'test/ui_to_api.json'
|
||||
with open(ui_path, 'r', encoding='utf-8') as f:
|
||||
ui_json = json.load(f)
|
||||
api_json = await convert_ui_to_api(ui_json, base_url)
|
||||
with open(api_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(api_json, f, indent=2, ensure_ascii=False)
|
||||
print(f"Converted {ui_path} to {api_path} using {base_url}/api/object_info")
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user