1212import math
1313
1414import torch
15+ import torch .nn .functional as F
1516import torchaudio
1617
1718import nodes
1819import comfy .model_management
1920import comfy .model_sampling
2021import comfy .nested_tensor
22+ import comfy .patcher_extension
2123import comfy .utils
2224import node_helpers
2325from comfy .ldm .minimax .model import FRAME_PER_TOKEN , FRAME_RESCALE
@@ -399,6 +401,206 @@ class ModelSamplingAdvanced(comfy.model_sampling.ModelSamplingAV, comfy.model_sa
399401 return io .NodeOutput (m )
400402
401403
404+ class MiniMaxH3FunControlPatch :
405+ def __init__ (self , model_patch , vae , control_video , mask , source_video , strength , sigma_start , sigma_end ):
406+ self .model_patch = model_patch
407+ self .vae = vae
408+ self .control_video = control_video
409+ self .mask = mask
410+ self .source_video = source_video
411+ self .strength = strength
412+ self .sigma_start = sigma_start
413+ self .sigma_end = sigma_end
414+ self .control_latent = None
415+ self .control_latent_shape = None
416+ self .control_stream = None
417+ self .active = False
418+
419+ def _fit_frames (self , frames , frame_count , width , height ):
420+ indices = torch .arange (frame_count , device = frames .device ).clamp (max = frames .shape [0 ] - 1 )
421+ return comfy .utils .common_upscale (frames [indices ], width , height , "bilinear" , "center" )
422+
423+ def _encode (self , frames , target_shape ):
424+ latent = self .vae .encode (frames .movedim (1 , - 1 )).to (torch .float32 )
425+ if tuple (latent .shape ) != target_shape :
426+ raise ValueError ("MiniMax H3 Fun VAE output shape {} does not match the target {}" .format (tuple (latent .shape ), target_shape ))
427+ return latent
428+
429+ def prepare_control_latent (self , target_shape ):
430+ target_shape = tuple (target_shape )
431+ if self .control_latent is not None and self .control_latent_shape == target_shape :
432+ return
433+
434+ latent_frames , latent_height , latent_width = target_shape [2 :]
435+ frame_count = max ((latent_frames - 2 ) // 5 , 0 ) * 17 + 5
436+ spatial_compression = self .vae .spacial_compression_encode ()
437+ width = latent_width * spatial_compression
438+ height = latent_height * spatial_compression
439+ loaded_models = comfy .model_management .loaded_models (only_currently_used = True )
440+
441+ try :
442+ hint = None
443+ if self .control_video is not None :
444+ frames = self ._fit_frames (self .control_video , frame_count , width , height )
445+ hint = self ._encode (frames , target_shape )
446+
447+ if self .mask is not None :
448+ mask = (self .mask .reshape (- 1 , 1 , self .mask .shape [- 2 ], self .mask .shape [- 1 ]) > 0.5 ).to (torch .float32 )
449+ indices = torch .arange (frame_count , device = mask .device ).clamp (max = mask .shape [0 ] - 1 )
450+ mask = comfy .utils .common_upscale (mask [indices ], width , height , "bilinear" , "center" )
451+ visibility = 1.0 - (mask > 0.5 ).to (torch .float32 )
452+ if self .source_video is None :
453+ source = torch .zeros (frame_count , 3 , height , width , dtype = visibility .dtype , device = visibility .device )
454+ else :
455+ source = self ._fit_frames (self .source_video , frame_count , width , height )
456+ masked_latent = self ._encode (source * visibility .to (source .device ), target_shape )
457+ if hint is None :
458+ hint = torch .zeros_like (masked_latent )
459+ visibility_latent = F .interpolate (
460+ visibility .squeeze (1 )[None , None ], size = (latent_frames , latent_height , latent_width ),
461+ mode = "trilinear" , align_corners = False )
462+ hint = torch .cat ([hint , visibility_latent .to (hint .device ), masked_latent .to (hint .device )], dim = 1 )
463+ finally :
464+ comfy .model_management .load_models_gpu (loaded_models )
465+
466+ self .control_latent = hint
467+ self .control_latent_shape = target_shape
468+
469+ def diffusion_model_wrapper (self , executor , x , timestep , context , transformer_options = {}, ** kwargs ):
470+ sigmas = transformer_options .get ("sigmas" )
471+ sigma = float (sigmas [0 ]) if sigmas is not None else float (timestep .flatten ()[0 ]) / 1000.0
472+ self .active = self .sigma_end <= sigma <= self .sigma_start
473+ self .control_stream = None
474+ if self .active :
475+ payload = kwargs .get ("minimax_payload" ) or {}
476+ if payload .get ("keyframes" ) or payload .get ("refs" ):
477+ raise ValueError ("MiniMax H3 Fun ControlNet does not support keyframe or reference conditioning" )
478+ self .prepare_control_latent (x [0 ].shape )
479+ try :
480+ return executor (x , timestep , context , transformer_options , ** kwargs )
481+ finally :
482+ self .control_stream = None
483+
484+ def before_block (self , block_index , args ):
485+ if not self .active or block_index != self .model_patch .model .injection_layers [0 ]:
486+ return
487+ self .control_latent = self .control_latent .to (args ["img" ].device )
488+ self .control_stream = self .model_patch .model .init_stream (
489+ args ["img" ], self .control_latent , args ["layout" ], args ["t_emb" ])
490+
491+ def after_block (self , block_index , args , out ):
492+ if not self .active :
493+ return out
494+ control_index = self .model_patch .model .injection_layers .index (block_index )
495+ self .control_stream , skip = self .model_patch .model .step (
496+ control_index , self .control_stream , args ["t_emb" ], args ["mod_segments" ], args ["rope_freqs" ],
497+ transformer_options = args ["transformer_options" ])
498+ skip [args ["layout" ].audio_pos .to (skip .device )] = 0
499+ out ["img" ].add_ (skip , alpha = self .strength )
500+ return out
501+
502+ def to (self , device_or_dtype ):
503+ if isinstance (device_or_dtype , torch .device ):
504+ if self .control_latent is not None :
505+ self .control_latent = self .control_latent .to (device_or_dtype )
506+ self .control_stream = None
507+ return self
508+
509+ def cleanup (self ):
510+ self .control_latent = None
511+ self .control_latent_shape = None
512+ self .control_stream = None
513+ self .active = False
514+
515+ def models (self ):
516+ return [self .model_patch ]
517+
518+ def register (self , model ):
519+ model .add_wrapper (comfy .patcher_extension .WrappersMP .DIFFUSION_MODEL , self .diffusion_model_wrapper )
520+ for block_index in self .model_patch .model .injection_layers :
521+ blocks_replace = model .model_options .get ("transformer_options" , {}).get ("patches_replace" , {}).get ("dit" , {})
522+ previous = blocks_replace .get (("double_block" , block_index ))
523+ model .set_model_patch_replace (
524+ MiniMaxH3FunControlBlockPatch (self , block_index , previous ), "dit" , "double_block" , block_index )
525+
526+
527+ class MiniMaxH3FunControlBlockPatch :
528+ def __init__ (self , control_patch , block_index , previous ):
529+ self .control_patch = control_patch
530+ self .block_index = block_index
531+ self .previous = previous
532+
533+ def __call__ (self , args , extra_args ):
534+ self .control_patch .before_block (self .block_index , args )
535+ if self .previous is None :
536+ out = extra_args ["original_block" ](args )
537+ else :
538+ out = self .previous (args , extra_args )
539+ return self .control_patch .after_block (self .block_index , args , out )
540+
541+ def to (self , device_or_dtype ):
542+ self .control_patch .to (device_or_dtype )
543+ if hasattr (self .previous , "to" ):
544+ self .previous = self .previous .to (device_or_dtype )
545+ return self
546+
547+ def cleanup (self ):
548+ self .control_patch .cleanup ()
549+ if hasattr (self .previous , "cleanup" ):
550+ self .previous .cleanup ()
551+
552+ def models (self ):
553+ models = self .control_patch .models ()
554+ if hasattr (self .previous , "models" ):
555+ models += self .previous .models ()
556+ return models
557+
558+
559+ class MiniMaxH3FunControlNetApply (io .ComfyNode ):
560+ @classmethod
561+ def define_schema (cls ):
562+ return io .Schema (
563+ node_id = "MiniMaxH3FunControlNetApply" ,
564+ description = "Apply a MiniMax H3 Fun ControlNet to a text-to-video model as a model patch." ,
565+ display_name = "Apply MiniMax H3 Fun ControlNet" ,
566+ search_aliases = ["minimax controlnet" , "h3 controlnet" , "video inpaint controlnet" ],
567+ category = "model/patch/minimax" ,
568+ inputs = [
569+ io .Model .Input ("model" ),
570+ io .ModelPatch .Input ("model_patch" ),
571+ io .Vae .Input ("vae" ),
572+ io .Float .Input ("strength" , default = 1.0 , min = 0.0 , max = 10.0 , step = 0.01 ),
573+ io .Float .Input ("start_percent" , default = 0.0 , min = 0.0 , max = 1.0 , step = 0.001 , advanced = True ),
574+ io .Float .Input ("end_percent" , default = 1.0 , min = 0.0 , max = 1.0 , step = 0.001 , advanced = True ),
575+ io .Image .Input ("control_video" , optional = True ),
576+ io .Mask .Input ("mask" , optional = True , tooltip = "1 marks the regions to regenerate." ),
577+ io .Image .Input ("source_video" , optional = True , tooltip = "Video behind the mask; only read when a mask is given." ),
578+ ],
579+ outputs = [io .Model .Output ()],
580+ )
581+
582+ @classmethod
583+ def execute (cls , model , model_patch , vae , strength , start_percent , end_percent ,
584+ control_video = None , mask = None , source_video = None ) -> io .NodeOutput :
585+ if strength == 0 or (control_video is None and mask is None ):
586+ return io .NodeOutput (model )
587+
588+ model_patched = model .clone ()
589+ model_sampling = model .get_model_object ("model_sampling" )
590+ patch = MiniMaxH3FunControlPatch (
591+ model_patch ,
592+ vae ,
593+ control_video [..., :3 ].movedim (- 1 , 1 ) if control_video is not None else None ,
594+ mask ,
595+ source_video [..., :3 ].movedim (- 1 , 1 ) if mask is not None and source_video is not None else None ,
596+ strength ,
597+ float (model_sampling .percent_to_sigma (start_percent )),
598+ float (model_sampling .percent_to_sigma (end_percent )),
599+ )
600+ patch .register (model_patched )
601+ return io .NodeOutput (model_patched )
602+
603+
402604class MiniMaxH3Extension (ComfyExtension ):
403605 async def get_node_list (self ):
404606 return [
@@ -407,6 +609,7 @@ async def get_node_list(self):
407609 MiniMaxH3AddGuide ,
408610 MiniMaxH3ReferenceToVideo ,
409611 MiniMaxH3SigmaShift ,
612+ MiniMaxH3FunControlNetApply ,
410613 ]
411614
412615
0 commit comments