From 31e27fceac715d0e632cabd412d7d4874dcd6cb8 Mon Sep 17 00:00:00 2001 From: WhatDreamsCost Date: Wed, 6 May 2026 00:18:09 -0500 Subject: [PATCH] Fixed every known issue with multi image loader and added text output to speech length calculator Fixed every known issue with multi image loader and added text output to speech length calculator --- js/multi_image_loader.js | 486 +++++++++++++++++++++--------------- speech_length_calculator.py | 9 +- 2 files changed, 293 insertions(+), 202 deletions(-) diff --git a/js/multi_image_loader.js b/js/multi_image_loader.js index db7099b..d1a05cb 100644 --- a/js/multi_image_loader.js +++ b/js/multi_image_loader.js @@ -6,10 +6,28 @@ app.registerExtension({ async nodeCreated(node) { if (node.comfyClass !== "MultiImageLoader") return; + // Helper to detect if we are in the new Nodes 2.0 / V3 Web Component frontend + let v3NodeElement = null; + function checkIsV3() { + if (v3NodeElement) return true; + let el = container.parentElement; + while (el) { + if ((el.tagName && el.tagName.toLowerCase().includes('comfy-node')) || + (el.classList && el.classList.contains('comfy-node'))) { + v3NodeElement = el; + return true; + } + el = el.parentElement || (el.getRootNode ? el.getRootNode().host : null); + } + return false; + } + // --- 1. UI Setup: Main Container --- const container = document.createElement("div"); container.style.cssText = ` width: 100%; + min-height: 250px; + min-width: 100px; /* Reduced from 400px to allow thin resizing in V3 */ background: #222222; border: 1px solid #353545; border-radius: 4px; @@ -25,7 +43,8 @@ app.registerExtension({ // Top Bar for Actions const topBar = document.createElement("div"); - topBar.style.cssText = "display: flex; justify-content: flex-start; align-items: center; width: 100%; gap: 8px;"; + // Added flex-wrap: wrap so buttons stack if the node gets extremely thin + topBar.style.cssText = "display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: center; width: 100%; gap: 8px;"; const uploadBtn = document.createElement("button"); uploadBtn.innerText = "Upload Images"; @@ -51,18 +70,26 @@ app.registerExtension({ topBar.appendChild(removeAllBtn); container.appendChild(topBar); - // The Grid Area - Setup to center the dynamically packed square blocks + const gridWrapper = document.createElement("div"); + gridWrapper.style.cssText = ` + position: relative; + flex-grow: 1; + width: 100%; + min-height: 0; + `; + const grid = document.createElement("div"); grid.style.cssText = ` - position: relative; /* Crucial for anti-flicker offset calculations */ - flex-grow: 1; + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; display: grid; gap: 8px; - width: 100%; justify-content: center; align-content: center; `; - container.appendChild(grid); + + gridWrapper.appendChild(grid); + container.appendChild(gridWrapper); const fileInput = document.createElement("input"); fileInput.type = "file"; @@ -74,60 +101,45 @@ app.registerExtension({ // Add the Widget to the Node const galleryWidget = node.addDOMWidget("Gallery", "html_gallery", container, { serialize: false }); - // Permanently neutralize the DOM widget's built-in computeSize to prevent infinite LiteGraph loops - galleryWidget.computeSize = () => [0, 0]; + galleryWidget.computeSize = function() { + const galleryY = this.last_y || 40; + const minOutputsHeight = (node.outputs ? node.outputs.length : 1) * 20; + const requiredGalleryHeight = Math.max(250, minOutputsHeight + 40 - galleryY); + return [150, requiredGalleryHeight]; // Changed minimum theoretical widget width + }; - // --- BUG FIX --- - // Find the paths widget and properly hide it from LiteGraph + // --- SAFELY HIDE THE IMAGE_PATHS WIDGET --- const pathsWidget = node.widgets.find(w => w.name === "image_paths"); if (pathsWidget) { - // Tell LiteGraph to ignore this widget for hit-testing and drawing - pathsWidget.hidden = true; + // Forcefully lock the hidden state against V3's reactive redraws + Object.defineProperty(pathsWidget, 'hidden', { + get: () => true, + set: () => {} // Ignore attempts by V3 to unhide it + }); + Object.defineProperty(pathsWidget, 'type', { + get: () => "hidden", + set: () => {} // Ignore attempts by V3 to reset the type + }); - // Return -4 to absorb LiteGraph's default 4px vertical margin between widgets, - // entirely eliminating its invisible "ghost" hitbox. - pathsWidget.computeSize = () => [0, -4]; - - // Aggressively neutralize the actual DOM element AND its ComfyUI wrapper using a global stylesheet - // ComfyUI's background render loop continually overwrites inline styles (like display: none). - // By assigning unique IDs and injecting a global !important CSS rule, we guarantee it remains dead. - if (pathsWidget.element) { - const uid = node.id || Math.random().toString(36).substring(2, 9); - pathsWidget.element.id = `hidden-paths-textarea-${uid}`; - if (pathsWidget.element.parentElement) { - pathsWidget.element.parentElement.id = `hidden-paths-wrapper-${uid}`; - } + pathsWidget.computeSize = function() { + return [0, 0]; + }; - // Inject the shield-killer style into the document head if it doesn't exist yet - if (!document.getElementById("multi-image-loader-shield-killer")) { - const style = document.createElement("style"); - style.id = "multi-image-loader-shield-killer"; - style.innerHTML = ` - [id^="hidden-paths-textarea-"], - [id^="hidden-paths-wrapper-"] { - display: none !important; - pointer-events: none !important; - position: absolute !important; - width: 0px !important; - height: 0px !important; - opacity: 0 !important; - z-index: -9999 !important; - overflow: hidden !important; - } - `; - document.head.appendChild(style); + // Catch for V3 delayed DOM rendering to ensure no stubborn inputs appear + const hideInterval = setInterval(() => { + if (pathsWidget.element) { + pathsWidget.element.style.display = "none"; } - } + }, 50); + setTimeout(() => clearInterval(hideInterval), 1000); } const oldCallback = pathsWidget?.callback; - // Centralized helper to prevent infinite loops when updating values internally function setWidgetValue(newPathsArray, isRearranging = false) { if (!pathsWidget) return; const val = newPathsArray.join("\n"); - // Temporarily silence the main callback const tempCallback = pathsWidget.callback; pathsWidget.callback = null; @@ -139,32 +151,29 @@ app.registerExtension({ } // --- 2. Logic: Output Syncing & Dynamic Packing --- - // Manages image_N outputs (slots 1+) while always preserving multi_output at slot 0 function syncOutputs(count) { if (!node.outputs) return; let changed = false; - // Target = multi_output (slot 0) + count image outputs const targetTotal = count + 1; + + const wasFresh = node.outputs.length >= 50; - // Remove excess outputs from the end, but NEVER remove slot 0 (multi_output) while (node.outputs.length > targetTotal && node.outputs.length > 1) { node.removeOutput(node.outputs.length - 1); changed = true; } - // Add missing image_N outputs after multi_output for (let i = node.outputs.length; i < targetTotal; i++) { node.addOutput(`image_${i}`, "IMAGE"); changed = true; } - if (changed) { - updateLayout(); + if (changed || wasFresh) { + updateLayout(wasFresh); } } - // Push-based notification: broadcast image count to all connected nodes function notifyConnectedNodes(imageCount) { if (!node.outputs) return; for (const output of node.outputs) { @@ -180,8 +189,7 @@ app.registerExtension({ } } - // 2D Square Packing Algorithm: Calculates the optimal grid sizes to fill empty space - function optimizeGrid(nodeW, containerH) { + function optimizeGrid(gridW, gridH) { const paths = (pathsWidget?.value || "").split(/\n|,/).map(s => s.trim()).filter(s => s); const N = paths.length; @@ -190,129 +198,187 @@ app.registerExtension({ grid.style.gridAutoRows = 'max-content'; return; } - - // Approximate the available internal working space - const gridW = nodeW - 22; // Container padding + border - const gridH = containerH - 60; // Top bar height + container padding + gap if (gridW <= 0 || gridH <= 0) return; let bestS = 0; let bestCols = 1; - // Test every possible column combination to find the one that yields the largest squares for (let c = 1; c <= N; c++) { const r = Math.ceil(N / c); - const maxW = (gridW - (c - 1) * 8) / c; - const maxH = (gridH - (r - 1) * 8) / r; + // Math.max guarantees we don't end up with negative max width in ultra-thin layouts + const maxW = Math.max(5, (gridW - (c - 1) * 8) / c); + const maxH = Math.max(5, (gridH - (r - 1) * 8) / r); const size = Math.min(maxW, maxH); - if (size > bestS) { + // By using >= instead of strict > (with a tiny 0.1 buffer for float precision), + // if multiple column counts yield the exact same optimal cell size + // (which happens when height is the bottleneck), we aggressively pack + // more items horizontally onto the row to fill empty space. + if (size >= bestS - 0.1) { bestS = size; bestCols = c; } } - bestS = Math.max(75, Math.floor(bestS)); // Keep a minimum size floor + // Allow grid cells to shrink down to 10px instead of 15 to prevent horizontal overflow in V3 + bestS = Math.max(10, Math.floor(bestS)); - // Force the grid to perfectly adopt the optimal maximum square scale grid.style.gridTemplateColumns = `repeat(${bestCols}, ${bestS}px)`; grid.style.gridAutoRows = `${bestS}px`; } - // Centralized measurement function shared by automatic updates and manual resizes - function getGalleryHeights() { - const baseHeight = node.computeSize()[1]; - - // Temporarily force natural height measurement using minimum settings - const oldHeight = container.style.height; - const oldCols = grid.style.gridTemplateColumns; - const oldRows = grid.style.gridAutoRows; - - container.style.height = 'fit-content'; - grid.style.gridTemplateColumns = `repeat(auto-fit, minmax(75px, 1fr))`; - grid.style.gridAutoRows = `max-content`; - - const naturalGalleryHeight = container.offsetHeight || 100; - const minNodeHeight = baseHeight + naturalGalleryHeight + 15; - - // Restore styles instantly - container.style.height = oldHeight; - grid.style.gridTemplateColumns = oldCols; - grid.style.gridAutoRows = oldRows; - - return { baseHeight, minNodeHeight }; + let v3EventsAttached = false; + + function enforceV3CSS() { + const isV3 = checkIsV3(); + if (isV3 && v3NodeElement) { + const paddingBottom = 15; + const galleryY = galleryWidget.last_y || 40; + const minOutputsHeight = (node.outputs ? node.outputs.length : 1) * 20; + const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40); + + // For Nodes 2.0 (V3), we remove the min-width constraint completely. + // We leave min-height so outputs don't bleed out vertically. + v3NodeElement.style.removeProperty('min-width'); + v3NodeElement.style.setProperty('min-height', absoluteMinHeight + 'px', 'important'); + + // Attach drag & drop to the entire V3 Web Component + if (!v3EventsAttached) { + v3EventsAttached = true; + v3NodeElement.addEventListener("dragover", (e) => { + e.preventDefault(); + }); + v3NodeElement.addEventListener("drop", (e) => { + if (e.dataTransfer && e.dataTransfer.files) { + const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/')); + if (files.length > 0) { + e.preventDefault(); + e.stopPropagation(); + handleFiles(files); + } + } + }); + } + } } let isLayouting = false; - let isFirstLayout = true; - function updateLayout(forceHeight = null) { + + function updateLayout(forceShrink = false) { if (isLayouting) return; isLayouting = true; - const { baseHeight, minNodeHeight } = getGalleryHeights(); + const isV3 = checkIsV3(); + const minW = isV3 ? 100 : 200; // 100 in V3, 440 in V1 + const paddingBottom = isV3 ? 15 : 25; // Apply extra 20px pad on V1 - let targetW = Math.max(node.size[0], 240); - let targetH = forceHeight !== null ? forceHeight : node.size[1]; - - if (isFirstLayout) { - targetH = 0; // Force shrink to minimum bounds on initial load - isFirstLayout = false; - } + const galleryY = galleryWidget.last_y || 40; + const minOutputsHeight = (node.outputs ? node.outputs.length : 1) * 20; + const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40); - // Enforce minimum height - targetH = Math.max(targetH, minNodeHeight); + node.min_size = [minW, absoluteMinHeight]; + enforceV3CSS(); + + let targetW = Math.max(node.size[0], minW); + let targetH = forceShrink ? absoluteMinHeight : node.size[1]; + + targetH = Math.max(targetH, absoluteMinHeight); if (node.size[0] !== targetW || node.size[1] !== targetH) { node.setSize([targetW, targetH]); app.graph.setDirtyCanvas(true, true); } - const availableGalleryHeight = targetH - baseHeight - 15; + const availableGalleryHeight = targetH - galleryY - paddingBottom; container.style.height = availableGalleryHeight + "px"; - // Recalculate and stretch image squares using the new node space - optimizeGrid(targetW, availableGalleryHeight); - isLayouting = false; } - // Intercept user dragging the node corner to strictly enforce size constraints + // --- OVERRIDE LOGIC FOR RESIZING --- const origOnResize = node.onResize; node.onResize = function(size) { + const isV3 = checkIsV3(); + const minW = isV3 ? 100 : 220; // Adjust limits based on frontend + const paddingBottom = isV3 ? 15 : 25; // Apply extra 20px pad on V1 + + const galleryY = galleryWidget.last_y || 40; + const minOutputsHeight = (this.outputs ? this.outputs.length : 1) * 20; + const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40); + + size[0] = Math.max(size[0], minW); + size[1] = Math.max(size[1], absoluteMinHeight); + if (origOnResize) origOnResize.call(this, size); - if (isLayouting) return; // Prevent duplicate cycles + if (isLayouting) return; - const { baseHeight, minNodeHeight } = getGalleryHeights(); + node.min_size = [minW, absoluteMinHeight]; + enforceV3CSS(); - // Prevent user from making node smaller than the minimum content - size[0] = Math.max(size[0], 240); - size[1] = Math.max(size[1], minNodeHeight); - - // Immediately apply the fluid heights for smooth dragging - const availableGalleryHeight = size[1] - baseHeight - 15; + const availableGalleryHeight = size[1] - galleryY - paddingBottom; container.style.height = availableGalleryHeight + "px"; - - // Expand the image slots instantly as you drag - optimizeGrid(size[0], availableGalleryHeight); }; - // Auto-adjust layout if dimensions change programmatically (e.g., undo/redo wrap reflows) - let lastWidth = -1; - let lastHeight = -1; + const origComputeSize = node.computeSize; + node.computeSize = function(out) { + const isV3 = checkIsV3(); + const minW = isV3 ? 100 : 220; + const paddingBottom = isV3 ? 15 : 25; + + let res = origComputeSize ? origComputeSize.apply(this, arguments) : [minW, 250]; + const galleryY = galleryWidget.last_y || 40; + const minOutputsHeight = (this.outputs ? this.outputs.length : 1) * 20; + const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40); + + this.min_size = [minW, absoluteMinHeight]; + res[0] = Math.max(res[0], minW); + res[1] = Math.max(res[1], absoluteMinHeight); + + enforceV3CSS(); + return res; + }; + + const origSetSize = node.setSize; + node.setSize = function(size) { + const isV3 = checkIsV3(); + const minW = isV3 ? 100 : 220; + const paddingBottom = isV3 ? 15 : 25; + + const galleryY = galleryWidget.last_y || 40; + const minOutputsHeight = (this.outputs ? this.outputs.length : 1) * 20; + const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40); + + size[0] = Math.max(size[0], minW); + size[1] = Math.max(size[1], absoluteMinHeight); + + if (origSetSize) { + origSetSize.call(this, size); + } else { + this.size = size; + } + enforceV3CSS(); + }; + + let lastObservedWidth = 0; + let lastObservedHeight = 0; + const resizeObserver = new ResizeObserver((entries) => { + enforceV3CSS(); for (const entry of entries) { - const currentWidth = entry.contentRect.width; - const currentHeight = entry.contentRect.height; - if ((lastWidth !== -1 && Math.abs(currentWidth - lastWidth) > 2) || - (lastHeight !== -1 && Math.abs(currentHeight - lastHeight) > 2)) { - requestAnimationFrame(() => updateLayout()); + const w = Math.round(entry.contentRect.width); + const h = Math.round(entry.contentRect.height); + + if (Math.abs(w - lastObservedWidth) > 1 || Math.abs(h - lastObservedHeight) > 1) { + lastObservedWidth = w; + lastObservedHeight = h; + if (h > 0) { + optimizeGrid(w, h); + } } - lastWidth = currentWidth; - lastHeight = currentHeight; } }); - resizeObserver.observe(container); + resizeObserver.observe(gridWrapper); // --- 3. Logic: Gallery Rendering --- let draggedNode = null; @@ -327,14 +393,12 @@ app.registerExtension({ if (!isRearranging) { syncOutputs(paths.length); } - // Cache image count on the node for easy access by connected nodes node._imageCount = paths.length; - // Push notification: immediately tell connected nodes about the new count notifyConnectedNodes(paths.length); paths.forEach((path, index) => { const item = document.createElement("div"); - item.dataset.path = path; // Store path on the node for easy retrieval + item.dataset.path = path; item.draggable = true; item.style.cssText = ` position: relative; @@ -349,14 +413,14 @@ app.registerExtension({ display: flex; align-items: center; justify-content: center; - will-change: transform; `; const img = document.createElement("img"); img.src = `/api/view?filename=${encodeURIComponent(path)}&type=input`; - img.style.cssText = "max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: none; display: block;"; + // Allow pointer-events so context menu interacts directly with the image + img.style.cssText = "max-width: 100%; max-height: 100%; object-fit: contain; pointer-events: auto; display: block;"; + img.draggable = false; // Prevent native browser ghost dragging on the image itself - // Delete Button const del = document.createElement("div"); del.style.cssText = ` position: absolute; top: 0; right: 0; @@ -381,7 +445,6 @@ app.registerExtension({ setWidgetValue(newPaths, false); }; - // Number Badge const numBadge = document.createElement("div"); numBadge.style.cssText = ` position: absolute; bottom: 0; left: 0; @@ -392,27 +455,37 @@ app.registerExtension({ `; numBadge.innerText = (index + 1).toString(); - // Dynamic Animated Drag & Drop Events + // Prevent LiteGraph context menu and instead show standard browser context menu (Copy, Save, Open) + item.addEventListener("contextmenu", (e) => { + e.stopPropagation(); + }); + item.ondragstart = (e) => { draggedNode = item; - // Delay opacity drop so the browser capture image remains opaque + + e.dataTransfer.setData('text/plain', path); + e.dataTransfer.effectAllowed = "move"; + setTimeout(() => { if (draggedNode === item) { - item.style.opacity = "0.4"; - item.style.pointerEvents = "none"; // Prevent drag ghost from capturing events + // Style as an empty dashed placeholder + item.style.background = "transparent"; + item.style.border = "2px dashed #666"; + // Hide the visual children (image, delete button, badge) + Array.from(item.children).forEach(c => c.style.opacity = "0"); } }, 0); - e.dataTransfer.effectAllowed = "move"; }; item.ondragend = () => { if (draggedNode) { - draggedNode.style.opacity = "1"; - draggedNode.style.pointerEvents = "auto"; + // Restore original appearance + draggedNode.style.background = "#000000"; + draggedNode.style.border = "1px solid #444"; + Array.from(draggedNode.children).forEach(c => c.style.opacity = "1"); } draggedNode = null; - // The DOM visually reorders during dragover. Here we finalize it to data. const newPaths = Array.from(grid.children).map(n => n.dataset.path); const currentVal = (pathsWidget?.value || "").trim(); if (newPaths.join("\n") !== currentVal) { @@ -422,34 +495,20 @@ app.registerExtension({ item.ondragover = (e) => { e.preventDefault(); - e.stopPropagation(); // Stop ComfyUI canvas listener from grabbing this internally + e.stopPropagation(); if (!draggedNode || draggedNode === item) return; - // ANTI-FLICKER 1: Prevent rapid swaps if the mouse hasn't moved physically. - // Reduced delay from 300ms to 50ms and distance to 5px to drastically improve responsiveness - // while still absorbing the immediate CSS transform shock. const distMoved = Math.hypot(e.clientX - lastSwapX, e.clientY - lastSwapY); if (Date.now() - lastSwapTime < 50 && distMoved < 5) { return; } - // ANTI-FLICKER 2: True logical target boundaries. - // Reduced the buffer from 25% down to 10%, meaning 80% of the target square - // is now an active drop zone, making it much easier to trigger a swap. - const gridRect = grid.getBoundingClientRect(); - const mouseX = e.clientX - gridRect.left; - const mouseY = e.clientY - gridRect.top; + const itemRect = item.getBoundingClientRect(); + const bufferX = itemRect.width * 0.25; + const bufferY = itemRect.height * 0.25; - const left = item.offsetLeft; - const top = item.offsetTop; - const width = item.offsetWidth; - const height = item.offsetHeight; - - const bufferX = width * 0.10; - const bufferY = height * 0.10; - - if (mouseX < left + bufferX || mouseX > left + width - bufferX || - mouseY < top + bufferY || mouseY > top + height - bufferY) { + if (e.clientX < itemRect.left + bufferX || e.clientX > itemRect.right - bufferX || + e.clientY < itemRect.top + bufferY || e.clientY > itemRect.bottom - bufferY) { return; } @@ -457,39 +516,13 @@ app.registerExtension({ const draggedIdx = items.indexOf(draggedNode); const targetIdx = items.indexOf(item); - // FLIP Animation Step 1: Record old positions - const rects = new Map(); - items.forEach(node => rects.set(node, node.getBoundingClientRect())); - - // Physically move the DOM element to the new slot + // Instantly snap the placeholder to its new position, moving items aside if (draggedIdx < targetIdx) { grid.insertBefore(draggedNode, item.nextSibling); } else { grid.insertBefore(draggedNode, item); } - // FLIP Animation Step 2: Calculate difference and animate - items.forEach(node => { - const oldRect = rects.get(node); - const newRect = node.getBoundingClientRect(); - const dx = oldRect.left - newRect.left; - const dy = oldRect.top - newRect.top; - - if (dx !== 0 || dy !== 0) { - // Instantly shift it visually back to the old spot - node.style.transition = 'none'; - node.style.transform = `translate(${dx}px, ${dy}px)`; - - // Force browser layout recalculation - node.offsetWidth; - - // Turn on transition and remove transform so it glides to its real new position - node.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)'; - node.style.transform = ''; - } - }); - - // Log the swap to lock out rapid feedback triggers lastSwapX = e.clientX; lastSwapY = e.clientY; lastSwapTime = Date.now(); @@ -497,8 +530,7 @@ app.registerExtension({ item.ondrop = (e) => { e.preventDefault(); - e.stopPropagation(); // Stop ComfyUI canvas listener - // Finalization is handled safely in ondragend + e.stopPropagation(); }; item.appendChild(img); @@ -508,7 +540,10 @@ app.registerExtension({ }); if (!isRearranging) { - requestAnimationFrame(() => updateLayout()); + requestAnimationFrame(() => { + updateLayout(); + if (gridWrapper.offsetWidth > 0) optimizeGrid(gridWrapper.offsetWidth, gridWrapper.offsetHeight); + }); } } @@ -535,29 +570,61 @@ app.registerExtension({ } } + // Apply drag & drop to LiteGraph node container bounds (V1) + const origOnDragDrop = node.onDragDrop; + node.onDragDrop = function(e) { + let handled = false; + if (e.dataTransfer && e.dataTransfer.files) { + const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/')); + if (files.length > 0) { + e.preventDefault(); + handleFiles(files); + handled = true; + } + } + if (!handled && origOnDragDrop) { + return origOnDragDrop.apply(this, arguments); + } + return handled; + }; + + const origOnDragOver = node.onDragOver; + node.onDragOver = function(e) { + if (e.dataTransfer && e.dataTransfer.items) { + const hasImage = Array.from(e.dataTransfer.items).some(f => f.kind === 'file' && f.type.startsWith('image/')); + if (hasImage) { + e.preventDefault(); + return true; + } + } + if (origOnDragOver) { + return origOnDragOver.apply(this, arguments); + } + return false; + }; + uploadBtn.onclick = () => fileInput.click(); fileInput.onchange = (e) => handleFiles(e.target.files); container.ondragover = (e) => { e.preventDefault(); - e.stopPropagation(); // Prevent ComfyUI from seeing the drag event over this node + e.stopPropagation(); container.style.borderColor = "#4CAF50"; }; container.ondragleave = (e) => { e.preventDefault(); - e.stopPropagation(); // Prevent ComfyUI from seeing the drag event leave + e.stopPropagation(); container.style.borderColor = "#353545"; }; container.ondrop = (e) => { e.preventDefault(); - e.stopPropagation(); // Crucial: Stop ComfyUI from capturing the dropped file and making a LoadImage node! + e.stopPropagation(); container.style.borderColor = "#353545"; if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files); }; // --- 5. Logic: Paste Handling --- const pasteHandler = (e) => { - // Only capture the paste if THIS specific node is currently selected in the graph if (app.canvas.selected_nodes && app.canvas.selected_nodes[node.id]) { const items = e.clipboardData?.items; if (!items) return; @@ -571,23 +638,21 @@ app.registerExtension({ if (files.length > 0) { e.preventDefault(); - e.stopImmediatePropagation(); // Crucial: Stops ComfyUI from turning the pasted image into a "Load Image" node + e.stopImmediatePropagation(); handleFiles(files); } } }; - // Use capture: true to intercept the event BEFORE ComfyUI's default global paste listener triggers document.addEventListener("paste", pasteHandler, { capture: true }); - // Clean up the global event listener if the node is deleted const origOnRemoved = node.onRemoved; node.onRemoved = function() { document.removeEventListener("paste", pasteHandler, { capture: true }); + resizeObserver.disconnect(); if (origOnRemoved) origOnRemoved.apply(this, arguments); }; - // Hooks the main callback for external state loads (e.g., undo/redo or initial graph load) if (pathsWidget) { pathsWidget.callback = (v) => { if (oldCallback) oldCallback.apply(pathsWidget, [v]); @@ -595,6 +660,29 @@ app.registerExtension({ }; } + // Run immediately to trim blank outputs before LiteGraph does its first layout calculations + refreshGallery(); + + // Enforce the tightest possible packing explicitly upon being dropped into the graph (V1 specifically) + const origOnAdded = node.onAdded; + node.onAdded = function() { + if (origOnAdded) origOnAdded.apply(this, arguments); + const isV3 = checkIsV3(); + if (!isV3) { + requestAnimationFrame(() => { + const galleryY = galleryWidget.last_y || 40; + const minOutputsHeight = (this.outputs ? this.outputs.length : 1) * 20; + const paddingBottom = 25; // Apply extra 20px pad on V1 + const absoluteMinHeight = Math.max(galleryY + 250 + paddingBottom, minOutputsHeight + 40); + // Force the node to snap to its absolute minimum size on initial drop + if (this.size && this.size[1] > absoluteMinHeight + 5) { + this.setSize([this.size[0], absoluteMinHeight]); + if (app.graph) app.graph.setDirtyCanvas(true, true); + } + }); + } + }; + setTimeout(() => refreshGallery(), 100); } }); \ No newline at end of file diff --git a/speech_length_calculator.py b/speech_length_calculator.py index 6ec7e4c..00d847b 100644 --- a/speech_length_calculator.py +++ b/speech_length_calculator.py @@ -15,8 +15,10 @@ class SpeechLengthCalculator: } } - RETURN_TYPES = ("INT", "INT", "INT") - RETURN_NAMES = ("slow_frame_count", "average_frame_count", "fast_frame_count") + # Added "STRING" to RETURN_TYPES + RETURN_TYPES = ("INT", "INT", "INT", "STRING") + # Added "text" to RETURN_NAMES + RETURN_NAMES = ("slow_frame_count", "average_frame_count", "fast_frame_count", "text") FUNCTION = "calculate_speech" CATEGORY = "text/speech" @@ -45,4 +47,5 @@ class SpeechLengthCalculator: avg_frames = calc_frames(130) fast_frames = calc_frames(160) - return (slow_frames, avg_frames, fast_frames) \ No newline at end of file + # Added active_text as the 4th returned value + return (slow_frames, avg_frames, fast_frames, active_text) \ No newline at end of file