import torch from torch import nn import comfy.sample import comfy.model_management import comfy.utils import gc import logging import nodes class ImageMotionInfluance: @classmethod def INPUT_TYPES(s): return {"required": { "image": ("IMAGE",), "move_range_x": ("INT", {"default": 0, "min": -150, "max": 150}), "frame_num": ("INT", {"default": 10, "min": 2, "max": 500}), "zoom": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.5, "step": 0.05}), }} RETURN_TYPES = ("IMAGE",) FUNCTION = "guide_motion" CATEGORY = "image/animation" def get_size(self, image): image_size = image.size() return int(image_size[2]), int(image_size[1]) def guide_motion(self, image, move_range_x, frame_num, zoom): img_width, img_height = self.get_size(image) step_size = abs(move_range_x) / (frame_num - 1) if move_range_x != 0 else 0 start_x = 0 if move_range_x > 0 else abs(move_range_x) if move_range_x < 0: start_x -= img_width batch = [] mirrored = torch.flip(image, [2]) for i in range(frame_num): x_pos = start_x + (step_size * i * (-1 if move_range_x < 0 else 1)) x_pos = int(x_pos) x_pos = x_pos % img_width if move_range_x != 0 else 0 current_zoom = (i / (frame_num - 1)) * zoom if zoom > 0 else 0 if current_zoom > 0: crop_width = int(img_width * (1 - current_zoom)) crop_height = int(img_height * (1 - current_zoom)) x_start = (img_width - crop_width) // 2 y_start = (img_height - crop_height) // 2 zoomed_original = torch.nn.functional.interpolate( image[:, y_start:y_start + crop_height, x_start:x_start + crop_width, :].permute(0, 3, 1, 2), size=(img_height, img_width), mode='bilinear' ).permute(0, 2, 3, 1) zoomed_mirror = torch.nn.functional.interpolate( mirrored[:, y_start:y_start + crop_height, x_start:x_start + crop_width, :].permute(0, 3, 1, 2), size=(img_height, img_width), mode='bilinear' ).permute(0, 2, 3, 1) else: zoomed_original = image zoomed_mirror = mirrored canvas = torch.zeros((1, img_height, img_width, image.shape[3])) remaining_width = img_width current_x = x_pos use_flipped = False while remaining_width > 0: width = min(img_width - current_x, remaining_width) current_image = zoomed_mirror if use_flipped else zoomed_original canvas[0, :, img_width - remaining_width:img_width - remaining_width + width, :] = \ current_image[0, :, current_x:current_x + width, :] remaining_width -= width current_x = 0 use_flipped = not use_flipped batch.append(canvas) return (torch.cat(batch, dim=0),) class MotionGuidedSampler(nn.Module): def __init__( self, motion_strength: float = 0.5, consistency_strength: float = 0.9, denoise_strength: float = 0.8 ): super().__init__() self.motion_strength = motion_strength self.consistency_strength = consistency_strength self.denoise_strength = denoise_strength logging.basicConfig(level=logging.INFO) self.logger = logging.getLogger(__name__) def extract_motion_vector(self, current_latent: torch.Tensor, prev_latent: torch.Tensor) -> torch.Tensor: try: return current_latent - prev_latent except RuntimeError as e: self.logger.error(f"Motion extraction error: {e}") return torch.zeros_like(current_latent) def apply_motion_vector(self, content: torch.Tensor, motion_vector: torch.Tensor) -> torch.Tensor: try: motion_applied = content + (motion_vector * self.motion_strength) if torch.isnan(motion_applied).any(): self.logger.warning("NaN detected in motion application") return content return motion_applied except RuntimeError as e: self.logger.error(f"Motion application error: {e}") return content def process_frames(self, latent_frames, model, positive, negative, seed, steps, cfg, sampler_name, scheduler, denoise): batch_size = latent_frames.shape[0] processed_frames = [] device = comfy.model_management.get_torch_device() latent_frames = latent_frames.to(device) pbar = comfy.utils.ProgressBar(batch_size) # Initialize sampler sampler = comfy.samplers.KSampler( model, steps=steps, device=device, sampler=sampler_name, scheduler=scheduler, denoise=denoise ) # Process first frame noise = comfy.sample.prepare_noise(latent_frames[0:1], seed, None).to(device) first_frame = sampler.sample( noise, positive, negative, cfg=cfg, latent_image=latent_frames[0:1], force_full_denoise=True ) processed_frames.append(first_frame) prev_orig = latent_frames[0:1].to(device) prev_styled = first_frame pbar.update(1) # Process remaining frames for i in range(1, batch_size): current_orig = latent_frames[i:i+1].to(device) motion = self.extract_motion_vector(current_orig, prev_orig) motion_guided = self.apply_motion_vector(prev_styled, motion) current_noise = comfy.sample.prepare_noise(motion_guided, seed + i, None).to(device) current_frame = sampler.sample( current_noise, positive, negative, cfg=cfg, latent_image=motion_guided, force_full_denoise=True ) processed_frames.append(current_frame) prev_orig = current_orig prev_styled = current_frame pbar.update(1) if i % 5 == 0: if torch.cuda.is_available(): torch.cuda.empty_cache() gc.collect() self.logger.info(f"Processed {i}/{batch_size} frames") result = torch.cat(processed_frames, dim=0) return result.to(device) def forward(self, latent_frames, model, positive, negative, noise_seed, steps, cfg, sampler_name, scheduler, denoise): try: return self.process_frames( latent_frames=latent_frames, model=model, positive=positive, negative=negative, seed=noise_seed, steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler, denoise=denoise ) except Exception as e: self.logger.error(f"Processing error: {str(e)}") raise e class HunyuanVideoSamplerSave: @classmethod def INPUT_TYPES(s): return { "required": { "model": ("MODEL",), "positive": ("CONDITIONING",), "negative": ("CONDITIONING",), "video_latents": ("LATENT",), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ), "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), "denoise": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.01}), } } RETURN_TYPES = ("LATENT",) FUNCTION = "sample" CATEGORY = "sampling" def sample(self, model, video_latents, positive, negative, seed, steps, cfg, sampler_name, scheduler, denoise): device = comfy.model_management.get_torch_device() # Print latent size for debugging print(f"Original latent size: {video_latents['samples'].size()}") # Use nodes.common_ksampler which works with hunyuan return nodes.common_ksampler( model, seed, steps, cfg, sampler_name, scheduler, positive, negative, video_latents, # Pass the entire latent dict denoise=denoise ) class ResizeImageForHunyuan: crop_methods = ["disabled", "center"] upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic"] # Define all supported aspect ratios and their sizes size_presets = { "1:1": [(256,256), (384,384), (512,512), (768,768)], "16:9": [(384,216), (512,288), (640,360), (768,432)], "4:3": [(384,288), (512,384), (640,480), (768,576)], "3:2": [(384,256), (512,336), (640,432), (768,512)], "2:1": [(384,192), (512,256), (640,320), (768,384)], "9:16": [(216,384), (288,512), (360,640), (432,768)], "3:4": [(288,384), (384,512), (480,640), (576,768)], "2:3": [(256,384), (336,512), (432,640), (512,768)], "1:2": [(192,384), (256,512), (320,640), (384,768)] } @classmethod def INPUT_TYPES(s): # Create size options list size_options = [] for ratio, sizes in s.size_presets.items(): for w, h in sizes: size_options.append(f"{ratio} ({w}x{h})") return {"required": { "image": ("IMAGE",), "size_preset": (size_options,), "upscale_method": (s.upscale_methods,), "crop": (s.crop_methods,) }} RETURN_TYPES = ('IMAGE',) FUNCTION = 'resize' CATEGORY = 'image/hunyuan' def parse_size_preset(self, preset): # Extract width and height from preset string like "16:9 (768x432)" size_part = preset.split(" ")[1].strip("()") width, height = map(int, size_part.split("x")) return width, height def upscale(self, image, upscale_method, width, height, crop): samples = image.movedim(-1,1) s = comfy.utils.common_upscale(samples, width, height, upscale_method, crop) s = s.movedim(1,-1) return (s,) def resize(self, image, size_preset, upscale_method, crop): # Get target width and height from preset w, h = self.parse_size_preset(size_preset) # Print info about the resize operation print(f'Resizing image from {image.shape[2]}x{image.shape[1]} to {w}x{h} ({size_preset.split()[0]} aspect ratio)') img = self.upscale(image, upscale_method, w, h, crop)[0] return (img, ) class EmptyVideoLatentForHunyuan: @classmethod def INPUT_TYPES(s): # Define aspect ratios for Hunyuan video that can work with home GPUs (max width 768) aspect_ratios = [ "256x256 (1:1)", "384x384 (1:1)", "512x512 (1:1)", "768x768 (1:1)", "384x216 (16:9)", "512x288 (16:9)", "640x360 (16:9)", "768x432 (16:9)", "384x288 (4:3)", "512x384 (4:3)", "640x480 (4:3)", "768x576 (4:3)", "384x256 (3:2)", "512x336 (3:2)", "640x432 (3:2)", "768x512 (3:2)", "384x192 (2:1)", "512x256 (2:1)", "640x320 (2:1)", "768x384 (2:1)", "216x384 (9:16)", "288x512 (9:16)", "360x640 (9:16)", "432x768 (9:16)", "288x384 (3:4)", "384x512 (3:4)", "480x640 (3:4)", "576x768 (3:4)", "256x384 (2:3)", "336x512 (2:3)", "432x640 (2:3)", "512x768 (2:3)", "192x384 (1:2)", "256x512 (1:2)", "320x640 (1:2)", "384x768 (1:2)" ] return {"required": { "resolution": (aspect_ratios, ), "length": ("INT", {"default": 25, "min": 1, "max": nodes.MAX_RESOLUTION, "step": 4}), "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}) }} RETURN_TYPES = ("LATENT",) FUNCTION = "generate" CATEGORY = "latent/video" def generate(self, resolution, length, batch_size=1): # Parse resolution string to get dimensions dimensions = resolution.split(' ')[0] width, height = map(int, dimensions.split('x')) # Ensure dimensions are multiples of 16 width = (width // 16) * 16 height = (height // 16) * 16 # Create latent with original format latent = torch.zeros([batch_size, 16, ((length - 1) // 4) + 1, height // 8, width // 8], device=comfy.model_management.intermediate_device()) return ({"samples": latent}, ) # Node registration NODE_CLASS_MAPPINGS = { "HunyuanVideoSamplerSave": HunyuanVideoSamplerSave, "ResizeImageForHunyuan": ResizeImageForHunyuan, "EmptyVideoLatentForHunyuan": EmptyVideoLatentForHunyuan, "ImageMotionInfluance": ImageMotionInfluance, } NODE_DISPLAY_NAME_MAPPINGS = { "HunyuanVideoSamplerSave": "Hunyuan Video Sampler Save", "ResizeImageForHunyuan": "Resize Image For Hunyuan", "EmptyVideoLatentForHunyuan": "Empty Video Latent For Hunyuan", "ImageMotionInfluance": "Image Motion Influence", }