Files
WhatDreamsCost-Dumas/js/speech_length_calculator.js
2026-03-25 15:56:40 -05:00

189 lines
9.2 KiB
JavaScript
Raw 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";
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;
// Add a read-only multiline widget to display the results natively in the node
const statsWidget = ComfyWidgets["STRING"](this, "Stats", ["STRING", { multiline: true }], app).widget;
// Style the widget so it looks like a stats display and prevent editing
if (statsWidget.inputEl) {
statsWidget.inputEl.readOnly = true;
statsWidget.inputEl.style.opacity = "0.9";
statsWidget.inputEl.style.backgroundColor = "rgba(0,0,0,0.3)";
statsWidget.inputEl.style.fontFamily = "monospace";
statsWidget.inputEl.style.pointerEvents = "none";
statsWidget.inputEl.style.height = "150px"; // Increased to fit new line
statsWidget.inputEl.style.minHeight = "150px";
statsWidget.inputEl.style.overflow = "hidden";
}
// Explicitly tell LiteGraph how much space this custom widget requires
statsWidget.computeSize = function() {
return [0, 155]; // 150px for the height + 5px padding
};
// Force the node to recalculate its dimensions to perfectly fit all widgets.
requestAnimationFrame(() => {
const targetWidth = 400;
const targetHeight = 400;
if (this.computeSize) {
const minSize = this.computeSize([targetWidth, targetHeight]);
this.size = [Math.max(targetWidth, minSize[0]), Math.max(targetHeight, minSize[1])];
} else {
this.size = [targetWidth, targetHeight];
}
this.setDirtyCanvas(true, true);
});
// Helper to fetch the current active text (from connected node OR internal widget)
this._getCurrentText = () => {
// Check if text_input (or a converted text widget) is connected via link
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) {
// Search for a text/value widget on the connected node
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;
}
}
}
// Fallback to our internal text widget
const textWidget = this.widgets.find(w => w.name === "text");
return textWidget ? (textWidget.value || "") : "";
};
// Track the last state so we only recalculate when values actually change
this._lastState = { text: null, fps: null, addTime: null };
const updateStats = () => {
const fpsWidget = this.widgets.find(w => w.name === "fps");
const additionalTimeWidget = this.widgets.find(w => w.name === "additional_time");
if (!fpsWidget || !statsWidget) return;
const text = this._getCurrentText();
const fps = fpsWidget.value || 24;
const additionalTime = additionalTimeWidget ? parseFloat(additionalTimeWidget.value) || 0 : 0;
// Prevent unnecessary recalculations if nothing has 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;
// Regex to find words inside standard or smart quotes
const regex = /"([^"]*)"|'([^']*)'|“([^”]*)”|([^]*)/g;
let match;
let quotedText = "";
while ((match = regex.exec(text)) !== null) {
quotedText += (match[1] || match[2] || match[3] || match[4] || "") + " ";
}
// Count words
const words = quotedText.trim().split(/\s+/).filter(w => w.length > 0);
const wordCount = words.length;
if (wordCount === 0 && additionalTime === 0) {
statsWidget.value = `Spoken Words: 0\nAdditional Time: 0s\nWPM = Words Per Minute\n(No text inside quotes found.)\n(Wrap spoken text in "quotes" to calculate)`;
return;
}
const formatTime = (wpm) => {
const baseMins = wordCount / wpm;
const totalSecs = (baseMins * 60) + additionalTime;
const mins = Math.floor(totalSecs / 60);
const secs = Math.round(totalSecs % 60);
const frames = Math.ceil(totalSecs * fps);
const secsStr = secs.toString().padStart(2, '0');
return {
time: `${mins}m ${secsStr}s`,
frames: frames.toString()
};
};
const slow = formatTime(100);
const avg = formatTime(130);
const fast = formatTime(160);
statsWidget.value =
`Spoken Words: ${wordCount}
Additional Time: ${additionalTime}s
WPM = Words Per Minute
--------------------------------------------
Speech Speed Length Frames
Slow (100 WPM) ${slow.time.padEnd(10)} ${slow.frames}
Avg (130 WPM) ${avg.time.padEnd(10)} ${avg.frames}
Fast (160 WPM) ${fast.time.padEnd(10)} ${fast.frames}`;
};
// Hook into LiteGraph's drawing cycle to actively poll the upstream node
// This guarantees the display updates if a connected node's text changes
const onDrawBackground = this.onDrawBackground;
this.onDrawBackground = function(ctx) {
if (onDrawBackground) onDrawBackground.apply(this, arguments);
updateStats();
};
// Bind initial events to update the stats in real-time natively
setTimeout(() => {
const textWidget = this.widgets.find(w => w.name === "text");
const fpsWidget = this.widgets.find(w => w.name === "fps");
const additionalTimeWidget = this.widgets.find(w => w.name === "additional_time");
// Update on text typing natively
if (textWidget && textWidget.inputEl) {
textWidget.inputEl.addEventListener("input", updateStats);
} else if (textWidget) {
const origCallback = textWidget.callback;
textWidget.callback = function() {
if (origCallback) origCallback.apply(this, arguments);
updateStats();
}
}
// Update on FPS changing
if (fpsWidget) {
const origFpsCallback = fpsWidget.callback;
fpsWidget.callback = function() {
if (origFpsCallback) origFpsCallback.apply(this, arguments);
updateStats();
}
}
// Update on Additional Time changing
if (additionalTimeWidget) {
const origAddCallback = additionalTimeWidget.callback;
additionalTimeWidget.callback = function() {
if (origAddCallback) origAddCallback.apply(this, arguments);
updateStats();
}
}
// Initial calculation
updateStats();
}, 100);
return r;
};
}
}
});