1
+ from inspect import cleandoc
1
2
from typing import Union
2
3
import logging
3
4
import torch
10
11
MinimaxFileRetrieveResponse ,
11
12
MinimaxTaskResultResponse ,
12
13
SubjectReferenceItem ,
13
- Model
14
+ MiniMaxModel
14
15
)
15
16
from comfy_api_nodes .apis .client import (
16
17
ApiEndpoint ,
@@ -84,7 +85,6 @@ def INPUT_TYPES(s):
84
85
FUNCTION = "generate_video"
85
86
CATEGORY = "api node/video/MiniMax"
86
87
API_NODE = True
87
- OUTPUT_NODE = True
88
88
89
89
async def generate_video (
90
90
self ,
@@ -121,7 +121,7 @@ async def generate_video(
121
121
response_model = MinimaxVideoGenerationResponse ,
122
122
),
123
123
request = MinimaxVideoGenerationRequest (
124
- model = Model (model ),
124
+ model = MiniMaxModel (model ),
125
125
prompt = prompt_text ,
126
126
callback_url = None ,
127
127
first_frame_image = image_url ,
@@ -251,7 +251,6 @@ def INPUT_TYPES(s):
251
251
FUNCTION = "generate_video"
252
252
CATEGORY = "api node/video/MiniMax"
253
253
API_NODE = True
254
- OUTPUT_NODE = True
255
254
256
255
257
256
class MinimaxSubjectToVideoNode (MinimaxTextToVideoNode ):
@@ -313,7 +312,181 @@ def INPUT_TYPES(s):
313
312
FUNCTION = "generate_video"
314
313
CATEGORY = "api node/video/MiniMax"
315
314
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 ),)
317
490
318
491
319
492
# A dictionary that contains all nodes you want to export with their names
@@ -322,11 +495,13 @@ def INPUT_TYPES(s):
322
495
"MinimaxTextToVideoNode" : MinimaxTextToVideoNode ,
323
496
"MinimaxImageToVideoNode" : MinimaxImageToVideoNode ,
324
497
# "MinimaxSubjectToVideoNode": MinimaxSubjectToVideoNode,
498
+ "MinimaxHailuoVideoNode" : MinimaxHailuoVideoNode ,
325
499
}
326
500
327
501
# A dictionary that contains the friendly/humanly readable titles for the nodes
328
502
NODE_DISPLAY_NAME_MAPPINGS = {
329
503
"MinimaxTextToVideoNode" : "MiniMax Text to Video" ,
330
504
"MinimaxImageToVideoNode" : "MiniMax Image to Video" ,
331
505
"MinimaxSubjectToVideoNode" : "MiniMax Subject to Video" ,
506
+ "MinimaxHailuoVideoNode" : "MiniMax Hailuo Video" ,
332
507
}
0 commit comments