[Minimax] Show video preview and embed workflow in ouput (#7)

This commit is contained in:
Christian Byrne 2025-04-21 07:39:52 +08:00 committed by Robin Huang
parent 9592c7af45
commit 0e32c61481

View File

@ -1,8 +1,15 @@
import os
import requests
from inspect import cleandoc from inspect import cleandoc
from comfy.comfy_types.node_typing import ComfyNodeABC, InputTypeDict, IO from comfy.comfy_types.node_typing import ComfyNodeABC, InputTypeDict, IO
from comfy_api_nodes.apis.client import ApiEndpoint, SynchronousOperation, HttpMethod, PollingOperation, EmptyRequest from comfy_api_nodes.apis.client import ApiEndpoint, SynchronousOperation, HttpMethod, PollingOperation, EmptyRequest
from comfy_api_nodes.apis.stubs import IdeogramGenerateRequest, IdeogramGenerateResponse, ImageRequest, MinimaxVideoGenerationRequest, MinimaxVideoGenerationResponse, MinimaxFileRetrieveResponse, MinimaxTaskResultResponse, Model from comfy_api_nodes.apis.stubs import IdeogramGenerateRequest, IdeogramGenerateResponse, ImageRequest, MinimaxVideoGenerationRequest, MinimaxVideoGenerationResponse, MinimaxFileRetrieveResponse, MinimaxTaskResultResponse, Model
import folder_paths
import logging import logging
from comfy.comfy_types.node_typing import FileLocator
import json
import av
def check_auth_token(auth_token): def check_auth_token(auth_token):
"""Verify that an auth token is present.""" """Verify that an auth token is present."""
@ -168,25 +175,55 @@ class MinimaxVideoNode:
""" """
Generates videos synchronously based on a prompt, and optional parameters using Minimax's API. Generates videos synchronously based on a prompt, and optional parameters using Minimax's API.
""" """
def __init__(self): def __init__(self):
pass self.output_dir = folder_paths.get_output_directory()
self.type = "output"
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
return { return {
"required": { "required": {
"prompt_text": ("STRING", { "prompt_text": (
"multiline": True, "STRING",
"default": "", {
"tooltip": "Text prompt to guide the video generation" "multiline": True,
}), "default": "",
"model": (["T2V-01", "I2V-01-Director", "S2V-01", "I2V-01", "I2V-01-live", "T2V-01"], { "tooltip": "Text prompt to guide the video generation",
"default": "T2V-01", },
"tooltip": "Model to use for video generation" ),
}), "filename_prefix": ("STRING", {"default": "ComfyUI"}),
"model": (
[
"T2V-01",
"I2V-01-Director",
"S2V-01",
"I2V-01",
"I2V-01-live",
"T2V-01",
],
{
"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": { "hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG" "prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
"auth_token": "AUTH_TOKEN_COMFY_ORG",
}, },
} }
@ -197,7 +234,16 @@ class MinimaxVideoNode:
API_NODE = True API_NODE = True
OUTPUT_NODE = True OUTPUT_NODE = True
def generate_video(self, prompt_text, seed=0, model="T2V-01", auth_token=None): 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( video_generate_operation = SynchronousOperation(
endpoint=ApiEndpoint( endpoint=ApiEndpoint(
path="/proxy/minimax/video_generation", path="/proxy/minimax/video_generation",
@ -211,9 +257,9 @@ class MinimaxVideoNode:
callback_url=None, callback_url=None,
first_frame_image=None, first_frame_image=None,
subject_reference=None, subject_reference=None,
prompt_optimizer=None prompt_optimizer=None,
), ),
auth_token=auth_token auth_token=auth_token,
) )
response = video_generate_operation.execute() response = video_generate_operation.execute()
@ -225,14 +271,12 @@ class MinimaxVideoNode:
method=HttpMethod.GET, method=HttpMethod.GET,
request_model=EmptyRequest, request_model=EmptyRequest,
response_model=MinimaxTaskResultResponse, response_model=MinimaxTaskResultResponse,
query_params={ query_params={"task_id": task_id},
"task_id": task_id
}
), ),
completed_statuses=["Success"], completed_statuses=["Success"],
failed_statuses=["Fail"], failed_statuses=["Fail"],
status_extractor=lambda x: x.status.value, status_extractor=lambda x: x.status.value,
auth_token=auth_token auth_token=auth_token,
) )
task_result = video_generate_operation.execute() task_result = video_generate_operation.execute()
@ -244,12 +288,10 @@ class MinimaxVideoNode:
method=HttpMethod.GET, method=HttpMethod.GET,
request_model=EmptyRequest, request_model=EmptyRequest,
response_model=MinimaxFileRetrieveResponse, response_model=MinimaxFileRetrieveResponse,
query_params={ query_params={"file_id": file_id},
"file_id": file_id
}
), ),
request=EmptyRequest(), request=EmptyRequest(),
auth_token=auth_token auth_token=auth_token,
) )
file_result = file_retrieve_operation.execute() file_result = file_retrieve_operation.execute()
@ -257,7 +299,45 @@ class MinimaxVideoNode:
logging.info(f"Generated video URL: {file_url}") logging.info(f"Generated video URL: {file_url}")
return (None,) # Construct the save path
full_output_folder, filename, counter, subfolder, filename_prefix = (
folder_paths.get_save_image_path(filename_prefix, self.output_dir)
)
file_basename = f"{filename}_{counter:05}_.mp4"
save_path = os.path.join(full_output_folder, file_basename)
# Download the video data
video_response = requests.get(file_url)
video_data = video_response.content
# Save the video data to a file
with open(save_path, "wb") as video_file:
video_file.write(video_data)
# Add workflow metadata to the video container
if prompt is not None or extra_pnginfo is not None:
try:
container = av.open(save_path, mode="r+")
if prompt is not None:
container.metadata["prompt"] = json.dumps(prompt)
if extra_pnginfo is not None:
for x in extra_pnginfo:
container.metadata[x] = json.dumps(extra_pnginfo[x])
container.close()
except Exception as e:
logging.warning(f"Failed to add metadata to video: {e}")
# Create a FileLocator for the frontend to use for the preview
results: list[FileLocator] = [
{
"filename": file_basename,
"subfolder": subfolder,
"type": self.type,
}
]
return {"ui": {"images": results, "animated": (True,)}}
# 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