Files

310 lines
15 KiB
Python

from typing_extensions import override
from comfy_api.latest import io
import torch
import torch.nn.functional as F
import json
from typing import List, Tuple, Optional, Any
import node_helpers
import comfy
import comfy.utils
class WanMultiFrameRefToVideo(io.ComfyNode):
RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "CONDITIONING", "LATENT")
RETURN_NAMES = ("positive_high", "positive_low", "negative", "latent")
CATEGORY = "ComfyUI-Wan22FMLF"
FUNCTION = "execute"
OUTPUT_NODE = False
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="WanMultiFrameRefToVideo",
display_name="Wan Multi-Frame Reference",
category="ComfyUI-Wan22FMLF",
inputs=[
io.Conditioning.Input("positive"),
io.Conditioning.Input("negative"),
io.Vae.Input("vae"),
io.Int.Input("width", default=832, min=16, max=8192, step=16, display_mode=io.NumberDisplay.number),
io.Int.Input("height", default=480, min=16, max=8192, step=16, display_mode=io.NumberDisplay.number),
io.Int.Input("length", default=81, min=1, max=8192, step=4, display_mode=io.NumberDisplay.number),
io.Int.Input("batch_size", default=1, min=1, max=4096, display_mode=io.NumberDisplay.number),
io.Image.Input("ref_images"),
io.Combo.Input("mode", ["NORMAL", "SINGLE_PERSON"], default="NORMAL", optional=True),
io.String.Input("ref_positions", default="", optional=True),
io.Float.Input("ref_strength_high", default=0.8, min=0.0, max=1.0, step=0.05, round=0.01, display_mode=io.NumberDisplay.slider, optional=True, display_name="ref_strength_high"),
io.Float.Input("end_frame_strength_high", default=1.0, min=0.0, max=1.0, step=0.05, round=0.01, display_mode=io.NumberDisplay.slider, optional=True, display_name="end_frame_strength_high"),
io.Float.Input("ref_strength_low", default=0.2, min=0.0, max=1.0, step=0.05, round=0.01, display_mode=io.NumberDisplay.slider, optional=True, display_name="ref_strength_low"),
io.Float.Input("start_frame_strength_low", default=1.0, min=0.0, max=1.0, step=0.05, round=0.01, display_mode=io.NumberDisplay.slider, optional=True, display_name="start_frame_strength_low"),
io.Float.Input("end_frame_strength_low", default=1.0, min=0.0, max=1.0, step=0.05, round=0.01, display_mode=io.NumberDisplay.slider, optional=True, display_name="end_frame_strength_low"),
io.Float.Input("structural_repulsion_boost", default=1.0, min=1.0, max=2.0, step=0.05, round=0.01, display_mode=io.NumberDisplay.slider, optional=True, display_name="structural_repulsion_boost"),
io.Int.Input("protect_zone_size", default=6, min=0, max=20, step=1, display_mode=io.NumberDisplay.slider, optional=True, display_name="protect_zone_size"),
io.Float.Input("time_smoothing_factor", default=0.2, min=0.0, max=1.0, step=0.05, round=0.01, display_mode=io.NumberDisplay.slider, optional=True, display_name="time_smoothing_factor"),
io.ClipVisionOutput.Input("clip_vision_output", optional=True),
],
outputs=[
io.Conditioning.Output(display_name="positive_high"),
io.Conditioning.Output(display_name="positive_low"),
io.Conditioning.Output(display_name="negative"),
io.Latent.Output(display_name="latent"),
],
)
@classmethod
def execute(cls, positive, negative, vae, width, height, length, batch_size, ref_images,
mode="NORMAL", ref_positions="", ref_strength_high=0.8, end_frame_strength_high=1.0,
ref_strength_low=0.2, start_frame_strength_low=1.0, end_frame_strength_low=1.0,
structural_repulsion_boost=1.0, protect_zone_size=6, time_smoothing_factor=0.2, clip_vision_output=None):
spacial_scale = vae.spacial_compression_encode()
latent_channels = vae.latent_channels
latent_t = ((length - 1) // 4) + 1
device = comfy.model_management.intermediate_device()
latent = torch.zeros([batch_size, latent_channels, latent_t,
height // spacial_scale, width // spacial_scale], device=device)
imgs = cls._resize_images(ref_images, width, height, device)
n_imgs = imgs.shape[0]
positions = cls._parse_positions(ref_positions, n_imgs, length)
def align_position(pos: int, total_frames: int) -> int:
latent_idx = pos // 4
aligned_pos = latent_idx * 4
aligned_pos = max(0, min(aligned_pos, total_frames - 1))
return aligned_pos
aligned_positions = [align_position(int(p), length) for p in positions]
for i in range(1, len(aligned_positions)):
if aligned_positions[i] <= aligned_positions[i-1] + 3:
aligned_positions[i] = min(aligned_positions[i-1] + 4, length - 1)
image = torch.ones((length, height, width, 3), device=device) * 0.5
mask_base = torch.ones((1, 1, latent_t * 4, latent.shape[-2], latent.shape[-1]), device=device)
mask_high_noise = mask_base.clone()
mask_low_noise = mask_base.clone()
for i, pos in enumerate(aligned_positions):
frame_idx = int(pos)
if i == 0:
image[frame_idx:frame_idx + 1] = imgs[i]
mask_high_noise[:, :, frame_idx:frame_idx + 4] = 0.0
low_start_mask_value = 1.0 - start_frame_strength_low
mask_low_noise[:, :, frame_idx:frame_idx + 4] = low_start_mask_value
elif i == n_imgs - 1:
image[-1:] = imgs[i]
mask_high_value = 1.0 - end_frame_strength_high
mask_high_noise[:, :, -4:] = mask_high_value
mask_low_value = 1.0 - end_frame_strength_low
mask_low_noise[:, :, -4:] = mask_low_value
else:
image[frame_idx:frame_idx + 1] = imgs[i]
mask_high_value = 1.0 - ref_strength_high
mask_high_noise[:, :, frame_idx:frame_idx + 4] = mask_high_value
concat_latent_image = vae.encode(image[:, :, :, :3])
concat_latent_image_high = concat_latent_image
if structural_repulsion_boost > 1.001 and length > 4 and n_imgs >= 2:
mask_h, mask_w = mask_high_noise.shape[-2], mask_high_noise.shape[-1]
boost_factor = structural_repulsion_boost - 1.0
def create_spatial_gradient(img1, img2):
if img1 is None or img2 is None:
return None
motion_diff = torch.abs(img2[0] - img1[0]).mean(dim=-1, keepdim=False)
motion_diff_4d = motion_diff.unsqueeze(0).unsqueeze(0)
motion_diff_scaled = F.interpolate(
motion_diff_4d,
size=(mask_h, mask_w),
mode='bilinear',
align_corners=False
)
motion_normalized = (motion_diff_scaled - motion_diff_scaled.min()) / (motion_diff_scaled.max() - motion_diff_scaled.min() + 1e-8)
spatial_gradient = 1.0 - motion_normalized * boost_factor * 2.5
spatial_gradient = torch.clamp(spatial_gradient, 0.02, 1.0)
return spatial_gradient[0, 0]
for i in range(n_imgs - 1):
pos1 = int(aligned_positions[i])
pos2 = int(aligned_positions[i + 1])
if pos2 > pos1 + 4:
start_end = pos1 + 4
ref_frame_start = pos2
transition_end = pos2
img1 = imgs[i:i+1].to(device)
img2 = imgs[i+1:i+2].to(device)
spatial_gradient = create_spatial_gradient(img1, img2)
if spatial_gradient is not None and transition_end > start_end:
transition_length = transition_end - start_end
for frame_idx in range(start_end, transition_end):
if frame_idx < ref_frame_start:
current_mask = mask_high_noise[:, :, frame_idx, :, :]
distance_to_start = (frame_idx - start_end) / max(1.0, transition_length)
distance_to_ref = abs(frame_idx - ref_frame_start) / max(1.0, protect_zone_size)
time_weight = 1.0 - distance_to_start * time_smoothing_factor
protect_weight = max(0.3, 1.0 - distance_to_ref) if frame_idx < ref_frame_start else 1.0
combined_weight = time_weight * protect_weight
adjusted_gradient = 1.0 - (1.0 - spatial_gradient) * combined_weight
mask_high_noise[:, :, frame_idx, :, :] = current_mask * adjusted_gradient
image_low_only = torch.ones((length, height, width, 3), device=device) * 0.5
for i, pos in enumerate(aligned_positions):
frame_idx = int(pos)
if i == 0:
if start_frame_strength_low > 0.0:
image_low_only[frame_idx:frame_idx + 1] = imgs[i]
elif i == n_imgs - 1:
if end_frame_strength_low > 0.0:
image_low_only[-1:] = imgs[i]
else:
if ref_strength_low > 0.0:
image_low_only[frame_idx:frame_idx + 1] = imgs[i]
concat_latent_image_low = vae.encode(image_low_only[:, :, :, :3])
# Print low noise mask distribution for debugging
if n_imgs >= 2:
print("\n=== Low Noise Mask Distribution ===")
print(f"Total frames: {length}, Reference frames: {n_imgs}")
print(f"Parameters: start_strength_low={start_frame_strength_low}, ref_strength_low={ref_strength_low}, end_strength_low={end_frame_strength_low}")
print("\nMask values at reference frame positions:")
for i, pos in enumerate(aligned_positions):
frame_idx = int(pos)
mask_samples = []
sample_frames = [max(0, frame_idx - 2), frame_idx, min(length - 1, frame_idx + 2), min(length - 1, frame_idx + 4)]
for sf in sample_frames:
if 0 <= sf < length:
mask_val = mask_low_noise[:, :, sf, :, :].mean().item()
mask_samples.append((sf, mask_val))
frame_type = "首帧" if i == 0 else ("末帧" if i == n_imgs - 1 else f"参考帧{i+1}")
print(f" {frame_type} (位置 {frame_idx}):")
for sf, mv in mask_samples:
print(f" Frame {sf}: mask = {mv:.3f}")
# Check for abrupt mask changes
print("\n检查参考帧之间的mask变化:")
for i in range(n_imgs - 1):
pos1 = int(aligned_positions[i])
pos2 = int(aligned_positions[i + 1])
# Check mask at boundaries
if pos1 + 4 < length:
mask_before = mask_low_noise[:, :, pos1 + 4, :, :].mean().item()
else:
mask_before = mask_low_noise[:, :, length - 1, :, :].mean().item()
if pos2 - 1 >= 0:
mask_after = mask_low_noise[:, :, pos2 - 1, :, :].mean().item()
else:
mask_after = mask_low_noise[:, :, 0, :, :].mean().item()
mask_at_ref = mask_low_noise[:, :, pos2, :, :].mean().item()
change = abs(mask_after - mask_before)
print(f" 参考帧{i+1} -> 参考帧{i+2}: frame {pos1+4} mask={mask_before:.3f}, frame {pos2-1} mask={mask_after:.3f}, 参考帧位置 mask={mask_at_ref:.3f}, 变化={change:.3f}")
if change > 0.2:
print(f" ⚠️ 警告: mask变化较大 ({change:.3f}),可能导致闪烁!")
print("=" * 40 + "\n")
mask_high_reshaped = mask_high_noise.view(1, mask_high_noise.shape[2] // 4, 4, mask_high_noise.shape[3], mask_high_noise.shape[4]).transpose(1, 2)
mask_low_reshaped = mask_low_noise.view(1, mask_low_noise.shape[2] // 4, 4, mask_low_noise.shape[3], mask_low_noise.shape[4]).transpose(1, 2)
positive_high_noise = node_helpers.conditioning_set_values(positive, {
"concat_latent_image": concat_latent_image_high,
"concat_mask": mask_high_reshaped
})
positive_low_noise = node_helpers.conditioning_set_values(positive, {
"concat_latent_image": concat_latent_image_low,
"concat_mask": mask_low_reshaped
})
negative_out = node_helpers.conditioning_set_values(negative, {
"concat_latent_image": concat_latent_image_high,
"concat_mask": mask_high_reshaped
})
if clip_vision_output is not None:
positive_low_noise = node_helpers.conditioning_set_values(positive_low_noise,
{"clip_vision_output": clip_vision_output})
negative_out = node_helpers.conditioning_set_values(negative_out,
{"clip_vision_output": clip_vision_output})
return io.NodeOutput(positive_high_noise, positive_low_noise, negative_out, {"samples": latent})
@classmethod
def _resize_images(cls, images, width, height, device):
images = images.to(device)
x = images.movedim(-1, 1)
x = comfy.utils.common_upscale(x, width, height, "bilinear", "center")
x = x.movedim(1, -1)
if x.shape[-1] == 4:
x = x[..., :3]
return x
@classmethod
def _parse_positions(cls, pos_str, n_imgs, length):
positions = []
s = (pos_str or "").strip()
if s:
try:
if s.startswith("["):
positions = json.loads(s)
else:
positions = [float(x.strip()) for x in s.split(",") if x.strip()]
except Exception:
positions = []
if not positions:
if n_imgs <= 1:
positions = [0]
else:
positions = [i * (length - 1) / (n_imgs - 1) for i in range(n_imgs)]
converted_positions = []
for p in positions:
if 0 <= p < 2.0:
converted_positions.append(int(p * (length - 1)))
else:
converted_positions.append(int(p))
converted_positions = [max(0, min(length - 1, p)) for p in converted_positions]
if len(converted_positions) > n_imgs:
converted_positions = converted_positions[:n_imgs]
elif len(converted_positions) < n_imgs:
converted_positions.extend([converted_positions[-1]] * (n_imgs - len(converted_positions)))
return converted_positions