| 992 | return (frame_number,) |
| 993 | |
| 994 | class WanVideoBlenderNode: |
| 995 | @classmethod |
| 996 | def INPUT_TYPES(s): |
| 997 | return { |
| 998 | "required": { |
| 999 | "overlap_frames": ("INT", {"default": 10, "min": 1, "max": 1000, "step": 1}), |
| 1000 | "video_1": ("IMAGE",), |
| 1001 | "video_2": ("IMAGE",), |
| 1002 | }, |
| 1003 | } |
| 1004 | |
| 1005 | RETURN_TYPES = ("IMAGE",) |
| 1006 | RETURN_NAMES = ("blended_video_frames",) |
| 1007 | FUNCTION = "blend_videos" |
| 1008 | CATEGORY = "Steerable-Motion" |
| 1009 | DESCRIPTION = "Blends two input videos with a cross-fade. The resolution of the second clip is resized to match the first." |
| 1010 | |
| 1011 | def _resize_video(self, video, target_height, target_width): |
| 1012 | """Resize a batch of frames (B,H,W,C) to (target_height,target_width) using Lanczos.""" |
| 1013 | if video.shape[1] == target_height and video.shape[2] == target_width: |
| 1014 | return video |
| 1015 | # (B, H, W, C) -> (B, C, H, W) |
| 1016 | video_permuted = video.permute(0, 3, 1, 2) |
| 1017 | resized = common_upscale(video_permuted, target_width, target_height, "lanczos", "disabled") # (B, C, H, W) |
| 1018 | return resized.permute(0, 2, 3, 1) |
| 1019 | |
| 1020 | def _cross_fade(self, tail, head, overlap_frames): |
| 1021 | """Blend two tensors of shape (overlap_frames,H,W,C) using linear alpha.""" |
| 1022 | device, dtype = tail.device, tail.dtype |
| 1023 | alphas = torch.linspace(0, 1, overlap_frames, device=device, dtype=dtype).view(-1, 1, 1, 1) |
| 1024 | blended = tail * (1 - alphas) + head * alphas |
| 1025 | return blended |
| 1026 | |
| 1027 | def blend_videos(self, overlap_frames, video_1, video_2): |
| 1028 | if video_1 is None or video_2 is None: |
| 1029 | raise ValueError("Both video_1 and video_2 are required.") |
| 1030 | |
| 1031 | # Reference dimensions and properties from first video |
| 1032 | ref_h, ref_w = video_1.shape[1:3] |
| 1033 | |
| 1034 | # Ensure second video matches size |
| 1035 | video_2_resized = self._resize_video(video_2, ref_h, ref_w) |
| 1036 | |
| 1037 | if video_1.shape[0] < overlap_frames or video_2_resized.shape[0] < overlap_frames: |
| 1038 | raise ValueError(f"One of the videos is shorter than overlap_frames={overlap_frames}.") |
| 1039 | |
| 1040 | # Extract segments for blending |
| 1041 | tail = video_1[-overlap_frames:] |
| 1042 | head = video_2_resized[:overlap_frames] |
| 1043 | blended = self._cross_fade(tail, head, overlap_frames) |
| 1044 | |
| 1045 | # Assemble new timeline |
| 1046 | final_video = torch.cat([ |
| 1047 | video_1[:-overlap_frames], |
| 1048 | blended, |
| 1049 | video_2_resized[overlap_frames:] |
| 1050 | ], dim=0) |
| 1051 |
nothing calls this directly
no outgoing calls
no test coverage detected