1+ from inspect import cleandoc
12from typing import Union
23import logging
34import torch
1011 MinimaxFileRetrieveResponse ,
1112 MinimaxTaskResultResponse ,
1213 SubjectReferenceItem ,
13- Model
14+ MiniMaxModel
1415)
1516from comfy_api_nodes .apis .client import (
1617 ApiEndpoint ,
@@ -84,7 +85,6 @@ def INPUT_TYPES(s):
8485 FUNCTION = "generate_video"
8586 CATEGORY = "api node/video/MiniMax"
8687 API_NODE = True
87- OUTPUT_NODE = True
8888
8989 async def generate_video (
9090 self ,
@@ -121,7 +121,7 @@ async def generate_video(
121121 response_model = MinimaxVideoGenerationResponse ,
122122 ),
123123 request = MinimaxVideoGenerationRequest (
124- model = Model (model ),
124+ model = MiniMaxModel (model ),
125125 prompt = prompt_text ,
126126 callback_url = None ,
127127 first_frame_image = image_url ,
@@ -251,7 +251,6 @@ def INPUT_TYPES(s):
251251 FUNCTION = "generate_video"
252252 CATEGORY = "api node/video/MiniMax"
253253 API_NODE = True
254- OUTPUT_NODE = True
255254
256255
257256class MinimaxSubjectToVideoNode (MinimaxTextToVideoNode ):
@@ -313,7 +312,181 @@ def INPUT_TYPES(s):
313312 FUNCTION = "generate_video"
314313 CATEGORY = "api node/video/MiniMax"
315314 API_NODE = True
316- OUTPUT_NODE = True
315+
316+
317+ class MinimaxHailuoVideoNode :
318+ """Generates videos from prompt, with optional start frame using the new MiniMax Hailuo-02 model."""
319+
320+ @classmethod
321+ def INPUT_TYPES (s ):
322+ return {
323+ "required" : {
324+ "prompt_text" : (
325+ "STRING" ,
326+ {
327+ "multiline" : True ,
328+ "default" : "" ,
329+ "tooltip" : "Text prompt to guide the video generation." ,
330+ },
331+ ),
332+ },
333+ "optional" : {
334+ "seed" : (
335+ IO .INT ,
336+ {
337+ "default" : 0 ,
338+ "min" : 0 ,
339+ "max" : 0xFFFFFFFFFFFFFFFF ,
340+ "control_after_generate" : True ,
341+ "tooltip" : "The random seed used for creating the noise." ,
342+ },
343+ ),
344+ "first_frame_image" : (
345+ IO .IMAGE ,
346+ {
347+ "tooltip" : "Optional image to use as the first frame to generate a video."
348+ },
349+ ),
350+ "prompt_optimizer" : (
351+ IO .BOOLEAN ,
352+ {
353+ "tooltip" : "Optimize prompt to improve generation quality when needed." ,
354+ "default" : True ,
355+ },
356+ ),
357+ "duration" : (
358+ IO .COMBO ,
359+ {
360+ "tooltip" : "The length of the output video in seconds." ,
361+ "default" : 6 ,
362+ "options" : [6 , 10 ],
363+ },
364+ ),
365+ "resolution" : (
366+ IO .COMBO ,
367+ {
368+ "tooltip" : "The dimensions of the video display. "
369+ "1080p corresponds to 1920 x 1080 pixels, 768p corresponds to 1366 x 768 pixels." ,
370+ "default" : "768P" ,
371+ "options" : ["768P" , "1080P" ],
372+ },
373+ ),
374+ },
375+ "hidden" : {
376+ "auth_token" : "AUTH_TOKEN_COMFY_ORG" ,
377+ "comfy_api_key" : "API_KEY_COMFY_ORG" ,
378+ "unique_id" : "UNIQUE_ID" ,
379+ },
380+ }
381+
382+ RETURN_TYPES = ("VIDEO" ,)
383+ DESCRIPTION = cleandoc (__doc__ or "" )
384+ FUNCTION = "generate_video"
385+ CATEGORY = "api node/video/MiniMax"
386+ API_NODE = True
387+
388+ async def generate_video (
389+ self ,
390+ prompt_text ,
391+ seed = 0 ,
392+ first_frame_image : torch .Tensor = None , # used for ImageToVideo
393+ prompt_optimizer = True ,
394+ duration = 6 ,
395+ resolution = "768P" ,
396+ model = "MiniMax-Hailuo-02" ,
397+ unique_id : Union [str , None ]= None ,
398+ ** kwargs ,
399+ ):
400+ if first_frame_image is None :
401+ validate_string (prompt_text , field_name = "prompt_text" )
402+
403+ if model == "MiniMax-Hailuo-02" and resolution .upper () == "1080P" and duration != 6 :
404+ raise Exception (
405+ "When model is MiniMax-Hailuo-02 and resolution is 1080P, duration is limited to 6 seconds."
406+ )
407+
408+ # upload image, if passed in
409+ image_url = None
410+ if first_frame_image is not None :
411+ image_url = (await upload_images_to_comfyapi (first_frame_image , max_images = 1 , auth_kwargs = kwargs ))[0 ]
412+
413+ video_generate_operation = SynchronousOperation (
414+ endpoint = ApiEndpoint (
415+ path = "/proxy/minimax/video_generation" ,
416+ method = HttpMethod .POST ,
417+ request_model = MinimaxVideoGenerationRequest ,
418+ response_model = MinimaxVideoGenerationResponse ,
419+ ),
420+ request = MinimaxVideoGenerationRequest (
421+ model = MiniMaxModel (model ),
422+ prompt = prompt_text ,
423+ callback_url = None ,
424+ first_frame_image = image_url ,
425+ prompt_optimizer = prompt_optimizer ,
426+ duration = duration ,
427+ resolution = resolution ,
428+ ),
429+ auth_kwargs = kwargs ,
430+ )
431+ response = await video_generate_operation .execute ()
432+
433+ task_id = response .task_id
434+ if not task_id :
435+ raise Exception (f"MiniMax generation failed: { response .base_resp } " )
436+
437+ average_duration = 120 if resolution == "768P" else 240
438+ video_generate_operation = PollingOperation (
439+ poll_endpoint = ApiEndpoint (
440+ path = "/proxy/minimax/query/video_generation" ,
441+ method = HttpMethod .GET ,
442+ request_model = EmptyRequest ,
443+ response_model = MinimaxTaskResultResponse ,
444+ query_params = {"task_id" : task_id },
445+ ),
446+ completed_statuses = ["Success" ],
447+ failed_statuses = ["Fail" ],
448+ status_extractor = lambda x : x .status .value ,
449+ estimated_duration = average_duration ,
450+ node_id = unique_id ,
451+ auth_kwargs = kwargs ,
452+ )
453+ task_result = await video_generate_operation .execute ()
454+
455+ file_id = task_result .file_id
456+ if file_id is None :
457+ raise Exception ("Request was not successful. Missing file ID." )
458+ file_retrieve_operation = SynchronousOperation (
459+ endpoint = ApiEndpoint (
460+ path = "/proxy/minimax/files/retrieve" ,
461+ method = HttpMethod .GET ,
462+ request_model = EmptyRequest ,
463+ response_model = MinimaxFileRetrieveResponse ,
464+ query_params = {"file_id" : int (file_id )},
465+ ),
466+ request = EmptyRequest (),
467+ auth_kwargs = kwargs ,
468+ )
469+ file_result = await file_retrieve_operation .execute ()
470+
471+ file_url = file_result .file .download_url
472+ if file_url is None :
473+ raise Exception (
474+ f"No video was found in the response. Full response: { file_result .model_dump ()} "
475+ )
476+ logging .info (f"Generated video URL: { file_url } " )
477+ if unique_id :
478+ if hasattr (file_result .file , "backup_download_url" ):
479+ message = f"Result URL: { file_url } \n Backup URL: { file_result .file .backup_download_url } "
480+ else :
481+ message = f"Result URL: { file_url } "
482+ PromptServer .instance .send_progress_text (message , unique_id )
483+
484+ video_io = await download_url_to_bytesio (file_url )
485+ if video_io is None :
486+ error_msg = f"Failed to download video from { file_url } "
487+ logging .error (error_msg )
488+ raise Exception (error_msg )
489+ return (VideoFromFile (video_io ),)
317490
318491
319492# A dictionary that contains all nodes you want to export with their names
@@ -322,11 +495,13 @@ def INPUT_TYPES(s):
322495 "MinimaxTextToVideoNode" : MinimaxTextToVideoNode ,
323496 "MinimaxImageToVideoNode" : MinimaxImageToVideoNode ,
324497 # "MinimaxSubjectToVideoNode": MinimaxSubjectToVideoNode,
498+ "MinimaxHailuoVideoNode" : MinimaxHailuoVideoNode ,
325499}
326500
327501# A dictionary that contains the friendly/humanly readable titles for the nodes
328502NODE_DISPLAY_NAME_MAPPINGS = {
329503 "MinimaxTextToVideoNode" : "MiniMax Text to Video" ,
330504 "MinimaxImageToVideoNode" : "MiniMax Image to Video" ,
331505 "MinimaxSubjectToVideoNode" : "MiniMax Subject to Video" ,
506+ "MinimaxHailuoVideoNode" : "MiniMax Hailuo Video" ,
332507}
0 commit comments