Refactor VideoContinuationGenerator for clarity

- Replaces confusing 'when_to_start' parameters with a single 'continuation_mode' dropdown.
- New modes ('Generate new content', 'Stitch to existing sequence') are goal-oriented for better UX.
- 'Generate new content' (default) uses control frames from C0 and masks the middle for inpainting.
- 'Stitch to existing sequence' skips overlapping control frames and treats them as known areas.
- This change makes the node's behavior more intuitive and easier to understand.
This commit is contained in:
POM
2025-06-23 11:27:08 +02:00
parent cfd1d8b256
commit 08cd413f0b
3 changed files with 276 additions and 12 deletions
+11 -12
View File
@@ -822,8 +822,7 @@ class VideoContinuationGenerator:
"end_frame": ("IMAGE", {"tooltip": "Optional single frame to place at the end of the continuation video."}), "end_frame": ("IMAGE", {"tooltip": "Optional single frame to place at the end of the continuation video."}),
"control_images": ("IMAGE", {"tooltip": "Optional control images to fill the empty frames."}), "control_images": ("IMAGE", {"tooltip": "Optional control images to fill the empty frames."}),
"inpaint_mask": ("MASK", {"tooltip": "Optional inpaint mask to use for the empty frames, overriding the default mask."}), "inpaint_mask": ("MASK", {"tooltip": "Optional inpaint mask to use for the empty frames, overriding the default mask."}),
"when_to_start_control_frames": (["beginning", "after overlap_frames"], {"default": "after overlap_frames", "tooltip": "If at beginning, control frames won't actually be active until after the context, but after that they'll continue on from after the overlap_frames number. If after overlap_frames, the first control frame will be placed after the context is over."}), "continuation_mode": (["Generate new content", "Stitch to existing sequence"], {"default": "Generate new content", "tooltip": "Choose 'Generate new content' if your control images are only for the new section. Choose 'Stitch to existing sequence' if your control images represent the full, final timeline."}),
"when_to_start_masks": (["beginning", "after overlap_frames"], {"default": "after overlap_frames", "tooltip": "Controls when mask generation begins. If at beginning, masks start from frame 0. If after overlap_frames, masks start after the overlap period to match control frame timing."}),
}, },
} }
@@ -833,7 +832,7 @@ class VideoContinuationGenerator:
CATEGORY = "Steerable-Motion" CATEGORY = "Steerable-Motion"
DESCRIPTION = "Creates a continuation video by placing overlap frames from the end of input video at the start, with optional end frame." DESCRIPTION = "Creates a continuation video by placing overlap frames from the end of input video at the start, with optional end frame."
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, when_to_start_control_frames="after overlap_frames", when_to_start_masks="after overlap_frames"): 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, continuation_mode="Generate new content"):
# 1. Validation and Setup # 1. Validation and Setup
total_output_frames = int(total_output_frames) total_output_frames = int(total_output_frames)
if (total_output_frames - 1) % 4 != 0: if (total_output_frames - 1) % 4 != 0:
@@ -878,11 +877,11 @@ class VideoContinuationGenerator:
if num_middle_frames > 0: if num_middle_frames > 0:
if control_images is not None: if control_images is not None:
log.info(f"Using 'control_images' to fill the {num_middle_frames} middle frames with '{when_to_start_control_frames}' mode.") log.info(f"Using 'control_images' to fill the {num_middle_frames} middle frames with '{continuation_mode}' mode.")
control_images_resized = common_upscale(control_images.movedim(-1, 1), frame_width, frame_height, "lanczos", "disabled").movedim(1, -1) control_images_resized = common_upscale(control_images.movedim(-1, 1), frame_width, frame_height, "lanczos", "disabled").movedim(1, -1)
if when_to_start_control_frames == "beginning": if continuation_mode == "Generate new content":
# Start from the beginning of control_images, regardless of overlap # Use control frames from the beginning of the sequence (C0, C1, C2...)
if control_images_resized.shape[0] < num_middle_frames: if control_images_resized.shape[0] < num_middle_frames:
log.warning(f"Provided 'control_images' have {control_images_resized.shape[0]} frames, less than needed ({num_middle_frames}). Padding with 'empty_frame_fill_level'.") log.warning(f"Provided 'control_images' have {control_images_resized.shape[0]} frames, less than needed ({num_middle_frames}). Padding with 'empty_frame_fill_level'.")
padding_needed = num_middle_frames - control_images_resized.shape[0] padding_needed = num_middle_frames - control_images_resized.shape[0]
@@ -890,12 +889,12 @@ class VideoContinuationGenerator:
middle_frames_part = torch.cat([control_images_resized, padding], dim=0) middle_frames_part = torch.cat([control_images_resized, padding], dim=0)
else: else:
middle_frames_part = control_images_resized[:num_middle_frames].clone() middle_frames_part = control_images_resized[:num_middle_frames].clone()
else: # "after overlap_frames" else: # "Stitch to existing sequence"
# Skip potential duplicate frames that overlap with the start section # Skip the first overlap_frames control images to avoid duplication
duplicate_count = min(actual_overlap_frames, control_images_resized.shape[0]) duplicate_count = min(actual_overlap_frames, control_images_resized.shape[0])
available_after_dup = control_images_resized.shape[0] - duplicate_count available_after_dup = control_images_resized.shape[0] - duplicate_count
if available_after_dup < num_middle_frames: if available_after_dup < num_middle_frames:
log.info(f"After removing {duplicate_count} overlapping frames, only {available_after_dup} control frames remain; padding {num_middle_frames - available_after_dup} frames with 'empty_frame_fill_level'.") log.info(f"After skipping {duplicate_count} control frames, only {available_after_dup} remain; padding {num_middle_frames - available_after_dup} frames with 'empty_frame_fill_level'.")
selected_control = control_images_resized[duplicate_count:] selected_control = control_images_resized[duplicate_count:]
padding_needed = num_middle_frames - selected_control.shape[0] padding_needed = num_middle_frames - selected_control.shape[0]
padding = torch.ones((padding_needed, frame_height, frame_width, num_channels), device=device, dtype=dtype) * empty_frame_fill_level padding = torch.ones((padding_needed, frame_height, frame_width, num_channels), device=device, dtype=dtype) * empty_frame_fill_level
@@ -912,14 +911,14 @@ class VideoContinuationGenerator:
# 6. Create Mask # 6. Create Mask
continuation_frame_masks = torch.ones((total_output_frames, frame_height, frame_width), device=device, dtype=dtype) continuation_frame_masks = torch.ones((total_output_frames, frame_height, frame_width), device=device, dtype=dtype)
# Apply mask logic based on when_to_start_masks parameter # Apply mask logic based on continuation_mode parameter
if when_to_start_masks == "beginning": if continuation_mode == "Generate new content":
# Set known frames (overlap and end) to 0.0, rest stay as 1.0 (inpaint) # Set known frames (overlap and end) to 0.0, rest stay as 1.0 (inpaint)
if actual_overlap_frames > 0: if actual_overlap_frames > 0:
continuation_frame_masks[0:actual_overlap_frames] = 0.0 continuation_frame_masks[0:actual_overlap_frames] = 0.0
if num_end_frames > 0: if num_end_frames > 0:
continuation_frame_masks[-num_end_frames:] = 0.0 continuation_frame_masks[-num_end_frames:] = 0.0
else: # "after overlap_frames" else: # "Stitch to existing sequence"
# Set known frames (overlap and end) to 0.0, but also set middle section based on control frame logic # Set known frames (overlap and end) to 0.0, but also set middle section based on control frame logic
if actual_overlap_frames > 0: if actual_overlap_frames > 0:
continuation_frame_masks[0:actual_overlap_frames] = 0.0 continuation_frame_masks[0:actual_overlap_frames] = 0.0
Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

+265
View File
@@ -0,0 +1,265 @@
import matplotlib.pyplot as plt
import numpy as np
def simulate_video_continuation_comprehensive(input_frames, control_frames, total_output_frames, overlap_frames,
when_to_start_control_frames, when_to_start_masks, end_frame=None):
"""
Comprehensive simulation of VideoContinuationGenerator logic
"""
print(f"\n=== Simulation: control='{when_to_start_control_frames}', masks='{when_to_start_masks}' ===")
# Step 1: Calculate actual overlap frames
actual_overlap_frames = min(overlap_frames, len(input_frames), total_output_frames)
# Step 2: Prepare start frames (from overlap)
overlap_start_idx = len(input_frames) - actual_overlap_frames
start_frames = input_frames[overlap_start_idx:overlap_start_idx + actual_overlap_frames]
# Step 3: Prepare end frame
num_end_frames = 1 if end_frame is not None and total_output_frames > actual_overlap_frames else 0
end_frames = [end_frame] if num_end_frames > 0 else []
# Step 4: Calculate middle frames needed
num_middle_frames = total_output_frames - actual_overlap_frames - num_end_frames
# Step 5: Fill middle frames based on control frame mode
middle_frames = []
control_frame_info = ""
if num_middle_frames > 0 and control_frames:
if when_to_start_control_frames == "beginning":
# Use control frames from the beginning of the sequence (C0, C1, C2...)
if len(control_frames) < num_middle_frames:
middle_frames = control_frames + ['EMPTY'] * (num_middle_frames - len(control_frames))
control_frame_info = f"Using first {len(control_frames)} control frames + {num_middle_frames - len(control_frames)} empty"
else:
middle_frames = control_frames[:num_middle_frames]
control_frame_info = f"Using first {num_middle_frames} control frames (C0-C{num_middle_frames-1})"
else: # "after overlap_frames"
# Skip the first overlap_frames control images to avoid duplication
duplicate_count = min(actual_overlap_frames, len(control_frames))
available_after_dup = len(control_frames) - duplicate_count
if available_after_dup < num_middle_frames:
selected_control = control_frames[duplicate_count:]
padding_needed = num_middle_frames - len(selected_control)
middle_frames = selected_control + ['EMPTY'] * padding_needed
control_frame_info = f"Skipped first {duplicate_count} control frames, using C{duplicate_count}-C{duplicate_count + len(selected_control) - 1} + {padding_needed} empty"
else:
middle_frames = control_frames[duplicate_count:duplicate_count + num_middle_frames]
control_frame_info = f"Skipped first {duplicate_count} control frames, using C{duplicate_count}-C{duplicate_count + num_middle_frames - 1}"
else:
middle_frames = ['EMPTY'] * num_middle_frames
control_frame_info = "No control frames, all empty"
# Step 6: Create masks based on mask mode
masks = [1.0] * total_output_frames # 1.0 = inpaint, 0.0 = known
if when_to_start_masks == "beginning":
# Set known frames (overlap and end) to 0.0, rest stay as 1.0 (inpaint)
for i in range(actual_overlap_frames):
masks[i] = 0.0 # overlap frames are known
for i in range(total_output_frames - num_end_frames, total_output_frames):
masks[i] = 0.0 # end frames are known
mask_info = "Standard: overlap and end frames known, middle frames inpaint"
else: # "after overlap_frames"
# Follow control frame logic
for i in range(actual_overlap_frames):
masks[i] = 0.0 # overlap frames are known
for i in range(total_output_frames - num_end_frames, total_output_frames):
masks[i] = 0.0 # end frames are known
# For middle section, follow the same logic as control frames
if control_frames and num_middle_frames > 0:
duplicate_count = min(actual_overlap_frames, len(control_frames))
available_after_dup = len(control_frames) - duplicate_count
if available_after_dup >= num_middle_frames:
# If we have enough control frames after skipping, set those middle frames as known (0.0)
middle_start = actual_overlap_frames
middle_end = middle_start + num_middle_frames
for i in range(middle_start, middle_end):
masks[i] = 0.0
mask_info = "Follows control logic: overlap, control-covered middle, and end frames known"
else:
mask_info = "Follows control logic: overlap and end frames known, partial middle coverage"
else:
mask_info = "No control frames: only overlap and end frames known"
# Step 7: Assemble final video
final_video = start_frames + middle_frames + end_frames
print(f"Control: {control_frame_info}")
print(f"Masks: {mask_info}")
print(f"Final video: {final_video}")
print(f"Masks: {['INPAINT' if m > 0.5 else 'KNOWN' for m in masks]}")
return {
'start_frames': start_frames,
'middle_frames': middle_frames,
'end_frames': end_frames,
'final_video': final_video,
'masks': masks,
'actual_overlap_frames': actual_overlap_frames,
'num_middle_frames': num_middle_frames,
'num_end_frames': num_end_frames,
'control_frame_info': control_frame_info,
'mask_info': mask_info
}
def visualize_comprehensive_simulation(results_list, scenario_names):
"""
Create a comprehensive visual representation showing both frames and masks
"""
fig, axes = plt.subplots(len(results_list), 2, figsize=(20, 4 * len(results_list)))
if len(results_list) == 1:
axes = axes.reshape(1, -1)
colors = {
'INPUT': '#87CEEB', # Sky blue
'CONTROL': '#98FB98', # Pale green
'END': '#FFA500', # Orange
'EMPTY': '#D3D3D3' # Light gray
}
mask_colors = {
'KNOWN': '#4169E1', # Royal blue
'INPAINT': '#FF6347' # Tomato red
}
for i, (results, scenario_name) in enumerate(zip(results_list, scenario_names)):
# Plot frames (left column)
ax_frames = axes[i, 0]
final_video = results['final_video']
frame_colors = []
frame_labels = []
for frame in final_video:
if isinstance(frame, str):
if frame == 'EMPTY':
frame_colors.append(colors['EMPTY'])
frame_labels.append('EMPTY')
elif frame == 'END':
frame_colors.append(colors['END'])
frame_labels.append('END')
else:
frame_colors.append(colors['CONTROL'])
frame_labels.append(frame)
else:
# Input frame (number)
frame_colors.append(colors['INPUT'])
frame_labels.append(f'I{frame}')
x_positions = range(len(final_video))
bars_frames = ax_frames.bar(x_positions, [1] * len(final_video),
color=frame_colors, edgecolor='black', linewidth=0.5)
# Add frame labels
for j, (bar, label) in enumerate(zip(bars_frames, frame_labels)):
ax_frames.text(bar.get_x() + bar.get_width()/2, bar.get_height()/2,
label, ha='center', va='center', fontsize=8, rotation=90)
ax_frames.set_title(f'{scenario_name}\nFrames: {results["control_frame_info"]}')
ax_frames.set_ylabel('Frame Content')
ax_frames.set_xlabel('Frame Position')
ax_frames.set_ylim(0, 1.2)
ax_frames.set_xticks(x_positions)
ax_frames.set_xticklabels([str(i) for i in x_positions])
# Plot masks (right column)
ax_masks = axes[i, 1]
masks = results['masks']
mask_color_list = [mask_colors['KNOWN'] if m < 0.5 else mask_colors['INPAINT'] for m in masks]
mask_label_list = ['KNOWN' if m < 0.5 else 'INPAINT' for m in masks]
bars_masks = ax_masks.bar(x_positions, [1] * len(masks),
color=mask_color_list, edgecolor='black', linewidth=0.5)
# Add mask labels
for j, (bar, label) in enumerate(zip(bars_masks, mask_label_list)):
ax_masks.text(bar.get_x() + bar.get_width()/2, bar.get_height()/2,
label, ha='center', va='center', fontsize=8, rotation=90)
ax_masks.set_title(f'Masks: {results["mask_info"]}')
ax_masks.set_ylabel('Mask Type')
ax_masks.set_xlabel('Frame Position')
ax_masks.set_ylim(0, 1.2)
ax_masks.set_xticks(x_positions)
ax_masks.set_xticklabels([str(i) for i in x_positions])
# Add legends
if i == 0: # Only add legend to first row
# Frame legend
frame_legend_elements = []
for frame_type, color in colors.items():
frame_legend_elements.append(plt.Rectangle((0,0),1,1, facecolor=color, label=frame_type))
ax_frames.legend(handles=frame_legend_elements, loc='upper right', bbox_to_anchor=(1.15, 1))
# Mask legend
mask_legend_elements = []
for mask_type, color in mask_colors.items():
mask_legend_elements.append(plt.Rectangle((0,0),1,1, facecolor=color, label=mask_type))
ax_masks.legend(handles=mask_legend_elements, loc='upper right', bbox_to_anchor=(1.15, 1))
plt.tight_layout()
plt.savefig('video_continuation_comprehensive_simulation.png', dpi=150, bbox_inches='tight')
plt.show()
def run_comprehensive_simulation():
# Test setup
input_frames = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 10 input frames
control_frames = [f'C{i}' for i in range(20)] # 20 control frames (enough for all scenarios)
total_output_frames = 17 # (17-1) % 4 == 0
overlap_frames = 3
end_frame = 'END'
print("=" * 80)
print("VIDEO CONTINUATION GENERATOR COMPREHENSIVE SIMULATION")
print("=" * 80)
print(f"Input frames: {input_frames}")
print(f"Control frames: {control_frames}")
print(f"Total output frames: {total_output_frames}")
print(f"Overlap frames: {overlap_frames}")
print(f"End frame: {end_frame}")
print(f"Middle frames needed: {total_output_frames - overlap_frames - 1} = {total_output_frames - overlap_frames - 1}")
# Test all combinations
scenarios = [
("beginning", "beginning", "Control: Beginning, Masks: Beginning"),
("beginning", "after overlap_frames", "Control: Beginning, Masks: After Overlap"),
("after overlap_frames", "beginning", "Control: After Overlap, Masks: Beginning"),
("after overlap_frames", "after overlap_frames", "Control: After Overlap, Masks: After Overlap"),
]
results_list = []
scenario_names = []
for control_mode, mask_mode, display_name in scenarios:
result = simulate_video_continuation_comprehensive(
input_frames, control_frames, total_output_frames,
overlap_frames, control_mode, mask_mode, end_frame
)
results_list.append(result)
scenario_names.append(display_name)
# Additional test: Insufficient control frames scenario
print("\n" + "="*50)
print("INSUFFICIENT CONTROL FRAMES TEST")
print("="*50)
short_control_frames = ['C0', 'C1', 'C2', 'C3', 'C4'] # Only 5 control frames
print(f"Short control frames: {short_control_frames}")
for control_mode, mask_mode, display_name in scenarios:
result = simulate_video_continuation_comprehensive(
input_frames, short_control_frames, total_output_frames,
overlap_frames, control_mode, mask_mode, end_frame
)
results_list.append(result)
scenario_names.append(f"{display_name} (Short Control)")
visualize_comprehensive_simulation(results_list, scenario_names)
if __name__ == "__main__":
run_comprehensive_simulation()