Files
WhatDreamsCost-Dumas/js/speech_length_calculator.js
WhatDreamsCost c719ce25d3 Updated Speech Length Calculator Node UI and added duration output to Load Audio UI node
Updated Speech Length Calculator Node UI and added duration output to Load Audio UI node
2026-05-03 01:26:56 -05:00

336 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { ComfyWidgets } from "../../scripts/widgets.js";
// 1. DEFINE CSS STYLES GLOBALLY ONCE
// We use DOM elements instead of Canvas drawing so it works flawlessly in ComfyUI V3
const cssStyles = `
.slc-ui-container {
width: 100%;
height: 100%;
min-height: 260px; /* Increased from 220px to prevent V1 cropping */
background-color: rgba(15, 15, 19, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 12px;
box-sizing: border-box;
display: flex;
flex-direction: column;
font-family: sans-serif;
color: #ffffff;
overflow: hidden;
pointer-events: auto; /* allows text selection */
}
.slc-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.slc-empty-title { color: #aaaaaa; font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.slc-empty-sub { color: #777777; font-size: 11px; }
.slc-headers {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.slc-header-block {
flex: 1;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 6px;
padding: 8px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.slc-header-title { color: #888888; font-size: 9px; font-weight: bold; margin-bottom: 4px; }
.slc-header-value { color: #ffffff; font-size: 16px; font-weight: bold; }
.slc-cards {
display: flex;
flex-direction: column;
gap: 8px;
}
.slc-card {
display: flex;
justify-content: space-between;
align-items: center;
background-color: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 4px;
padding: 8px 12px 8px 16px;
position: relative;
overflow: hidden;
}
.slc-card::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
}
.slc-card.slow::before { background-color: #93c5fd; }
.slc-card.avg::before { background-color: #86efac; }
.slc-card.fast::before { background-color: #fca5a5; }
.slc-card-left { display: flex; flex-direction: column; }
.slc-card-speed { font-size: 12px; font-weight: bold; margin-bottom: 2px; }
.slc-card.slow .slc-card-speed { color: #93c5fd; }
.slc-card.avg .slc-card-speed { color: #86efac; }
.slc-card.fast .slc-card-speed { color: #fca5a5; }
.slc-card-wpm { font-size: 10px; color: #aaaaaa; }
.slc-card-right { display: flex; flex-direction: column; align-items: flex-end; }
.slc-card-time { font-size: 13px; font-weight: bold; color: #ffffff; margin-bottom: 2px; }
.slc-card-frames { font-size: 11px; font-family: monospace; color: #888888; }
.slc-legend {
margin-top: auto;
padding-top: 8px;
color: #999999;
font-size: 10px;
font-weight: 500;
}
`;
// Inject styles to document head safely
if (!document.getElementById("speech-length-calculator-styles")) {
const styleEl = document.createElement("style");
styleEl.id = "speech-length-calculator-styles";
styleEl.textContent = cssStyles;
document.head.appendChild(styleEl);
}
// 2. HELPER TO GENERATE THE HTML CONTENT
function buildUIHTML(statsData) {
if (!statsData || statsData.empty) {
return `
<div class="slc-empty">
<div class="slc-empty-title">Awaiting Script</div>
<div class="slc-empty-sub">Wrap spoken text inside "quotes"</div>
</div>
`;
}
return `
<div class="slc-headers">
<div class="slc-header-block">
<div class="slc-header-title">SPOKEN WORDS</div>
<div class="slc-header-value">${statsData.wordCount}</div>
</div>
<div class="slc-header-block">
<div class="slc-header-title">ADDED TIME</div>
<div class="slc-header-value">${statsData.additionalTime}s</div>
</div>
</div>
<div class="slc-cards">
<div class="slc-card slow">
<div class="slc-card-left">
<div class="slc-card-speed">SLOW</div>
<div class="slc-card-wpm">100 WPM</div>
</div>
<div class="slc-card-right">
<div class="slc-card-time">${statsData.slow.time}</div>
<div class="slc-card-frames">${statsData.slow.frames} frames</div>
</div>
</div>
<div class="slc-card avg">
<div class="slc-card-left">
<div class="slc-card-speed">AVG</div>
<div class="slc-card-wpm">130 WPM</div>
</div>
<div class="slc-card-right">
<div class="slc-card-time">${statsData.avg.time}</div>
<div class="slc-card-frames">${statsData.avg.frames} frames</div>
</div>
</div>
<div class="slc-card fast">
<div class="slc-card-left">
<div class="slc-card-speed">FAST</div>
<div class="slc-card-wpm">160 WPM</div>
</div>
<div class="slc-card-right">
<div class="slc-card-time">${statsData.fast.time}</div>
<div class="slc-card-frames">${statsData.fast.frames} frames</div>
</div>
</div>
</div>
<div class="slc-legend">WPM = Words Per Minute</div>
`;
}
app.registerExtension({
name: "Comfy.SpeechLengthCalculator",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SpeechLengthCalculator") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
// 3. CREATE THE DOM WIDGET
const uiContainer = document.createElement("div");
uiContainer.className = "slc-ui-container";
uiContainer.innerHTML = buildUIHTML({ empty: true });
// Attach to node: standard addDOMWidget works natively in both V1 and V3 Frontends
let statsWidget;
if (typeof this.addDOMWidget === "function") {
statsWidget = this.addDOMWidget("Stats", "HTML", uiContainer, {
serialize: false,
hideOnZoom: false
});
} else {
// Fallback for very old unpatched V1 installations
statsWidget = ComfyWidgets["STRING"](this, "Stats", ["STRING", { multiline: true }], app).widget;
if (statsWidget.inputEl) {
statsWidget.inputEl.style.display = "none";
if (statsWidget.inputEl.parentNode) {
statsWidget.inputEl.parentNode.appendChild(uiContainer);
}
}
}
// Tell LiteGraph exactly how much vertical space to hold for our UI
statsWidget.computeSize = function() {
return [0, 280]; // Increased from 240 to prevent bottom cropping in V1
};
// Ensure the node is wide enough on initial creation
requestAnimationFrame(() => {
const minWidth = 340;
if (this.size[0] < minWidth) this.size[0] = minWidth;
// Fine tune initial height to account for the larger computeSize and make text box taller
if (this.size[1] < 500) this.size[1] = 500;
this.setDirtyCanvas(true, true);
});
// Fetch text from input link OR widget
this._getCurrentText = () => {
const inputSlot = this.inputs && this.inputs.find(i => i.name === "text_input" || (i.name === "text" && i.link));
if (inputSlot && inputSlot.link) {
const link = app.graph.links[inputSlot.link];
if (link) {
const sourceNode = app.graph.getNodeById(link.origin_id);
if (sourceNode && sourceNode.widgets) {
const w = sourceNode.widgets.find(w => w.name === "value" || w.name === "text" || w.name === "Text" || w.type === "customtext" || w.type === "STRING");
if (w && typeof w.value === "string") return w.value;
}
}
}
const textWidget = this.widgets && this.widgets.find(w => w.name === "text");
return textWidget ? (textWidget.value || "") : "";
};
this._lastState = { text: null, fps: null, addTime: null };
const updateStats = () => {
const fpsWidget = this.widgets && this.widgets.find(w => w.name === "fps");
const additionalTimeWidget = this.widgets && this.widgets.find(w => w.name === "additional_time");
if (!fpsWidget) return;
const text = this._getCurrentText();
const fps = fpsWidget.value || 24;
const additionalTime = additionalTimeWidget ? parseFloat(additionalTimeWidget.value) || 0 : 0;
// Skip expensive calculations if nothing changed
if (this._lastState.text === text &&
this._lastState.fps === fps &&
this._lastState.addTime === additionalTime) {
return;
}
this._lastState.text = text;
this._lastState.fps = fps;
this._lastState.addTime = additionalTime;
const regex = /"([^"]*)"|'([^']*)'|“([^”]*)”|([^]*)/g;
let match;
let quotedText = "";
while ((match = regex.exec(text)) !== null) {
quotedText += (match[1] || match[2] || match[3] || match[4] || "") + " ";
}
const words = quotedText.trim().split(/\s+/).filter(w => w.length > 0);
const wordCount = words.length;
const formatTime = (wpm) => {
const baseMins = wordCount / wpm;
const totalSecs = (baseMins * 60) + additionalTime;
const mins = Math.floor(totalSecs / 60);
let secs = totalSecs % 60;
secs = Math.ceil(secs * 10) / 10;
const frames = Math.ceil(totalSecs * fps);
const secsStr = secs.toFixed(1);
const timeStr = mins > 0 ? `${mins}m ${secsStr}s` : `${secsStr}s`;
return {
time: timeStr,
frames: frames.toString()
};
};
const statsData = {
empty: (wordCount === 0 && additionalTime === 0),
wordCount: wordCount,
additionalTime: additionalTime,
slow: formatTime(100),
avg: formatTime(130),
fast: formatTime(160)
};
// 4. INJECT HTML TO THE DOM
// Replaces the heavy canvas redrawing!
uiContainer.innerHTML = buildUIHTML(statsData);
this.setDirtyCanvas(true, false);
};
// We still use onDrawBackground as an efficient silent trigger
// to auto-update in case upstream nodes silently change their text
const onDrawBackground = this.onDrawBackground;
this.onDrawBackground = function(ctx) {
if (onDrawBackground) onDrawBackground.apply(this, arguments);
updateStats();
};
// Bind events to update in real time based on node interactions
setTimeout(() => {
const textWidget = this.widgets && this.widgets.find(w => w.name === "text");
const fpsWidget = this.widgets && this.widgets.find(w => w.name === "fps");
const additionalTimeWidget = this.widgets && this.widgets.find(w => w.name === "additional_time");
if (textWidget) {
const origCallback = textWidget.callback;
textWidget.callback = function() {
if (origCallback) origCallback.apply(this, arguments);
updateStats();
}
}
if (fpsWidget) {
const origFpsCallback = fpsWidget.callback;
fpsWidget.callback = function() {
if (origFpsCallback) origFpsCallback.apply(this, arguments);
updateStats();
}
}
if (additionalTimeWidget) {
const origAddCallback = additionalTimeWidget.callback;
additionalTimeWidget.callback = function() {
if (origAddCallback) origAddCallback.apply(this, arguments);
updateStats();
}
}
updateStats();
}, 100);
return r;
};
}
}
});