From 4396d8e8058a3c0638adf0fe1896eff3a683b35f Mon Sep 17 00:00:00 2001 From: jueming Date: Fri, 29 Aug 2025 10:01:16 +0800 Subject: [PATCH] Update: Added support for first and last frames --- nodes/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 160 bytes .../video_generation.cpython-310.pyc | Bin 0 -> 7483 bytes nodes/video_generation.py | 82 ++++++++++++------ 3 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 nodes/__pycache__/__init__.cpython-310.pyc create mode 100644 nodes/__pycache__/video_generation.cpython-310.pyc diff --git a/nodes/__pycache__/__init__.cpython-310.pyc b/nodes/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..072878b73ae211e09defc3e9814159360138f165 GIT binary patch literal 160 zcmd1j<>g`k0w;<8=^jA(F^Gc<7=auIATDMB5-AM944RC7D;bJF!U*D*vwmoCYEiL% zR%vQ(W?s5}dS*#RX;OSaVo8R+bAE1GWvHjFm#?mGW?rUmVuh|_fTw<5eoAVwetbMo ds3bmKub}c4hfQvNN@-529mw!vCLqDW005}}B~kzY literal 0 HcmV?d00001 diff --git a/nodes/__pycache__/video_generation.cpython-310.pyc b/nodes/__pycache__/video_generation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..715fa96802884b6112a1fe6aa0d0a7000761be99 GIT binary patch literal 7483 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 J(xDD({{tQtoAdwx literal 0 HcmV?d00001 diff --git a/nodes/video_generation.py b/nodes/video_generation.py index a33995d..d664ca1 100644 --- a/nodes/video_generation.py +++ b/nodes/video_generation.py @@ -29,16 +29,19 @@ class MiniMaxVideoGeneration: "prompt_optimizer": ("BOOLEAN", {"default": True}), }, "optional": { - "image": ("IMAGE", { - "tooltip": "Required for I2V models (I2V-01-Director, I2V-01, I2V-01-live). Optional for MiniMax-Hailuo-02 and other models." + "first_frame_image": ("IMAGE", { + "tooltip": "Required for I2V models (I2V-01-Director, I2V-01, I2V-01-live) and MiniMax-Hailuo-02 with 512P resolution. Optional for other cases." + }), + "last_frame_image": ("IMAGE", { + "tooltip": "Only supported by MiniMax-Hailuo-02 model. Model will generate video ending with this frame. Not supported with 512P resolution." }), "duration": (["6", "10"], { "default": "6", - "tooltip": "Video duration in seconds. 01 series: only 6s. MiniMax-Hailuo-02: 6s or 10s (10s only available for 768P resolution)." + "tooltip": "Video duration in seconds. 01 series: only 6s. MiniMax-Hailuo-02: 6s or 10s (10s only available for 512P and 768P resolution)." }), - "resolution": (["768P", "1080P"], { + "resolution": (["512P", "768P", "1080P"], { "default": "768P", - "tooltip": "Video resolution. Only applicable to MiniMax-Hailuo-02 model. 01 series uses fixed resolution." + "tooltip": "Video resolution. Only applicable to MiniMax-Hailuo-02 model. 01 series uses fixed 720P resolution." }), "callback_url": ("STRING", { "multiline": False, @@ -53,7 +56,7 @@ class MiniMaxVideoGeneration: FUNCTION = "generate_video" CATEGORY = "JM-MiniMax-API/Video" - def generate_video(self, api_key, model, prompt, prompt_optimizer, image=None, duration="6", resolution="768P", callback_url=""): + def generate_video(self, api_key, model, prompt, prompt_optimizer, first_frame_image=None, last_frame_image=None, duration="6", resolution="768P", callback_url=""): if not api_key: raise ValueError("API Key must be provided") @@ -64,21 +67,39 @@ class MiniMaxVideoGeneration: hailuo_models = ["MiniMax-Hailuo-02"] # Validate model-specific requirements - if model in i2v_models and image is None: - raise ValueError(f"Model {model} requires an image input. Please connect an image to the 'image' input.") + if model in i2v_models and first_frame_image is None: + raise ValueError(f"Model {model} requires a first frame image input. Please connect an image to the 'first_frame_image' input.") if model in t2v_models and not prompt.strip(): raise ValueError(f"Model {model} requires a text prompt. Please provide a description in the 'prompt' field.") - # Validate duration and resolution combinations for MiniMax-Hailuo-02 + # Validate MiniMax-Hailuo-02 specific requirements if model in hailuo_models: duration_int = int(duration) + + # Check duration and resolution combinations if duration_int == 10 and resolution == "1080P": - raise ValueError("MiniMax-Hailuo-02 model does not support 10s duration with 1080P resolution. Please use 768P for 10s duration or 6s for 1080P.") + raise ValueError("MiniMax-Hailuo-02 model does not support 10s duration with 1080P resolution. Please use 512P or 768P for 10s duration or 6s for 1080P.") + + # Check 512P resolution requirements + if resolution == "512P" and first_frame_image is None: + raise ValueError("MiniMax-Hailuo-02 model with 512P resolution requires a first frame image. Please connect an image to the 'first_frame_image' input.") + + # Check last_frame_image with 512P + if resolution == "512P" and last_frame_image is not None: + raise ValueError("MiniMax-Hailuo-02 model does not support last_frame_image with 512P resolution. Please use 768P or 1080P resolution.") + + # Validate last_frame_image is only used with MiniMax-Hailuo-02 + if last_frame_image is not None and model not in hailuo_models: + raise ValueError(f"last_frame_image is only supported by MiniMax-Hailuo-02 model, but current model is {model}.") # Validate duration for 01 series models if model not in hailuo_models and duration != "6": print(f"Warning: Duration parameter is not applicable to {model}. Using default 6s duration.") + + # Validate resolution for 01 series models + if model not in hailuo_models and resolution != "768P": + print(f"Warning: Resolution parameter is not applicable to {model}. 01 series models use fixed 720P resolution.") try: # Prepare API request @@ -99,24 +120,24 @@ class MiniMaxVideoGeneration: payload["duration"] = int(duration) payload["resolution"] = resolution - # Handle image input for I2V models and MiniMax-Hailuo-02 - if (model in i2v_models and image is not None) or (model in hailuo_models and image is not None): + # Helper function to convert ComfyUI image to base64 + def convert_image_to_base64(image_tensor, image_name): # Convert ComfyUI image tensor to PIL Image - if len(image.shape) == 4: # Batch dimension + if len(image_tensor.shape) == 4: # Batch dimension # Take the first image from batch - image_tensor = image[0] + tensor = image_tensor[0] else: - image_tensor = image + tensor = image_tensor # Convert from tensor format (H, W, C) with values 0-1 to PIL Image import torch import numpy as np # Convert tensor to numpy array and scale to 0-255 - if isinstance(image_tensor, torch.Tensor): - image_np = image_tensor.cpu().numpy() + if isinstance(tensor, torch.Tensor): + image_np = tensor.cpu().numpy() else: - image_np = image_tensor + image_np = tensor # Ensure values are in range 0-1, then scale to 0-255 image_np = np.clip(image_np, 0, 1) @@ -130,11 +151,11 @@ class MiniMaxVideoGeneration: aspect_ratio = width / height if aspect_ratio <= 2/5 or aspect_ratio >= 5/2: - raise ValueError(f"Image aspect ratio ({aspect_ratio:.2f}) must be between 2:5 and 5:2") + raise ValueError(f"{image_name} aspect ratio ({aspect_ratio:.2f}) must be between 2:5 and 5:2") min_dimension = min(width, height) if min_dimension < 300: - raise ValueError(f"Image short side ({min_dimension}px) must be at least 300px") + raise ValueError(f"{image_name} short side ({min_dimension}px) must be at least 300px") # Convert PIL Image to base64 import io @@ -151,16 +172,23 @@ class MiniMaxVideoGeneration: # Check file size (should be < 20MB) file_size = buffer.getbuffer().nbytes if file_size > 20 * 1024 * 1024: # 20MB - raise ValueError(f"Image file size ({file_size/1024/1024:.1f}MB) exceeds 20MB limit") + raise ValueError(f"{image_name} file size ({file_size/1024/1024:.1f}MB) exceeds 20MB limit") # Convert to base64 image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') - # Add image to payload - payload["first_frame_image"] = f"data:image/jpeg;base64,{image_base64}" + print(f"{image_name} converted to base64, size: {len(image_base64)} characters") + print(f"{image_name} dimensions: {width}x{height}, aspect ratio: {aspect_ratio:.2f}") - print(f"Image converted to base64, size: {len(image_base64)} characters") - print(f"Image dimensions: {width}x{height}, aspect ratio: {aspect_ratio:.2f}") + return f"data:image/jpeg;base64,{image_base64}" + + # Handle first frame image + if first_frame_image is not None: + payload["first_frame_image"] = convert_image_to_base64(first_frame_image, "First frame image") + + # Handle last frame image (only for MiniMax-Hailuo-02) + if last_frame_image is not None: + payload["last_frame_image"] = convert_image_to_base64(last_frame_image, "Last frame image") # Add optional callback URL if provided if callback_url.strip(): @@ -173,6 +201,10 @@ class MiniMaxVideoGeneration: if model in hailuo_models: print(f"Duration: {duration}s") print(f"Resolution: {resolution}") + if first_frame_image is not None: + print(f"First frame image: provided") + if last_frame_image is not None: + print(f"Last frame image: provided") if callback_url: print(f"Callback URL: {callback_url}")