Updated Load Audio UI Node

- added duration widget
- added ability to drag and move the entire selection bar
This commit is contained in:
WhatDreamsCost
2026-05-01 16:26:45 -05:00
parent 71d3d82522
commit ff555ed2ac
3 changed files with 87 additions and 17 deletions

View File

@@ -245,9 +245,6 @@ app.registerExtension({
const widget = this.addDOMWidget("audio_ui", "audio_ui", container); const widget = this.addDOMWidget("audio_ui", "audio_ui", container);
// --- DEFAULT SIZE FOR NEW NODES --- // --- DEFAULT SIZE FOR NEW NODES ---
// We set the width to 475 initially.
// If a workflow is loading, LiteGraph will overwrite this size
// automatically after this function finishes.
this.size = [475, this.computeSize()[1]]; this.size = [475, this.computeSize()[1]];
widget.computeSize = function(width) { widget.computeSize = function(width) {
@@ -259,6 +256,49 @@ app.registerExtension({
const audioWidget = node.widgets && node.widgets.find(w => w.name === "audio"); const audioWidget = node.widgets && node.widgets.find(w => w.name === "audio");
const startWidget = node.widgets && node.widgets.find(w => w.name === "start_time"); const startWidget = node.widgets && node.widgets.find(w => w.name === "start_time");
const endWidget = node.widgets && node.widgets.find(w => w.name === "end_time"); const endWidget = node.widgets && node.widgets.find(w => w.name === "end_time");
const durationWidget = node.widgets && node.widgets.find(w => w.name === "duration");
let duration = 0;
let dragging = null;
let dragOffset = 0;
let dragSelectionWidth = 0;
let isUpdatingDuration = false; // Flag to prevent infinite loops
// Hook into the Python-generated native duration widget
if (durationWidget) {
const origCallback = durationWidget.callback;
durationWidget.callback = function(v) {
// If we're internally triggering it, ignore it
if (!duration || isUpdatingDuration) {
if (origCallback) origCallback.apply(this, arguments);
return;
}
isUpdatingDuration = true;
let d = parseFloat(v) || 0;
if (d < 0) d = 0;
if (d > duration) d = duration;
let s = startWidget ? parseFloat(startWidget.value) || 0 : 0;
let newStart = s;
let newEnd = s + d;
// If adding duration pushes past the end of the audio, shift the start time back instead
if (newEnd > duration) {
newEnd = duration;
newStart = duration - d;
}
if (startWidget) startWidget.value = parseFloat(newStart.toFixed(2));
if (endWidget) endWidget.value = parseFloat(newEnd.toFixed(2));
updateUI(true);
app.graph.setDirtyCanvas(true, false);
if (origCallback) origCallback.apply(this, arguments);
isUpdatingDuration = false;
};
}
if (audioWidget) { if (audioWidget) {
const updateAudio = () => { const updateAudio = () => {
@@ -306,9 +346,6 @@ app.registerExtension({
} }
}; };
let duration = 0;
let dragging = null;
const formatTime = (secs) => { const formatTime = (secs) => {
if (secs < 60) return secs.toFixed(1) + "s"; if (secs < 60) return secs.toFixed(1) + "s";
const m = Math.floor(secs / 60); const m = Math.floor(secs / 60);
@@ -360,7 +397,16 @@ app.registerExtension({
endHandle.style.left = `${ePct}%`; endHandle.style.left = `${ePct}%`;
fill.style.left = `${sPct}%`; fill.style.left = `${sPct}%`;
fill.style.width = `${ePct - sPct}%`; fill.style.width = `${ePct - sPct}%`;
trimLength.textContent = `Trimmed: ${formatTime(e - s)}`;
// Sync native duration widget seamlessly to match UI handles
const currentDur = parseFloat((e - s).toFixed(2));
trimLength.textContent = `Trimmed: ${currentDur}s`;
if (durationWidget && durationWidget.value !== currentDur) {
isUpdatingDuration = true;
durationWidget.value = currentDur;
isUpdatingDuration = false;
}
if (syncPlayer && audioEl.readyState >= 1) { audioEl.currentTime = s; } if (syncPlayer && audioEl.readyState >= 1) { audioEl.currentTime = s; }
}; };
@@ -370,13 +416,13 @@ app.registerExtension({
// Handle trim reset for new audio selection // Handle trim reset for new audio selection
if (node._should_reset_trim) { if (node._should_reset_trim) {
if (startWidget) startWidget.value = 0; if (startWidget) startWidget.value = 0;
if (endWidget) endWidget.value = parseFloat(duration.toFixed(3)); if (endWidget) endWidget.value = parseFloat(duration.toFixed(2));
node._should_reset_trim = false; node._should_reset_trim = false;
} else { } else {
// Default clamping logic for initial load or out-of-bounds saved values // Default clamping logic for initial load or out-of-bounds saved values
let e = endWidget ? parseFloat(endWidget.value) || 0 : 0; let e = endWidget ? parseFloat(endWidget.value) || 0 : 0;
if (endWidget && (e === 0 || e > duration)) { if (endWidget && (e === 0 || e > duration)) {
endWidget.value = parseFloat(duration.toFixed(3)); endWidget.value = parseFloat(duration.toFixed(2));
} }
} }
@@ -414,12 +460,20 @@ app.registerExtension({
const val = (x / rect.width) * duration; const val = (x / rect.width) * duration;
let s = startWidget ? parseFloat(startWidget.value) || 0 : 0; let s = startWidget ? parseFloat(startWidget.value) || 0 : 0;
let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration; let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration;
if (Math.abs(val - s) < Math.abs(val - e_val)) {
// Define a "handle tolerance" zone (approx 10px on each side) to prioritize resizing over dragging
const handleTolerance = (10 / rect.width) * duration;
if (val > s + handleTolerance && val < e_val - handleTolerance) {
dragging = 'center';
dragOffset = val - s;
dragSelectionWidth = e_val - s;
} else if (Math.abs(val - s) < Math.abs(val - e_val)) {
dragging = 'start'; dragging = 'start';
if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(3)); if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(2));
} else { } else {
dragging = 'end'; dragging = 'end';
if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(3)); if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(2));
} }
updateUI(true); app.graph.setDirtyCanvas(true, false); updateUI(true); app.graph.setDirtyCanvas(true, false);
sliderBox.setPointerCapture(e.pointerId); sliderBox.setPointerCapture(e.pointerId);
@@ -432,10 +486,25 @@ app.registerExtension({
const val = (x / rect.width) * duration; const val = (x / rect.width) * duration;
if (dragging === 'start') { if (dragging === 'start') {
let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration; let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration;
if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(3)); if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(2));
} else if (dragging === 'end') { } else if (dragging === 'end') {
const s = startWidget ? parseFloat(startWidget.value) || 0 : 0; const s = startWidget ? parseFloat(startWidget.value) || 0 : 0;
if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(3)); if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(2));
} else if (dragging === 'center') {
let newStart = val - dragOffset;
let newEnd = newStart + dragSelectionWidth;
// Clamp to bounds
if (newStart < 0) {
newStart = 0;
newEnd = dragSelectionWidth;
} else if (newEnd > duration) {
newEnd = duration;
newStart = duration - dragSelectionWidth;
}
if(startWidget) startWidget.value = parseFloat(newStart.toFixed(2));
if(endWidget) endWidget.value = parseFloat(newEnd.toFixed(2));
} }
updateUI(true); app.graph.setDirtyCanvas(true, false); updateUI(true); app.graph.setDirtyCanvas(true, false);
}; };

View File

@@ -61,9 +61,10 @@ class LoadAudioUI:
return { return {
"required": { "required": {
"audio": (files, {"audio_upload": True}), "audio": (files, {"audio_upload": True}), # Moved to the top so it appears first
"start_time": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}), "start_time": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}),
"end_time": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}), "end_time": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}),
"duration": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100000.0, "step": 0.01}),
}, },
"optional": { "optional": {
"audioUI": ("AUDIO_UI",) "audioUI": ("AUDIO_UI",)
@@ -82,7 +83,7 @@ class LoadAudioUI:
# where our fallback silence logic can handle the missing file gracefully. # where our fallback silence logic can handle the missing file gracefully.
return True return True
def load_audio(self, audio, start_time, end_time, **kwargs): def load_audio(self, audio, start_time, end_time, duration, **kwargs):
# Determine the annotated file path if a file is actually selected # Determine the annotated file path if a file is actually selected
# We wrap this in a try/except because get_annotated_filepath can fail if # We wrap this in a try/except because get_annotated_filepath can fail if
# the input string is malformed or doesn't follow expected paths. # the input string is malformed or doesn't follow expected paths.

View File

@@ -1,7 +1,7 @@
[project] [project]
name = "WhatDreamsCost-ComfyUI" name = "WhatDreamsCost-ComfyUI"
description = "A variety of custom ComfyUI nodes and workflows for creatives." description = "A variety of custom ComfyUI nodes and workflows for creatives."
version = "1.2.4" version = "1.2.5"
license = {file = "LICENSE"} license = {file = "LICENSE"}
# classifiers = [ # classifiers = [
# # For OS-independent nodes (works on all operating systems) # # For OS-independent nodes (works on all operating systems)