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)的使用上限。 -- **特性**: 当达到限额时自动停止任务并抛出提示,防止额度超支。 -- **示例工作流**: -![Quota Settings Workflow](example_workflows/QuotaSettings.jpg) - ---- - -### `图像生成(Seedream 3)` -- **模式**: 不连接 `输入图像` 为文生图,连接则为图生图。 - -**示例工作流**: -![Seedream 3 Workflow](example_workflows/Seedream%203.jpg) - ---- - -### `图像生成(Seedream 4)` -支持 `doubao-seedream-4.5`。 -- **输入图像**: 支持单张或多张(Batch)图像作为参考。 -- **启用组图生成**: 开启后可一次性生成多张内容关联的图片。 - -**示例工作流**: -![Seedream 4 Workflow](example_workflows/Seedream%204.jpg) - ---- - -### `图像生成(Seedream 5)` -支持最新的 `doubao-seedream-5.0-lite`。 -- **输入图像**: 支持单张或多张(Batch)图像作为参考。 -- **启用组图生成**: 开启后可一次性生成多张内容关联的图片。 -- **联网搜索**: 开启后模型可自主搜索互联网内容(如实时信息、商品等)辅助生成。 - -**示例工作流**: -![Seedream 5 Workflow](example_workflows/Seedream%205.jpg) - ---- - -### `视频生成(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) -![Seedance 2 Workflow](example_workflows/Seedance%202.jpg) - ---- - -### `视觉理解(Visual Understanding)` -支持 `doubao-seed-2.0` 系列模型。 -- **多模态输入**: 支持上传图片或视频进行理解和问答。 -- **多轮对话**: 支持开启多轮对话模式,保持上下文。 -- **深度思考**: 支持开启深度思考模式,提升复杂问题的推理能力。 - -**示例工作流**: -![Visual Understanding Workflow](example_workflows/VisualUnderstanding.jpg) - -## 📓 示例工作流 - - -您可以在 `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));