Enhance VideoContinuationGenerator: Add instead_of_input_frames option and improve parameter naming

- Add 'instead_of_input_frames' option to both when_to_start_control_images and when_to_start_inpaint_masks
- Rename parameters for clarity: when_to_start_control_frames → when_to_start_control_images, when_to_start_masks → when_to_start_inpaint_masks
- Enable full control replacement: control images can now replace overlap frames + middle frames + end frame
- Improve inpaint mask logic: when_to_start_inpaint_masks now affects custom masks properly
- Add comprehensive logic for insufficient control images with graceful fallbacks
- Maintain backward compatibility while adding powerful new functionality
This commit is contained in:
POM
2025-06-23 15:58:54 +02:00
parent 8e2a9a8130
commit 76264adfc7
2 changed files with 58 additions and 276 deletions
+58 -11
View File
@@ -822,8 +822,8 @@ class VideoContinuationGenerator:
"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."}),
"inpaint_mask": ("MASK", {"tooltip": "Optional inpaint mask to use for the empty frames, overriding the default mask."}),
"when_to_start_control_frames": (["beginning_of_generation", "after_overlap_frames"], {"default": "beginning_of_generation", "tooltip": "If beginning_of_generation is selected, aligns control frames with the start of the output (frame 0). Overlap frames from the input video will take priority, so control frames will become visible starting after the overlap period. If after_overlap_frames is selected, control frames will start being placed after the overlap frames from the input video."}),
"when_to_start_masks": (["beginning_of_generation", "after_overlap_frames"], {"default": "beginning_of_generation", "tooltip": "If beginning_of_generation is selected, aligns masks with the start of the output (frame 0). If after_overlap_frames is selected, masks will start being placed after the overlap frames from the input video."}),
"when_to_start_control_images": (["beginning_of_generation", "after_overlap_frames", "instead_of_input_frames"], {"default": "beginning_of_generation", "tooltip": "If beginning_of_generation is selected, aligns control images with the start of the output (frame 0). Overlap frames from the input video will take priority, so control images will become visible starting after the overlap period. If after_overlap_frames is selected, control images will start being placed after the overlap frames from the input video. If instead_of_input_frames is selected, control images will replace the overlap frames entirely."}),
"when_to_start_inpaint_masks": (["beginning_of_generation", "after_overlap_frames", "instead_of_input_frames"], {"default": "beginning_of_generation", "tooltip": "If beginning_of_generation is selected, aligns inpaint masks with the start of the output (frame 0). If after_overlap_frames is selected, inpaint masks will start being placed after the overlap frames from the input video. If instead_of_input_frames is selected, inpaint masks will replace the overlap frames entirely."}),
},
}
@@ -833,7 +833,7 @@ class VideoContinuationGenerator:
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."
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="beginning_of_generation", when_to_start_masks="beginning_of_generation"):
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_images="beginning_of_generation", when_to_start_inpaint_masks="beginning_of_generation"):
# 1. Validation and Setup
total_output_frames = int(total_output_frames)
if (total_output_frames - 1) % 4 != 0:
@@ -878,10 +878,10 @@ class VideoContinuationGenerator:
if num_middle_frames > 0:
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 '{when_to_start_control_images}' mode.")
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_of_generation":
if when_to_start_control_images == "beginning_of_generation":
# Skip the first overlap_frames control images to avoid duplication
duplicate_count = min(actual_overlap_frames, control_images_resized.shape[0])
available_after_dup = control_images_resized.shape[0] - duplicate_count
@@ -893,7 +893,7 @@ class VideoContinuationGenerator:
middle_frames_part = torch.cat([selected_control, padding], dim=0)
else:
middle_frames_part = control_images_resized[duplicate_count:duplicate_count + num_middle_frames].clone()
else: # "after_overlap_frames"
elif when_to_start_control_images == "after_overlap_frames":
# Use control frames from the beginning of the sequence (C0, C1, C2...)
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'.")
@@ -902,6 +902,21 @@ class VideoContinuationGenerator:
middle_frames_part = torch.cat([control_images_resized, padding], dim=0)
else:
middle_frames_part = control_images_resized[:num_middle_frames].clone()
else: # "instead_of_input_frames"
# Replace overlap frames with control images, use remaining for middle frames and potentially end frame
total_frames_needed = actual_overlap_frames + num_middle_frames + num_end_frames
if control_images_resized.shape[0] < total_frames_needed:
log.warning(f"Provided 'control_images' have {control_images_resized.shape[0]} frames, less than needed ({total_frames_needed} = {actual_overlap_frames} overlap + {num_middle_frames} middle + {num_end_frames} end). Padding with 'empty_frame_fill_level'.")
padding_needed = total_frames_needed - control_images_resized.shape[0]
padding = torch.ones((padding_needed, frame_height, frame_width, num_channels), device=device, dtype=dtype) * empty_frame_fill_level
extended_control = torch.cat([control_images_resized, padding], dim=0)
else:
extended_control = control_images_resized[:total_frames_needed].clone()
# Split into overlap replacement, middle parts, and end frame replacement
start_frames_part = extended_control[:actual_overlap_frames]
middle_frames_part = extended_control[actual_overlap_frames:actual_overlap_frames + num_middle_frames]
if num_end_frames > 0:
end_frame_part = extended_control[actual_overlap_frames + num_middle_frames:actual_overlap_frames + num_middle_frames + num_end_frames]
else:
log.info(f"No 'control_images', filling {num_middle_frames} middle frames with level {empty_frame_fill_level}.")
middle_frames_part = torch.ones((num_middle_frames, frame_height, frame_width, num_channels), device=device, dtype=dtype) * empty_frame_fill_level
@@ -912,8 +927,8 @@ class VideoContinuationGenerator:
# 6. Create Mask
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
if when_to_start_masks == "beginning_of_generation":
# Apply mask logic based on when_to_start_inpaint_masks parameter
if when_to_start_inpaint_masks == "beginning_of_generation":
# Set known frames (overlap and end) to 0.0, but also set middle section based on control frame logic
if actual_overlap_frames > 0:
continuation_frame_masks[0:actual_overlap_frames] = 0.0
@@ -929,16 +944,29 @@ class VideoContinuationGenerator:
middle_start = actual_overlap_frames
middle_end = middle_start + num_middle_frames
continuation_frame_masks[middle_start:middle_end] = 0.0
else: # "after_overlap_frames"
elif when_to_start_inpaint_masks == "after_overlap_frames":
# Set known frames (overlap and end) to 0.0, rest stay as 1.0 (inpaint)
if actual_overlap_frames > 0:
continuation_frame_masks[0:actual_overlap_frames] = 0.0
if num_end_frames > 0:
continuation_frame_masks[-num_end_frames:] = 0.0
else: # "instead_of_input_frames"
# All frames become inpaint areas (1.0) by default when replacing input frames entirely
# If control images replace all frames (overlap + middle + end), those areas can be set as known
if control_images is not None and when_to_start_control_images == "instead_of_input_frames":
total_frames_needed = actual_overlap_frames + num_middle_frames + num_end_frames
if control_images.shape[0] >= total_frames_needed:
# Set all sections as known since we have enough control images to replace everything
continuation_frame_masks[:] = 0.0
elif control_images.shape[0] >= actual_overlap_frames + num_middle_frames:
# Set middle section as known since we have enough control images for overlap + middle
middle_start = actual_overlap_frames
middle_end = middle_start + num_middle_frames
continuation_frame_masks[middle_start:middle_end] = 0.0
# 7. Handle optional inpaint_mask override
# 7. Handle optional inpaint_mask with when_to_start_inpaint_masks logic
if inpaint_mask is not None:
log.info("Processing provided 'inpaint_mask', which will override the automatically generated mask.")
log.info(f"Processing provided 'inpaint_mask' with '{when_to_start_inpaint_masks}' timing.")
processed_mask = common_upscale(inpaint_mask.unsqueeze(1), frame_width, frame_height, "nearest-exact", "disabled").squeeze(1).to(device)
if processed_mask.shape[0] != total_output_frames:
@@ -949,6 +977,25 @@ class VideoContinuationGenerator:
else:
processed_mask = processed_mask[:total_output_frames]
# Apply when_to_start_inpaint_masks logic to the provided mask
if when_to_start_inpaint_masks == "beginning_of_generation":
# Use the provided mask as-is, but preserve known frames (overlap and end)
if actual_overlap_frames > 0:
processed_mask[0:actual_overlap_frames] = 0.0 # Keep overlap frames as known
if num_end_frames > 0:
processed_mask[-num_end_frames:] = 0.0 # Keep end frame as known
elif when_to_start_inpaint_masks == "after_overlap_frames":
# Only apply the provided mask after overlap frames
if actual_overlap_frames > 0:
processed_mask[0:actual_overlap_frames] = 0.0 # Keep overlap frames as known
# The provided mask affects frames starting after overlap
if num_end_frames > 0:
processed_mask[-num_end_frames:] = 0.0 # Keep end frame as known
else: # "instead_of_input_frames"
# Use the provided mask starting from frame 0, affecting the entire timeline
# No frames are preserved as "known" - the provided mask has full control
pass # The provided mask affects the entire timeline including overlap frames and end frame
continuation_frame_masks = processed_mask.to(dtype=dtype)
log.info(f"Generated continuation video. Start: {actual_overlap_frames} frames, Middle: {num_middle_frames} frames, End: {num_end_frames} frames.")
-265
View File
@@ -1,265 +0,0 @@
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()