| from typing import Union
|
| import logging
|
| import torch
|
|
|
| from comfy.comfy_types.node_typing import IO
|
| from comfy_api.input_impl.video_types import VideoFromFile
|
| from comfy_api_nodes.apis import (
|
| MinimaxVideoGenerationRequest,
|
| MinimaxVideoGenerationResponse,
|
| MinimaxFileRetrieveResponse,
|
| MinimaxTaskResultResponse,
|
| SubjectReferenceItem,
|
| Model
|
| )
|
| from comfy_api_nodes.apis.client import (
|
| ApiEndpoint,
|
| HttpMethod,
|
| SynchronousOperation,
|
| PollingOperation,
|
| EmptyRequest,
|
| )
|
| from comfy_api_nodes.apinode_utils import (
|
| download_url_to_bytesio,
|
| upload_images_to_comfyapi,
|
| validate_string,
|
| )
|
| from server import PromptServer
|
|
|
|
|
| I2V_AVERAGE_DURATION = 114
|
| T2V_AVERAGE_DURATION = 234
|
|
|
| class MinimaxTextToVideoNode:
|
| """
|
| Generates videos synchronously based on a prompt, and optional parameters using MiniMax's API.
|
| """
|
|
|
| AVERAGE_DURATION = T2V_AVERAGE_DURATION
|
|
|
| @classmethod
|
| def INPUT_TYPES(s):
|
| return {
|
| "required": {
|
| "prompt_text": (
|
| "STRING",
|
| {
|
| "multiline": True,
|
| "default": "",
|
| "tooltip": "Text prompt to guide the video generation",
|
| },
|
| ),
|
| "model": (
|
| [
|
| "T2V-01",
|
| "T2V-01-Director",
|
| ],
|
| {
|
| "default": "T2V-01",
|
| "tooltip": "Model to use for video generation",
|
| },
|
| ),
|
| },
|
| "optional": {
|
| "seed": (
|
| IO.INT,
|
| {
|
| "default": 0,
|
| "min": 0,
|
| "max": 0xFFFFFFFFFFFFFFFF,
|
| "control_after_generate": True,
|
| "tooltip": "The random seed used for creating the noise.",
|
| },
|
| ),
|
| },
|
| "hidden": {
|
| "auth_token": "AUTH_TOKEN_COMFY_ORG",
|
| "comfy_api_key": "API_KEY_COMFY_ORG",
|
| "unique_id": "UNIQUE_ID",
|
| },
|
| }
|
|
|
| RETURN_TYPES = ("VIDEO",)
|
| DESCRIPTION = "Generates videos from prompts using MiniMax's API"
|
| FUNCTION = "generate_video"
|
| CATEGORY = "api node/video/MiniMax"
|
| API_NODE = True
|
| OUTPUT_NODE = True
|
|
|
| def generate_video(
|
| self,
|
| prompt_text,
|
| seed=0,
|
| model="T2V-01",
|
| image: torch.Tensor=None,
|
| subject: torch.Tensor=None,
|
| unique_id: Union[str, None]=None,
|
| **kwargs,
|
| ):
|
| '''
|
| Function used between MiniMax nodes - supports T2V, I2V, and S2V, based on provided arguments.
|
| '''
|
| if image is None:
|
| validate_string(prompt_text, field_name="prompt_text")
|
|
|
| image_url = None
|
| if image is not None:
|
| image_url = upload_images_to_comfyapi(image, max_images=1, auth_kwargs=kwargs)[0]
|
|
|
|
|
| subject_reference = None
|
| if subject is not None:
|
| subject_url = upload_images_to_comfyapi(subject, max_images=1, auth_kwargs=kwargs)[0]
|
| subject_reference = [SubjectReferenceItem(image=subject_url)]
|
|
|
|
|
| video_generate_operation = SynchronousOperation(
|
| endpoint=ApiEndpoint(
|
| path="/proxy/minimax/video_generation",
|
| method=HttpMethod.POST,
|
| request_model=MinimaxVideoGenerationRequest,
|
| response_model=MinimaxVideoGenerationResponse,
|
| ),
|
| request=MinimaxVideoGenerationRequest(
|
| model=Model(model),
|
| prompt=prompt_text,
|
| callback_url=None,
|
| first_frame_image=image_url,
|
| subject_reference=subject_reference,
|
| prompt_optimizer=None,
|
| ),
|
| auth_kwargs=kwargs,
|
| )
|
| response = video_generate_operation.execute()
|
|
|
| task_id = response.task_id
|
| if not task_id:
|
| raise Exception(f"MiniMax generation failed: {response.base_resp}")
|
|
|
| video_generate_operation = PollingOperation(
|
| poll_endpoint=ApiEndpoint(
|
| path="/proxy/minimax/query/video_generation",
|
| method=HttpMethod.GET,
|
| request_model=EmptyRequest,
|
| response_model=MinimaxTaskResultResponse,
|
| query_params={"task_id": task_id},
|
| ),
|
| completed_statuses=["Success"],
|
| failed_statuses=["Fail"],
|
| status_extractor=lambda x: x.status.value,
|
| estimated_duration=self.AVERAGE_DURATION,
|
| node_id=unique_id,
|
| auth_kwargs=kwargs,
|
| )
|
| task_result = video_generate_operation.execute()
|
|
|
| file_id = task_result.file_id
|
| if file_id is None:
|
| raise Exception("Request was not successful. Missing file ID.")
|
| file_retrieve_operation = SynchronousOperation(
|
| endpoint=ApiEndpoint(
|
| path="/proxy/minimax/files/retrieve",
|
| method=HttpMethod.GET,
|
| request_model=EmptyRequest,
|
| response_model=MinimaxFileRetrieveResponse,
|
| query_params={"file_id": int(file_id)},
|
| ),
|
| request=EmptyRequest(),
|
| auth_kwargs=kwargs,
|
| )
|
| file_result = file_retrieve_operation.execute()
|
|
|
| file_url = file_result.file.download_url
|
| if file_url is None:
|
| raise Exception(
|
| f"No video was found in the response. Full response: {file_result.model_dump()}"
|
| )
|
| logging.info(f"Generated video URL: {file_url}")
|
| if unique_id:
|
| if hasattr(file_result.file, "backup_download_url"):
|
| message = f"Result URL: {file_url}\nBackup URL: {file_result.file.backup_download_url}"
|
| else:
|
| message = f"Result URL: {file_url}"
|
| PromptServer.instance.send_progress_text(message, unique_id)
|
|
|
| video_io = download_url_to_bytesio(file_url)
|
| if video_io is None:
|
| error_msg = f"Failed to download video from {file_url}"
|
| logging.error(error_msg)
|
| raise Exception(error_msg)
|
| return (VideoFromFile(video_io),)
|
|
|
|
|
| class MinimaxImageToVideoNode(MinimaxTextToVideoNode):
|
| """
|
| Generates videos synchronously based on an image and prompt, and optional parameters using MiniMax's API.
|
| """
|
|
|
| AVERAGE_DURATION = I2V_AVERAGE_DURATION
|
|
|
| @classmethod
|
| def INPUT_TYPES(s):
|
| return {
|
| "required": {
|
| "image": (
|
| IO.IMAGE,
|
| {
|
| "tooltip": "Image to use as first frame of video generation"
|
| },
|
| ),
|
| "prompt_text": (
|
| "STRING",
|
| {
|
| "multiline": True,
|
| "default": "",
|
| "tooltip": "Text prompt to guide the video generation",
|
| },
|
| ),
|
| "model": (
|
| [
|
| "I2V-01-Director",
|
| "I2V-01",
|
| "I2V-01-live",
|
| ],
|
| {
|
| "default": "I2V-01",
|
| "tooltip": "Model to use for video generation",
|
| },
|
| ),
|
| },
|
| "optional": {
|
| "seed": (
|
| IO.INT,
|
| {
|
| "default": 0,
|
| "min": 0,
|
| "max": 0xFFFFFFFFFFFFFFFF,
|
| "control_after_generate": True,
|
| "tooltip": "The random seed used for creating the noise.",
|
| },
|
| ),
|
| },
|
| "hidden": {
|
| "auth_token": "AUTH_TOKEN_COMFY_ORG",
|
| "comfy_api_key": "API_KEY_COMFY_ORG",
|
| "unique_id": "UNIQUE_ID",
|
| },
|
| }
|
|
|
| RETURN_TYPES = ("VIDEO",)
|
| DESCRIPTION = "Generates videos from an image and prompts using MiniMax's API"
|
| FUNCTION = "generate_video"
|
| CATEGORY = "api node/video/MiniMax"
|
| API_NODE = True
|
| OUTPUT_NODE = True
|
|
|
|
|
| class MinimaxSubjectToVideoNode(MinimaxTextToVideoNode):
|
| """
|
| Generates videos synchronously based on an image and prompt, and optional parameters using MiniMax's API.
|
| """
|
|
|
| AVERAGE_DURATION = T2V_AVERAGE_DURATION
|
|
|
| @classmethod
|
| def INPUT_TYPES(s):
|
| return {
|
| "required": {
|
| "subject": (
|
| IO.IMAGE,
|
| {
|
| "tooltip": "Image of subject to reference video generation"
|
| },
|
| ),
|
| "prompt_text": (
|
| "STRING",
|
| {
|
| "multiline": True,
|
| "default": "",
|
| "tooltip": "Text prompt to guide the video generation",
|
| },
|
| ),
|
| "model": (
|
| [
|
| "S2V-01",
|
| ],
|
| {
|
| "default": "S2V-01",
|
| "tooltip": "Model to use for video generation",
|
| },
|
| ),
|
| },
|
| "optional": {
|
| "seed": (
|
| IO.INT,
|
| {
|
| "default": 0,
|
| "min": 0,
|
| "max": 0xFFFFFFFFFFFFFFFF,
|
| "control_after_generate": True,
|
| "tooltip": "The random seed used for creating the noise.",
|
| },
|
| ),
|
| },
|
| "hidden": {
|
| "auth_token": "AUTH_TOKEN_COMFY_ORG",
|
| "comfy_api_key": "API_KEY_COMFY_ORG",
|
| "unique_id": "UNIQUE_ID",
|
| },
|
| }
|
|
|
| RETURN_TYPES = ("VIDEO",)
|
| DESCRIPTION = "Generates videos from an image and prompts using MiniMax's API"
|
| FUNCTION = "generate_video"
|
| CATEGORY = "api node/video/MiniMax"
|
| API_NODE = True
|
| OUTPUT_NODE = True
|
|
|
|
|
|
|
|
|
| NODE_CLASS_MAPPINGS = {
|
| "MinimaxTextToVideoNode": MinimaxTextToVideoNode,
|
| "MinimaxImageToVideoNode": MinimaxImageToVideoNode,
|
|
|
| }
|
|
|
|
|
| NODE_DISPLAY_NAME_MAPPINGS = {
|
| "MinimaxTextToVideoNode": "MiniMax Text to Video",
|
| "MinimaxImageToVideoNode": "MiniMax Image to Video",
|
| "MinimaxSubjectToVideoNode": "MiniMax Subject to Video",
|
| }
|
|
|