Optional drag and drop + AB overlap

This commit is contained in:
JJ
2026-04-04 21:47:27 +01:00
parent f7e498ef9e
commit 205df36ccf
2 changed files with 83 additions and 16 deletions
+41 -16
View File
@@ -331,7 +331,8 @@ class VideoAudioMergeAB:
"required": {
"image_A": ("IMAGE",),
"image_B": ("IMAGE",),
"FlamesOverlap": ("INT", {"default": 15, "min": 0, "max": 1000, "step": 1}),
"Overlap_A": ("INT", {"default": 24, "min": 0, "max": 1000, "step": 1}),
"Overlap_B": ("INT", {"default": 24, "min": 0, "max": 1000, "step": 1}),
"video_fps": ("FLOAT", {"default": 24.0, "min": 1.0, "max": 120.0, "step": 0.5}),
"easing_clamp": ("FLOAT", {"default": 0.5, "min": -1.0, "max": 2.0, "step": 0.01}),
"easing_type": (["linear", "ease_in", "ease_out", "ease_in_out"],),
@@ -350,7 +351,7 @@ class VideoAudioMergeAB:
FUNCTION = "merge_ab"
CATEGORY = "WanVideoWrapper_QQ/video"
def merge_ab(self, image_A, image_B, FlamesOverlap, video_fps, easing_clamp, easing_type, normalize_volume, b_volume_gain, cut_mode, audio_A=None, audio_B=None):
def merge_ab(self, image_A, image_B, Overlap_A, Overlap_B, video_fps, easing_clamp, easing_type, normalize_volume, b_volume_gain, cut_mode, audio_A=None, audio_B=None):
device = image_A.device
num_a = image_A.shape[0]
num_b = image_B.shape[0]
@@ -363,7 +364,8 @@ class VideoAudioMergeAB:
image_A = self._resize_to_target(image_A, num_a, target_h, target_w, target_c)
image_B = self._resize_to_target(image_B, num_b, target_h, target_w, target_c)
safe_overlap = min(FlamesOverlap, num_a // 2, num_b // 2)
# Calculate safe overlap: use min of both overlaps, clamped to available frames
safe_overlap = min(Overlap_A, Overlap_B, num_a // 2, num_b // 2)
# 2. Video Merge Logic
if cut_mode:
@@ -1349,31 +1351,54 @@ class VideoLoopMove:
"image": ("IMAGE",),
"num_frames": ("INT", {"default": 16, "min": 0, "max": 10000, "step": 1}),
},
"optional": {
"audio": ("AUDIO",),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
RETURN_TYPES = ("IMAGE", "AUDIO")
RETURN_NAMES = ("image", "audio")
FUNCTION = "loop_move"
CATEGORY = "WanVideoWrapper_QQ/video"
def loop_move(self, image, num_frames):
def loop_move(self, image, num_frames, audio=None):
total_frames = image.shape[0]
# Handle edge cases
if num_frames <= 0:
return (image,)
if num_frames >= total_frames:
# If num_frames >= total, just reverse or return as-is
# Let's wrap it: effectively no change needed
return (image,)
result_image = image
elif num_frames >= total_frames:
# If num_frames >= total, just return as-is
result_image = image
else:
# Split: last num_frames go to front, rest follow
moved_part = image[-num_frames:] # Last N frames
remaining_part = image[:-num_frames] # First (total - N) frames
result_image = torch.cat([moved_part, remaining_part], dim=0)
# Split: last num_frames go to front, rest follow
moved_part = image[-num_frames:] # Last N frames
remaining_part = image[:-num_frames] # First (total - N) frames
# Apply same loop move to audio if provided
if audio is not None:
waveform = audio["waveform"]
sample_rate = audio["sample_rate"]
total_samples = waveform.shape[-1]
result = torch.cat([moved_part, remaining_part], dim=0)
# Calculate samples per frame ratio
samples_per_frame = total_samples / total_frames
num_samples_to_move = int(num_frames * samples_per_frame)
return (result,)
if num_samples_to_move > 0 and num_samples_to_move < total_samples:
# Apply same loop move to audio
moved_audio = waveform[..., -num_samples_to_move:]
remaining_audio = waveform[..., :-num_samples_to_move]
result_audio_waveform = torch.cat([moved_audio, remaining_audio], dim=-1)
else:
result_audio_waveform = waveform
result_audio = {"waveform": result_audio_waveform, "sample_rate": sample_rate}
else:
result_audio = audio
return (result_image, result_audio)
NODE_CLASS_MAPPINGS = {
+42
View File
@@ -15,11 +15,39 @@ import { createOnNodeCreatedWrapper } from './node_setup.js';
app.registerExtension({
name: 'PowerLoadVideo',
settings: [
{
id: 'SA-Nodes.overwriteDragAndDrop',
name: 'Overwrite drag and drop',
category: ['SA-Nodes', 'Power Load Video', 'Overwrite drag and drop'],
tooltip: 'When enabled, dragging video files onto the canvas creates Power Load Video nodes instead of default Load Video nodes. Also enables image drag-and-drop to create LoadImage nodes.',
type: 'boolean',
defaultValue: false,
},
],
commands: [
{
id: 'SA-Nodes.ToggleOverwriteDragAndDrop',
label: 'Toggle overwrite drag and drop',
active: () => app.ui.settings.getSettingValue('SA-Nodes.overwriteDragAndDrop'),
function: () => {
const cur = app.ui.settings.getSettingValue('SA-Nodes.overwriteDragAndDrop');
app.ui.settings.setSettingValue('SA-Nodes.overwriteDragAndDrop', !cur);
},
},
],
/**
* Intercept canvas-level drag-and-drop to create Power Load Video nodes
* instead of default Load Video nodes
*/
async setup() {
// Check if overwrite drag and drop is enabled
const overwriteDragAndDrop = app.ui.settings.getSettingValue('SA-Nodes.overwriteDragAndDrop');
if (!overwriteDragAndDrop) {
return;
}
// Get the canvas element - try multiple approaches
let canvas = null;
@@ -423,6 +451,20 @@ app.registerExtension({
},
async beforeRegisterNodeDef(nodeType, nodeData) {
// Add SA-Nodes category to the node list for easy discovery
if (nodeData?.category && nodeData.category.startsWith('Power/')) {
// Extract the subcategory (e.g., 'Video' from 'Power/Video')
const subcategory = nodeData.category.split('/')[1];
if (subcategory) {
// Add to SA-Nodes category as well for easy discovery
if (!nodeType.category) {
nodeType.category = 'SA-Nodes/' + subcategory;
} else {
nodeType.categories = ['SA-Nodes/' + subcategory, nodeData.category];
}
}
}
if (nodeData?.name === 'PowerLoadVideo') {
// Note: We intentionally keep video_upload flag so ComfyUI serializes
// the combo widget value for persistence across tab switches and workflow reloads.