191 lines
6.8 KiB
Python
191 lines
6.8 KiB
Python
import torch
|
|
from torch import nn
|
|
import comfy.sample
|
|
import comfy.model_management
|
|
import comfy.utils
|
|
import gc
|
|
import logging
|
|
|
|
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 CohernetVideoSampler:
|
|
@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": (["euler", "euler_ancestral", "heun", "dpm_2",
|
|
"dpm_2_ancestral", "lms", "dpm_fast", "dpm_adaptive",
|
|
"dpmpp_2s_ancestral", "dpmpp_sde", "dpmpp_2m"],),
|
|
"scheduler": (["simple", "karras", "exponential", "normal", "ddim_uniform"],),
|
|
"denoise": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
"motion_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
|
|
"consistency_strength": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.05}),
|
|
"denoise_strength": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 1.0, "step": 0.05}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("LATENT",)
|
|
FUNCTION = "sample"
|
|
CATEGORY = "sampling"
|
|
|
|
def sample(self, model, video_latents, positive, negative, seed, steps,
|
|
cfg, sampler_name, scheduler, denoise, motion_strength,
|
|
consistency_strength, denoise_strength):
|
|
|
|
sampler = MotionGuidedSampler(
|
|
motion_strength=motion_strength,
|
|
consistency_strength=consistency_strength,
|
|
denoise_strength=denoise_strength
|
|
)
|
|
|
|
try:
|
|
samples = sampler(
|
|
latent_frames=video_latents['samples'],
|
|
model=model,
|
|
positive=positive,
|
|
negative=negative,
|
|
noise_seed=seed,
|
|
steps=steps,
|
|
cfg=cfg,
|
|
sampler_name=sampler_name,
|
|
scheduler=scheduler,
|
|
denoise=denoise
|
|
)
|
|
|
|
return ({"samples": samples},)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Sampling error: {str(e)}")
|
|
raise e
|
|
|
|
# Node registration
|
|
NODE_CLASS_MAPPINGS = {
|
|
"CohernetVideoSampler": CohernetVideoSampler
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"CohernetVideoSampler": "Cohernet Video Sampler"
|
|
} |