Move and edit Minimax node to nodes_minimax.py (#50)

This commit is contained in:
Jedrzej Kosinski 2025-04-30 01:51:57 -05:00
parent 5d22bfb0bb
commit c2187140e4
3 changed files with 172 additions and 150 deletions

View File

@ -2,31 +2,23 @@ import base64
import io import io
import math import math
from inspect import cleandoc from inspect import cleandoc
from typing import Literal, Optional from typing import Optional
from comfy.utils import common_upscale from comfy.utils import common_upscale
from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeDict from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeDict
from comfy_api.input_impl.video_types import VideoFromFile
from comfy_api_nodes.apis import ( from comfy_api_nodes.apis import (
OpenAIImageEditRequest, OpenAIImageEditRequest,
OpenAIImageGenerationRequest, OpenAIImageGenerationRequest,
OpenAIImageEditRequest, OpenAIImageEditRequest,
OpenAIImageGenerationResponse, OpenAIImageGenerationResponse,
MinimaxVideoGenerationRequest,
MinimaxVideoGenerationResponse,
MinimaxFileRetrieveResponse,
MinimaxTaskResultResponse,
IdeogramGenerateRequest, IdeogramGenerateRequest,
IdeogramGenerateResponse, IdeogramGenerateResponse,
ImageRequest, ImageRequest,
Model
) )
from comfy_api_nodes.apis.client import ( from comfy_api_nodes.apis.client import (
ApiClient, ApiClient,
ApiEndpoint, ApiEndpoint,
HttpMethod, HttpMethod,
SynchronousOperation, SynchronousOperation,
PollingOperation,
EmptyRequest,
UploadRequest, UploadRequest,
UploadResponse, UploadResponse,
) )
@ -37,7 +29,6 @@ import requests
import torch import torch
import math import math
import base64 import base64
import logging
import uuid import uuid
import folder_paths import folder_paths
from io import BytesIO from io import BytesIO
@ -996,144 +987,6 @@ class IdeogramTextToImage(ComfyNodeABC):
# def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen): # def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen):
# return "" # return ""
class MinimaxTextToVideoNode:
"""
Generates videos synchronously based on a prompt, and optional parameters using Minimax's API.
"""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type: Literal["output"] = "output"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"prompt_text": (
"STRING",
{
"multiline": True,
"default": "",
"tooltip": "Text prompt to guide the video generation",
},
),
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
"model": (
[
"T2V-01",
"I2V-01-Director",
"S2V-01",
"I2V-01",
"I2V-01-live",
],
{
"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": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
"auth_token": "AUTH_TOKEN_COMFY_ORG",
},
}
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,
filename_prefix,
seed=0,
model="T2V-01",
prompt=None,
extra_pnginfo=None,
auth_token=None,
):
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=None,
subject_reference=None,
prompt_optimizer=None,
),
auth_token=auth_token,
)
response = video_generate_operation.execute()
task_id = response.task_id
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,
auth_token=auth_token,
)
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_token=auth_token,
)
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}")
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),)
# A dictionary that contains all nodes you want to export with their names # A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique # NOTE: names should be globally unique
@ -1142,7 +995,6 @@ NODE_CLASS_MAPPINGS = {
"OpenAIDalle3": OpenAIDalle3, "OpenAIDalle3": OpenAIDalle3,
"OpenAIGPTImage1": OpenAIGPTImage1, "OpenAIGPTImage1": OpenAIGPTImage1,
"IdeogramTextToImage": IdeogramTextToImage, "IdeogramTextToImage": IdeogramTextToImage,
"MinimaxTextToVideoNode": MinimaxTextToVideoNode,
} }
# A dictionary that contains the friendly/humanly readable titles for the nodes # A dictionary that contains the friendly/humanly readable titles for the nodes
@ -1151,5 +1003,4 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"OpenAIDalle3": "OpenAI DALL·E 3", "OpenAIDalle3": "OpenAI DALL·E 3",
"OpenAIGPTImage1": "OpenAI GPT Image 1", "OpenAIGPTImage1": "OpenAI GPT Image 1",
"IdeogramTextToImage": "Ideogram Text to Image", "IdeogramTextToImage": "Ideogram Text to Image",
"MinimaxTextToVideoNode": "Minimax Text to Video",
} }

View File

@ -0,0 +1,170 @@
from typing import Literal
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,
Model
)
from comfy_api_nodes.apis.client import (
ApiEndpoint,
HttpMethod,
SynchronousOperation,
PollingOperation,
EmptyRequest,
)
from comfy_api_nodes.nodes_api import (
download_url_to_bytesio,
)
import logging
import folder_paths
class MinimaxTextToVideoNode:
"""
Generates videos synchronously based on a prompt, and optional parameters using Minimax's API.
"""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type: Literal["output"] = "output"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"prompt_text": (
"STRING",
{
"multiline": True,
"default": "",
"tooltip": "Text prompt to guide the video generation",
},
),
"model": (
[
"T2V-01",
"I2V-01-Director",
"S2V-01",
"I2V-01",
"I2V-01-live",
],
{
"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",
},
}
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",
auth_token=None,
):
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=None,
subject_reference=None,
prompt_optimizer=None,
),
auth_token=auth_token,
)
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,
auth_token=auth_token,
)
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_token=auth_token,
)
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}")
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),)
# A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique
NODE_CLASS_MAPPINGS = {
"MinimaxTextToVideoNode": MinimaxTextToVideoNode,
}
# A dictionary that contains the friendly/humanly readable titles for the nodes
NODE_DISPLAY_NAME_MAPPINGS = {
"MinimaxTextToVideoNode": "Minimax Text to Video",
}

View File

@ -2263,6 +2263,7 @@ def init_builtin_extra_nodes():
api_nodes_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_api_nodes") api_nodes_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_api_nodes")
api_nodes_files = [ api_nodes_files = [
"nodes_api.py", "nodes_api.py",
"nodes_minimax.py",
"nodes_veo2.py", "nodes_veo2.py",
"nodes_kling.py", "nodes_kling.py",
"nodes_runway.py", "nodes_runway.py",