Spaces:
Running
on
Zero
Running
on
Zero
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 | |
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, # used for ImageToVideo | |
subject: torch.Tensor=None, # used for SubjectToVideo | |
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") | |
# upload image, if passed in | |
image_url = None | |
if image is not None: | |
image_url = upload_images_to_comfyapi(image, max_images=1, auth_kwargs=kwargs)[0] | |
# TODO: figure out how to deal with subject properly, API returns invalid params when using S2V-01 model | |
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 | |
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 | |
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 | |
# A dictionary that contains all nodes you want to export with their names | |
# NOTE: names should be globally unique | |
NODE_CLASS_MAPPINGS = { | |
"MinimaxTextToVideoNode": MinimaxTextToVideoNode, | |
"MinimaxImageToVideoNode": MinimaxImageToVideoNode, | |
# "MinimaxSubjectToVideoNode": MinimaxSubjectToVideoNode, | |
} | |
# A dictionary that contains the friendly/humanly readable titles for the nodes | |
NODE_DISPLAY_NAME_MAPPINGS = { | |
"MinimaxTextToVideoNode": "MiniMax Text to Video", | |
"MinimaxImageToVideoNode": "MiniMax Image to Video", | |
"MinimaxSubjectToVideoNode": "MiniMax Subject to Video", | |
} | |