import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; app.registerExtension({ name: "Comfy.LoadAudioUI", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "LoadAudioUI") { const onNodeCreated = nodeType.prototype.onNodeCreated; const onDrawBackground = nodeType.prototype.onDrawBackground; // --- V1 LiteGraph Image Preview Hider --- nodeType.prototype.onDrawBackground = function (ctx) { if (onDrawBackground) { onDrawBackground.apply(this, arguments); } }; nodeType.prototype.onNodeCreated = function () { // Ensure standard creation logic runs first const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; const node = this; // Capture the node instance // Track if we are in the initial loading phase to prevent resetting saved trim values node._initializing = true; node._should_reset_trim = false; // ==================================================================== // FIX: HIDE DEFAULT COMFYUI AUDIO PLAYER SAFELY // ==================================================================== setTimeout(() => { if (node.widgets) { const nativeWidgetIndex = node.widgets.findIndex(w => w.name === "audioUI"); if (nativeWidgetIndex !== -1) { const w = node.widgets[nativeWidgetIndex]; if (w.element) { w.element.style.display = "none"; w.element.style.height = "0px"; w.element.style.position = "absolute"; w.element.style.pointerEvents = "none"; } w.type = "hidden"; w.hidden = true; w.computeSize = () => [0, 0]; // Only update height to account for the hidden widget, // preserving the width (whether it's the default 475 or a user-saved value). const currentWidth = node.size[0]; const recommendedHeight = node.computeSize()[1]; node.setSize([currentWidth, recommendedHeight]); if (app.graph) { app.graph.setDirtyCanvas(true, true); } } } }, 10); // ==================================================================== // --- THE CORE FIX FOR COMFYUI V2 --- Object.defineProperty(node, 'imgs', { get: function() { return undefined; }, set: function(val) { /* Ignore attempts by ComfyUI to set an image preview */ }, configurable: true }); // Shared upload handler const handleFileUpload = async (file) => { if (!file.type.startsWith('audio/') && !file.type.startsWith('video/')) return false; try { const body = new FormData(); body.append("image", file); body.append("type", "input"); body.append("subfolder", ""); const resp = await api.fetchApi("/upload/image", { method: "POST", body, }); if (resp.status === 200) { const data = await resp.json(); const audioWidget = node.widgets && node.widgets.find(w => w.name === "audio"); if (audioWidget) { // Manual upload should always reset the trim range node._should_reset_trim = true; audioWidget.value = data.name; if (audioWidget.options && audioWidget.options.values && !audioWidget.options.values.includes(data.name)) { audioWidget.options.values.push(data.name); } if (audioWidget.callback) { audioWidget.callback(data.name); } app.graph.setDirtyCanvas(true, false); } } } catch (err) { console.error("Error uploading dragged audio file:", err); } return true; }; this.onDragDrop = function(e) { if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) { const file = e.dataTransfer.files[0]; if (file.type.startsWith('audio/') || file.type.startsWith('video/')) { handleFileUpload(file); return true; } } return false; }; // 1. Build the Main Custom HTML Container const container = document.createElement("div"); const defaultBg = "rgba(30, 30, 30, 0.9)"; Object.assign(container.style, { display: "flex", flexDirection: "column", gap: "10px", width: "100%", padding: "10px", boxSizing: "border-box", background: defaultBg, borderRadius: "6px", color: "white", fontFamily: "sans-serif", marginTop: "8px", flexShrink: "0", transition: "background 0.2s" }); const playerTop = document.createElement("div"); Object.assign(playerTop.style, { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "0 2px", marginBottom: "-4px" }); const playerTitle = document.createElement("span"); playerTitle.textContent = "No audio selected"; Object.assign(playerTitle.style, { fontSize: "11px", color: "#aaa", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: "140px" }); const trimLength = document.createElement("span"); Object.assign(trimLength.style, { fontSize: "11px", color: "#38bdf8", fontWeight: "bold", background: "rgba(56, 189, 248, 0.1)", padding: "3px 6px", borderRadius: "4px", whiteSpace: "nowrap" }); trimLength.textContent = "Trimmed: 0.0s"; playerTop.appendChild(playerTitle); playerTop.appendChild(trimLength); container.appendChild(playerTop); const audioEl = document.createElement("audio"); audioEl.controls = true; audioEl.style.width = "100%"; audioEl.style.height = "40px"; audioEl.style.outline = "none"; container.appendChild(audioEl); const trimArea = document.createElement("div"); Object.assign(trimArea.style, { display: "flex", flexDirection: "column", gap: "6px", background: "rgba(0, 0, 0, 0.35)", padding: "12px", borderRadius: "6px", border: "1px solid rgba(255, 255, 255, 0.05)" }); const timeRuler = document.createElement("div"); Object.assign(timeRuler.style, { position: "relative", width: "100%", height: "22px", fontSize: "10px", color: "#aaa", pointerEvents: "none", userSelect: "none" }); trimArea.appendChild(timeRuler); const sliderBox = document.createElement("div"); Object.assign(sliderBox.style, { position: "relative", width: "100%", height: "24px", background: "#111", borderRadius: "4px", cursor: "pointer", userSelect: "none", boxShadow: "inset 0 1px 3px rgba(0,0,0,0.5)" }); const fill = document.createElement("div"); Object.assign(fill.style, { position: "absolute", height: "100%", background: "rgba(14, 165, 233, 0.35)", pointerEvents: "none" }); sliderBox.appendChild(fill); const createHandle = (color) => { const h = document.createElement("div"); Object.assign(h.style, { position: "absolute", top: "0", width: "8px", height: "100%", background: color, transform: "translateX(-50%)", pointerEvents: "none", boxShadow: "0 0 4px rgba(0,0,0,0.8)", borderRadius: "2px" }); return h; }; const startHandle = createHandle("#38bdf8"); const endHandle = createHandle("#38bdf8"); sliderBox.appendChild(startHandle); sliderBox.appendChild(endHandle); trimArea.appendChild(sliderBox); container.appendChild(trimArea); // 4. Attach container to the node UI const widget = this.addDOMWidget("audio_ui", "audio_ui", container); // --- 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]]; widget.computeSize = function(width) { return [width, 200]; }; // 5. Bind Node Data to UI dynamically setTimeout(() => { const audioWidget = node.widgets && node.widgets.find(w => w.name === "audio"); const startWidget = node.widgets && node.widgets.find(w => w.name === "start_time"); const endWidget = node.widgets && node.widgets.find(w => w.name === "end_time"); if (audioWidget) { const updateAudio = () => { if (!audioWidget.value || audioWidget.value === "none") { playerTitle.textContent = "No audio selected"; return; } let filename = audioWidget.value; let subfolder = ""; if (filename.includes("/") || filename.includes("\\")) { const sep = filename.includes("/") ? "/" : "\\"; const parts = filename.split(sep); filename = parts.pop(); subfolder = parts.join("/"); } playerTitle.textContent = filename; audioEl.src = api.apiURL(`/view?filename=${encodeURIComponent(filename)}&type=input&subfolder=${encodeURIComponent(subfolder)}`); }; audioWidget.callback = function() { // If user manually changes the dropdown, flag for trim reset if (!node._initializing) { node._should_reset_trim = true; } updateAudio(); }; updateAudio(); } container.ondragover = (e) => { e.preventDefault(); e.stopPropagation(); container.style.background = "rgba(14, 165, 233, 0.2)"; }; container.ondragleave = (e) => { e.preventDefault(); e.stopPropagation(); container.style.background = defaultBg; }; container.ondrop = async (e) => { e.preventDefault(); e.stopPropagation(); container.style.background = defaultBg; if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { handleFileUpload(e.dataTransfer.files[0]); } }; let duration = 0; let dragging = null; const formatTime = (secs) => { if (secs < 60) return secs.toFixed(1) + "s"; const m = Math.floor(secs / 60); const s = (secs % 60).toFixed(1); return `${m}:${s.padStart(4, '0')}`; }; const updateRuler = () => { timeRuler.innerHTML = ''; if (!duration) return; const numMajorTicks = 5; const subTicks = 4; const totalTicks = (numMajorTicks - 1) * subTicks; for (let i = 0; i <= totalTicks; i++) { const pct = i / totalTicks; const t = duration * pct; const isMajor = i % subTicks === 0; const tickWrapper = document.createElement("div"); Object.assign(tickWrapper.style, { position: "absolute", left: `${pct * 100}%`, top: "0", display: "flex", flexDirection: "column", alignItems: "center", transform: "translateX(-50%)" }); if (i === 0) { tickWrapper.style.transform = "none"; tickWrapper.style.alignItems = "flex-start"; } if (i === totalTicks) { tickWrapper.style.transform = "translateX(-100%)"; tickWrapper.style.alignItems = "flex-end"; } const line = document.createElement("div"); Object.assign(line.style, { width: isMajor ? "2px" : "1px", height: isMajor ? "6px" : "4px", background: isMajor ? "#aaa" : "#555", marginBottom: "2px", borderRadius: "1px" }); tickWrapper.appendChild(line); if (isMajor) { const label = document.createElement("div"); label.textContent = formatTime(t); tickWrapper.appendChild(label); } timeRuler.appendChild(tickWrapper); } }; const updateUI = (syncPlayer = false) => { if (!duration) return; let s = startWidget ? parseFloat(startWidget.value) || 0 : 0; let e = endWidget ? parseFloat(endWidget.value) || 0 : 0; if (e === 0 || e > duration) e = duration; if (s > e) s = e; const sPct = (s / duration) * 100; const ePct = (e / duration) * 100; startHandle.style.left = `${sPct}%`; endHandle.style.left = `${ePct}%`; fill.style.left = `${sPct}%`; fill.style.width = `${ePct - sPct}%`; trimLength.textContent = `Trimmed: ${formatTime(e - s)}`; if (syncPlayer && audioEl.readyState >= 1) { audioEl.currentTime = s; } }; audioEl.onloadedmetadata = () => { duration = audioEl.duration; // Handle trim reset for new audio selection if (node._should_reset_trim) { if (startWidget) startWidget.value = 0; if (endWidget) endWidget.value = parseFloat(duration.toFixed(3)); node._should_reset_trim = false; } else { // Default clamping logic for initial load or out-of-bounds saved values let e = endWidget ? parseFloat(endWidget.value) || 0 : 0; if (endWidget && (e === 0 || e > duration)) { endWidget.value = parseFloat(duration.toFixed(3)); } } updateRuler(); updateUI(); app.graph.setDirtyCanvas(true, false); }; audioEl.ontimeupdate = () => { if (dragging || !duration) return; let s = startWidget ? parseFloat(startWidget.value) || 0 : 0; let e = endWidget ? parseFloat(endWidget.value) || duration : duration; if (e === 0) e = duration; if (audioEl.currentTime >= e) { audioEl.pause(); audioEl.currentTime = s; } }; audioEl.onplay = () => { let s = startWidget ? parseFloat(startWidget.value) || 0 : 0; let e = endWidget ? parseFloat(endWidget.value) || duration : duration; if (e === 0) e = duration; if (audioEl.currentTime < s || audioEl.currentTime >= e) { audioEl.currentTime = s; } }; [startWidget, endWidget].forEach(w => { if (w) { const orig = w.callback; w.callback = function() { updateUI(true); if(orig) orig.apply(this, arguments); }; } }); sliderBox.onpointerdown = (e) => { if (!duration) return; const rect = sliderBox.getBoundingClientRect(); const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width)); const val = (x / rect.width) * duration; let s = startWidget ? parseFloat(startWidget.value) || 0 : 0; let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration; if (Math.abs(val - s) < Math.abs(val - e_val)) { dragging = 'start'; if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(3)); } else { dragging = 'end'; if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(3)); } updateUI(true); app.graph.setDirtyCanvas(true, false); sliderBox.setPointerCapture(e.pointerId); }; sliderBox.onpointermove = (e) => { if (!dragging || !duration) return; const rect = sliderBox.getBoundingClientRect(); const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width)); const val = (x / rect.width) * duration; if (dragging === 'start') { let e_val = endWidget ? parseFloat(endWidget.value) || duration : duration; if(startWidget) startWidget.value = parseFloat(Math.min(val, e_val).toFixed(3)); } else if (dragging === 'end') { const s = startWidget ? parseFloat(startWidget.value) || 0 : 0; if(endWidget) endWidget.value = parseFloat(Math.max(val, s).toFixed(3)); } updateUI(true); app.graph.setDirtyCanvas(true, false); }; sliderBox.onpointerup = (e) => { dragging = null; sliderBox.releasePointerCapture(e.pointerId); }; // Exit initialization phase setTimeout(() => { node._initializing = false; }, 500); }, 100); return r; } } } });