diff --git a/README.md b/README.md index 48e6d78..0c7745c 100644 --- a/README.md +++ b/README.md @@ -156,9 +156,48 @@ When using T2V-01-Director or I2V-01-Director models, you can use movement instr 3. Select an I2V model (I2V-01-Director recommended) 4. Enter API key and optionally write a prompt with movement instructions 5. Run to get a task_id -6. Use **Check Video Status** node to monitor progress -7. Once status is "success", use **Download Video** node to save the video +6. Use **Check Video Status** node to monitor progress (it will automatically wait until completion) +7. Once status is "success", use **Download Video** node with the file_id to save the video ## License MIT License + +### Check Video Status Node + +This node checks the status of video generation tasks and waits until completion. + +#### Input Parameters: +- **api_key**: MiniMax API key +- **task_id**: Task ID from Video Generation node +- **check_interval**: Check interval in seconds (default: 30, range: 10-300) +- **max_wait_time**: Maximum wait time in seconds (default: 1800/30 minutes, range: 5 minutes-2 hours) + +#### Output: +- **status**: Task status (processing, success, failed) +- **file_id**: File ID of the generated video (required for downloading) +- **video_url**: Download URL for the generated video (may be empty) +- **cover_image_url**: URL for the video cover image + +#### Features: +- **Automatic polling**: Continuously checks status until completion or failure +- **Progress tracking**: Shows elapsed time, remaining time, and attempt count +- **Timeout protection**: Prevents infinite waiting with configurable maximum wait time +- **Smart intervals**: Customizable check intervals to balance responsiveness and API usage + +### Download Video Node + +This node downloads generated videos using the file_id from the status check. + +#### Input Parameters: +- **api_key**: MiniMax API key +- **file_id**: File ID from Check Video Status node +- **filename_prefix**: Prefix for the downloaded video file + +#### Output: +- **video_path**: Absolute path to the downloaded video file + +#### Process: +1. Uses file_id to retrieve download URL from MiniMax file API +2. Downloads the video file with progress tracking +3. Saves to ComfyUI output directory with timestamp diff --git a/README_CN.md b/README_CN.md index 5970ca3..3bf51f4 100644 --- a/README_CN.md +++ b/README_CN.md @@ -56,29 +56,43 @@ ### 检查视频状态节点 -此节点检查视频生成任务的状态。 +此节点检查视频生成任务的状态并等待直到完成。 #### 输入参数: - **api_key**: MiniMax API 密钥 - **task_id**: 来自视频生成节点的任务 ID +- **check_interval**: 检查间隔(秒)(默认:30,范围:10-300) +- **max_wait_time**: 最大等待时间(秒)(默认:1800/30分钟,范围:5分钟-2小时) #### 输出: - **status**: 任务状态(processing, success, failed) -- **file_id**: 生成视频的文件 ID -- **video_url**: 生成视频的下载 URL +- **file_id**: 生成视频的文件 ID(下载时必需) +- **video_url**: 生成视频的下载 URL(可能为空) - **cover_image_url**: 视频封面图像的 URL +#### 功能特性: +- **自动轮询**: 持续检查状态直到完成或失败 +- **进度跟踪**: 显示已用时间、剩余时间和尝试次数 +- **超时保护**: 通过可配置的最大等待时间防止无限等待 +- **智能间隔**: 可自定义检查间隔,平衡响应性和 API 使用 + ### 下载视频节点 -此节点将生成的视频从 URL 下载到本地存储。 +此节点使用状态检查的 file_id 下载生成的视频。 #### 输入参数: -- **video_url**: 来自检查视频状态节点的视频 URL +- **api_key**: MiniMax API 密钥 +- **file_id**: 来自检查视频状态节点的文件 ID - **filename_prefix**: 下载视频文件的前缀 #### 输出: - **video_path**: 下载视频文件的绝对路径 +#### 处理流程: +1. 使用 file_id 从 MiniMax 文件 API 获取下载 URL +2. 带进度跟踪的视频文件下载 +3. 保存到 ComfyUI 输出目录并添加时间戳 + ## 视频工作流示例 ### 文生视频工作流: @@ -94,8 +108,8 @@ 3. 选择 I2V 模型(推荐 I2V-01-Director) 4. 输入 API 密钥,可选择性地编写带有运镜指令的提示词 5. 运行以获取 task_id -6. 使用**检查视频状态**节点监控进度 -7. 状态为 "success" 后,使用**下载视频**节点保存视频 +6. 使用**检查视频状态**节点监控进度(将自动等待直到完成) +7. 状态为 "success" 后,使用**下载视频**节点和 file_id 保存视频 ## 安装 diff --git a/nodes/download_video.py b/nodes/download_video.py index 871aa58..e039194 100644 --- a/nodes/download_video.py +++ b/nodes/download_video.py @@ -1,21 +1,23 @@ import os import requests import time +import json import folder_paths from urllib.parse import urlparse class DownloadVideo: """ - Download Video from URL node for ComfyUI + Download Video using file_id from MiniMax API """ def __init__(self): - pass + self.retrieve_api = "https://api.minimaxi.chat/v1/files/retrieve" @classmethod def INPUT_TYPES(cls): return { "required": { - "video_url": ("STRING", {"multiline": False, "placeholder": "Video URL from status check"}), + "api_key": ("STRING", {"multiline": False}), + "file_id": ("STRING", {"multiline": False, "placeholder": "File ID from video status check"}), "filename_prefix": ("STRING", {"default": "minimax_video", "multiline": False}), } } @@ -25,19 +27,69 @@ class DownloadVideo: FUNCTION = "download_video" CATEGORY = "JM-MiniMax-API/Video" - def download_video(self, video_url, filename_prefix): - if not video_url or not video_url.strip(): - raise ValueError("Video URL must be provided") + def download_video(self, api_key, file_id, filename_prefix): + if not api_key or not file_id: + raise ValueError("API Key and File ID must be provided") - video_url = video_url.strip() + file_id = file_id.strip() try: - # Validate URL - parsed_url = urlparse(video_url) - if not parsed_url.scheme or not parsed_url.netloc: - raise ValueError("Invalid video URL provided") + print(f"Step 1: Retrieving download URL for file_id: {file_id}") - print(f"Downloading video from: {video_url}") + # Step 1: Get download URL using file_id + retrieve_url = f"{self.retrieve_api}?file_id={file_id}" + headers = { + 'authorization': f'Bearer {api_key}', + } + + print(f"Retrieve URL: {retrieve_url}") + + response = requests.get(retrieve_url, headers=headers, timeout=30) + + print(f"Retrieve response status code: {response.status_code}") + + if response.status_code != 200: + raise RuntimeError(f"Failed to retrieve file info. Status code: {response.status_code}") + + try: + retrieve_data = response.json() + print(f"Retrieve response data: {json.dumps(retrieve_data, indent=2)}") + except json.JSONDecodeError: + print(f"Raw retrieve response content: {response.content}") + raise RuntimeError("Failed to decode JSON response from file retrieve API") + + # Check for API errors + base_resp = retrieve_data.get("base_resp", {}) + status_code = base_resp.get("status_code") + status_msg = base_resp.get("status_msg", "Unknown error") + + if status_code is not None and status_code != 0: + error_messages = { + 1002: "Rate limit exceeded, please try again later", + 1004: "Authentication failed, please check your API key", + 1008: "Insufficient account balance", + 2013: "Invalid parameters, please check your file_id", + 2049: "Invalid API key, please check your API key" + } + error_msg = error_messages.get(status_code, f"API Error {status_code}: {status_msg}") + raise RuntimeError(error_msg) + + # Extract download URL + file_info = retrieve_data.get("file", {}) + download_url = file_info.get("download_url", "") + + if not download_url: + raise RuntimeError("No download URL found in API response") + + print(f"Download URL retrieved: {download_url}") + + # Step 2: Download the video file + print(f"Step 2: Downloading video from URL") + + # Validate download URL + parsed_url = urlparse(download_url) + if not parsed_url.scheme or not parsed_url.netloc: + raise ValueError("Invalid download URL received from API") # Create output directory output_dir = folder_paths.get_output_directory() @@ -66,11 +118,13 @@ class DownloadVideo: video_filepath = os.path.join(output_dir, video_filename) # Download video with progress tracking - with requests.get(video_url, stream=True, timeout=60) as response: - response.raise_for_status() + print(f"Downloading to: {video_filepath}") + + with requests.get(download_url, stream=True, timeout=120) as download_response: + download_response.raise_for_status() # Get content length for progress tracking - content_length = response.headers.get('content-length') + content_length = download_response.headers.get('content-length') if content_length: total_size = int(content_length) print(f"Video file size: {total_size / (1024*1024):.1f} MB") @@ -80,7 +134,7 @@ class DownloadVideo: downloaded_size = 0 with open(video_filepath, 'wb') as f: - for chunk in response.iter_content(chunk_size=8192): + for chunk in download_response.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded_size += len(chunk) @@ -90,13 +144,15 @@ class DownloadVideo: progress = (downloaded_size / total_size) * 100 print(f"Download progress: {progress:.1f}%") - print(f"Video downloaded successfully to: {video_filepath}") - print(f"Final file size: {os.path.getsize(video_filepath) / (1024*1024):.1f} MB") + final_size = os.path.getsize(video_filepath) + print(f"Video downloaded successfully!") + print(f"File path: {video_filepath}") + print(f"Final file size: {final_size / (1024*1024):.1f} MB") return (os.path.abspath(video_filepath),) except requests.exceptions.RequestException as e: - print(f"Download error: {str(e)}") + print(f"Request error: {str(e)}") raise RuntimeError(f"Failed to download video: {str(e)}") except Exception as e: print(f"Unexpected error: {str(e)}")