| 809 | return (guidance_video_output.cpu().float(), final_frame_masks.cpu().float()) |
| 810 | |
| 811 | class VideoContinuationGenerator: |
| 812 | @classmethod |
| 813 | def INPUT_TYPES(s): |
| 814 | return { |
| 815 | "required": { |
| 816 | "input_video_frames": ("IMAGE", {"tooltip": "Input video frames to create continuation from."}), |
| 817 | "total_output_frames": ("INT", {"default": 81, "min": 1, "max": 10000, "step": 4, "tooltip": "Total number of frames for the output continuation video. Must satisfy: (frames - 1) divisible by 4."}), |
| 818 | "overlap_frames": ("INT", {"default": 3, "min": 1, "max": 50, "step": 1, "tooltip": "Number of frames from the end of input video to use as overlap at the start."}), |
| 819 | "empty_frame_fill_level": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Grayscale level (0.0 black, 1.0 white) for empty continuation frames."}), |
| 820 | }, |
| 821 | "optional": { |
| 822 | "end_frame": ("IMAGE", {"tooltip": "Optional single frame to place at the end of the continuation video."}), |
| 823 | "control_images": ("IMAGE", {"tooltip": "Optional control images to fill the empty frames."}), |
| 824 | "inpaint_mask": ("MASK", {"tooltip": "Optional inpaint mask to use for the empty frames, overriding the default mask."}), |
| 825 | "how_to_use_control_images": (["start_sequence_at_beginning_and_prioritise_input_frames", "start_sequence_after_overlap_frames_and_prioritise_input_frames"], {"default": "start_sequence_at_beginning_and_prioritise_input_frames", "tooltip": "If start_sequence_at_beginning_and_prioritise_input_frames is selected, control images align with frame 0 but input overlap frames take priority, so control images become visible after the overlap period. If start_sequence_after_overlap_frames_and_prioritise_input_frames is selected, control images start being placed after the overlap frames from the input video."}), |
| 826 | "how_to_use_inpaint_masks": (["start_sequence_at_beginning_and_prioritise_input_frames", "start_sequence_after_overlap_frames_and_prioritise_input_frames"], {"default": "start_sequence_at_beginning_and_prioritise_input_frames", "tooltip": "If start_sequence_at_beginning_and_prioritise_input_frames is selected, inpaint masks align with frame 0 but preserve input overlap frames as known. If start_sequence_after_overlap_frames_and_prioritise_input_frames is selected, inpaint masks only affect frames after the overlap period."}), |
| 827 | }, |
| 828 | } |
| 829 | |
| 830 | RETURN_TYPES = ("IMAGE", "MASK",) |
| 831 | RETURN_NAMES = ("continuation_video_frames", "continuation_frame_masks",) |
| 832 | FUNCTION = "generate_continuation_video" |
| 833 | CATEGORY = "Steerable-Motion" |
| 834 | DESCRIPTION = "Creates a continuation video by placing overlap frames from the end of input video at the start, with optional end frame." |
| 835 | |
| 836 | def generate_continuation_video(self, input_video_frames, total_output_frames, overlap_frames, empty_frame_fill_level, end_frame=None, control_images=None, inpaint_mask=None, how_to_use_control_images="start_sequence_at_beginning_and_prioritise_input_frames", how_to_use_inpaint_masks="start_sequence_at_beginning_and_prioritise_input_frames"): |
| 837 | # 1. Validation and Setup |
| 838 | total_output_frames = int(total_output_frames) |
| 839 | if (total_output_frames - 1) % 4 != 0: |
| 840 | raise ValueError("total_output_frames must satisfy (frames - 1) divisible by 4") |
| 841 | |
| 842 | if input_video_frames is None or input_video_frames.shape[0] == 0: |
| 843 | log.error("Input video_frames is empty. Cannot proceed.") |
| 844 | dummy_height, dummy_width, dummy_channels = 64, 64, 3 |
| 845 | return (torch.zeros((total_output_frames, dummy_height, dummy_width, dummy_channels), dtype=torch.float32), |
| 846 | torch.ones((total_output_frames, dummy_height, dummy_width), dtype=torch.float32)) |
| 847 | |
| 848 | device = input_video_frames.device |
| 849 | dtype = input_video_frames.dtype |
| 850 | batch_size_input, frame_height, frame_width, num_channels = input_video_frames.shape |
| 851 | |
| 852 | # 2. Prepare Start Frames (from overlap) |
| 853 | actual_overlap_frames = min(overlap_frames, batch_size_input, total_output_frames) |
| 854 | if actual_overlap_frames < overlap_frames: |
| 855 | log.warning(f"Requested {overlap_frames} overlap frames but input video only has {batch_size_input} frames or total output is smaller. Using {actual_overlap_frames} instead.") |
| 856 | |
| 857 | overlap_start_idx = batch_size_input - actual_overlap_frames |
| 858 | start_frames_part = input_video_frames[overlap_start_idx : overlap_start_idx + actual_overlap_frames].clone() |
| 859 | |
| 860 | # 3. Prepare End Frame |
| 861 | end_frame_part = torch.empty((0, frame_height, frame_width, num_channels), device=device, dtype=dtype) |
| 862 | num_end_frames = 0 |
| 863 | if end_frame is not None and end_frame.shape[0] > 0 and total_output_frames > actual_overlap_frames: |
| 864 | num_end_frames = 1 |
| 865 | end_frame_processed = end_frame[0].clone().to(device=device, dtype=dtype) |
| 866 | |
| 867 | if end_frame_processed.shape != (frame_height, frame_width, num_channels): |
| 868 | log.info(f"Resizing end_frame from {end_frame_processed.shape} to {(frame_height, frame_width, num_channels)}.") |
nothing calls this directly
no outgoing calls
no test coverage detected