|
|
from inspect import cleandoc |
|
|
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, |
|
|
MiniMaxModel |
|
|
) |
|
|
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 |
|
|
|
|
|
async 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 = (await upload_images_to_comfyapi(image, max_images=1, auth_kwargs=kwargs))[0] |
|
|
|
|
|
|
|
|
subject_reference = None |
|
|
if subject is not None: |
|
|
subject_url = (await 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=MiniMaxModel(model), |
|
|
prompt=prompt_text, |
|
|
callback_url=None, |
|
|
first_frame_image=image_url, |
|
|
subject_reference=subject_reference, |
|
|
prompt_optimizer=None, |
|
|
), |
|
|
auth_kwargs=kwargs, |
|
|
) |
|
|
response = await 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 = await 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 = await 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 = await 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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
class MinimaxHailuoVideoNode: |
|
|
"""Generates videos from prompt, with optional start frame using the new MiniMax Hailuo-02 model.""" |
|
|
|
|
|
@classmethod |
|
|
def INPUT_TYPES(s): |
|
|
return { |
|
|
"required": { |
|
|
"prompt_text": ( |
|
|
"STRING", |
|
|
{ |
|
|
"multiline": True, |
|
|
"default": "", |
|
|
"tooltip": "Text prompt to guide the 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.", |
|
|
}, |
|
|
), |
|
|
"first_frame_image": ( |
|
|
IO.IMAGE, |
|
|
{ |
|
|
"tooltip": "Optional image to use as the first frame to generate a video." |
|
|
}, |
|
|
), |
|
|
"prompt_optimizer": ( |
|
|
IO.BOOLEAN, |
|
|
{ |
|
|
"tooltip": "Optimize prompt to improve generation quality when needed.", |
|
|
"default": True, |
|
|
}, |
|
|
), |
|
|
"duration": ( |
|
|
IO.COMBO, |
|
|
{ |
|
|
"tooltip": "The length of the output video in seconds.", |
|
|
"default": 6, |
|
|
"options": [6, 10], |
|
|
}, |
|
|
), |
|
|
"resolution": ( |
|
|
IO.COMBO, |
|
|
{ |
|
|
"tooltip": "The dimensions of the video display. " |
|
|
"1080p corresponds to 1920 x 1080 pixels, 768p corresponds to 1366 x 768 pixels.", |
|
|
"default": "768P", |
|
|
"options": ["768P", "1080P"], |
|
|
}, |
|
|
), |
|
|
}, |
|
|
"hidden": { |
|
|
"auth_token": "AUTH_TOKEN_COMFY_ORG", |
|
|
"comfy_api_key": "API_KEY_COMFY_ORG", |
|
|
"unique_id": "UNIQUE_ID", |
|
|
}, |
|
|
} |
|
|
|
|
|
RETURN_TYPES = ("VIDEO",) |
|
|
DESCRIPTION = cleandoc(__doc__ or "") |
|
|
FUNCTION = "generate_video" |
|
|
CATEGORY = "api node/video/MiniMax" |
|
|
API_NODE = True |
|
|
|
|
|
async def generate_video( |
|
|
self, |
|
|
prompt_text, |
|
|
seed=0, |
|
|
first_frame_image: torch.Tensor=None, |
|
|
prompt_optimizer=True, |
|
|
duration=6, |
|
|
resolution="768P", |
|
|
model="MiniMax-Hailuo-02", |
|
|
unique_id: Union[str, None]=None, |
|
|
**kwargs, |
|
|
): |
|
|
if first_frame_image is None: |
|
|
validate_string(prompt_text, field_name="prompt_text") |
|
|
|
|
|
if model == "MiniMax-Hailuo-02" and resolution.upper() == "1080P" and duration != 6: |
|
|
raise Exception( |
|
|
"When model is MiniMax-Hailuo-02 and resolution is 1080P, duration is limited to 6 seconds." |
|
|
) |
|
|
|
|
|
|
|
|
image_url = None |
|
|
if first_frame_image is not None: |
|
|
image_url = (await upload_images_to_comfyapi(first_frame_image, max_images=1, auth_kwargs=kwargs))[0] |
|
|
|
|
|
video_generate_operation = SynchronousOperation( |
|
|
endpoint=ApiEndpoint( |
|
|
path="/proxy/minimax/video_generation", |
|
|
method=HttpMethod.POST, |
|
|
request_model=MinimaxVideoGenerationRequest, |
|
|
response_model=MinimaxVideoGenerationResponse, |
|
|
), |
|
|
request=MinimaxVideoGenerationRequest( |
|
|
model=MiniMaxModel(model), |
|
|
prompt=prompt_text, |
|
|
callback_url=None, |
|
|
first_frame_image=image_url, |
|
|
prompt_optimizer=prompt_optimizer, |
|
|
duration=duration, |
|
|
resolution=resolution, |
|
|
), |
|
|
auth_kwargs=kwargs, |
|
|
) |
|
|
response = await video_generate_operation.execute() |
|
|
|
|
|
task_id = response.task_id |
|
|
if not task_id: |
|
|
raise Exception(f"MiniMax generation failed: {response.base_resp}") |
|
|
|
|
|
average_duration = 120 if resolution == "768P" else 240 |
|
|
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=average_duration, |
|
|
node_id=unique_id, |
|
|
auth_kwargs=kwargs, |
|
|
) |
|
|
task_result = await 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 = await 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 = await 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),) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = { |
|
|
"MinimaxTextToVideoNode": MinimaxTextToVideoNode, |
|
|
"MinimaxImageToVideoNode": MinimaxImageToVideoNode, |
|
|
|
|
|
"MinimaxHailuoVideoNode": MinimaxHailuoVideoNode, |
|
|
} |
|
|
|
|
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = { |
|
|
"MinimaxTextToVideoNode": "MiniMax Text to Video", |
|
|
"MinimaxImageToVideoNode": "MiniMax Image to Video", |
|
|
"MinimaxSubjectToVideoNode": "MiniMax Subject to Video", |
|
|
"MinimaxHailuoVideoNode": "MiniMax Hailuo Video", |
|
|
} |
|
|
|