From 205df36ccf194866ee466b5bdc71aa5eba76cc6b Mon Sep 17 00:00:00 2001 From: JJ Date: Sat, 4 Apr 2026 21:47:27 +0100 Subject: [PATCH] Optional drag and drop + AB overlap --- nodes/video_nodes_extra.py | 57 +++++++++++++++++------- web/power_load_video/power_load_video.js | 42 +++++++++++++++++ 2 files changed, 83 insertions(+), 16 deletions(-) diff --git a/nodes/video_nodes_extra.py b/nodes/video_nodes_extra.py index be87c2d..8ad7211 100644 --- a/nodes/video_nodes_extra.py +++ b/nodes/video_nodes_extra.py @@ -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 = { diff --git a/web/power_load_video/power_load_video.js b/web/power_load_video/power_load_video.js index 9dee4d8..583ba69 100644 --- a/web/power_load_video/power_load_video.js +++ b/web/power_load_video/power_load_video.js @@ -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.