diff --git a/README.md b/README.md index b7f0e3b..61a37b0 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - 视频时长固定5秒 - 自动下载和保存视频文件 -### 3. Volcengine Img Edit V3.0 - 图生图指令编辑节点 (新增) +### 3. Volcengine Img Edit V3.0 - 图生图指令编辑节点 - 火山引擎图生图3.0指令编辑模型 - 根据文字指令智能编辑图片内容 - 支持精确的编辑强度控制(0.1-1.0) @@ -28,6 +28,16 @@ - 保持原图主体结构的同时进行局部编辑 - 自动保存编辑后的图片 +### 4. Volcengine Doubao Seedance - 豆包Seedance视频生成节点 (新增) +- 火山引擎豆包Seedance视频生成模型 +- 支持文生视频和图生视频两种模式 +- 支持所有模型文本命令参数:分辨率、宽高比、时长、帧率等 +- 支持多种视频分辨率:480p、720p、1080p +- 支持9种宽高比和2种特殊比例模式 +- 支持5秒和10秒视频时长选择 +- 异步任务处理,自动轮询查询结果 +- 自动下载和保存视频文件 + ## 安装 1. 克隆此仓库到 ComfyUI 的 custom_nodes 目录: @@ -71,7 +81,7 @@ pip install -r requirements.txt - **seed**: 随机种子 (可选,-1表示随机) - **filename_prefix**: 保存文件名前缀 (可选) -### Volcengine Img Edit V3.0 使用 (新增功能) +### Volcengine Img Edit V3.0 使用 1. 在节点列表中找到 `JM-Volcengine-API/ImgEdit` 分类 2. 添加 `Volcengine Img Edit V3.0` 节点 3. 配置参数: @@ -84,6 +94,24 @@ pip install -r requirements.txt - **filename_prefix**: 保存文件名前缀 (可选) - **return_url**: 是否返回图片URL链接 (可选,默认True,24小时有效) +### Volcengine Doubao Seedance 使用 (新增功能) +1. 在节点列表中找到 `JM-Volcengine-API/Video` 分类 +2. 添加 `Volcengine Doubao Seedance` 节点 +3. 配置参数: + - **ark_api_key**: 火山方舟API密钥 + - **model**: 模型ID (默认doubao-seedance-1-0-pro-250528) + - **prompt**: 视频生成提示词,支持中英文 + - **image**: 输入图片 (可选,图生视频模式) + - **image_role**: 图片角色 (可选,first_frame或last_frame) + - **resolution**: 视频分辨率 (可选,480p/720p/1080p) + - **ratio**: 视频宽高比 (可选,9种比例+特殊模式) + - **duration**: 视频时长 (可选,5秒或10秒) + - **framepersecond**: 帧率 (可选,16或24) + - **watermark**: 是否包含水印 (可选) + - **seed**: 随机种子 (可选,-1表示随机) + - **camerafixed**: 是否固定摄像头 (可选) + - **filename_prefix**: 保存文件名前缀 (可选) + ## 参数说明 ### SeeDream V3 参数 @@ -114,7 +142,7 @@ pip install -r requirements.txt - 支持中英文,建议使用简洁明确的描述 - 可以包含镜头切换、人物动作、情绪演绎等描述 -### Img Edit V3.0 参数 (新增) +### Img Edit V3.0 参数 - **prompt**: 编辑指令,支持中英文,建议长度<=120字符,描述具体的编辑需求,例如: - "把天空改成夜晚" - "给人物添加墨镜" @@ -136,6 +164,41 @@ pip install -r requirements.txt - 局部编辑时指令描述尽量精准 - 发现编辑效果不明显时,可以调整scale数值 +### Doubao Seedance 参数 (新增) +- **支持的视频分辨率**: + - 480p → 低分辨率 + - 720p → 标准高清(默认) + - 1080p → 全高清 + +- **支持的宽高比**: + - 21:9 → 超宽屏比例 + - 16:9 → 标准横屏比例 + - 4:3 → 传统电视比例 + - 1:1 → 正方形比例 + - 3:4 → 竖屏比例 + - 9:16 → 手机竖屏比例 + - 9:21 → 超高竖屏比例 + - keep_ratio → 保持输入图片的宽高比 + - adaptive → 根据输入图片自动选择最合适的宽高比(默认) + +- **视频时长**: + - 5秒(默认) + - 10秒 + +- **帧率选项**: + - 16fps → 较低帧率 + - 24fps → 标准帧率(默认) + +- **图片角色**(图生视频模式): + - first_frame → 首帧图片(默认) + - last_frame → 尾帧图片 + +- **使用建议**: + - 提示词描述要清晰具体,避免过于抽象 + - 图生视频模式下,输入图片质量会影响生成效果 + - 建议根据实际需求选择合适的分辨率和时长 + - 使用keep_ratio或adaptive可以更好地适配输入图片 + ## 输出说明 ### SeeDream V3 输出 @@ -147,11 +210,14 @@ pip install -r requirements.txt - **video_url**: 生成的视频URL链接 (有效期1小时) - **local_video_path**: 本地保存的视频文件路径 -### Img Edit V3.0 输出 (新增) +### Img Edit V3.0 输出 - **image**: 编辑后的图片张量,可连接到其他节点 - **image_url**: 图片的URL链接(当return_url=True时)或Base64数据信息(当return_url=False时) - **local_image_path**: 本地保存的图片文件路径 +### Doubao Seedance 输出 (新增) +- **video_path**: 本地保存的视频文件路径 + ## 注意事项 ### 通用注意事项 @@ -172,7 +238,7 @@ pip install -r requirements.txt - 输入图片和提示词需要通过内容审核 - 建议根据输入图片的实际比例选择合适的aspect_ratio -### Img Edit V3.0 特定注意事项 (新增) +### Img Edit V3.0 特定注意事项 - 使用SeedEdit 3.0指令编辑模型,服务标识固定为"seededit_v3.0" - 编辑效果取决于输入图片质量和编辑指令的准确性 - 建议使用清晰、具体的编辑指令以获得更好的效果 @@ -182,6 +248,17 @@ pip install -r requirements.txt - 编辑后的图片保持原图的基本构图和主体结构 - 输入图片格式支持JPEG、PNG,最大5MB,分辨率最大4096×4096 +### Doubao Seedance 特定注意事项 (新增) +- 视频生成为异步处理,通常需要几分钟时间 +- 程序会自动轮询查询结果,请耐心等待 +- 支持文生视频和图生视频两种模式 +- 图生视频模式下,输入图片质量会影响生成效果 +- 生成的视频URL有效期为24小时,请及时下载保存 +- 输入图片和提示词需要通过内容审核 +- 建议根据实际用途选择合适的分辨率和时长参数 +- 所有模型文本命令参数都可以通过UI直接设置 +- 使用keep_ratio或adaptive可以更好地适配输入图片比例 + ## 错误处理 ### 常见错误及解决方案 @@ -198,7 +275,7 @@ pip install -r requirements.txt - **任务超时**: 可能服务器繁忙,可稍后重试 - **宽高比不匹配**: 建议选择与输入图片比例接近的aspect_ratio -#### Img Edit V3.0 (新增) +#### Img Edit V3.0 - **认证失败**: 检查AccessKey和SecretKey是否正确并有相应权限 - **编辑指令错误**: 检查编辑指令是否清晰明确,避免过于复杂的描述 - **提示词过长**: 确保提示词长度不超过120字符 @@ -207,6 +284,16 @@ pip install -r requirements.txt - **任务超时**: 可能服务器繁忙,任务处理时间过长,可稍后重试 - **任务未找到或过期**: 任务可能已过期(12小时),请重新提交 +#### Doubao Seedance (新增) +- **认证失败**: 检查ARK API密钥是否正确并有相应权限 +- **模型不可用**: 确认所选模型是否已开通并可正常使用 +- **提示词审核未通过**: 检查提示词内容是否符合平台规范 +- **图片审核未通过**: 检查输入图片内容是否符合平台规范 +- **参数错误**: 确认所有参数值是否在支持范围内 +- **任务超时**: 可能服务器繁忙,可稍后重试 +- **视频生成失败**: 检查提示词和参数设置,重新尝试 +- **下载失败**: 网络连接问题,检查网络状态 + ## 技术实现 ### SeeDream V3 @@ -221,7 +308,7 @@ pip install -r requirements.txt - 完整的错误处理和重试机制 - 视频文件自动下载和命名管理 -### Img Edit V3.0 (新增) +### Img Edit V3.0 - 实现完整的火山引擎SeedEdit 3.0异步接口调用 - 支持任务提交和轮询查询的异步处理机制 - 智能图片格式转换和处理 @@ -229,6 +316,16 @@ pip install -r requirements.txt - 完善的错误处理和异常捕获 - 自动文件命名和保存管理 +### Doubao Seedance (新增) +- 实现火山方舟标准API调用流程 +- 支持异步任务提交和轮询查询机制 +- 完整的模型文本命令参数支持 +- 智能图片格式转换和Base64编码 +- 支持文生视频和图生视频两种模式 +- 完善的错误处理和重试机制 +- 视频文件自动下载和命名管理 +- 支持所有官方模型参数配置 + ## 系统要求 - ComfyUI 环境 - Python 3.8+ @@ -237,7 +334,16 @@ pip install -r requirements.txt ## 更新日志 -### v3.0.0 (最新) +### v4.0.0 (最新) +- 新增 Volcengine Doubao Seedance 视频生成节点 +- 支持火山引擎豆包Seedance模型的文生视频和图生视频功能 +- 完整支持所有模型文本命令参数:分辨率、宽高比、时长、帧率、水印、种子、摄像头固定等 +- 支持3种分辨率、9种宽高比和2种特殊比例模式 +- 实现异步任务处理和自动轮询查询 +- 自动视频下载和文件命名管理 +- 优化用户界面参数配置 + +### v3.0.0 - 新增 Volcengine Img Edit V3.0 图生图指令编辑节点(SeedEdit 3.0) - 支持根据文字指令智能编辑图片内容 - 实现异步任务处理机制,支持任务提交和轮询查询 diff --git a/__init__.py b/__init__.py index 1c29298..2f25c33 100644 --- a/__init__.py +++ b/__init__.py @@ -1,17 +1,20 @@ from .nodes.volcengine_seedream_v3 import VolcengineSeeDreamV3Node from .nodes.volcengine_i2v_s2pro import VolcengineI2VS2Pro from .nodes.volcengine_img_edit_v3 import VolcengineImgEditV3 +from .nodes.volcengine_doubao_seedance import VolcengineDoubaoSeedance NODE_CLASS_MAPPINGS = { "volcengine-seedream-v3": VolcengineSeeDreamV3Node, "volcengine-i2v-s2pro": VolcengineI2VS2Pro, - "volcengine-img-edit-v3": VolcengineImgEditV3 + "volcengine-img-edit-v3": VolcengineImgEditV3, + "volcengine-doubao-seedance": VolcengineDoubaoSeedance } NODE_DISPLAY_NAME_MAPPINGS = { "volcengine-seedream-v3": "Volcengine SeeDream V3", "volcengine-i2v-s2pro": "Volcengine I2V S2.0Pro", - "volcengine-img-edit-v3": "Volcengine Img Edit V3.0" + "volcengine-img-edit-v3": "Volcengine Img Edit V3.0", + "volcengine-doubao-seedance": "Volcengine Doubao Seedance" } __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/nodes/volcengine_doubao_seedance.py b/nodes/volcengine_doubao_seedance.py new file mode 100644 index 0000000..bab386d --- /dev/null +++ b/nodes/volcengine_doubao_seedance.py @@ -0,0 +1,347 @@ +import json +import time +import base64 +import hashlib +import hmac +import datetime +from urllib.parse import urlencode +import requests +import torch +import numpy as np +from PIL import Image +import io +import os +import folder_paths + +class VolcengineDoubaoSeedance: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "ark_api_key": ("STRING", { + "default": "", + "multiline": False, + "tooltip": "火山方舟API密钥" + }), + "model": (["doubao-seedance-1-0-pro-250528"], { + "default": "doubao-seedance-1-0-pro-250528", + "tooltip": "模型ID" + }), + "prompt": ("STRING", { + "default": "", + "multiline": True, + "tooltip": "视频生成提示词,支持中英文" + }), + }, + "optional": { + "image": ("IMAGE", { + "tooltip": "输入图片(图生视频模式)" + }), + "image_role": (["first_frame", "last_frame"], { + "default": "first_frame", + "tooltip": "图片角色:首帧或尾帧" + }), + "resolution": (["480p", "720p", "1080p"], { + "default": "720p", + "tooltip": "视频分辨率" + }), + "ratio": (["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "9:21", "keep_ratio", "adaptive"], { + "default": "adaptive", + "tooltip": "视频宽高比" + }), + "duration": ([5, 10], { + "default": 5, + "tooltip": "视频时长(秒)" + }), + "framepersecond": ([16, 24], { + "default": 24, + "tooltip": "帧率" + }), + "watermark": ("BOOLEAN", { + "default": False, + "tooltip": "是否包含水印" + }), + "seed": ("INT", { + "default": -1, + "min": -1, + "max": 2**32 - 1, + "tooltip": "随机种子,-1表示随机" + }), + "camerafixed": ("BOOLEAN", { + "default": False, + "tooltip": "是否固定摄像头" + }), + "filename_prefix": ("STRING", { + "default": "doubao_seedance", + "tooltip": "保存文件名前缀" + }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("video_path",) + FUNCTION = "generate_video" + CATEGORY = "JM-Volcengine-API/Video" + DESCRIPTION = "火山引擎豆包Seedance视频生成模型 - 支持文生视频和图生视频" + + def __init__(self): + self.base_url = "https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks" + + def image_to_base64(self, image_tensor): + """将ComfyUI图片张量转换为Base64""" + # image_tensor shape: [1, H, W, 3] + if image_tensor.dim() == 4: + image_tensor = image_tensor.squeeze(0) # 移除batch维度 + + # 转换为numpy数组并确保数据类型正确 + image_np = image_tensor.cpu().numpy() + if image_np.dtype != np.uint8: + image_np = (image_np * 255).astype(np.uint8) + + # 转换为PIL图像 + pil_image = Image.fromarray(image_np) + + # 转换为Base64 + buffer = io.BytesIO() + pil_image.save(buffer, format='PNG') + image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') + + return f"data:image/png;base64,{image_base64}" + + def build_text_command(self, prompt, resolution="720p", ratio="adaptive", duration=5, + framepersecond=24, watermark=False, seed=-1, camerafixed=False): + """构建文本命令字符串""" + commands = [] + + # 添加基础提示词 + text_content = prompt.strip() + + # 添加参数命令 + if resolution != "720p": + commands.append(f"--resolution {resolution}") + + if ratio != "adaptive": + commands.append(f"--ratio {ratio}") + + if duration != 5: + commands.append(f"--duration {duration}") + + if framepersecond != 24: + commands.append(f"--framepersecond {framepersecond}") + + if watermark: + commands.append("--watermark true") + + if seed != -1: + commands.append(f"--seed {seed}") + + if camerafixed: + commands.append("--camerafixed true") + + # 组合完整的文本内容 + if commands: + text_content += " " + " ".join(commands) + + return text_content + + def create_task(self, ark_api_key, model, content_list): + """创建视频生成任务""" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {ark_api_key}" + } + + payload = { + "model": model, + "content": content_list + } + + try: + response = requests.post(self.base_url, headers=headers, json=payload, timeout=30) + response.raise_for_status() + + result = response.json() + if "id" in result: + return result["id"] + else: + print(f"创建任务失败: {result}") + return None + + except Exception as e: + print(f"创建任务时发生错误: {str(e)}") + return None + + def query_task(self, ark_api_key, task_id, max_retries=60, retry_interval=10): + """查询任务结果""" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {ark_api_key}" + } + + query_url = f"{self.base_url}/{task_id}" + + for attempt in range(max_retries): + try: + response = requests.get(query_url, headers=headers, timeout=30) + response.raise_for_status() + + result = response.json() + status = result.get("status") + + print(f"查询任务 {task_id} 状态: {status} (尝试 {attempt + 1}/{max_retries})") + + if status == "succeeded": + content = result.get("content", {}) + video_url = content.get("video_url") + if video_url: + print(f"任务完成,视频URL: {video_url}") + return {"status": "success", "video_url": video_url, "result": result} + else: + print("任务成功但未找到视频URL") + return {"status": "error", "message": "未找到视频URL"} + + elif status == "failed": + error_info = result.get("error", {}) + error_message = error_info.get("message", "任务失败") + print(f"任务失败: {error_message}") + return {"status": "error", "message": error_message} + + elif status == "cancelled": + print("任务被取消") + return {"status": "error", "message": "任务被取消"} + + elif status in ["queued", "running"]: + print(f"任务进行中,状态: {status},等待 {retry_interval} 秒后重试...") + time.sleep(retry_interval) + continue + + else: + print(f"未知状态: {status}") + time.sleep(retry_interval) + continue + + except Exception as e: + print(f"查询任务时发生错误: {str(e)}") + if attempt < max_retries - 1: + time.sleep(retry_interval) + continue + else: + return {"status": "error", "message": f"查询任务失败: {str(e)}"} + + return {"status": "error", "message": "任务超时"} + + def download_video(self, video_url, filename_prefix): + """下载视频到ComfyUI output目录""" + try: + # 确保输出目录存在 + output_dir = folder_paths.get_output_directory() + os.makedirs(output_dir, exist_ok=True) + + # 生成唯一文件名 + counter = 1 + while True: + filename = f"{filename_prefix}_{counter:04d}.mp4" + file_path = os.path.join(output_dir, filename) + if not os.path.exists(file_path): + break + counter += 1 + + print(f"开始下载视频到: {file_path}") + + # 下载视频 + response = requests.get(video_url, stream=True, timeout=300) + response.raise_for_status() + + with open(file_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + + print(f"视频下载成功: {file_path}") + return file_path + + except Exception as e: + print(f"下载视频时发生错误: {str(e)}") + return None + + def generate_video(self, ark_api_key, model, prompt, image=None, image_role="first_frame", + resolution="720p", ratio="adaptive", duration=5, framepersecond=24, + watermark=False, seed=-1, camerafixed=False, filename_prefix="doubao_seedance"): + """主要的视频生成函数""" + + # 验证必需参数 + if not ark_api_key: + return ("错误:请提供有效的ARK API密钥",) + + if not prompt.strip(): + return ("错误:请提供视频生成提示词",) + + try: + # 构建内容数组 + content_list = [] + + # 构建文本命令 + text_with_commands = self.build_text_command( + prompt, resolution, ratio, duration, framepersecond, + watermark, seed, camerafixed + ) + + # 添加文本内容 + content_list.append({ + "type": "text", + "text": text_with_commands + }) + + # 如果有图片,添加图片内容(图生视频模式) + if image is not None: + print("检测到输入图片,使用图生视频模式") + image_base64 = self.image_to_base64(image) + + image_content = { + "type": "image_url", + "image_url": { + "url": image_base64 + } + } + + # 如果指定了图片角色,添加role字段 + if image_role in ["first_frame", "last_frame"]: + image_content["role"] = image_role + + content_list.append(image_content) + else: + print("使用文生视频模式") + + print(f"创建视频生成任务...") + print(f"模型: {model}") + print(f"提示词: {text_with_commands}") + + # 创建任务 + task_id = self.create_task(ark_api_key, model, content_list) + + if not task_id: + return ("错误:任务创建失败",) + + print(f"任务创建成功,task_id: {task_id}") + print("开始查询任务状态...") + + # 查询任务结果 + result = self.query_task(ark_api_key, task_id) + + if result["status"] != "success": + return (f"错误:{result['message']}",) + + video_url = result["video_url"] + print(f"获取到视频URL: {video_url}") + + # 下载视频 + video_path = self.download_video(video_url, filename_prefix) + + if not video_path: + return ("错误:视频下载失败",) + + return (video_path,) + + except Exception as e: + print(f"生成视频时发生错误: {str(e)}") + return (f"错误:{str(e)}",) \ No newline at end of file