From 2d355ef08eb6bdc1b9934ea398279dd482c2b24d Mon Sep 17 00:00:00 2001
From: fkxianzhou <368684850@qq.com>
Date: Wed, 8 Apr 2026 15:28:34 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=A4=9A=E5=9B=BE=E8=BE=93?=
=?UTF-8?q?=E5=85=A5=E5=A4=84=E7=90=86=E5=B9=B6=E6=B7=BB=E5=8A=A0=E8=A7=86?=
=?UTF-8?q?=E9=A2=91=E4=B8=8A=E4=BC=A0=E7=BC=93=E5=AD=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 314 +++++++++++++-------------
example_workflows/Seedance 2.json | 2 +-
example_workflows/Seedream 4.json | 2 +-
example_workflows/Seedream 5.json | 2 +-
nodes/constants.py | 31 ++-
nodes/nodes_image.py | 125 +++++------
nodes/nodes_shared.py | 304 ++++++++++++++++---------
nodes/nodes_video.py | 357 ++++++++++++++++++++----------
pyproject.toml | 2 +-
web/js/jimeng_dynamic_widgets.js | 310 +++++++-------------------
10 files changed, 777 insertions(+), 672 deletions(-)
diff --git a/README.md b/README.md
index 858c868..83a7a9e 100644
--- a/README.md
+++ b/README.md
@@ -1,154 +1,166 @@

-
-
-# ComfyUI 即梦 API 节点
-
-本项目为 [ComfyUI](https://github.com/comfyanonymous/ComfyUI) 提供了火山方舟的视觉模型(即梦/豆包) API 节点。用户可以通过这些节点在 ComfyUI 中使用多种图像生成和视频生成功能。
-
-* Seedance 2.0 面向企业用户开放公测,项目已支持 `Seedance 2.0` 与 `Seedance 2.0 fast` 视频生成节点,可用于文生视频、多模态参考、视频编辑与视频延长场景。
-
-## ⚠️ 模型下线与节点弃用说明
-
-火山方舟平台会随着模型迭代逐步下线旧版本模型。根据官方公告,`doubao-seedream-3-0-t2i-250415` 与 `doubao-seedance-1-0-lite-i2v-250428 / doubao-seedance-1-0-lite-t2v-250428` 已进入下线流程,平台将逐步下调配额并在到期后完成服务下线与替换。
-
-- **已标记为即将弃用**:
- - `图像生成(Seedream 3)`(对应模型:doubao-seedream-3-0-t2i-250415)
- - `视频生成(参考图生视频)`(对应模型:doubao-seedance-1-0-lite-i2v-250428 / doubao-seedance-1-0-lite-t2v-250428)
-- **建议使用新节点**:
- - `图像生成(Seedream 3)` → `图像生成(Seedream 5)`
- - `视频生成(参考图生视频)` → `视频生成(Seedance 2.0)`
-
-
-* 如在使用过程中遇到问题,请通过 [ISSUES](https://github.com/fkxianzhou/ComfyUI-Jimeng-API/issues) 反馈。
-
-* 暂不支持 ComfyUI 的 Nodes 2.0 新版 UI,如遇到显示异常:
- 1. 点击左侧/顶部 ComfyUI 图标 打开菜单。
- 2. 找到并关闭 Nodes 2.0 选项,切换回传统界面。
-
-## ✨ 项目特性
-
-- **多 Key 管理**:支持在配置文件中设置多个 API Key,并在节点中灵活切换,方便管理不同账户或配额。
-- **异步与并发**:所有核心节点均支持任务的异步提交和并发生成,无需阻塞队列,大幅提升批量生成效率。
-- **友好交互**:提供清晰的控制台进度提示和完善的异常处理机制,报错信息直观,便于快速排查问题。
-
-## 📦 安装
-
-方式1: **克隆仓库**:
- 打开终端,`cd` 到 ComfyUI 的 `custom_nodes` 目录,运行:
- ```bash
+
+
+# ComfyUI 即梦 API 节点
+
+本项目为 [ComfyUI](https://github.com/comfyanonymous/ComfyUI) 提供了火山方舟的视觉模型(即梦/豆包) API 节点。用户可以通过这些节点在 ComfyUI 中使用多种图像生成和视频生成功能。
+
+- Seedance 2.0 面向企业用户开放公测,项目已支持 `Seedance 2.0` 与 `Seedance 2.0 fast` 视频生成节点,可用于文生视频、多模态参考、视频编辑与视频延长场景。
+
+## ⚠️ 模型下线与节点弃用说明
+
+火山方舟平台会随着模型迭代逐步下线旧版本模型。目前`doubao-seedream-3-0` 与 `doubao-seedance-1-0-lite` 已进入下线流程,平台将逐步下调配额并在到期后完成服务下线与替换。
+
+- **已标记为即将弃用**:
+ - `图像生成(Seedream 3)`(对应模型:doubao-seedream-3-0)
+ - `视频生成(参考图生视频)`(对应模型:doubao-seedance-1-0-lite)
+- **建议使用新节点**:
+ - `图像生成(Seedream 3)` → `图像生成(Seedream 5)`
+ - `视频生成(参考图生视频)` → `视频生成(Seedance 2.0)`
+
+* 如在使用过程中遇到问题,请通过 [ISSUES](https://github.com/fkxianzhou/ComfyUI-Jimeng-API/issues) 反馈。
+* 暂不支持 ComfyUI 的 Nodes 2.0 新版 UI,如遇到显示异常:
+ 1. 点击左侧/顶部 ComfyUI 图标 打开菜单。
+ 2. 找到并关闭 Nodes 2.0 选项,切换回传统界面。
+
+## ✨ 项目特性
+
+- **多 Key 管理**:支持在配置文件中设置多个 API Key,并在节点中灵活切换,方便管理不同账户或配额。
+- **异步与并发**:所有核心节点均支持任务的异步提交和并发生成,无需阻塞队列,大幅提升批量生成效率。
+- **友好交互**:提供清晰的控制台进度提示和完善的异常处理机制,报错信息直观,便于快速排查问题。
+
+## 📦 安装
+
+方式1: **克隆仓库**:
+打开终端,`cd` 到 ComfyUI 的 `custom_nodes` 目录,运行:
+`bash
git clone https://github.com/fkxianzhou/ComfyUI-Jimeng-API
- ```
-
-方式2: **使用ComfyUI Manager下载**。
-
-## ⚙️ 设置:配置 API 密钥
-
-### 方式 1:手动配置
-
-1. 在插件根目录中找到 `api_keys.json.example` 文件。
-2. 将其**重命名**为 `api_keys.json`。
-3. 打开文件并填入您的密钥信息([从此获取 Key](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey))。
-
-### 方式 2:节点内配置
-
-1. 在 ComfyUI 中添加 **Jimeng API Client** 节点。
-2. 在 `key_name` 下拉框中选择 **Custom**。
-3. 在弹出的输入框中填入您的 API Key。
-4. (可选)在 `new_key_name` 中填入一个名称(如 "MyKey"),运行一次后该 Key 将被自动保存。
- * *注意:保存后需刷新浏览器页面,新密钥才会显示在下拉列表中。*
-
-## 📖 功能节点列表
-
-(所有节点均位于 `JimengAI` 菜单下)
-
-- **基础设置**:
- - `火山方舟 API 客户端`: **(必须)** 用于加载并创建一个可供其他节点使用的客户端实例。
- - `Jimeng 配额设置`: 用于设置图像和视频生成的配额限制,防止意外消耗过多资源。
-- **图像生成**:
- - `图像生成(Seedream 3)`: 支持**文生图**和**图生图**模式的基础生成节点。
- - `图像生成(Seedream 4)`: 高级图像生成节点,支持多图输入、**组图生成**以及 4.5 模型。
- - `图像生成(Seedream 5)`: 最新一代图像生成节点,支持多图输入、组图生成以及**联网搜索**。
-- **视频生成**:
- - `视频生成(Seedance 1.0)`: 核心视频生成节点,支持文生视频、图生视频(首/尾帧)。
- - `视频生成(Seedance 1.5 Pro)`: 支持**音频生成**和**智能时长**的高级视频生成节点。
- - `视频生成(Seedance 2.0)`: 支持**图片/视频/音频多模态参考**、**视频编辑/延长**与**联网搜索增强**。
- - `视频生成(参考图生视频)`: 根据 1-4 张**参考图像**生成视频。
- - `视频生成任务列表查询`: 用于查询和管理在 API 上运行的任务历史。
-- **视觉理解**:
- - `视觉理解(Visual Understanding)`: 使用 Seed 2.0 模型进行视觉理解对话,支持图片/视频输入和多轮对话。
-
-## 📑 节点详解
-
-### `火山方舟 API 客户端 (Jimeng API Client)`
-加载 `api_keys.json` 中的密钥配置。这是所有工作流的起点。
-- **输入**: `密钥名称` (在 JSON 中配置的 customName)。
-- **输出**: `客户端` 实例。
-
-### `Jimeng 配额设置 (Jimeng Quota Settings)`
-允许为当前客户端设置图像(张数)和视频(Tokens)的使用上限。
-- **特性**: 当达到限额时自动停止任务并抛出提示,防止额度超支。
-- **示例工作流**:
-
-
----
-
-### `图像生成(Seedream 3)`
-- **模式**: 不连接 `输入图像` 为文生图,连接则为图生图。
-
-**示例工作流**:
-
-
----
-
-### `图像生成(Seedream 4)`
-支持 `doubao-seedream-4.5`。
-- **输入图像**: 支持单张或多张(Batch)图像作为参考。
-- **启用组图生成**: 开启后可一次性生成多张内容关联的图片。
-
-**示例工作流**:
-
-
----
-
-### `图像生成(Seedream 5)`
-支持最新的 `doubao-seedream-5.0-lite`。
-- **输入图像**: 支持单张或多张(Batch)图像作为参考。
-- **启用组图生成**: 开启后可一次性生成多张内容关联的图片。
-- **联网搜索**: 开启后模型可自主搜索互联网内容(如实时信息、商品等)辅助生成。
-
-**示例工作流**:
-
-
----
-
-### `视频生成(Seedance 1.0 / 1.5 Pro / 2.0)`
-- **1.0 特性**: 支持首/尾帧视频生成,支持非阻塞异步模式。
-- **1.5 Pro 特性**: 支持**音效生成**和**智能时长**控制。
-- **2.0 特性**: 支持最多 **9 张参考图 + 3 段参考视频 + 3 段参考音频**,可覆盖文生视频、多模态参考生视频、视频编辑、视频延长与联网搜索增强。
-
-### `视频生成(参考图生视频)`
-支持上传 1-4 张图像作为风格或内容参考生成视频。
-
-### `视频生成任务列表查询`
-支持按状态、模型版本或任务 ID 过滤查询任务历史。
-
-**示例工作流**:
-
-
-
----
-
-### `视觉理解(Visual Understanding)`
-支持 `doubao-seed-2.0` 系列模型。
-- **多模态输入**: 支持上传图片或视频进行理解和问答。
-- **多轮对话**: 支持开启多轮对话模式,保持上下文。
-- **深度思考**: 支持开启深度思考模式,提升复杂问题的推理能力。
-
-**示例工作流**:
-
-
-## 📓 示例工作流
-
-
-您可以在 `example_workflows` 目录中找到所有节点的示例工作流。
+ `
+
+
+方式2: **使用ComfyUI Manager下载**。
+
+## ⚙️ 设置:配置 API 密钥
+
+### 方式 1:手动配置
+
+1. 在插件根目录中找到 `api_keys.json.example` 文件。
+2. 将其**重命名**为 `api_keys.json`。
+3. 打开文件并填入您的密钥信息([从此获取 Key](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey))。
+
+### 方式 2:节点内配置
+
+1. 在 ComfyUI 中添加 **Jimeng API Client** 节点。
+2. 在 `key_name` 下拉框中选择 **Custom**。
+3. 在弹出的输入框中填入您的 API Key。
+4. (可选)在 `new_key_name` 中填入一个名称(如 "MyKey"),运行一次后该 Key 将被自动保存。
+ - *注意:保存后需刷新浏览器页面,新密钥才会显示在下拉列表中。*
+
+## 📖 功能节点列表
+
+(所有节点均位于 `JimengAI` 菜单下)
+
+- **基础设置**:
+ - `火山方舟 API 客户端`: **(必须)** 用于加载并创建一个可供其他节点使用的客户端实例。
+ - `Jimeng 配额设置`: 用于设置图像和视频生成的配额限制,防止意外消耗过多资源。
+- **图像生成**:
+ - `图像生成(Seedream 3)`: 支持**文生图**和**图生图**模式的基础生成节点。
+ - `图像生成(Seedream 4)`: 高级图像生成节点,支持多图输入、**组图生成**以及 4.5 模型。
+ - `图像生成(Seedream 5)`: 最新一代图像生成节点,支持多图输入、组图生成以及**联网搜索**。
+- **视频生成**:
+ - `视频生成(Seedance 1.0)`: 核心视频生成节点,支持文生视频、图生视频(首/尾帧)。
+ - `视频生成(Seedance 1.5 Pro)`: 支持**音频生成**和**智能时长**的高级视频生成节点。
+ - `视频生成(Seedance 2.0)`: 支持**图片/视频/音频多模态参考**、**视频编辑/延长**与**联网搜索增强**。
+ - `视频生成(参考图生视频)`: 根据 1-4 张**参考图像**生成视频。
+ - `视频生成任务列表查询`: 用于查询和管理在 API 上运行的任务历史。
+- **视觉理解**:
+ - `视觉理解(Visual Understanding)`: 使用 Seed 2.0 模型进行视觉理解对话,支持图片/视频输入和多轮对话。
+
+## 📑 节点详解
+
+### `火山方舟 API 客户端 (Jimeng API Client)`
+
+加载 `api_keys.json` 中的密钥配置。这是所有工作流的起点。
+
+- **输入**: `密钥名称` (在 JSON 中配置的 customName)。
+- **输出**: `客户端` 实例。
+
+### `Jimeng 配额设置 (Jimeng Quota Settings)`
+
+允许为当前客户端设置图像(张数)和视频(Tokens)的使用上限。
+
+- **特性**: 当达到限额时自动停止任务并抛出提示,防止额度超支。
+- **示例工作流**:
+ !\[Quota Settings Workflow]\(example\_workflows/QuotaSettings.jpg null)
+
+***
+
+### `图像生成(Seedream 3)`
+
+- **模式**: 不连接 `输入图像` 为文生图,连接则为图生图。
+
+**示例工作流**:
+!\[Seedream 3 Workflow]\(example\_workflows/Seedream%203.jpg null)
+
+***
+
+### `图像生成(Seedream 4)`
+
+支持 `doubao-seedream-4.5`。
+
+- **输入图像**: 支持单张或多张(Batch)图像作为参考。
+- **启用组图生成**: 开启后可一次性生成多张内容关联的图片。
+
+**示例工作流**:
+!\[Seedream 4 Workflow]\(example\_workflows/Seedream%204.jpg null)
+
+***
+
+### `图像生成(Seedream 5)`
+
+支持最新的 `doubao-seedream-5.0-lite`。
+
+- **输入图像**: 支持单张或多张(Batch)图像作为参考。
+- **启用组图生成**: 开启后可一次性生成多张内容关联的图片。
+- **联网搜索**: 开启后模型可自主搜索互联网内容(如实时信息、商品等)辅助生成。
+
+**示例工作流**:
+!\[Seedream 5 Workflow]\(example\_workflows/Seedream%205.jpg null)
+
+***
+
+### `视频生成(Seedance 1.0 / 1.5 Pro / 2.0)`
+
+- **1.0 特性**: 支持首/尾帧视频生成。
+- **1.5 Pro 特性**: 支持**音效生成**和**智能时长**控制。
+- **2.0 特性**: 支持最多 **9 张参考图 + 3 段参考视频 + 3 段参考音频**,可覆盖文生视频、多模态参考生视频、视频编辑、视频延长与联网搜索增强。
+
+### `视频生成(参考图生视频)`
+
+支持上传 1-4 张图像作为风格或内容参考生成视频。
+
+### `视频生成任务列表查询`
+
+支持按状态、模型版本或任务 ID 过滤查询任务历史。
+
+**示例工作流**:
+!\[Seedance 1 Workflow]\(example\_workflows/Seedance%201.jpg null)
+!\[Seedance 2 Workflow]\(example\_workflows/Seedance%202.jpg null)
+
+***
+
+### `视觉理解(Seed 2.0)`
+
+支持 `doubao-seed-2.0` 系列模型。
+
+- **多模态输入**: 支持上传图片或视频进行理解和问答。
+- **多轮对话**: 支持开启多轮对话模式,保持上下文。
+- **深度思考**: 支持开启深度思考模式,提升复杂问题的推理能力。
+
+**示例工作流**:
+!\[Visual Understanding Workflow]\(example\_workflows/VisualUnderstanding.jpg null)
+
+## 📓 示例工作流
+
+您可以在 `example_workflows` 目录中找到所有节点的示例工作流。
diff --git a/example_workflows/Seedance 2.json b/example_workflows/Seedance 2.json
index 39686c9..ffb92a5 100644
--- a/example_workflows/Seedance 2.json
+++ b/example_workflows/Seedance 2.json
@@ -1 +1 @@
-{"id":"abdd7651-c7cb-4449-8d48-7f0a97927bc2","revision":0,"last_node_id":23,"last_link_id":146,"nodes":[{"id":14,"type":"SaveVideo","pos":[710,530],"size":[270,110],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"视频","name":"video","type":"VIDEO","link":146},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"格式","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"编码器","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.18.5","Node name for S&R":"SaveVideo"},"widgets_values":["Jimeng/Video/Seedance2","auto","auto"]},{"id":16,"type":"LoadImage","pos":[-320,570],"size":[280,310],"flags":{},"order":0,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[118]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]},{"id":19,"type":"LoadVideo","pos":[-320,980],"size":[280,320],"flags":{},"order":1,"mode":4,"inputs":[{"localized_name":"文件","name":"file","type":"COMBO","widget":{"name":"file"},"link":null},{"localized_name":"选择要上传的文件","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"视频","name":"VIDEO","type":"VIDEO","links":[143]}],"properties":{"cnr_id":"comfy-core","ver":"0.16.4","Node name for S&R":"LoadVideo"},"widgets_values":["herobanner.mp4","image"]},{"id":21,"type":"JimengVideoQueryTasks","pos":[1020,530],"size":[270,250],"flags":{},"order":8,"mode":4,"inputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","link":145},{"localized_name":"页码","name":"page_num","type":"INT","widget":{"name":"page_num"},"link":null},{"localized_name":"每页数量","name":"page_size","type":"INT","widget":{"name":"page_size"},"link":null},{"localized_name":"任务状态","name":"status","type":"COMBO","widget":{"name":"status"},"link":null},{"localized_name":"服务等级","name":"service_tier","type":"COMBO","widget":{"name":"service_tier"},"link":null},{"localized_name":"任务ID","name":"task_ids","type":"STRING","widget":{"name":"task_ids"},"link":null},{"localized_name":"模型选择","name":"model_version","type":"COMBO","widget":{"name":"model_version"},"link":null},{"localized_name":"种子","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"查询结果","name":"task_list_json","type":"STRING","links":[125]},{"localized_name":"任务数量","name":"total_tasks","type":"INT","links":null}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"7ad8b1aaee36008de7aa83af43457c684ec40cb8","Node name for S&R":"JimengVideoQueryTasks"},"widgets_values":[1,10,"all","default","","all",0,"randomize"]},{"id":22,"type":"ShowText|pysssss","pos":[1310,530],"size":[400,250],"flags":{},"order":10,"mode":4,"inputs":[{"label":"文本","localized_name":"文本","name":"text","type":"STRING","link":125}],"outputs":[{"label":"字符串","localized_name":"字符串","name":"STRING","shape":6,"type":"STRING","links":null}],"title":"展示文本","properties":{"cnr_id":"comfyui-custom-scripts","ver":"f2838ed5e59de4d73cde5c98354b87a8d3200190","Node name for S&R":"ShowText|pysssss"},"widgets_values":[]},{"id":20,"type":"Fast Groups Bypasser (rgthree)","pos":[-10,710],"size":[270,230],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[{"label":"可选连接","name":"OPT_CONNECTION","type":"*","links":null}],"properties":{"matchColors":"","matchTitle":"","showNav":true,"showAllGraphs":true,"sort":"position","customSortAlphabet":"","toggleRestriction":"default"}},{"id":15,"type":"LoadImage","pos":[-640,570],"size":[280,310],"flags":{},"order":3,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[136]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]},{"id":18,"type":"LoadImage","pos":[-640,980],"size":[280,320],"flags":{},"order":4,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[142]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]},{"id":23,"type":"LoadAudio","pos":[-320,1400],"size":[280,140],"flags":{},"order":5,"mode":4,"inputs":[{"localized_name":"音频","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"音频UI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"音频","name":"AUDIO","type":"AUDIO","links":[144]}],"properties":{"cnr_id":"comfy-core","ver":"0.18.5","Node name for S&R":"LoadAudio"},"widgets_values":["ComfyUI.WAV",null,null]},{"id":12,"type":"JimengAPIClient","pos":[-10,530],"size":[270,120],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"API Key","name":"new_api_key","type":"STRING","widget":{"name":"new_api_key"},"link":null},{"localized_name":"保存名称 (可选)","name":"new_key_name","type":"STRING","widget":{"name":"new_key_name"},"link":null},{"localized_name":"密钥","name":"key_name","type":"COMBO","widget":{"name":"key_name"},"link":null}],"outputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","links":[115,145]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"e6a3fe5fe52f1f62c52746fbb7e6059a4ec034f9","Node name for S&R":"JimengAPIClient"},"widgets_values":["","","Custom"]},{"id":13,"type":"JimengSeedance2","pos":[290,530],"size":[400,500],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","link":115},{"localized_name":"首帧图像","name":"first_frame_image","shape":7,"type":"IMAGE","link":136},{"localized_name":"尾帧图像","name":"last_frame_image","shape":7,"type":"IMAGE","link":118},{"localized_name":"参考图像 1","name":"ref_image_1","shape":7,"type":"IMAGE","link":142},{"localized_name":"参考视频 1","name":"ref_video_1","shape":7,"type":"VIDEO","link":143},{"localized_name":"参考音频 1","name":"ref_audio_1","shape":7,"type":"AUDIO","link":144},{"localized_name":"模型版本","name":"model_version","type":"COMBO","widget":{"name":"model_version"},"link":null},{"localized_name":"提示词","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"使用随机种子","name":"enable_random_seed","type":"BOOLEAN","widget":{"name":"enable_random_seed"},"link":null},{"localized_name":"种子","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"分辨率","name":"resolution","type":"COMBO","widget":{"name":"resolution"},"link":null},{"localized_name":"宽高比","name":"aspect_ratio","type":"COMBO","widget":{"name":"aspect_ratio"},"link":null},{"localized_name":"智能时长","name":"auto_duration","type":"BOOLEAN","widget":{"name":"auto_duration"},"link":null},{"localized_name":"时长 (秒)","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"生成音频","name":"generate_audio","type":"BOOLEAN","widget":{"name":"generate_audio"},"link":null},{"localized_name":"联网搜索","name":"enable_web_search","type":"BOOLEAN","widget":{"name":"enable_web_search"},"link":null},{"localized_name":"生成数量","name":"generation_count","type":"INT","widget":{"name":"generation_count"},"link":null},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"保存尾帧","name":"save_last_frame_batch","type":"BOOLEAN","widget":{"name":"save_last_frame_batch"},"link":null},{"localized_name":"非阻塞模式","name":"non_blocking","type":"BOOLEAN","widget":{"name":"non_blocking"},"link":null},{"label":"参考图片 2","name":"ref_image_2","type":"IMAGE","link":null},{"label":"参考视频 2","name":"ref_video_2","type":"VIDEO","link":null},{"label":"参考音频 2","name":"ref_audio_2","type":"AUDIO","link":null}],"outputs":[{"localized_name":"视频","name":"video","type":"VIDEO","links":[146]},{"localized_name":"最后一帧","name":"last_frame","type":"IMAGE","links":null},{"localized_name":"响应","name":"response","type":"STRING","links":[]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"e6a3fe5fe52f1f62c52746fbb7e6059a4ec034f9","Node name for S&R":"JimengSeedance2"},"widgets_values":["doubao-seedance-2-0","",true,0,"randomize","720p","adaptive",false,5,true,false,1,"Jimeng/Video/Batch/Seedance",false,false]}],"links":[[115,12,0,13,0,"JIMENG_CLIENT"],[118,16,0,13,2,"IMAGE"],[125,21,0,22,0,"STRING"],[136,15,0,13,1,"IMAGE"],[142,18,0,13,3,"IMAGE"],[143,19,0,13,4,"VIDEO"],[144,23,0,13,5,"AUDIO"],[145,12,0,21,0,"JIMENG_CLIENT"],[146,13,0,14,0,"VIDEO"]],"groups":[{"id":1,"title":"首帧图像","bounding":[-650,500,300,390],"color":"#3f789e","font_size":24,"flags":{}},{"id":2,"title":"尾帧图像","bounding":[-330,500,300,390],"color":"#3f789e","font_size":24,"flags":{}},{"id":3,"title":"参考图像","bounding":[-650,910,300,400],"color":"#3f789e","font_size":24,"flags":{}},{"id":4,"title":"参考视频","bounding":[-330,910,300,400],"color":"#3f789e","font_size":24,"flags":{}},{"id":5,"title":"任务查询","bounding":[1010,460,710,330],"color":"#3f789e","font_size":24,"flags":{}},{"id":6,"title":"Seedance 2.0","bounding":[280,460,710,1090],"color":"#3f789e","font_size":24,"flags":{}},{"id":7,"title":"参考音频","bounding":[-330,1330,300,220],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":2.2976460519322215,"offset":[88.55337619857198,-289.1400309396441]}},"version":0.4}
\ No newline at end of file
+{"id":"abdd7651-c7cb-4449-8d48-7f0a97927bc2","revision":0,"last_node_id":24,"last_link_id":155,"nodes":[{"id":14,"type":"SaveVideo","pos":[710,530],"size":[270,110],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"视频","name":"video","type":"VIDEO","link":155},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"格式","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"编码器","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.18.5","Node name for S&R":"SaveVideo"},"widgets_values":["Jimeng/Video/Seedance2","auto","auto"]},{"id":21,"type":"JimengVideoQueryTasks","pos":[1020,530],"size":[270,250],"flags":{},"order":7,"mode":4,"inputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","link":145},{"localized_name":"页码","name":"page_num","type":"INT","widget":{"name":"page_num"},"link":null},{"localized_name":"每页数量","name":"page_size","type":"INT","widget":{"name":"page_size"},"link":null},{"localized_name":"任务状态","name":"status","type":"COMBO","widget":{"name":"status"},"link":null},{"localized_name":"服务等级","name":"service_tier","type":"COMBO","widget":{"name":"service_tier"},"link":null},{"localized_name":"任务ID","name":"task_ids","type":"STRING","widget":{"name":"task_ids"},"link":null},{"localized_name":"模型选择","name":"model_version","type":"COMBO","widget":{"name":"model_version"},"link":null},{"localized_name":"种子","name":"seed","type":"INT","widget":{"name":"seed"},"link":null}],"outputs":[{"localized_name":"查询结果","name":"task_list_json","type":"STRING","links":[125]},{"localized_name":"任务数量","name":"total_tasks","type":"INT","links":null}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"7ad8b1aaee36008de7aa83af43457c684ec40cb8","Node name for S&R":"JimengVideoQueryTasks"},"widgets_values":[1,10,"all","default","","all",0,"randomize"]},{"id":22,"type":"ShowText|pysssss","pos":[1310,530],"size":[400,250],"flags":{},"order":10,"mode":4,"inputs":[{"label":"文本","localized_name":"文本","name":"text","type":"STRING","link":125}],"outputs":[{"label":"字符串","localized_name":"字符串","name":"STRING","shape":6,"type":"STRING","links":null}],"title":"展示文本","properties":{"cnr_id":"comfyui-custom-scripts","ver":"f2838ed5e59de4d73cde5c98354b87a8d3200190","Node name for S&R":"ShowText|pysssss"},"widgets_values":[]},{"id":20,"type":"Fast Groups Bypasser (rgthree)","pos":[-10,710],"size":[270,230],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"label":"可选连接","name":"OPT_CONNECTION","type":"*","links":null}],"properties":{"matchColors":"","matchTitle":"","showNav":true,"showAllGraphs":true,"sort":"position","customSortAlphabet":"","toggleRestriction":"default"}},{"id":12,"type":"JimengAPIClient","pos":[-10,530],"size":[270,120],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"API Key","name":"new_api_key","type":"STRING","widget":{"name":"new_api_key"},"link":null},{"localized_name":"保存名称 (可选)","name":"new_key_name","type":"STRING","widget":{"name":"new_key_name"},"link":null},{"localized_name":"密钥","name":"key_name","type":"COMBO","widget":{"name":"key_name"},"link":null}],"outputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","links":[145,148]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"e6a3fe5fe52f1f62c52746fbb7e6059a4ec034f9","Node name for S&R":"JimengAPIClient"},"widgets_values":["","","Custom"]},{"id":15,"type":"LoadImage","pos":[-640,570],"size":[280,310],"flags":{},"order":2,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[150]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]},{"id":16,"type":"LoadImage","pos":[-320,570],"size":[280,310],"flags":{},"order":3,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[151]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]},{"id":18,"type":"LoadImage","pos":[-640,980],"size":[280,320],"flags":{},"order":4,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[152]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]},{"id":19,"type":"LoadVideo","pos":[-320,980],"size":[280,320],"flags":{},"order":5,"mode":4,"inputs":[{"localized_name":"文件","name":"file","type":"COMBO","widget":{"name":"file"},"link":null},{"localized_name":"选择要上传的文件","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"视频","name":"VIDEO","type":"VIDEO","links":[153]}],"properties":{"cnr_id":"comfy-core","ver":"0.16.4","Node name for S&R":"LoadVideo"},"widgets_values":["herobanner.mp4","image"]},{"id":23,"type":"LoadAudio","pos":[-320,1400],"size":[280,140],"flags":{},"order":6,"mode":4,"inputs":[{"localized_name":"音频","name":"audio","type":"COMBO","widget":{"name":"audio"},"link":null},{"localized_name":"音频UI","name":"audioUI","type":"AUDIO_UI","widget":{"name":"audioUI"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"AUDIOUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"音频","name":"AUDIO","type":"AUDIO","links":[154]}],"properties":{"cnr_id":"comfy-core","ver":"0.18.5","Node name for S&R":"LoadAudio"},"widgets_values":["ComfyUI.WAV",null,null]},{"id":24,"type":"JimengSeedance2","pos":[290,530],"size":[400,520],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","link":148},{"label":"参考图片 1","localized_name":"参考图片 1","name":"ref_images.ref_image_1","shape":7,"type":"IMAGE","link":152},{"label":"参考图片 2","localized_name":"参考图片 2","name":"ref_images.ref_image_2","shape":7,"type":"IMAGE","link":null},{"label":"参考视频 1","localized_name":"参考视频 1","name":"ref_videos.ref_video_1","shape":7,"type":"VIDEO","link":153},{"label":"参考视频 2","localized_name":"参考视频 2","name":"ref_videos.ref_video_2","shape":7,"type":"VIDEO","link":null},{"label":"参考音频 1","localized_name":"参考音频 1","name":"ref_audios.ref_audio_1","shape":7,"type":"AUDIO","link":154},{"label":"参考音频 2","localized_name":"参考音频 2","name":"ref_audios.ref_audio_2","shape":7,"type":"AUDIO","link":null},{"localized_name":"首帧图像","name":"first_frame_image","shape":7,"type":"IMAGE","link":150},{"localized_name":"尾帧图像","name":"last_frame_image","shape":7,"type":"IMAGE","link":151},{"localized_name":"模型版本","name":"model_version","type":"COMBO","widget":{"name":"model_version"},"link":null},{"localized_name":"提示词","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"使用随机种子","name":"enable_random_seed","type":"BOOLEAN","widget":{"name":"enable_random_seed"},"link":null},{"localized_name":"种子","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"分辨率","name":"resolution","type":"COMBO","widget":{"name":"resolution"},"link":null},{"localized_name":"宽高比","name":"aspect_ratio","type":"COMBO","widget":{"name":"aspect_ratio"},"link":null},{"localized_name":"智能时长","name":"auto_duration","type":"BOOLEAN","widget":{"name":"auto_duration"},"link":null},{"localized_name":"时长 (秒)","name":"duration","type":"INT","widget":{"name":"duration"},"link":null},{"localized_name":"生成音频","name":"generate_audio","type":"BOOLEAN","widget":{"name":"generate_audio"},"link":null},{"localized_name":"联网搜索","name":"enable_web_search","type":"BOOLEAN","widget":{"name":"enable_web_search"},"link":null},{"localized_name":"生成数量","name":"generation_count","type":"INT","widget":{"name":"generation_count"},"link":null},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"保存尾帧","name":"save_last_frame_batch","type":"BOOLEAN","widget":{"name":"save_last_frame_batch"},"link":null},{"localized_name":"非阻塞模式","name":"non_blocking","type":"BOOLEAN","widget":{"name":"non_blocking"},"link":null}],"outputs":[{"localized_name":"视频","name":"video","type":"VIDEO","links":[155]},{"localized_name":"最后一帧","name":"last_frame","type":"IMAGE","links":null},{"localized_name":"响应","name":"response","type":"STRING","links":null}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"8cd6b9f716d172b53413943739c2d0f1a3e99fdd","Node name for S&R":"JimengSeedance2"},"widgets_values":["doubao-seedance-2-0","",true,0,"randomize","720p","adaptive",false,5,true,false,1,"Jimeng/Video/Batch/Seedance",false,false]}],"links":[[125,21,0,22,0,"STRING"],[145,12,0,21,0,"JIMENG_CLIENT"],[148,12,0,24,0,"JIMENG_CLIENT"],[150,15,0,24,7,"IMAGE"],[151,16,0,24,8,"IMAGE"],[152,18,0,24,1,"IMAGE"],[153,19,0,24,3,"VIDEO"],[154,23,0,24,5,"AUDIO"],[155,24,0,14,0,"VIDEO"]],"groups":[{"id":1,"title":"首帧图像","bounding":[-650,500,300,390],"color":"#3f789e","font_size":24,"flags":{}},{"id":2,"title":"尾帧图像","bounding":[-330,500,300,390],"color":"#3f789e","font_size":24,"flags":{}},{"id":3,"title":"参考图像","bounding":[-650,910,300,400],"color":"#3f789e","font_size":24,"flags":{}},{"id":4,"title":"参考视频","bounding":[-330,910,300,400],"color":"#3f789e","font_size":24,"flags":{}},{"id":5,"title":"任务查询","bounding":[1010,460,710,330],"color":"#3f789e","font_size":24,"flags":{}},{"id":6,"title":"Seedance 2.0","bounding":[280,460,710,1090],"color":"#3f789e","font_size":24,"flags":{}},{"id":7,"title":"参考音频","bounding":[-330,1330,300,220],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":1.1840306430379757,"offset":[379.8792192580295,-368.4446896678731]}},"version":0.4}
\ No newline at end of file
diff --git a/example_workflows/Seedream 4.json b/example_workflows/Seedream 4.json
index 58a5e12..27acf65 100644
--- a/example_workflows/Seedream 4.json
+++ b/example_workflows/Seedream 4.json
@@ -1 +1 @@
-{"id":"f477984c-0d7e-476e-8f0b-5a312c5d9de4","revision":0,"last_node_id":82,"last_link_id":314,"nodes":[{"id":45,"type":"SaveImage","pos":[1370,470],"size":[270,270],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"图片","name":"images","type":"IMAGE","link":313},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.65","Node name for S&R":"SaveImage"},"widgets_values":["Jimeng/Image/Seedream4"]},{"id":71,"type":"Fast Groups Bypasser (rgthree)","pos":[350,650],"size":[270,130],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"label":"可选连接","name":"OPT_CONNECTION","type":"*","links":null}],"properties":{"matchColors":"","matchTitle":"","showNav":true,"showAllGraphs":true,"sort":"position","customSortAlphabet":"","toggleRestriction":"default"}},{"id":75,"type":"Note","pos":[650,820],"size":[280,150],"flags":{},"order":1,"mode":4,"inputs":[],"outputs":[],"title":"注释","properties":{},"widgets_values":["Seedream 4 支持最多14张参考图片作为输入。\n节点默认提供1个参考图像接口,连接后自动增加新的输入接口(最多14个)。\n可复制多个节点实现批量处理任务(仅供测试)。"],"color":"#432","bgcolor":"#653"},{"id":79,"type":"ShowText|pysssss","pos":[1370,800],"size":[270,290],"flags":{},"order":2,"mode":0,"inputs":[{"label":"文本","localized_name":"text","name":"text","type":"STRING","link":314}],"outputs":[{"label":"字符串","localized_name":"字符串","name":"STRING","shape":6,"type":"STRING","links":null}],"title":"展示文本","properties":{"cnr_id":"comfyui-custom-scripts","ver":"609f3afaa74b2f88ef9ce8d939626065e3247469","Node name for S&R":"ShowText|pysssss"},"widgets_values":[]},{"id":74,"type":"LoadImage","pos":[650,470],"size":[280,310],"flags":{},"order":4,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[312]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]},{"id":72,"type":"JimengAPIClient","pos":[350,470],"size":[270,114],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"API Key","name":"new_api_key","type":"STRING","widget":{"name":"new_api_key"},"link":null},{"localized_name":"保存名称 (可选)","name":"new_key_name","type":"STRING","widget":{"name":"new_key_name"},"link":null},{"localized_name":"密钥","name":"key_name","type":"COMBO","widget":{"name":"key_name"},"link":null}],"outputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","links":[311]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"ed6cf979bb10d44536faa132990eddb6f8419f1c","Node name for S&R":"JimengAPIClient"},"widgets_values":["","","Custom"]},{"id":82,"type":"JimengSeedream4","pos":[960,470],"size":[400,304],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","link":311},{"localized_name":"图像 1","name":"images","shape":7,"type":"IMAGE","link":312},{"label":"图像 2","name":"image_2","type":"IMAGE","link":null}],"outputs":[{"localized_name":"图像","name":"images","type":"IMAGE","links":[313]},{"localized_name":"响应","name":"response","type":"STRING","links":[314]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"06bc088993ae8f1b52f752549f2008972b881a38","Node name for S&R":"JimengSeedream4"},"widgets_values":["doubao-seedream-4.5","","2K (adaptive)",2048,2048,0,"randomize",false,1,1,false]}],"links":[[311,72,0,82,0,"JIMENG_CLIENT"],[312,74,0,82,1,"IMAGE"],[313,82,0,45,0,"IMAGE"],[314,82,1,79,0,"STRING"]],"groups":[{"id":2,"title":"参考图像","bounding":[640,390,300,590],"color":"#3f789e","font_size":24,"flags":{}},{"id":4,"title":"单任务","bounding":[950,390,700,710],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":0.8676836861768369,"offset":[-63.97161555865932,-342.1129595198485]},"frontendVersion":"1.18.9","workflowRendererVersion":"LG","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4}
\ No newline at end of file
+{"id":"f477984c-0d7e-476e-8f0b-5a312c5d9de4","revision":0,"last_node_id":85,"last_link_id":330,"nodes":[{"id":45,"type":"SaveImage","pos":[1370,470],"size":[270,270],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"图片","name":"images","type":"IMAGE","link":327},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.65","Node name for S&R":"SaveImage"},"widgets_values":["Jimeng/Image/Seedream4"]},{"id":71,"type":"Fast Groups Bypasser (rgthree)","pos":[350,650],"size":[270,130],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"label":"可选连接","name":"OPT_CONNECTION","type":"*","links":null}],"properties":{"matchColors":"","matchTitle":"","showNav":true,"showAllGraphs":true,"sort":"position","customSortAlphabet":"","toggleRestriction":"default"}},{"id":75,"type":"Note","pos":[650,820],"size":[280,150],"flags":{},"order":1,"mode":4,"inputs":[],"outputs":[],"title":"注释","properties":{},"widgets_values":["Seedream 4 支持最多14张参考图片作为输入。\n节点默认提供1个参考图像接口,连接后自动增加新的输入接口(最多14个)。\n可复制多个节点实现批量处理任务(仅供测试)。"],"color":"#432","bgcolor":"#653"},{"id":79,"type":"ShowText|pysssss","pos":[1370,800],"size":[270,290],"flags":{},"order":2,"mode":0,"inputs":[{"label":"文本","localized_name":"文本","name":"text","type":"STRING","link":328}],"outputs":[{"label":"字符串","localized_name":"字符串","name":"STRING","shape":6,"type":"STRING","links":null}],"title":"展示文本","properties":{"cnr_id":"comfyui-custom-scripts","ver":"609f3afaa74b2f88ef9ce8d939626065e3247469","Node name for S&R":"ShowText|pysssss"},"widgets_values":[]},{"id":85,"type":"JimengSeedream4","pos":[960,470],"size":[400,304],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","link":329},{"label":"图像 1","localized_name":"图像 1","name":"images.image_1","shape":7,"type":"IMAGE","link":330},{"label":"图像 2","localized_name":"图像 2","name":"images.image_2","shape":7,"type":"IMAGE","link":null},{"localized_name":"模型版本","name":"model_version","type":"COMBO","widget":{"name":"model_version"},"link":null},{"localized_name":"提示词","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"尺寸","name":"size","type":"COMBO","widget":{"name":"size"},"link":null},{"localized_name":"宽度","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"高度","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"种子","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"启用组图生成","name":"enable_group_generation","type":"BOOLEAN","widget":{"name":"enable_group_generation"},"link":null},{"localized_name":"最大图像数","name":"max_images","type":"INT","widget":{"name":"max_images"},"link":null},{"localized_name":"生成数量","name":"generation_count","type":"INT","widget":{"name":"generation_count"},"link":null},{"localized_name":"水印","name":"watermark","type":"BOOLEAN","widget":{"name":"watermark"},"link":null}],"outputs":[{"localized_name":"图像","name":"images","type":"IMAGE","links":[327]},{"localized_name":"响应","name":"response","type":"STRING","links":[328]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"8cd6b9f716d172b53413943739c2d0f1a3e99fdd","Node name for S&R":"JimengSeedream4"},"widgets_values":["doubao-seedream-4.5","","2K (adaptive)",2048,2048,0,"randomize",false,1,1,false]},{"id":72,"type":"JimengAPIClient","pos":[350,470],"size":[270,120],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"API Key","name":"new_api_key","type":"STRING","widget":{"name":"new_api_key"},"link":null},{"localized_name":"保存名称 (可选)","name":"new_key_name","type":"STRING","widget":{"name":"new_key_name"},"link":null},{"localized_name":"密钥","name":"key_name","type":"COMBO","widget":{"name":"key_name"},"link":null}],"outputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","links":[329]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"ed6cf979bb10d44536faa132990eddb6f8419f1c","Node name for S&R":"JimengAPIClient"},"widgets_values":["","","Custom"]},{"id":74,"type":"LoadImage","pos":[650,470],"size":[280,310],"flags":{},"order":3,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[330]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]}],"links":[[327,85,0,45,0,"IMAGE"],[328,85,1,79,0,"STRING"],[329,72,0,85,0,"JIMENG_CLIENT"],[330,74,0,85,1,"IMAGE"]],"groups":[{"id":2,"title":"参考图像","bounding":[640,390,300,590],"color":"#3f789e","font_size":24,"flags":{}},{"id":4,"title":"单任务","bounding":[950,390,700,710],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":1.2703756849315073,"offset":[-239.04065072091237,-285.77960944815186]},"frontendVersion":"1.18.9","workflowRendererVersion":"LG","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4}
\ No newline at end of file
diff --git a/example_workflows/Seedream 5.json b/example_workflows/Seedream 5.json
index ef8b706..1a49a9d 100644
--- a/example_workflows/Seedream 5.json
+++ b/example_workflows/Seedream 5.json
@@ -1 +1 @@
-{"id":"fde0eb51-78e5-47e8-9915-ce47b6fa14a9","revision":0,"last_node_id":81,"last_link_id":308,"nodes":[{"id":71,"type":"Fast Groups Bypasser (rgthree)","pos":[350,650],"size":[270,130],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"label":"可选连接","name":"OPT_CONNECTION","type":"*","links":null}],"properties":{"matchColors":"","matchTitle":"","showNav":true,"showAllGraphs":true,"sort":"position","customSortAlphabet":"","toggleRestriction":"default"}},{"id":79,"type":"ShowText|pysssss","pos":[1370,800],"size":[270,290],"flags":{},"order":1,"mode":0,"inputs":[{"label":"文本","localized_name":"text","name":"text","type":"STRING","link":308}],"outputs":[{"label":"字符串","localized_name":"字符串","name":"STRING","shape":6,"type":"STRING","links":null}],"title":"展示文本","properties":{"cnr_id":"comfyui-custom-scripts","ver":"609f3afaa74b2f88ef9ce8d939626065e3247469","Node name for S&R":"ShowText|pysssss"},"widgets_values":[]},{"id":45,"type":"SaveImage","pos":[1370,470],"size":[270,270],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"图片","name":"images","type":"IMAGE","link":307},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.65","Node name for S&R":"SaveImage"},"widgets_values":["Jimeng/Image/Seedream5"]},{"id":75,"type":"Note","pos":[650,820],"size":[280,150],"flags":{},"order":4,"mode":4,"inputs":[],"outputs":[],"title":"注释","properties":{},"widgets_values":["Seedream 5 支持最多14张参考图片作为输入。\n节点默认提供1个参考图像接口,连接后自动增加新的输入接口(最多14个)。\n可复制多个节点实现批量处理任务(仅供测试)。"],"color":"#432","bgcolor":"#653"},{"id":72,"type":"JimengAPIClient","pos":[350,470],"size":[270,114],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"API Key","name":"new_api_key","type":"STRING","widget":{"name":"new_api_key"},"link":null},{"localized_name":"保存名称 (可选)","name":"new_key_name","type":"STRING","widget":{"name":"new_key_name"},"link":null},{"localized_name":"密钥","name":"key_name","type":"COMBO","widget":{"name":"key_name"},"link":null}],"outputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","links":[304]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"ed6cf979bb10d44536faa132990eddb6f8419f1c","Node name for S&R":"JimengAPIClient"},"widgets_values":["","","Custom"]},{"id":74,"type":"LoadImage","pos":[650,470],"size":[280,310],"flags":{},"order":2,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[306]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]},{"id":81,"type":"JimengSeedream5","pos":[960,470],"size":[400,328],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","link":304},{"localized_name":"图像 1","name":"images","shape":7,"type":"IMAGE","link":306},{"localized_name":"模型版本","name":"model_version","type":"COMBO","widget":{"name":"model_version"},"link":null},{"localized_name":"提示词","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"尺寸","name":"size","type":"COMBO","widget":{"name":"size"},"link":null},{"localized_name":"宽度","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"高度","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"种子","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"启用组图生成","name":"enable_group_generation","type":"BOOLEAN","widget":{"name":"enable_group_generation"},"link":null},{"localized_name":"最大图片数","name":"max_images","type":"INT","widget":{"name":"max_images"},"link":null},{"localized_name":"启用联网搜索","name":"enable_web_search","type":"BOOLEAN","widget":{"name":"enable_web_search"},"link":null},{"localized_name":"生成数量","name":"generation_count","type":"INT","widget":{"name":"generation_count"},"link":null},{"localized_name":"水印","name":"watermark","type":"BOOLEAN","widget":{"name":"watermark"},"link":null},{"label":"图像 2","name":"image_2","type":"IMAGE","link":null}],"outputs":[{"localized_name":"图像","name":"images","type":"IMAGE","links":[307]},{"localized_name":"响应","name":"response","type":"STRING","links":[308]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"06bc088993ae8f1b52f752549f2008972b881a38","Node name for S&R":"JimengSeedream5"},"widgets_values":["doubao-seedream-5.0-lite","","2K (adaptive)",2048,2048,0,"randomize",false,1,false,1,false]}],"links":[[304,72,0,81,0,"JIMENG_CLIENT"],[306,74,0,81,1,"IMAGE"],[307,81,0,45,0,"IMAGE"],[308,81,1,79,0,"STRING"]],"groups":[{"id":2,"title":"参考图像","bounding":[640,390,300,590],"color":"#3f789e","font_size":24,"flags":{}},{"id":4,"title":"单任务","bounding":[950,390,700,710],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":1.15488698630137,"offset":[-366.11113582245804,-366.82879781274926]},"frontendVersion":"1.18.9","workflowRendererVersion":"LG","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4}
\ No newline at end of file
+{"id":"fde0eb51-78e5-47e8-9915-ce47b6fa14a9","revision":0,"last_node_id":83,"last_link_id":321,"nodes":[{"id":71,"type":"Fast Groups Bypasser (rgthree)","pos":[350,650],"size":[270,130],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"label":"可选连接","name":"OPT_CONNECTION","type":"*","links":null}],"properties":{"matchColors":"","matchTitle":"","showNav":true,"showAllGraphs":true,"sort":"position","customSortAlphabet":"","toggleRestriction":"default"}},{"id":79,"type":"ShowText|pysssss","pos":[1370,800],"size":[270,290],"flags":{},"order":1,"mode":0,"inputs":[{"label":"文本","localized_name":"文本","name":"text","type":"STRING","link":319}],"outputs":[{"label":"字符串","localized_name":"字符串","name":"STRING","shape":6,"type":"STRING","links":null}],"title":"展示文本","properties":{"cnr_id":"comfyui-custom-scripts","ver":"609f3afaa74b2f88ef9ce8d939626065e3247469","Node name for S&R":"ShowText|pysssss"},"widgets_values":[]},{"id":45,"type":"SaveImage","pos":[1370,470],"size":[270,270],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"图片","name":"images","type":"IMAGE","link":318},{"localized_name":"文件名前缀","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.65","Node name for S&R":"SaveImage"},"widgets_values":["Jimeng/Image/Seedream5"]},{"id":75,"type":"Note","pos":[650,820],"size":[280,150],"flags":{},"order":2,"mode":4,"inputs":[],"outputs":[],"title":"注释","properties":{},"widgets_values":["Seedream 5 支持最多14张参考图片作为输入。\n节点默认提供1个参考图像接口,连接后自动增加新的输入接口(最多14个)。\n可复制多个节点实现批量处理任务(仅供测试)。"],"color":"#432","bgcolor":"#653"},{"id":83,"type":"JimengSeedream5","pos":[960,470],"size":[400,328],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","link":320},{"label":"图像 1","localized_name":"图像 1","name":"images.image_1","shape":7,"type":"IMAGE","link":321},{"label":"图像 2","localized_name":"图像 2","name":"images.image_2","shape":7,"type":"IMAGE","link":null},{"localized_name":"模型版本","name":"model_version","type":"COMBO","widget":{"name":"model_version"},"link":null},{"localized_name":"提示词","name":"prompt","type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"尺寸","name":"size","type":"COMBO","widget":{"name":"size"},"link":null},{"localized_name":"宽度","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"高度","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"种子","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"启用组图生成","name":"enable_group_generation","type":"BOOLEAN","widget":{"name":"enable_group_generation"},"link":null},{"localized_name":"最大图像数","name":"max_images","type":"INT","widget":{"name":"max_images"},"link":null},{"localized_name":"启用联网搜索","name":"enable_web_search","type":"BOOLEAN","widget":{"name":"enable_web_search"},"link":null},{"localized_name":"生成数量","name":"generation_count","type":"INT","widget":{"name":"generation_count"},"link":null},{"localized_name":"水印","name":"watermark","type":"BOOLEAN","widget":{"name":"watermark"},"link":null}],"outputs":[{"localized_name":"图像","name":"images","type":"IMAGE","links":[318]},{"localized_name":"响应","name":"response","type":"STRING","links":[319]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"8cd6b9f716d172b53413943739c2d0f1a3e99fdd","Node name for S&R":"JimengSeedream5"},"widgets_values":["doubao-seedream-5.0-lite","","2K (adaptive)",2048,2048,0,"randomize",false,1,false,1,false]},{"id":72,"type":"JimengAPIClient","pos":[350,470],"size":[270,120],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"API Key","name":"new_api_key","type":"STRING","widget":{"name":"new_api_key"},"link":null},{"localized_name":"保存名称 (可选)","name":"new_key_name","type":"STRING","widget":{"name":"new_key_name"},"link":null},{"localized_name":"密钥","name":"key_name","type":"COMBO","widget":{"name":"key_name"},"link":null}],"outputs":[{"localized_name":"客户端","name":"client","type":"JIMENG_CLIENT","links":[320]}],"properties":{"cnr_id":"ComfyUI-Jimeng-API","ver":"ed6cf979bb10d44536faa132990eddb6f8419f1c","Node name for S&R":"JimengAPIClient"},"widgets_values":["","","Custom"]},{"id":74,"type":"LoadImage","pos":[650,470],"size":[280,310],"flags":{},"order":3,"mode":4,"inputs":[{"localized_name":"图像","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"选择文件上传","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"图像","name":"IMAGE","type":"IMAGE","links":[321]},{"localized_name":"遮罩","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.52","Node name for S&R":"LoadImage"},"widgets_values":["comfyui_logo.png","image"]}],"links":[[318,83,0,45,0,"IMAGE"],[319,83,1,79,0,"STRING"],[320,72,0,83,0,"JIMENG_CLIENT"],[321,74,0,83,1,"IMAGE"]],"groups":[{"id":2,"title":"参考图像","bounding":[640,390,300,590],"color":"#3f789e","font_size":24,"flags":{}},{"id":4,"title":"单任务","bounding":[950,390,700,710],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ds":{"scale":1.2703756849315073,"offset":[-133.12532883403992,-275.23430110772387]},"frontendVersion":"1.18.9","workflowRendererVersion":"LG","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4}
\ No newline at end of file
diff --git a/nodes/constants.py b/nodes/constants.py
index 37df1c6..1aab2e8 100644
--- a/nodes/constants.py
+++ b/nodes/constants.py
@@ -24,6 +24,25 @@ VIDEO_MAX_SEED = 4294967295
VIDEO_DEFAULT_TIMEOUT = 172800
VIDEO_MIN_TIMEOUT = 3600
VIDEO_MAX_TIMEOUT = 259200
+IMAGE_MIN_EDGE = 300
+IMAGE_MAX_EDGE = 6000
+IMAGE_MIN_RATIO = 0.4
+IMAGE_MAX_RATIO = 2.5
+REF_IMAGE_MAX_SIZE_MB = 30.0
+REF_IMAGE_MAX_TOTAL_REQUEST_MB = 64.0
+REF_MEDIA_MIN_DURATION = 1.8
+REF_MEDIA_MAX_DURATION = 15.2
+REF_VIDEO_MIN_DURATION = REF_MEDIA_MIN_DURATION
+REF_VIDEO_MAX_DURATION = REF_MEDIA_MAX_DURATION
+REF_VIDEO_MAX_TOTAL_DURATION = REF_MEDIA_MAX_DURATION
+REF_VIDEO_MAX_SIZE_MB = 50.0
+REF_VIDEO_MIN_PIXELS = 409600
+REF_VIDEO_MAX_PIXELS = 927408
+REF_AUDIO_MIN_DURATION = REF_MEDIA_MIN_DURATION
+REF_AUDIO_MAX_DURATION = REF_MEDIA_MAX_DURATION
+REF_AUDIO_MAX_TOTAL_DURATION = REF_MEDIA_MAX_DURATION
+REF_AUDIO_MAX_SIZE_MB = 15.0
+REF_AUDIO_MAX_TOTAL_REQUEST_MB = 64.0
DEFAULT_VISUAL_SYSTEM_PROMPT = "你叫豆包,是由字节跳动开发的AI。"
VIDEO_FRAME_RATE = 24.0
VIDEO_MIN_FRAMES = 29
@@ -66,8 +85,8 @@ LOG_TRANSLATIONS = {
"batch_failed_reason": " - {msg}: {count} 次",
"debug_create_request_params": "创建任务请求参数: {params}",
"log_raw_api_response": "原始 API 响应: {raw}",
- "upload_ref_video_start": "开始上传参考视频 ({index}/{total})...",
- "upload_ref_video_done": "参考视频上传完成 ({index}/{total})。",
+ "upload_ref_video_start": "开始上传参考视频(已完成: {done},待上传: {pending})...",
+ "upload_ref_video_done": "参考视频上传完成(已完成: {done},待上传: {pending})。",
# 轮询提示
"polling_single": "任务 {task_id}: 运行中... 已耗时 {elapsed}s / 预估 {max}s",
@@ -186,6 +205,7 @@ LOG_TRANSLATIONS = {
"OutputTextSensitiveContentDetected": "生成的文字可能包含敏感信息,请您更换输入内容后重试。",
"OutputImageSensitiveContentDetected": "生成的图像可能包含敏感信息,请您更换输入内容后重试。",
"OutputVideoSensitiveContentDetected": "生成的视频可能包含敏感信息,请您更换输入内容后重试。",
+ "InputMediaMayContainRealPerson": "输入的图片或视频可能包含真人内容,请更换素材后重试。",
"InvalidImageURL": "无法解析或处理图片,可能是 Base64 格式不正确、图片数据损坏或格式不支持。",
"InvalidImageDetail": "image_url 中的 detail 参数值无效,只接受 'auto', 'high', 'low'。",
"MissingParameter": "请求缺少必要参数,请查阅 API 文档。",
@@ -218,8 +238,8 @@ LOG_TRANSLATIONS = {
"batch_failed_reason": " - {msg}: {count} times",
"debug_create_request_params": "Create request params: {params}",
"log_raw_api_response": "Raw API response: {raw}",
- "upload_ref_video_start": "Uploading reference video ({index}/{total})...",
- "upload_ref_video_done": "Reference video upload completed ({index}/{total}).",
+ "upload_ref_video_start": "Uploading reference video (done: {done}, pending: {pending})...",
+ "upload_ref_video_done": "Reference video upload completed (done: {done}, pending: {pending}).",
# Updated
"polling_single": "Task {task_id}: Running... {elapsed}s / {max}s elapsed",
@@ -328,6 +348,7 @@ LOG_TRANSLATIONS = {
"OutputTextSensitiveContentDetected": "Generated text contains sensitive content (400).",
"OutputImageSensitiveContentDetected": "Generated image contains sensitive content (400).",
"OutputVideoSensitiveContentDetected": "Generated video contains sensitive content (400).",
+ "InputMediaMayContainRealPerson": "Request failed: input image or video may contain a real person.",
"InvalidImageURL": "Invalid Image URL (400).",
"InvalidImageDetail": "Invalid Image Detail parameter (400).",
"MissingParameter": "Missing Parameter (400).",
@@ -343,6 +364,8 @@ ERROR_TEXT_MATCH_RULES = {
"output image may contain sensitive information": "OutputImageSensitiveContentDetected",
"input text may contain sensitive information": "InputTextSensitiveContentDetected",
"input image may contain sensitive information": "InputImageSensitiveContentDetected",
+ "the request failed because the input video may contain real person": "InputMediaMayContainRealPerson",
+ "the request failed because the input image may contain real person": "InputMediaMayContainRealPerson",
"output video may contain sensitive information": "OutputVideoSensitiveContentDetected",
"generated text contains sensitive content": "OutputTextSensitiveContentDetected",
"API key or AK/SK in the request is missing or invalid": "AuthenticationError",
diff --git a/nodes/nodes_image.py b/nodes/nodes_image.py
index 3c5b1a4..86c2d31 100644
--- a/nodes/nodes_image.py
+++ b/nodes/nodes_image.py
@@ -74,6 +74,65 @@ def validate_custom_size(width, height, min_pixels, max_pixels):
return f"{width}x{height}"
+def _get_dynamic_input_order(name):
+ match = None
+ try:
+ import re
+ match = re.search(r"(\d+)$", str(name))
+ except Exception:
+ match = None
+ if match:
+ return int(match.group(1))
+ return 999
+
+
+def _create_seedream_autogrow_input():
+ return comfy_io.Autogrow.Input(
+ "images",
+ template=comfy_io.Autogrow.TemplateNames(
+ input=comfy_io.Image.Input("image", optional=True),
+ names=[f"image_{idx}" for idx in range(1, 15)],
+ min=1,
+ ),
+ )
+
+
+def _prepare_multi_image_inputs(images=None, **kwargs):
+ input_image_tensors = []
+
+ if isinstance(images, dict):
+ sorted_items = sorted(images.items(), key=lambda item: _get_dynamic_input_order(item[0]))
+ for _, image_tensor in sorted_items:
+ if isinstance(image_tensor, torch.Tensor):
+ input_image_tensors.append(image_tensor)
+ elif isinstance(images, torch.Tensor):
+ input_image_tensors.append(images)
+
+ for key in sorted([k for k in kwargs.keys() if k.startswith("image_")], key=_get_dynamic_input_order):
+ image_tensor = kwargs[key]
+ if isinstance(image_tensor, torch.Tensor):
+ input_image_tensors.append(image_tensor)
+
+ n_input_images = 0
+ image_b64_list = []
+ for tensor in input_image_tensors:
+ batch_size = tensor.shape[0]
+ n_input_images += batch_size
+ for i in range(batch_size):
+ image_b64_list.append(_image_to_base64(tensor[i : i + 1]))
+
+ image_param = None
+ if n_input_images > 0:
+ if n_input_images == 1:
+ image_param = f"data:image/jpeg;base64,{image_b64_list[0]}"
+ else:
+ image_param = [
+ f"data:image/jpeg;base64,{image_b64}" for image_b64 in image_b64_list
+ ]
+
+ return n_input_images, image_param
+
+
class JimengSeedream3(comfy_io.ComfyNode):
"""
Jimeng Seedream 3 图像生成节点。
@@ -239,7 +298,7 @@ class JimengSeedream4(comfy_io.ComfyNode):
enable_group_generation=True,
)
+ [
- comfy_io.Image.Input("images", optional=True),
+ _create_seedream_autogrow_input(),
],
hidden=[comfy_io.Hidden.unique_id, comfy_io.Hidden.prompt],
outputs=[
@@ -273,27 +332,7 @@ class JimengSeedream4(comfy_io.ComfyNode):
model_id = list(SEEDREAM_4_MODEL_MAP.values())[0]
sequential_param = "auto" if enable_group_generation else "disabled"
-
- input_image_tensors = []
- if images is not None:
- input_image_tensors.append(images)
-
- sorted_keys = sorted([k for k in kwargs.keys() if k.startswith("image_")],
- key=lambda x: int(x.split("_")[-1]) if x.split("_")[-1].isdigit() else 999)
-
- for k in sorted_keys:
- img = kwargs[k]
- if isinstance(img, torch.Tensor):
- input_image_tensors.append(img)
-
- n_input_images = 0
- image_b64_list = []
-
- for tensor in input_image_tensors:
- batch_size = tensor.shape[0]
- n_input_images += batch_size
- for i in range(batch_size):
- image_b64_list.append(_image_to_base64(tensor[i : i + 1]))
+ n_input_images, image_param = _prepare_multi_image_inputs(images, **kwargs)
if sequential_param == "auto":
total_count = n_input_images + max_images
@@ -319,15 +358,6 @@ class JimengSeedream4(comfy_io.ComfyNode):
else:
size_str = size.split(" ")[0]
- image_param = None
- if n_input_images > 0:
- if n_input_images == 1:
- image_param = f"data:image/jpeg;base64,{image_b64_list[0]}"
- else:
- image_param = [
- f"data:image/jpeg;base64,{b64}" for b64 in image_b64_list
- ]
-
seq_options = None
if sequential_param == "auto":
seq_options = SequentialImageGenerationOptions(max_images=max_images)
@@ -410,7 +440,7 @@ class JimengSeedream5(comfy_io.ComfyNode):
enable_web_search=True,
)
+ [
- comfy_io.Image.Input("images", optional=True),
+ _create_seedream_autogrow_input(),
],
hidden=[comfy_io.Hidden.unique_id, comfy_io.Hidden.prompt],
outputs=[
@@ -445,27 +475,7 @@ class JimengSeedream5(comfy_io.ComfyNode):
model_id = list(SEEDREAM_5_MODEL_MAP.values())[0]
sequential_param = "auto" if enable_group_generation else "disabled"
-
- input_image_tensors = []
- if images is not None:
- input_image_tensors.append(images)
-
- sorted_keys = sorted([k for k in kwargs.keys() if k.startswith("image_")],
- key=lambda x: int(x.split("_")[-1]) if x.split("_")[-1].isdigit() else 999)
-
- for k in sorted_keys:
- img = kwargs[k]
- if isinstance(img, torch.Tensor):
- input_image_tensors.append(img)
-
- n_input_images = 0
- image_b64_list = []
-
- for tensor in input_image_tensors:
- batch_size = tensor.shape[0]
- n_input_images += batch_size
- for i in range(batch_size):
- image_b64_list.append(_image_to_base64(tensor[i : i + 1]))
+ n_input_images, image_param = _prepare_multi_image_inputs(images, **kwargs)
if sequential_param == "auto":
total_count = n_input_images + max_images
@@ -488,15 +498,6 @@ class JimengSeedream5(comfy_io.ComfyNode):
else:
size_str = size.split(" ")[0]
- image_param = None
- if n_input_images > 0:
- if n_input_images == 1:
- image_param = f"data:image/jpeg;base64,{image_b64_list[0]}"
- else:
- image_param = [
- f"data:image/jpeg;base64,{b64}" for b64 in image_b64_list
- ]
-
seq_options = None
if sequential_param == "auto":
seq_options = SequentialImageGenerationOptions(max_images=max_images)
diff --git a/nodes/nodes_shared.py b/nodes/nodes_shared.py
index 6b30309..4a19582 100644
--- a/nodes/nodes_shared.py
+++ b/nodes/nodes_shared.py
@@ -14,6 +14,7 @@ import torch.nn.functional as F
import requests
import cv2
import asyncio
+import threading
from volcenginesdkarkruntime import Ark
from comfy_api.latest import io as comfy_io
@@ -64,9 +65,6 @@ jimeng_api_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
API_KEYS_FILE = os.path.join(jimeng_api_dir, "api_keys.json")
FILES_UPLOAD_CACHE_FILE = os.path.join(jimeng_api_dir, "files_upload_cache.json")
-API_KEYS_CONFIG = []
-CURRENT_LANG = "en"
-
JimengClientType = comfy_io.Custom("JIMENG_CLIENT")
@@ -83,12 +81,104 @@ def detect_system_language():
return "en"
+class LocalizationState:
+ def __init__(self, default_lang="en"):
+ self._lock = threading.RLock()
+ self._lang = "en"
+ self.set_language(default_lang)
+
+ def set_language(self, lang):
+ normalized_lang = lang if lang in LOG_TRANSLATIONS else "en"
+ with self._lock:
+ self._lang = normalized_lang
+
+ def refresh_from_system(self):
+ self.set_language(detect_system_language())
+
+ def get_language(self):
+ with self._lock:
+ return self._lang
+
+ def get_mapping(self):
+ return LOG_TRANSLATIONS.get(self.get_language(), LOG_TRANSLATIONS["en"])
+
+
+LOCALIZATION_STATE = LocalizationState(detect_system_language())
+
+
+class ApiKeyStore:
+ def __init__(self, config_file):
+ self.config_file = config_file
+ self._lock = threading.RLock()
+ self._items = []
+
+ def load(self):
+ loaded_items = []
+ LOCALIZATION_STATE.refresh_from_system()
+
+ try:
+ if os.path.exists(self.config_file):
+ with open(self.config_file, "r", encoding="utf-8") as f:
+ keys_data = json.load(f)
+ if isinstance(keys_data, list):
+ for item in keys_data:
+ if "customName" in item and "apiKey" in item:
+ loaded_items.append(item)
+ except Exception as e:
+ log_msg("api_load_error", e=e)
+
+ with self._lock:
+ self._items = loaded_items
+
+ def save(self):
+ with self._lock:
+ serializable_items = list(self._items)
+ try:
+ with open(self.config_file, "w", encoding="utf-8") as f:
+ json.dump(serializable_items, f, indent=2, ensure_ascii=False)
+ except Exception as e:
+ logger.error(f"Failed to save API key: {e}")
+ return False
+ return True
+
+ def upsert(self, name, key):
+ with self._lock:
+ updated = False
+ for item in self._items:
+ if item["customName"] == name:
+ item["apiKey"] = key
+ updated = True
+ break
+
+ if not updated:
+ self._items.append({"customName": name, "apiKey": key})
+
+ return self.save()
+
+ def get_items(self):
+ with self._lock:
+ return [dict(item) for item in self._items]
+
+ def get_key_names(self):
+ with self._lock:
+ return [item["customName"] for item in self._items]
+
+ def find_api_key(self, key_name):
+ with self._lock:
+ for item in self._items:
+ if item["customName"] == key_name:
+ return item["apiKey"]
+ return None
+
+
+API_KEY_STORE = ApiKeyStore(API_KEYS_FILE)
+
+
def get_text(key, **kwargs):
"""
获取指定 key 的本地化文本。
"""
- global CURRENT_LANG
- mapping = LOG_TRANSLATIONS.get(CURRENT_LANG, LOG_TRANSLATIONS["en"])
+ mapping = LOCALIZATION_STATE.get_mapping()
msg = mapping.get(key, LOG_TRANSLATIONS["en"].get(key, key))
if kwargs:
try:
@@ -102,8 +192,7 @@ def log_msg(key, default_msg="", **kwargs):
"""
记录本地化日志信息。
"""
- global CURRENT_LANG
- mapping = LOG_TRANSLATIONS.get(CURRENT_LANG, LOG_TRANSLATIONS["en"])
+ mapping = LOCALIZATION_STATE.get_mapping()
msg = mapping.get(key, None)
if not msg:
msg = LOG_TRANSLATIONS["en"].get(key, default_msg)
@@ -150,8 +239,7 @@ def format_api_error(e):
格式化 API 错误信息。
尝试解析错误代码并返回对应的本地化错误描述。
"""
- global CURRENT_LANG
- mapping = LOG_TRANSLATIONS.get(CURRENT_LANG, LOG_TRANSLATIONS["en"])
+ mapping = LOCALIZATION_STATE.get_mapping()
error_map = mapping.get("api_errors", {})
fallback_map = LOG_TRANSLATIONS["en"].get("api_errors", {})
@@ -206,47 +294,15 @@ def load_api_keys():
"""
加载 API 密钥配置文件 (api_keys.json)。
"""
- global API_KEYS_CONFIG, CURRENT_LANG
- API_KEYS_CONFIG = []
- CURRENT_LANG = detect_system_language()
-
- if not os.path.exists(API_KEYS_FILE):
- pass
-
- try:
- if os.path.exists(API_KEYS_FILE):
- with open(API_KEYS_FILE, "r", encoding="utf-8") as f:
- keys_data = json.load(f)
- if isinstance(keys_data, list):
- for item in keys_data:
- if "customName" in item and "apiKey" in item:
- API_KEYS_CONFIG.append(item)
- except Exception as e:
- log_msg("api_load_error", e=e)
+ API_KEY_STORE.load()
def save_api_key(name, key):
"""
保存新的 API Key 到配置文件。
"""
- global API_KEYS_CONFIG
-
- updated = False
- for item in API_KEYS_CONFIG:
- if item["customName"] == name:
- item["apiKey"] = key
- updated = True
- break
-
- if not updated:
- API_KEYS_CONFIG.append({"customName": name, "apiKey": key})
-
- try:
- with open(API_KEYS_FILE, "w", encoding="utf-8") as f:
- json.dump(API_KEYS_CONFIG, f, indent=2, ensure_ascii=False)
+ if API_KEY_STORE.upsert(name, key):
logger.info(f"Saved API Key: {name}")
- except Exception as e:
- logger.error(f"Failed to save API key: {e}")
def validate_api_key(api_key: str) -> bool:
@@ -272,9 +328,6 @@ def validate_api_key(api_key: str) -> bool:
return False
-FILES_UPLOAD_CACHE = {}
-
-
def _normalize_expire_seconds(expire_seconds):
expire_seconds = int(expire_seconds if expire_seconds is not None else 604800)
return max(86400, min(expire_seconds, 2592000))
@@ -317,51 +370,87 @@ def _compute_file_sha256(file_path):
return hasher.hexdigest()
-def load_files_upload_cache():
- global FILES_UPLOAD_CACHE
- FILES_UPLOAD_CACHE = {}
- if not os.path.exists(FILES_UPLOAD_CACHE_FILE):
- return
- try:
- with open(FILES_UPLOAD_CACHE_FILE, "r", encoding="utf-8") as f:
- cache_data = json.load(f)
- if not isinstance(cache_data, dict):
- return
+class UploadCacheStore:
+ def __init__(self, cache_file):
+ self.cache_file = cache_file
+ self._lock = threading.RLock()
+ self._data = {}
+
+ def load(self):
+ loaded_data = {}
+ if os.path.exists(self.cache_file):
+ try:
+ with open(self.cache_file, "r", encoding="utf-8") as f:
+ cache_data = json.load(f)
+ if isinstance(cache_data, dict):
+ now_ts = int(time.time())
+ for raw_key, entry in cache_data.items():
+ cache_key = _normalize_cache_key(raw_key)
+ if cache_key is None or not isinstance(entry, dict):
+ continue
+ file_id = entry.get("file_id")
+ expire_at = int(entry.get("expire_at", 0) or 0)
+ if file_id and expire_at > now_ts:
+ loaded_data[cache_key] = {
+ "file_id": file_id,
+ "expire_at": expire_at
+ }
+ except Exception as e:
+ logger.error(f"Failed to load upload cache: {e}")
+ with self._lock:
+ self._data = loaded_data
+
+ def save(self):
now_ts = int(time.time())
- for raw_key, entry in cache_data.items():
- cache_key = _normalize_cache_key(raw_key)
- if cache_key is None or not isinstance(entry, dict):
- continue
- file_id = entry.get("file_id")
- expire_at = int(entry.get("expire_at", 0) or 0)
- if file_id and expire_at > now_ts:
- FILES_UPLOAD_CACHE[cache_key] = {
- "file_id": file_id,
- "expire_at": expire_at
- }
- except Exception as e:
- logger.error(f"Failed to load upload cache: {e}")
+ serializable_data = {}
+ with self._lock:
+ for raw_key, entry in self._data.items():
+ cache_key = _normalize_cache_key(raw_key)
+ if cache_key is None or not isinstance(entry, dict):
+ continue
+ file_id = entry.get("file_id")
+ expire_at = int(entry.get("expire_at", 0) or 0)
+ if file_id and expire_at > now_ts:
+ serializable_data[_serialize_cache_key(cache_key)] = {
+ "file_id": file_id,
+ "expire_at": expire_at
+ }
+ try:
+ with open(self.cache_file, "w", encoding="utf-8") as f:
+ json.dump(serializable_data, f, indent=2, ensure_ascii=False)
+ except Exception as e:
+ logger.error(f"Failed to save upload cache: {e}")
+
+ def get(self, cache_key):
+ with self._lock:
+ return self._data.get(cache_key)
+
+ def set(self, cache_key, entry):
+ with self._lock:
+ self._data[cache_key] = entry
+
+ def pop(self, cache_key, default=None):
+ with self._lock:
+ return self._data.pop(cache_key, default)
+
+ def contains(self, cache_key):
+ with self._lock:
+ return cache_key in self._data
+
+ def keys(self):
+ with self._lock:
+ return list(self._data.keys())
+
+
+UPLOAD_CACHE_STORE = UploadCacheStore(FILES_UPLOAD_CACHE_FILE)
+
+
+def load_files_upload_cache():
+ UPLOAD_CACHE_STORE.load()
def save_files_upload_cache():
- now_ts = int(time.time())
- serializable_data = {}
- for raw_key, entry in FILES_UPLOAD_CACHE.items():
- cache_key = _normalize_cache_key(raw_key)
- if cache_key is None or not isinstance(entry, dict):
- continue
- file_id = entry.get("file_id")
- expire_at = int(entry.get("expire_at", 0) or 0)
- if file_id and expire_at > now_ts:
- serializable_data[_serialize_cache_key(cache_key)] = {
- "file_id": file_id,
- "expire_at": expire_at
- }
- try:
- with open(FILES_UPLOAD_CACHE_FILE, "w", encoding="utf-8") as f:
- json.dump(serializable_data, f, indent=2, ensure_ascii=False)
- except Exception as e:
- logger.error(f"Failed to save upload cache: {e}")
+ UPLOAD_CACHE_STORE.save()
load_files_upload_cache()
@@ -371,12 +460,10 @@ async def upload_file_to_ark(client, file_path, fps=None, expire_seconds=604800,
"""
使用 client.ark.files.create 上传文件。
"""
- global FILES_UPLOAD_CACHE
-
expire_seconds = _normalize_expire_seconds(expire_seconds)
if fps is None:
try:
- file_identity = f"sha256:{_compute_file_sha256(file_path)}"
+ file_identity = f"sha256:{await asyncio.to_thread(_compute_file_sha256, file_path)}"
except Exception:
file_identity = file_path
else:
@@ -386,12 +473,12 @@ async def upload_file_to_ark(client, file_path, fps=None, expire_seconds=604800,
now_ts = int(time.time())
expire_at = now_ts + expire_seconds
- cached_entry = FILES_UPLOAD_CACHE.get(cache_key)
+ cached_entry = UPLOAD_CACHE_STORE.get(cache_key)
legacy_path_key = None
matched_cache_key = cache_key
if cached_entry is None and fps is None and file_identity != file_path:
legacy_path_key = (file_path, None, expire_seconds)
- cached_entry = FILES_UPLOAD_CACHE.get(legacy_path_key)
+ cached_entry = UPLOAD_CACHE_STORE.get(legacy_path_key)
if cached_entry is not None:
matched_cache_key = legacy_path_key
if isinstance(cached_entry, dict):
@@ -407,9 +494,9 @@ async def upload_file_to_ark(client, file_path, fps=None, expire_seconds=604800,
remote_expire_at = int(getattr(remote_file_info, "expire_at", 0) or 0)
if remote_status == "active" and remote_expire_at > now_ts:
cached_entry["expire_at"] = remote_expire_at
- if legacy_path_key is not None and legacy_path_key in FILES_UPLOAD_CACHE:
- FILES_UPLOAD_CACHE.pop(legacy_path_key, None)
- FILES_UPLOAD_CACHE[cache_key] = cached_entry
+ if legacy_path_key is not None and UPLOAD_CACHE_STORE.contains(legacy_path_key):
+ UPLOAD_CACHE_STORE.pop(legacy_path_key, None)
+ UPLOAD_CACHE_STORE.set(cache_key, cached_entry)
save_files_upload_cache()
log_msg("visual_found_file", path=file_path)
if return_meta:
@@ -420,9 +507,9 @@ async def upload_file_to_ark(client, file_path, fps=None, expire_seconds=604800,
return cached_file_id
except Exception:
if cached_expire_at > now_ts:
- if legacy_path_key is not None and legacy_path_key in FILES_UPLOAD_CACHE:
- FILES_UPLOAD_CACHE.pop(legacy_path_key, None)
- FILES_UPLOAD_CACHE[cache_key] = cached_entry
+ if legacy_path_key is not None and UPLOAD_CACHE_STORE.contains(legacy_path_key):
+ UPLOAD_CACHE_STORE.pop(legacy_path_key, None)
+ UPLOAD_CACHE_STORE.set(cache_key, cached_entry)
save_files_upload_cache()
log_msg("visual_found_file", path=file_path)
if return_meta:
@@ -431,17 +518,17 @@ async def upload_file_to_ark(client, file_path, fps=None, expire_seconds=604800,
"expire_at": cached_expire_at
}
return cached_file_id
- FILES_UPLOAD_CACHE.pop(matched_cache_key, None)
+ UPLOAD_CACHE_STORE.pop(matched_cache_key, None)
save_files_upload_cache()
elif cached_entry is not None:
- FILES_UPLOAD_CACHE.pop(cache_key, None)
+ UPLOAD_CACHE_STORE.pop(cache_key, None)
save_files_upload_cache()
identity_candidates = {file_identity}
if fps is None and file_identity != file_path:
identity_candidates.add(file_path)
stale_keys = []
- for existing_key in list(FILES_UPLOAD_CACHE.keys()):
+ for existing_key in UPLOAD_CACHE_STORE.keys():
normalized_existing_key = _normalize_cache_key(existing_key)
if normalized_existing_key is None:
continue
@@ -456,7 +543,7 @@ async def upload_file_to_ark(client, file_path, fps=None, expire_seconds=604800,
if stale_keys and hasattr(client.ark, "files"):
cache_changed = False
for stale_key in stale_keys:
- stale_entry = FILES_UPLOAD_CACHE.get(stale_key)
+ stale_entry = UPLOAD_CACHE_STORE.get(stale_key)
if isinstance(stale_entry, dict):
stale_file_id = stale_entry.get("file_id")
if stale_file_id:
@@ -467,7 +554,7 @@ async def upload_file_to_ark(client, file_path, fps=None, expire_seconds=604800,
)
except Exception as e:
logger.error(f"Delete stale file failed for {stale_file_id}: {e}")
- FILES_UPLOAD_CACHE.pop(stale_key, None)
+ UPLOAD_CACHE_STORE.pop(stale_key, None)
cache_changed = True
if cache_changed:
save_files_upload_cache()
@@ -502,10 +589,10 @@ async def upload_file_to_ark(client, file_path, fps=None, expire_seconds=604800,
await wait_for_file_active(client, file_id)
- FILES_UPLOAD_CACHE[cache_key] = {
+ UPLOAD_CACHE_STORE.set(cache_key, {
"file_id": file_id,
"expire_at": expire_at
- }
+ })
save_files_upload_cache()
if return_meta:
return {
@@ -692,7 +779,7 @@ class JimengAPIClient(comfy_io.ComfyNode):
@classmethod
def define_schema(cls) -> comfy_io.Schema:
load_api_keys()
- key_names = [key["customName"] for key in API_KEYS_CONFIG]
+ key_names = API_KEY_STORE.get_key_names()
key_names.append("Custom")
return comfy_io.Schema(
@@ -727,10 +814,7 @@ class JimengAPIClient(comfy_io.ComfyNode):
print(get_text("info_new_key_saved", name=new_key_name.strip()))
else:
- for key_info in API_KEYS_CONFIG:
- if key_info["customName"] == key_name:
- api_key = key_info["apiKey"]
- break
+ api_key = API_KEY_STORE.find_api_key(key_name)
if not api_key:
log_msg("api_key_not_found", key_name=key_name)
diff --git a/nodes/nodes_video.py b/nodes/nodes_video.py
index 3f03068..4ae24d6 100644
--- a/nodes/nodes_video.py
+++ b/nodes/nodes_video.py
@@ -10,6 +10,7 @@ import logging
import base64
import io
import wave
+import hashlib
from urllib.parse import urlparse
import folder_paths
@@ -69,6 +70,9 @@ logging.getLogger("httpx").setLevel(logging.ERROR)
NON_BLOCKING_TASK_CACHE = {}
LAST_SEEDANCE_1_5_DRAFT_TASK_ID = {}
+COMFY_VIDEO_UPLOAD_CACHE = {}
+COMFY_VIDEO_UPLOAD_CACHE_TTL_SECONDS = 86400
+COMFY_VIDEO_UPLOAD_CACHE_MAX_ENTRIES = 256
def _raise_if_text_params(prompt: str, text_params: list[str]) -> None:
@@ -77,27 +81,73 @@ def _raise_if_text_params(prompt: str, text_params: list[str]) -> None:
raise JimengException(get_text("popup_param_not_allowed").format(param=i))
-from .constants import VIDEO_MAX_SEED, VIDEO_DEFAULT_TIMEOUT
+def _get_dynamic_input_order(name: str) -> int:
+ parts = str(name).rsplit("_", 1)
+ if len(parts) == 2 and parts[1].isdigit():
+ return int(parts[1])
+ return 999
-IMAGE_MIN_EDGE = 300
-IMAGE_MAX_EDGE = 6000
-IMAGE_MIN_RATIO = 0.4
-IMAGE_MAX_RATIO = 2.5
-REF_IMAGE_MAX_SIZE_MB = 30.0
-REF_IMAGE_MAX_TOTAL_REQUEST_MB = 64.0
-REF_VIDEO_MIN_DURATION = 2.0
-REF_VIDEO_MAX_DURATION = 15.0
-REF_VIDEO_MAX_TOTAL_DURATION = 15.0
-REF_VIDEO_MAX_SIZE_MB = 50.0
-REF_VIDEO_MIN_PIXELS = 409600
-REF_VIDEO_MAX_PIXELS = 927408
+def _create_autogrow_input(name, input_template, prefix, min_slots, max_slots):
+ return comfy_io.Autogrow.Input(
+ name,
+ template=comfy_io.Autogrow.TemplatePrefix(
+ input=input_template,
+ prefix=prefix,
+ min=min_slots,
+ max=max_slots,
+ ),
+ )
-REF_AUDIO_MIN_DURATION = 2.0
-REF_AUDIO_MAX_DURATION = 15.0
-REF_AUDIO_MAX_TOTAL_DURATION = 15.0
-REF_AUDIO_MAX_SIZE_MB = 15.0
-REF_AUDIO_MAX_TOTAL_REQUEST_MB = 64.0
+
+def _create_named_autogrow_input(name, input_template, names, min_slots):
+ return comfy_io.Autogrow.Input(
+ name,
+ template=comfy_io.Autogrow.TemplateNames(
+ input=input_template,
+ names=names,
+ min=min_slots,
+ ),
+ )
+
+
+def _collect_dynamic_inputs(values=None, kwargs=None, prefix=None):
+ collected = []
+
+ if isinstance(values, dict):
+ sorted_items = sorted(values.items(), key=lambda item: _get_dynamic_input_order(item[0]))
+ collected.extend([value for _, value in sorted_items])
+ elif values is not None:
+ collected.append(values)
+
+ if kwargs and prefix:
+ sorted_keys = sorted([key for key in kwargs.keys() if key.startswith(prefix)], key=_get_dynamic_input_order)
+ collected.extend([kwargs[key] for key in sorted_keys])
+
+ return [value for value in collected if value is not None]
+
+
+from .constants import (
+ VIDEO_MAX_SEED,
+ VIDEO_DEFAULT_TIMEOUT,
+ IMAGE_MIN_EDGE,
+ IMAGE_MAX_EDGE,
+ IMAGE_MIN_RATIO,
+ IMAGE_MAX_RATIO,
+ REF_IMAGE_MAX_SIZE_MB,
+ REF_IMAGE_MAX_TOTAL_REQUEST_MB,
+ REF_VIDEO_MIN_DURATION,
+ REF_VIDEO_MAX_DURATION,
+ REF_VIDEO_MAX_TOTAL_DURATION,
+ REF_VIDEO_MAX_SIZE_MB,
+ REF_VIDEO_MIN_PIXELS,
+ REF_VIDEO_MAX_PIXELS,
+ REF_AUDIO_MIN_DURATION,
+ REF_AUDIO_MAX_DURATION,
+ REF_AUDIO_MAX_TOTAL_DURATION,
+ REF_AUDIO_MAX_SIZE_MB,
+ REF_AUDIO_MAX_TOTAL_REQUEST_MB,
+)
class JimengVideoBase:
"""
@@ -212,12 +262,139 @@ class JimengVideoBase:
return len(stream_source.getvalue())
return 0
+ def _build_comfy_video_upload_cache_key(self, video):
+ try:
+ stream_source = video.get_stream_source()
+ except Exception:
+ return None
+
+ if isinstance(stream_source, str):
+ path = os.path.abspath(stream_source)
+ if not os.path.exists(path):
+ return None
+ st = os.stat(path)
+ return f"path:{path}|{int(st.st_size)}|{int(st.st_mtime_ns)}"
+
+ def _hash_buffer(size, reader):
+ hasher = hashlib.sha256()
+ sample_size = min(size, 1024 * 1024)
+ head = reader(0, sample_size)
+ hasher.update(head)
+ if size > sample_size:
+ tail = reader(size - sample_size, sample_size)
+ hasher.update(tail)
+ hasher.update(str(int(size)).encode("utf-8"))
+ return hasher.hexdigest()
+
+ if hasattr(stream_source, "getbuffer"):
+ buffer_view = stream_source.getbuffer()
+ size = int(buffer_view.nbytes)
+ digest = _hash_buffer(
+ size,
+ lambda start, length: bytes(buffer_view[start : start + length]),
+ )
+ return f"buffer:{size}|{digest}"
+
+ if hasattr(stream_source, "getvalue"):
+ raw = stream_source.getvalue()
+ size = len(raw)
+ digest = _hash_buffer(size, lambda start, length: raw[start : start + length])
+ return f"bytes:{size}|{digest}"
+
+ return None
+
+ def _prune_comfy_video_upload_cache(self):
+ now_ts = time.time()
+ expired_keys = [
+ key
+ for key, entry in COMFY_VIDEO_UPLOAD_CACHE.items()
+ if not isinstance(entry, dict)
+ or not entry.get("url")
+ or float(entry.get("expire_at", 0.0) or 0.0) <= now_ts
+ ]
+ for key in expired_keys:
+ COMFY_VIDEO_UPLOAD_CACHE.pop(key, None)
+
+ if len(COMFY_VIDEO_UPLOAD_CACHE) <= COMFY_VIDEO_UPLOAD_CACHE_MAX_ENTRIES:
+ return
+
+ sorted_items = sorted(
+ COMFY_VIDEO_UPLOAD_CACHE.items(),
+ key=lambda kv: float(kv[1].get("saved_at", 0.0) or 0.0),
+ )
+ remove_count = len(COMFY_VIDEO_UPLOAD_CACHE) - COMFY_VIDEO_UPLOAD_CACHE_MAX_ENTRIES
+ for key, _ in sorted_items[:remove_count]:
+ COMFY_VIDEO_UPLOAD_CACHE.pop(key, None)
+
+ def _get_cached_comfy_video_url(self, cache_key):
+ if not cache_key:
+ return None
+ self._prune_comfy_video_upload_cache()
+ entry = COMFY_VIDEO_UPLOAD_CACHE.get(cache_key)
+ if not isinstance(entry, dict):
+ return None
+ return (entry.get("url") or "").strip() or None
+
+ def _save_cached_comfy_video_url(self, cache_key, video_url):
+ if not cache_key:
+ return
+ normalized_url = (video_url or "").strip()
+ if not normalized_url:
+ return
+ now_ts = time.time()
+ COMFY_VIDEO_UPLOAD_CACHE[cache_key] = {
+ "url": normalized_url,
+ "saved_at": now_ts,
+ "expire_at": now_ts + COMFY_VIDEO_UPLOAD_CACHE_TTL_SECONDS,
+ }
+ self._prune_comfy_video_upload_cache()
+
+ def _get_video_duration_seconds(self, video, stream_source):
+ duration_fallback = None
+ try:
+ duration_fallback = float(video.get_duration())
+ except Exception:
+ duration_fallback = None
+
+ def _safe_numeric_from_video(method_name):
+ getter = getattr(video, method_name, None)
+ if not callable(getter):
+ return None
+ try:
+ val = float(getter())
+ if val > 0:
+ return val
+ except Exception:
+ return None
+ return None
+
+ fps = _safe_numeric_from_video("get_fps") or _safe_numeric_from_video("get_frame_rate")
+ frame_count = _safe_numeric_from_video("get_frame_count")
+ if fps and frame_count:
+ return frame_count / fps
+
+ if isinstance(stream_source, str) and os.path.exists(stream_source):
+ cap = cv2.VideoCapture(stream_source)
+ try:
+ if cap.isOpened():
+ cap_fps = float(cap.get(cv2.CAP_PROP_FPS) or 0.0)
+ cap_frames = float(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0.0)
+ if cap_fps > 0 and cap_frames > 0:
+ return cap_frames / cap_fps
+ finally:
+ cap.release()
+
+ if duration_fallback is not None and duration_fallback > 0:
+ return duration_fallback
+
+ raise JimengException(get_text("popup_ref_video_invalid"))
+
def _validate_single_reference_video(self, video):
try:
container_format = str(video.get_container_format() or "").lower()
width, height = video.get_dimensions()
- duration = float(video.get_duration())
stream_source = video.get_stream_source()
+ duration = self._get_video_duration_seconds(video, stream_source)
size_bytes = self._get_video_stream_size_bytes(stream_source)
except Exception as e:
raise JimengException(get_text("popup_ref_video_invalid").format(msg=str(e)))
@@ -1050,9 +1227,24 @@ class JimengSeedance2(JimengVideoBase, comfy_io.ComfyNode):
+ [
comfy_io.Image.Input("first_frame_image", optional=True),
comfy_io.Image.Input("last_frame_image", optional=True),
- comfy_io.Image.Input("ref_image_1", optional=True),
- comfy_io.Video.Input("ref_video_1", optional=True),
- comfy_io.Audio.Input("ref_audio_1", optional=True),
+ _create_named_autogrow_input(
+ "ref_images",
+ comfy_io.Image.Input("ref_image", optional=True),
+ [f"ref_image_{idx}" for idx in range(1, 10)],
+ 1,
+ ),
+ _create_named_autogrow_input(
+ "ref_videos",
+ comfy_io.Video.Input("ref_video", optional=True),
+ [f"ref_video_{idx}" for idx in range(1, 4)],
+ 1,
+ ),
+ _create_named_autogrow_input(
+ "ref_audios",
+ comfy_io.Audio.Input("ref_audio", optional=True),
+ [f"ref_audio_{idx}" for idx in range(1, 4)],
+ 1,
+ ),
],
hidden=[
comfy_io.Hidden.auth_token_comfy_org,
@@ -1087,21 +1279,10 @@ class JimengSeedance2(JimengVideoBase, comfy_io.ComfyNode):
non_blocking,
first_frame_image=None,
last_frame_image=None,
- ref_image_1=None,
- ref_image_2=None,
- ref_image_3=None,
- ref_image_4=None,
- ref_image_5=None,
- ref_image_6=None,
- ref_image_7=None,
- ref_image_8=None,
- ref_image_9=None,
- ref_video_1=None,
- ref_video_2=None,
- ref_video_3=None,
- ref_audio_1=None,
- ref_audio_2=None,
- ref_audio_3=None,
+ ref_images=None,
+ ref_videos=None,
+ ref_audios=None,
+ **kwargs,
) -> comfy_io.NodeOutput:
node_id = cls.hidden.unique_id
@@ -1110,20 +1291,11 @@ class JimengSeedance2(JimengVideoBase, comfy_io.ComfyNode):
content = []
total_image_request_bytes = 0
+ ref_images = _collect_dynamic_inputs(ref_images, kwargs, "ref_image_")
+ ref_videos = _collect_dynamic_inputs(ref_videos, kwargs, "ref_video_")
+ ref_audios = _collect_dynamic_inputs(ref_audios, kwargs, "ref_audio_")
- for img in [
- first_frame_image,
- last_frame_image,
- ref_image_1,
- ref_image_2,
- ref_image_3,
- ref_image_4,
- ref_image_5,
- ref_image_6,
- ref_image_7,
- ref_image_8,
- ref_image_9,
- ]:
+ for img in [first_frame_image, last_frame_image] + ref_images:
helper._validate_reference_image_constraints(img)
total_image_request_bytes += helper._append_image_content(
@@ -1136,41 +1308,14 @@ class JimengSeedance2(JimengVideoBase, comfy_io.ComfyNode):
content, last_frame_image, "last_frame"
)
- has_any_reference_inputs = any(
- img is not None
- for img in [
- ref_image_1,
- ref_image_2,
- ref_image_3,
- ref_image_4,
- ref_image_5,
- ref_image_6,
- ref_image_7,
- ref_image_8,
- ref_image_9,
- ]
- ) or any(v is not None for v in [ref_video_1, ref_video_2, ref_video_3]) or any(
- audio is not None for audio in [ref_audio_1, ref_audio_2, ref_audio_3]
- )
+ has_any_reference_inputs = bool(ref_images or ref_videos or ref_audios)
if (first_frame_image is not None or last_frame_image is not None) and has_any_reference_inputs:
raise JimengException(get_text("popup_first_last_conflict_with_refs"))
- helper._validate_reference_videos_constraints(
- [ref_video_1, ref_video_2, ref_video_3],
- )
+ helper._validate_reference_videos_constraints(ref_videos)
- for img in [
- ref_image_1,
- ref_image_2,
- ref_image_3,
- ref_image_4,
- ref_image_5,
- ref_image_6,
- ref_image_7,
- ref_image_8,
- ref_image_9,
- ]:
+ for img in ref_images:
total_image_request_bytes += helper._append_image_content(
content, img, "reference_image"
)
@@ -1183,35 +1328,39 @@ class JimengSeedance2(JimengVideoBase, comfy_io.ComfyNode):
)
)
- uploaded_video_urls: list[str | None] = [None, None, None]
- ref_videos = [ref_video_1, ref_video_2, ref_video_3]
- if any(v is not None for v in ref_videos):
+ uploaded_video_urls = []
+ if ref_videos:
try:
from comfy_api_nodes.util import upload_video_to_comfyapi
except Exception as e:
raise JimengException(get_text("popup_req_failed").format(msg=str(e)))
for idx, v in enumerate(ref_videos):
- if v is None:
+ cache_key = helper._build_comfy_video_upload_cache_key(v)
+ cached_video_url = helper._get_cached_comfy_video_url(cache_key)
+ if cached_video_url:
+ uploaded_video_urls.append(cached_video_url)
continue
- log_msg("upload_ref_video_start", index=idx + 1, total=3)
- uploaded_video_urls[idx] = await upload_video_to_comfyapi(
+ done_before = len(uploaded_video_urls)
+ pending_before = max(0, len(ref_videos) - done_before)
+ log_msg("upload_ref_video_start", done=done_before, pending=pending_before)
+ uploaded_video_url = await upload_video_to_comfyapi(
cls,
v,
wait_label=None,
)
- log_msg("upload_ref_video_done", index=idx + 1, total=3)
+ helper._save_cached_comfy_video_url(cache_key, uploaded_video_url)
+ uploaded_video_urls.append(uploaded_video_url)
+ done_after = len(uploaded_video_urls)
+ pending_after = max(0, len(ref_videos) - done_after)
+ log_msg("upload_ref_video_done", done=done_after, pending=pending_after)
- final_video_urls = [
- (uploaded_video_urls[0] or "").strip(),
- (uploaded_video_urls[1] or "").strip(),
- (uploaded_video_urls[2] or "").strip(),
- ]
+ final_video_urls = [(uploaded_video_url or "").strip() for uploaded_video_url in uploaded_video_urls]
for video_url in final_video_urls:
helper._append_media_url_content(content, video_url, "video_url", "reference_video")
total_audio_duration = 0.0
total_audio_request_bytes = 0
- for audio in [ref_audio_1, ref_audio_2, ref_audio_3]:
+ for audio in ref_audios:
appended = helper._append_audio_content(content, audio, "reference_audio")
if appended is None:
continue
@@ -1236,29 +1385,13 @@ class JimengSeedance2(JimengVideoBase, comfy_io.ComfyNode):
)
has_image_reference = any(
- img is not None
- for img in [
- first_frame_image,
- last_frame_image,
- ref_image_1,
- ref_image_2,
- ref_image_3,
- ref_image_4,
- ref_image_5,
- ref_image_6,
- ref_image_7,
- ref_image_8,
- ref_image_9,
- ]
- )
+ img is not None for img in [first_frame_image, last_frame_image]
+ ) or bool(ref_images)
has_video_reference = any(
(url or "").strip()
for url in final_video_urls
)
- has_audio_reference = any(
- audio is not None
- for audio in [ref_audio_1, ref_audio_2, ref_audio_3]
- )
+ has_audio_reference = bool(ref_audios)
prompt = (prompt or "").strip()
if not prompt and not content:
diff --git a/pyproject.toml b/pyproject.toml
index 62feee1..1266844 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI-Jimeng-API"
description = "在 ComfyUI 中使用火山方舟API提供的即梦(豆包)Seedream 和 Seedance 模型。 / Using the Volcano Ark API to utilize the Seedream and Seedance models of Jimeng (Doubao) in ComfyUI."
-version = "2.3.0"
+version = "2.3.1"
license = {file = "LICENSE"}
dependencies = ["volcengine-python-sdk[ark]>=5.0.12"]
diff --git a/web/js/jimeng_dynamic_widgets.js b/web/js/jimeng_dynamic_widgets.js
index 82b3772..dfce243 100644
--- a/web/js/jimeng_dynamic_widgets.js
+++ b/web/js/jimeng_dynamic_widgets.js
@@ -156,6 +156,75 @@ function applyBottomPadding(node) {
return changed;
}
+const AUTOGROW_LABEL_RULES = {
+ JimengSeedream4: [
+ { prefix: "image_", zhLabel: "图像", enLabel: "Image" },
+ ],
+ JimengSeedream5: [
+ { prefix: "image_", zhLabel: "图像", enLabel: "Image" },
+ ],
+ JimengSeedance2: [
+ { prefix: "ref_image_", zhLabel: "参考图片", enLabel: "Ref Image" },
+ { prefix: "ref_video_", zhLabel: "参考视频", enLabel: "Ref Video" },
+ { prefix: "ref_audio_", zhLabel: "参考音频", enLabel: "Ref Audio" },
+ ],
+};
+
+function getAutogrowLabelRules(node) {
+ if (!node || !node.comfyClass) return [];
+ return AUTOGROW_LABEL_RULES[node.comfyClass] || [];
+}
+
+function escapeRegex(text) {
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+function setInputDisplayText(input, text) {
+ const changed =
+ input.label !== text ||
+ input.localized_name !== text ||
+ input.display_name !== text;
+ input.label = text;
+ input.localized_name = text;
+ input.display_name = text;
+ return changed;
+}
+
+function applyAutogrowInputLabels(node) {
+ if (!node.inputs || node.inputs.length === 0) return false;
+ const rules = getAutogrowLabelRules(node);
+ if (!rules || rules.length === 0) return false;
+
+ const isZh = navigator.language.startsWith("zh");
+ let changed = false;
+
+ for (const input of node.inputs) {
+ if (!input || !input.name) continue;
+ const inputName = String(input.name);
+ for (const rule of rules) {
+ // 兼容 Autogrow 生成的命名形式
+ const pattern = new RegExp(`(?:^|\\.)${escapeRegex(rule.prefix)}(\\d+)$`);
+ const matched = inputName.match(pattern);
+ if (!matched) continue;
+ const suffixNum = parseInt(matched[1], 10);
+ if (!Number.isFinite(suffixNum)) continue;
+ const displayLabel = isZh ? rule.zhLabel : rule.enLabel;
+ const expectedLabel = `${displayLabel} ${suffixNum}`;
+ if (setInputDisplayText(input, expectedLabel)) {
+ changed = true;
+ }
+ break;
+ }
+ }
+ return changed;
+}
+
+function refreshAutogrowInputLabels(node) {
+ if (applyAutogrowInputLabels(node)) {
+ app.graph.setDirtyCanvas(true, true);
+ }
+}
+
/**
* 执行组件联动逻辑
* @param {object} node - 节点实例
@@ -329,236 +398,19 @@ app.registerExtension({
return r;
};
- // 动态控制 Seedream4/5 节点的图像输入接口
- if (node.comfyClass === "JimengSeedream4" || node.comfyClass === "JimengSeedream5") {
- const onConnectionsChange = node.onConnectionsChange;
- node.onConnectionsChange = function (type, index, connected, link_info, slot) {
- const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined;
+ const onConnectionsChange = node.onConnectionsChange;
+ node.onConnectionsChange = function (type, index, connected, link_info, slot) {
+ const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined;
+ if (!this._isConfiguring) {
+ refreshAutogrowInputLabels(this);
+ }
+ return r;
+ };
- if (this._isConfiguring) return r;
-
- if (type !== 1) return r;
-
- if (this._isUpdatingInputs) return r;
- this._isUpdatingInputs = true;
-
- try {
- let extraHeight = 0;
- if (this.size && this.computeSize && !this.flags?.collapsed) {
- const currentMinHeight = this.computeSize()[1];
- const currentActualHeight = this.size[1];
- extraHeight = Math.max(0, currentActualHeight - currentMinHeight);
- }
-
- const prefix = "image_";
- const MAX_INPUTS = 14;
-
- const allInputs = this.inputs || [];
- const dynamicInputs = [];
- let baseInputIndex = -1;
-
- for (let i = 0; i < allInputs.length; i++) {
- const name = allInputs[i].name;
- if (name === 'images') {
- baseInputIndex = i;
- dynamicInputs.push({ index: i, name: name, input: allInputs[i] });
- } else if (name.startsWith(prefix)) {
- dynamicInputs.push({ index: i, name: name, input: allInputs[i] });
- }
- }
-
- if (baseInputIndex === -1) return r;
-
- const activeLinks = [];
-
- for (const item of dynamicInputs) {
- if (item.input.link !== null) {
- const link = app.graph.links[item.input.link];
- if (link) {
- if (link.target_id !== this.id) continue;
- let slotNum = 1;
- if (item.name !== 'images') {
- const parsed = parseInt(item.name.slice(prefix.length), 10);
- if (!Number.isNaN(parsed)) slotNum = parsed;
- }
- activeLinks.push({
- slotNum,
- origin_id: link.origin_id,
- origin_slot: link.origin_slot,
- type: link.type
- });
- }
- }
- }
-
- activeLinks.sort((a, b) => a.slotNum - b.slotNum);
-
- const neededSlots = activeLinks.length + 1;
- const finalSlots = Math.min(neededSlots, MAX_INPUTS);
-
- const lastDynamicIndex = dynamicInputs[dynamicInputs.length - 1].index;
- const firstDynamicIndex = dynamicInputs[0].index; // This is 'images'
-
- for (let i = lastDynamicIndex; i > firstDynamicIndex; i--) {
- if (this.inputs[i].name.startsWith("image_")) {
- this.removeInput(i);
- }
- }
-
- for (let slot = 2; slot <= finalSlots; slot++) {
- const name = `image_${slot}`;
- this.addInput(name, "IMAGE");
- const input = this.inputs[this.inputs.length - 1];
- if (input) {
- const isZh = navigator.language.startsWith("zh");
- input.label = isZh ? `图像 ${slot}` : `Image ${slot}`;
- }
- }
-
- const nameToIndex = {};
- for (let i = 0; i < this.inputs.length; i++) {
- const n = this.inputs[i].name;
- if (n === 'images' || n.startsWith(prefix)) nameToIndex[n] = i;
- }
-
- for (let k = 0; k < activeLinks.length; k++) {
- const linkInfo = activeLinks[k];
- const targetName = (k === 0) ? 'images' : `image_${k + 1}`;
- const targetSlotIndex = nameToIndex[targetName];
- if (targetSlotIndex === undefined) continue;
-
- const sourceNode = app.graph.getNodeById(linkInfo.origin_id);
- if (sourceNode) {
- sourceNode.connect(linkInfo.origin_slot, this, targetSlotIndex);
- }
- }
-
- updateNodeHeight(this, extraHeight);
-
- } catch (e) {
- console.error("[Jimeng] Error updating dynamic inputs:", e);
- } finally {
- this._isUpdatingInputs = false;
- }
-
- return r;
- };
- }
-
- if (node.comfyClass === "JimengSeedance2") {
- const onConnectionsChange = node.onConnectionsChange;
- node.onConnectionsChange = function (type, index, connected, link_info, slot) {
- const r = onConnectionsChange ? onConnectionsChange.apply(this, arguments) : undefined;
-
- if (this._isConfiguring) return r;
- if (type !== 1) return r;
- if (this._isUpdatingInputs) return r;
- this._isUpdatingInputs = true;
-
- try {
- let extraHeight = 0;
- if (this.size && this.computeSize && !this.flags?.collapsed) {
- const currentMinHeight = this.computeSize()[1];
- const currentActualHeight = this.size[1];
- extraHeight = Math.max(0, currentActualHeight - currentMinHeight);
- }
-
- const isZh = navigator.language.startsWith("zh");
-
- const updateGroup = (prefix, baseName, maxSlots, inputType, zhLabel, enLabel) => {
- const allInputs = this.inputs || [];
- let baseIndex = -1;
- const groupInputs = [];
-
- for (let i = 0; i < allInputs.length; i++) {
- const name = allInputs[i].name;
- if (name === baseName) {
- baseIndex = i;
- groupInputs.push({ index: i, name, input: allInputs[i] });
- } else if (name.startsWith(prefix)) {
- groupInputs.push({ index: i, name, input: allInputs[i] });
- }
- }
-
- if (baseIndex === -1) return;
-
- const activeLinks = [];
- for (const item of groupInputs) {
- if (item.input.link !== null) {
- const link = app.graph.links[item.input.link];
- if (link) {
- if (link.target_id !== this.id) continue;
- let slotNum = 1;
- if (item.name !== baseName) {
- const parsed = parseInt(item.name.slice(prefix.length), 10);
- if (!Number.isNaN(parsed)) slotNum = parsed;
- }
- activeLinks.push({
- slotNum,
- origin_id: link.origin_id,
- origin_slot: link.origin_slot,
- type: link.type
- });
- }
- }
- }
-
- activeLinks.sort((a, b) => a.slotNum - b.slotNum);
- const finalSlots = Math.min(activeLinks.length + 1, maxSlots);
-
- for (let i = this.inputs.length - 1; i >= 0; i--) {
- const n = this.inputs[i].name;
- if (n !== baseName && n.startsWith(prefix)) {
- this.removeInput(i);
- }
- }
-
- baseIndex = (this.inputs || []).findIndex(inp => inp.name === baseName);
- if (baseIndex === -1) return;
-
- for (let slotNum = 2; slotNum <= finalSlots; slotNum++) {
- const name = `${prefix}${slotNum}`;
- this.addInput(name, inputType);
- const input = this.inputs[this.inputs.length - 1];
- if (input) {
- input.label = isZh ? `${zhLabel} ${slotNum}` : `${enLabel} ${slotNum}`;
- }
- const moved = this.inputs.pop();
- this.inputs.splice(baseIndex + (slotNum - 1), 0, moved);
- }
-
- const nameToIndex = {};
- for (let i = 0; i < this.inputs.length; i++) {
- const n = this.inputs[i].name;
- if (n === baseName || n.startsWith(prefix)) nameToIndex[n] = i;
- }
-
- for (let k = 0; k < activeLinks.length && k < maxSlots; k++) {
- const linkInfo = activeLinks[k];
- const targetName = (k === 0) ? baseName : `${prefix}${k + 1}`;
- const targetSlotIndex = nameToIndex[targetName];
- if (targetSlotIndex === undefined) continue;
- const sourceNode = app.graph.getNodeById(linkInfo.origin_id);
- if (sourceNode) {
- sourceNode.connect(linkInfo.origin_slot, this, targetSlotIndex);
- }
- }
- };
-
- updateGroup("ref_image_", "ref_image_1", 9, "IMAGE", "参考图片", "Ref Image");
- updateGroup("ref_video_", "ref_video_1", 3, "VIDEO", "参考视频", "Ref Video");
- updateGroup("ref_audio_", "ref_audio_1", 3, "AUDIO", "参考音频", "Ref Audio");
-
- updateNodeHeight(this, extraHeight);
- } catch (e) {
- console.error("[Jimeng] Error updating dynamic inputs:", e);
- } finally {
- this._isUpdatingInputs = false;
- }
-
- return r;
- };
- }
+ refreshAutogrowInputLabels(node);
+ setTimeout(() => refreshAutogrowInputLabels(node), 0);
+ setTimeout(() => refreshAutogrowInputLabels(node), 120);
+ setTimeout(() => refreshAutogrowInputLabels(node), 360);
// 筛选需监听的组件
const widgetsToWatch = node.widgets?.filter(w => TARGET_WIDGETS.includes(w.name));