Changed the structure of extensions in the ExtrasNode folder, moved them to extensions in separate files, for convenience

This commit is contained in:
AlekPet
2025-06-26 23:05:30 +03:00
parent f55297d795
commit 6f84c33442
9 changed files with 1602 additions and 1528 deletions
@@ -0,0 +1,130 @@
.alekpet_extras_node_speechrecognition_box {
margin: 0px;
padding: 0px;
position: absolute;
text-align: center;
cursor: pointer;
font-size: 1rem;
display: flex;
justify-content: center;
flex-flow: column;
width: 45px;
font-family: monospace;
align-items: center;
margin-top: 3px;
}
.alekpet_extras_node_speechrecognition_row {
position: relative;
display: flex;
gap: 3px;
justify-content: center;
align-items: center;
}
.alekpet_extras_node_recognition_icon {
width: 8px;
height: 8px;
border: 1px solid var(--input-text);
background: transparent;
border-radius: 50%;
padding: 0;
margin: 0px 5px 0px 0px;
display: inline-block;
}
.alekpet_extras_node_recognition_icon_active {
border: 1px solid var(--input-text);
background: var(--error-text);
border-radius: 0;
}
.alekpet_extras_node_recognition_icon_box {
display: flex;
justify-content: center;
align-items: baseline;
gap: 3px;
transition: all 0.7s;
margin-top: 2px;
}
.alekpet_extras_node_recognition_icon:hover {
background: var(--error-text);
}
.alekpet_extras_node_recognition_aborted {
transition: all 0.3s;
font-size: 0.6rem;
}
.alekpet_extras_node_recognition_aborted:hover {
color: var(--error-text);
}
.alekpet_extras_node_info {
display: flex;
align-self: center;
}
.alekpet_extras_node_speech_recognition_checkbox {
width: 10px;
height: 10px;
accent-color: var(--input-text);
padding: 0;
margin: 0;
}
.alekpet_extras_node_speech_recognition_checkbox:not(:checked) {
opacity: 0.5;
}
.alekpet_extras_node_recognition_save {
accent-color: var(--p-red-400);
}
.alekpet_extras_node_speech_icon {
font-size: 0.6em;
opacity: 1;
line-height: 1;
margin-top: 2px;
}
.alekpet_extras_node_speech_icon_playing {
opacity: 0.7;
}
.alekpet_extras_node_speech_icon_playing::after {
content: "✖";
position: absolute;
right: 3px;
color: var(--error-text);
}
/* Settings */
.alekpet_extras_node_speech_recognition_settings_props_label {
background: #646363;
color: #cdcbcb;
border-radius: 5px;
padding: 3px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
}
.alekpet_extras_node_speech_recognition_settings_props_label:hover {
background: darkgrey;
color: white;
}
.alekpet_extras_node_speech_recognition_settings_props_input {
max-width: 10px;
padding: 0;
margin: 0;
outline: 0;
}
.alekpet_extras_node_speech_recognition_settings_button {
display: flex;
font-size: 0.6rem;
}
-131
View File
@@ -1,134 +1,3 @@
.alekpet_extras_node_speechrecognition_box {
margin: 0px;
padding: 0px;
position: absolute;
text-align: center;
cursor: pointer;
font-size: 1rem;
display: flex;
justify-content: center;
flex-flow: column;
width: 45px;
font-family: monospace;
align-items: center;
margin-top: 3px;
}
.alekpet_extras_node_speechrecognition_row {
position: relative;
display: flex;
gap: 3px;
justify-content: center;
align-items: center;
}
.alekpet_extras_node_recognition_icon {
width: 8px;
height: 8px;
border: 1px solid var(--input-text);
background: transparent;
border-radius: 50%;
padding: 0;
margin: 0px 5px 0px 0px;
display: inline-block;
}
.alekpet_extras_node_recognition_icon_active {
border: 1px solid var(--input-text);
background: var(--error-text);
border-radius: 0;
}
.alekpet_extras_node_recognition_icon_box {
display: flex;
justify-content: center;
align-items: baseline;
gap: 3px;
transition: all 0.7s;
margin-top: 2px;
}
.alekpet_extras_node_recognition_icon:hover {
background: var(--error-text);
}
.alekpet_extras_node_recognition_aborted {
transition: all 0.3s;
font-size: 0.6rem;
}
.alekpet_extras_node_recognition_aborted:hover {
color: var(--error-text);
}
.alekpet_extras_node_info {
display: flex;
align-self: center;
}
.alekpet_extras_node_speech_recognition_checkbox {
width: 10px;
height: 10px;
accent-color: var(--input-text);
padding: 0;
margin: 0;
}
.alekpet_extras_node_speech_recognition_checkbox:not(:checked) {
opacity: 0.5;
}
.alekpet_extras_node_recognition_save {
accent-color: var(--p-red-400);
}
.alekpet_extras_node_speech_icon {
font-size: 0.6em;
opacity: 1;
line-height: 1;
margin-top: 2px;
}
.alekpet_extras_node_speech_icon_playing {
opacity: 0.7;
}
.alekpet_extras_node_speech_icon_playing::after {
content: "✖";
position: absolute;
right: 3px;
color: var(--error-text);
}
/* Settings */
.alekpet_extras_node_speech_recognition_settings_props_label {
background: #646363;
color: #cdcbcb;
border-radius: 5px;
padding: 3px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
}
.alekpet_extras_node_speech_recognition_settings_props_label:hover {
background: darkgrey;
color: white;
}
.alekpet_extras_node_speech_recognition_settings_props_input {
max-width: 10px;
padding: 0;
margin: 0;
outline: 0;
}
.alekpet_extras_node_speech_recognition_settings_button {
display: flex;
font-size: 0.6rem;
}
/* Style custom Confirm buttons */
.alekpet_confButtons {
border-radius: 3px;
+1 -710
View File
@@ -7,564 +7,17 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { ComfyWidgets } from "../../scripts/widgets.js";
import { $el } from "../../scripts/ui.js";
import { isValidStyle, comfyuiDesktopConfirm } from "./utils.js";
import { addStylesheet } from "../../scripts/utils.js";
import {
SpeechWidget,
makeColorWidget,
createPreiviewSize,
speechRect,
SpeechSynthesis,
checkboxLSCheckedByKey,
} from "./lib/extrasnode/extras_node_widgets.js";
import { RecognationSpeechDialog } from "./lib/extrasnode/extras_node_dialogs.js";
import { makeColorWidget } from "./lib/extrasnode/extras_node_widgets.js";
const convertIdClass = (text) => text.replaceAll(".", "_");
const idExt = "alekpet.ExtrasNode";
// LocalStorage settings
const PreviewImageSizeLS = localStorage.getItem(
`Comfy.Settings.${idExt}.PreviewImage`
);
const PreviewImageColorTextLS = localStorage.getItem(
`Comfy.Settings.${idExt}.PreviewImageColorText`
);
const PreviewImageColorBgLS = localStorage.getItem(
`Comfy.Settings.${idExt}.PreviewImageColorBg`
);
// Recognation & speech
const SpeechAndRecognationSpeechLS = localStorage.getItem(
`Comfy.Settings.${idExt}.SpeechAndRecognationSpeech`
);
// end - Recognation & speech
// Preview image, video and audio select list combo
const PreviewImageVideoComboLS = localStorage.getItem(
`Comfy.Settings.${idExt}.PreviewImageVideoCombo`
);
// Settings set values from LS or default
let PreviewImageSize = PreviewImageSizeLS
? JSON.parse(PreviewImageSizeLS)
: false,
PreviewImageColorText =
PreviewImageColorTextLS && PreviewImageColorTextLS.trim() !== ""
? PreviewImageColorTextLS
: document.documentElement.style.getPropertyValue("--input-text") ||
"#dddddd",
PreviewImageColorBg = PreviewImageColorBgLS ? PreviewImageColorBgLS : "",
// Speech & Recognition widget settings
SpeechAndRecognationSpeech = SpeechAndRecognationSpeechLS
? JSON.parse(SpeechAndRecognationSpeechLS)
: true,
SpeechAndRecognationSpeechSaveAs = JSON.parse(
localStorage.getItem(`${idExt}.SpeechAndRecognationSpeechSaveAs`),
false
),
// Preview image, video and audio select list combo
PreviewImageVideoCombo = PreviewImageVideoComboLS
? JSON.parse(PreviewImageVideoComboLS)
: true;
// Preview image, video and audio content loading function
const SUPPORTS_FORMAT = {
image: ["jpg", "jpeg", "bmp", "png", "gif", "tiff", "avif"],
video: ["mp4", "webm"],
audio: ["ogg", "wav", "mp3", "webm"],
};
const loadingContent = (src) => {
return new Promise((res, rej) => {
let ext = src.slice(src.lastIndexOf(".") + 1).toLowerCase();
ext = /\w+/.exec(ext)[0];
if (SUPPORTS_FORMAT.image.includes(ext)) {
const img = new Image();
img.crossOrigin = "";
img.onload = (e) => res({ raw: img, type: "image", src });
img.onerror = (err) => rej(err);
img.src = src;
}
if (SUPPORTS_FORMAT.video.includes(ext)) {
const video = document.querySelector(".preview_vid");
video.onerror = (err) => rej(err);
video.addEventListener("canplay", (e) => {
if (video.videoWidth !== 0) {
res({ raw: video, type: "video", src });
}
});
video.src = src;
}
if (SUPPORTS_FORMAT.audio.includes(ext)) {
const audio = document.querySelector(".preview_audio");
audio.onerror = (err) => rej(err);
audio.addEventListener("canplaythrough", (e) => {
res({ raw: audio, type: "audio", src });
});
audio.src = src;
}
});
};
function drawVid(raw, canvas, ctx, type) {
ctx.drawImage(raw, 0, 0, canvas.width, canvas.height);
canvas.requanim = requestAnimationFrame(
drawVid.bind(this, raw, canvas, ctx, type)
);
}
//
// Register Extension
app.registerExtension({
name: idExt,
init() {
addStylesheet("css/extrasnode/extras_node_styles.css", import.meta.url);
// -- Preview image, audio, video --
// Close when click on item inside context menu
function closePreviewContextClick() {
if (this.root?.preview_content_combo) {
this.root.preview_content_combo.remove();
this.root.preview_content_combo = null;
}
}
// Add preview element
function addPreviewModule(params, element) {
if (!PreviewImageVideoCombo) return;
const [name, value, options] = params;
if (value instanceof Object) return;
if (!this.root?.preview_content_combo) {
const preview_content_combo = $el(
"div.preview_content_combo",
{
style: {
position: "absolute",
maxWidth: "160px",
zIndex: 9999999,
},
parent: document.body,
},
[
$el("canvas.preview_img", {
style: { width: "100%" },
}),
$el("video.preview_vid", {
style: { opacity: 0, width: 0, height: 0 },
crossOrigin: "anonymous",
autoplay: false,
muted: false,
preload: "auto",
}),
$el("audio.preview_audio", {
style: { opacity: 0, position: "absolute", left: 0 },
crossOrigin: "anonymous",
autoplay: false,
muted: false,
preload: "auto",
controls: true,
}),
]
);
this.root.preview_content_combo = preview_content_combo;
}
LiteGraph.pointerListenerAdd(element, "enter", (e) => {
if (element?.dataset?.value) {
const body_rect = document.body.getBoundingClientRect();
const root_rect = this.root.getBoundingClientRect();
const { x, y } = element.getBoundingClientRect();
//const preview_rect = this.root.preview_content_combo.getBoundingClientRect();
//const scale = app.graph.extra.ds.scale;
const canvas = this.root.preview_content_combo.children[0];
const ctx = canvas.getContext("2d");
loadingContent(
api.apiURL(
`/view?filename=${encodeURIComponent(
element.dataset.value
)}&type=input`
)
).then(({ raw, type, src }) => {
this.root.preview_content_combo.style.maxWidth =
type === "audio" ? "300px" : "160px";
const previewWidth = parseInt(
this.root.preview_content_combo.style.maxWidth
);
// if (scale >= 1) {
// this.root.preview_content_combo.style.top = `${
// (y - root_rect.top) / scale
// }px`;
// } else {
// this.root.preview_content_combo.style.top = `${
// y - root_rect.top
// }px`;
// }
this.root.preview_content_combo.style.top = `${y}px`;
if (
body_rect.width &&
root_rect.right + previewWidth > body_rect.width
) {
this.root.preview_content_combo.style.left = `${
root_rect.left - previewWidth - 10
}px`;
} else {
this.root.preview_content_combo.style.left = `${
root_rect.left + root_rect.width + 10
}px`;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
const video = document.querySelector(".preview_vid");
const audio = document.querySelector(".preview_audio");
if (type === "image") {
canvas.width = raw.naturalWidth;
canvas.height = raw.naturalHeight;
if (!video.paused) {
video.pause();
video.currentTime = 0;
}
cancelAnimationFrame(canvas.requanim);
} else if (type === "video") {
canvas.width = raw.videoWidth;
canvas.height = raw.videoHeight;
video.play();
} else if (type === "audio") {
Object.assign(audio.style, {
opacity: 1,
display: "",
});
audio.play();
}
if (type === "video" || type === "image") {
Object.assign(audio.style, {
opacity: 0,
display: "none",
});
!audio.paused && audio.pause();
ctx.drawImage(raw, 0, 0, canvas.width, canvas.height);
}
if (type === "video")
canvas.requanim = requestAnimationFrame(
drawVid.bind(this, raw, canvas, ctx, type)
);
});
}
});
LiteGraph.pointerListenerAdd(element, "leave", (e) => {
if (element?.dataset?.value) {
const canvas = this.root.preview_content_combo.children[0];
const video = document.querySelector(".preview_vid");
if (!video.paused) {
video.pause();
video.currentTime = 0;
}
cancelAnimationFrame(canvas.requanim);
}
});
}
// AddItem
const addItem = ContextMenu.prototype.addItem;
ContextMenu.prototype.addItem = function () {
const element = addItem?.apply(this, arguments);
addPreviewModule.call(this, arguments, element);
};
// CloseItem
const closeItem = ContextMenu.prototype.close;
ContextMenu.prototype.close = function () {
closeItem?.apply(this, arguments);
closePreviewContextClick.call(this);
};
// Old verison comfyui, support context menu LiteGraph path...
// AddItem
const addItemLitegraph = LiteGraph.ContextMenu.prototype.addItem;
LiteGraph.ContextMenu.prototype.addItem = function () {
const element = addItemLitegraph?.apply(this, arguments);
addPreviewModule.call(this, arguments, element);
};
// CloseItem
const closeItemLitegraph = LiteGraph.ContextMenu.prototype.close;
LiteGraph.ContextMenu.prototype.close = function () {
closeItemLitegraph?.apply(this, arguments);
closePreviewContextClick.call(this);
};
const originalCloseAllContextMenus = LiteGraph.closeAllContextMenus;
LiteGraph.closeAllContextMenus = function () {
originalCloseAllContextMenus?.apply(this, arguments);
const previewCombo = document.querySelector(".preview_content_combo");
if (previewCombo) {
previewCombo.remove();
}
};
// -- end - Preview image, audio, video --
// PreviewImage settings ui
app.ui.settings.addSetting({
id: `${idExt}.PreviewImage`,
name: "🔸 Preview Image",
defaultValue: true,
type: (name, sett, val) => {
const newUI = document.querySelector(".p-dialog-header");
return $el("tr", [
!newUI
? $el("td", [
$el("label", {
textContent: name,
for: convertIdClass(`${idExt}.PreviewImage_size_checkbox`),
}),
])
: "",
$el("td", [
$el(
"label",
{
style: { display: "block" },
textContent: "Display image size: ",
for: convertIdClass(`${idExt}.PreviewImage_size_checkbox`),
},
[
$el("input", {
id: convertIdClass(`${idExt}.PreviewImage_size_checkbox`),
type: "checkbox",
checked: val,
onchange: (e) => {
const checked = !!e.target.checked;
PreviewImageSize = checked;
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImage`,
checked
);
sett(checked);
},
}),
]
),
$el(
"label",
{
textContent: "Color text: ",
style: { display: "block" },
},
[
$el("input", {
id: convertIdClass(`${idExt}.PreviewImage_color_input`),
type: "color",
value: PreviewImageColorText,
onchange: (e) => {
const colorVal =
e.target.value ||
document.documentElement.style.getPropertyValue(
"--input-text"
) ||
"#dddddd";
PreviewImageColorText = colorVal;
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImageColorText`,
PreviewImageColorText
);
},
}),
]
),
$el(
"label",
{
textContent: "Color background: ",
style: { display: "block" },
},
[
$el("input", {
id: convertIdClass(
`${idExt}.PreviewImage_color_background_input`
),
type: "color",
value: PreviewImageColorBg,
onchange: (e) => {
const colorVal = e.target.value || "";
PreviewImageColorBg = colorVal;
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImageColorBg`,
PreviewImageColorBg
);
},
}),
]
),
$el("button", {
textContent: "Reset colors",
onclick: () => {
PreviewImageColorText = isValidStyle(
"color",
document.documentElement.style.getPropertyValue(
"--input-text"
) || "#ddd"
).color_hex;
PreviewImageColorBg = "";
document.querySelector(
"#alekpet_ExtrasNode_PreviewImage_color_input"
).value = PreviewImageColorText;
document.querySelector(
"#alekpet_ExtrasNode_PreviewImage_color_background_input"
).value = PreviewImageColorBg;
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImageColorText`,
PreviewImageColorText
);
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImageColorBg`,
PreviewImageColorBg
);
},
style: {
display: "block",
},
}),
]),
]);
},
});
// Speech & Recognition speech settings ui
app.ui.settings.addSetting({
id: `${idExt}.SpeechAndRecognationSpeech`,
name: "🔸 Speak text & Recognition speech",
defaultValue: true,
type: (name, sett, val) => {
const newUI = document.querySelector(".p-dialog-header");
return $el("tr", [
!newUI
? $el("td", [
$el("label", {
textContent: name,
for: convertIdClass(
`${idExt}.SpeechAndRecognationSpeech_show`
),
}),
])
: "",
$el("td", [
$el(
"label",
{
style: { display: "flex", alignItems: "center" },
textContent: "Enabled: ",
for: convertIdClass(`${idExt}.SpeechAndRecognationSpeech_show`),
},
[
$el("input", {
id: convertIdClass(
`${idExt}.SpeechAndRecognationSpeech_show`
),
type: "checkbox",
checked: val,
onchange: async (e) => {
const checked = !!e.target.checked;
SpeechAndRecognationSpeech = checked;
localStorage.setItem(
`Comfy.Settings.${idExt}.SpeechAndRecognationSpeech`,
checked
);
if (await comfyuiDesktopConfirm("Reload page?")) {
location.reload();
}
sett(checked);
},
}),
$el("span", {
textContent: "(Then reload the page)",
style: { fontSize: "0.6rem", color: "yellow" },
}),
]
),
$el(
"div",
{
style: {
display: "flex",
gap: "5px",
margin: "5px 0",
},
title: "Show modal window when saving recorded audio.",
},
[
$el("span", { textContent: "Output Save as?" }),
$el("input", {
type: "checkbox",
checked: SpeechAndRecognationSpeechSaveAs,
onchange: (e) => {
localStorage.setItem(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
!!e.target.checked
);
SpeechAndRecognationSpeechSaveAs = !!e.target.checked;
checkboxLSCheckedByKey(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
".alekpet_extras_node_recognition_saveAs"
);
},
}),
]
),
$el("button", {
textContent: "Speech settings",
onclick: () => {
new RecognationSpeechDialog().show();
},
style: {
display: "block",
},
}),
]),
]);
},
});
// Preview image, video and audio settings ui
app.ui.settings.addSetting({
id: `${idExt}.PreviewImageVideoCombo`,
name: "🔸 Preview images and videos in the list",
defaultValue: true,
type: "boolean",
onChange(value) {
PreviewImageVideoCombo = value;
},
});
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// --- Preview Text Node
@@ -698,172 +151,10 @@ app.registerExtension({
};
break;
}
case "PreviewImage": {
// Node Created
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const ret = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
if (PreviewImageSize)
createPreiviewSize(this, nodeData.name, {
color: PreviewImageColorText,
});
return ret;
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function ({ images }) {
onExecuted?.apply(this, arguments);
if (PreviewImageSize !== undefined) {
let widgetRes = this?.widgets?.find(
(w) => w.type === "show_resolution"
);
if (!PreviewImageSize) {
if (widgetRes) {
for (const w of this.widgets) {
if (w.type === "show_resolution") {
w?.onRemove();
}
}
this.widgets.length = 0;
this.onResize(this.size);
}
} else {
if (!widgetRes) {
widgetRes = createPreiviewSize(this, nodeData.name, {
color: PreviewImageColorText,
});
this.onResize(this.size);
} else {
widgetRes.element.style.color = PreviewImageColorText;
}
if (images.length) {
const image = new Image();
image.onload = () => {
widgetRes.value = `<div style="border: solid 1px var(--border-color); border-radius: 4px; padding: 5px; ${
PreviewImageColorBg !== ""
? "background:" + PreviewImageColorBg
: ""
}">Width: ${image.naturalWidth}, Height: ${
image.naturalHeight
}</div>`;
};
image.src = api.apiURL(
"/view?" +
new URLSearchParams(images[0]).toString() +
app.getPreviewFormatParam() +
app.getRandParam()
);
}
}
}
};
break;
}
default: {
break;
}
}
// -- Speech & Recognition speech widget
// If ui settings is true and SpeechSynthesis or speechRecognition is not undefined
if (SpeechAndRecognationSpeech && (speechRect || SpeechSynthesis)) {
let nodeIsMultiString = false;
let outputNode = false;
if (nodeData?.input && nodeData?.input?.required) {
for (const inp of Object.keys(nodeData.input.required)) {
if (nodeData.input.required[inp][1]?.multiline) {
const type = nodeData.input.required[inp][0];
if (["STRING"].includes(type)) {
nodeIsMultiString = true;
break;
}
}
}
}
if (nodeData?.output) {
for (const out of nodeData.output) {
if (["STRING"].includes(out)) {
nodeIsMultiString = true;
outputNode = true;
break;
}
}
}
if (nodeIsMultiString) {
// Node Created
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
const ret = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
// Find all widget type customtext
const widgetsTextMulti = this?.widgets?.filter((w) =>
!outputNode
? ["customtext", "converted-widget"].includes(w.type)
: ["customtext"].includes(w.type)
);
const isIncludesSpeech = this?.widgets?.some(
(w) => w.type === "speak_and_recognation_type"
);
await new Promise((res) =>
setTimeout(() => {
res();
}, 16 * this.widgets?.length ?? 1)
);
if (!isIncludesSpeech && widgetsTextMulti?.length) {
widgetsTextMulti.forEach(async (w) => {
this.addCustomWidget(
SpeechWidget(this, "speak_and_recognation", [false, true], w)
);
});
}
return ret;
};
// onConfigure
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = async function (w) {
onConfigure?.apply(this, arguments);
if (w?.widgets_values?.length) {
await new Promise((res) =>
setTimeout(() => {
res();
}, 16 * this.widgets?.length ?? 1)
);
const ids_speech_clear = this.widgets.reduce(function (
arr,
el,
idx
) {
if (el.type === "speak_and_recognation_type") arr.push(idx);
return arr;
},
[]);
for (const id of ids_speech_clear)
this?.widgets[id]?.callback(w.widgets_values[id]);
}
};
}
}
// -- end - Speech & Recognition speech widget
},
});
+241
View File
@@ -0,0 +1,241 @@
/*
* Title: Extras extension - Preview Image Size
* Author: AlekPet
* Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode
*/
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { $el } from "../../scripts/ui.js";
import { isValidStyle } from "./utils.js";
import { createPreiviewSize } from "./lib/extrasnode/extras_node_widgets.js";
const convertIdClass = (text) => text.replaceAll(".", "_");
const idExt = "alekpet.ExtrasNode";
// LocalStorage settings
const PreviewImageSizeLS = localStorage.getItem(
`Comfy.Settings.${idExt}.PreviewImage`
);
const PreviewImageColorTextLS = localStorage.getItem(
`Comfy.Settings.${idExt}.PreviewImageColorText`
);
const PreviewImageColorBgLS = localStorage.getItem(
`Comfy.Settings.${idExt}.PreviewImageColorBg`
);
// Settings set values from LS or default
let PreviewImageSize = PreviewImageSizeLS
? JSON.parse(PreviewImageSizeLS)
: false,
PreviewImageColorText =
PreviewImageColorTextLS && PreviewImageColorTextLS.trim() !== ""
? PreviewImageColorTextLS
: document.documentElement.style.getPropertyValue("--input-text") ||
"#dddddd",
PreviewImageColorBg = PreviewImageColorBgLS ? PreviewImageColorBgLS : "";
// -- Extension: PreviewImageSize --
app.registerExtension({
name: `${idExt}.PreviewImageSize`,
init() {
// PreviewImage settings ui
app.ui.settings.addSetting({
id: `${idExt}.PreviewImage`,
name: "🔸 Preview Image Size",
defaultValue: true,
type: (name, sett, val) => {
const newUI = document.querySelector(".p-dialog-header");
return $el("tr", [
!newUI
? $el("td", [
$el("label", {
textContent: name,
for: convertIdClass(`${idExt}.PreviewImage_size_checkbox`),
}),
])
: "",
$el("td", [
$el(
"label",
{
style: { display: "block" },
textContent: "Display image size: ",
for: convertIdClass(`${idExt}.PreviewImage_size_checkbox`),
},
[
$el("input", {
id: convertIdClass(`${idExt}.PreviewImage_size_checkbox`),
type: "checkbox",
checked: val,
onchange: (e) => {
const checked = !!e.target.checked;
PreviewImageSize = checked;
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImage`,
checked
);
sett(checked);
},
}),
]
),
$el(
"label",
{
textContent: "Color text: ",
style: { display: "block" },
},
[
$el("input", {
id: convertIdClass(`${idExt}.PreviewImage_color_input`),
type: "color",
value: PreviewImageColorText,
onchange: (e) => {
const colorVal =
e.target.value ||
document.documentElement.style.getPropertyValue(
"--input-text"
) ||
"#dddddd";
PreviewImageColorText = colorVal;
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImageColorText`,
PreviewImageColorText
);
},
}),
]
),
$el(
"label",
{
textContent: "Color background: ",
style: { display: "block" },
},
[
$el("input", {
id: convertIdClass(
`${idExt}.PreviewImage_color_background_input`
),
type: "color",
value: PreviewImageColorBg,
onchange: (e) => {
const colorVal = e.target.value || "";
PreviewImageColorBg = colorVal;
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImageColorBg`,
PreviewImageColorBg
);
},
}),
]
),
$el("button", {
textContent: "Reset colors",
onclick: () => {
PreviewImageColorText = isValidStyle(
"color",
document.documentElement.style.getPropertyValue(
"--input-text"
) || "#ddd"
).color_hex;
PreviewImageColorBg = "";
document.querySelector(
"#alekpet_ExtrasNode_PreviewImage_color_input"
).value = PreviewImageColorText;
document.querySelector(
"#alekpet_ExtrasNode_PreviewImage_color_background_input"
).value = PreviewImageColorBg;
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImageColorText`,
PreviewImageColorText
);
localStorage.setItem(
`Comfy.Settings.${idExt}.PreviewImageColorBg`,
PreviewImageColorBg
);
},
style: {
display: "block",
},
}),
]),
]);
},
});
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "PreviewImage") {
// Node Created
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const ret = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
if (PreviewImageSize)
createPreiviewSize(this, nodeData.name, {
color: PreviewImageColorText,
});
return ret;
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function ({ images }) {
onExecuted?.apply(this, arguments);
if (PreviewImageSize !== undefined) {
let widgetRes = this?.widgets?.find(
(w) => w.type === "show_resolution"
);
if (!PreviewImageSize) {
if (widgetRes) {
for (const w of this.widgets) {
if (w.type === "show_resolution") {
w?.onRemove();
}
}
this.widgets.length = 0;
this.onResize(this.size);
}
} else {
if (!widgetRes) {
widgetRes = createPreiviewSize(this, nodeData.name, {
color: PreviewImageColorText,
});
this.onResize(this.size);
} else {
widgetRes.element.style.color = PreviewImageColorText;
}
if (images.length) {
const image = new Image();
image.onload = () => {
widgetRes.value = `<div style="border: solid 1px var(--border-color); border-radius: 4px; padding: 5px; ${
PreviewImageColorBg !== ""
? "background:" + PreviewImageColorBg
: ""
}">Width: ${image.naturalWidth}, Height: ${
image.naturalHeight
}</div>`;
};
image.src = api.apiURL(
"/view?" +
new URLSearchParams(images[0]).toString() +
app.getPreviewFormatParam() +
app.getRandParam()
);
}
}
}
};
}
},
});
+294
View File
@@ -0,0 +1,294 @@
/*
* Title: Extras extension - Preview Media (image, audio, video)
* Author: AlekPet
* Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode
*/
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { $el } from "../../scripts/ui.js";
const idExt = "alekpet.ExtrasNode";
// LocalStorage settings - Preview image, video and audio select list combo
const PreviewImageVideoComboLS = localStorage.getItem(
`Comfy.Settings.${idExt}.PreviewImageVideoCombo`
);
// Settings set values from LS or default - Preview image, video and audio select list combo
let PreviewImageVideoCombo = PreviewImageVideoComboLS
? JSON.parse(PreviewImageVideoComboLS)
: true;
// Preview image, video and audio content loading function
const SUPPORTS_FORMAT = {
image: ["jpg", "jpeg", "bmp", "png", "gif", "tiff", "avif"],
video: ["mp4", "webm"],
audio: ["ogg", "wav", "mp3", "webm"],
};
const loadingContent = (src) => {
return new Promise((res, rej) => {
let ext = src.slice(src.lastIndexOf(".") + 1).toLowerCase();
ext = /\w+/.exec(ext)[0];
if (SUPPORTS_FORMAT.image.includes(ext)) {
const img = new Image();
img.crossOrigin = "";
img.onload = (e) => res({ raw: img, type: "image", src });
img.onerror = (err) => rej(err);
img.src = src;
}
if (SUPPORTS_FORMAT.video.includes(ext)) {
const video = document.querySelector(".preview_vid");
video.onerror = (err) => rej(err);
video.addEventListener("canplay", (e) => {
if (video.videoWidth !== 0) {
res({ raw: video, type: "video", src });
}
});
video.src = src;
}
if (SUPPORTS_FORMAT.audio.includes(ext)) {
const audio = document.querySelector(".preview_audio");
audio.onerror = (err) => rej(err);
audio.addEventListener("canplaythrough", (e) => {
res({ raw: audio, type: "audio", src });
});
audio.src = src;
}
});
};
function drawVid(raw, canvas, ctx, type) {
ctx.drawImage(raw, 0, 0, canvas.width, canvas.height);
canvas.requanim = requestAnimationFrame(
drawVid.bind(this, raw, canvas, ctx, type)
);
}
//
// -- Extension: Preview image, audio, video --
app.registerExtension({
name: `${idExt}.PreviewMedia`,
init() {
// Preview image, video and audio settings ui
app.ui.settings.addSetting({
id: `${idExt}.PreviewImageVideoCombo`,
name: "🔸 Preview images and videos in the list",
defaultValue: true,
type: "boolean",
onChange(value) {
PreviewImageVideoCombo = value;
},
});
// -- Preview image, audio, video --
// Close when click on item inside context menu
function closePreviewContextClick() {
if (this.root?.preview_content_combo) {
this.root.preview_content_combo.remove();
this.root.preview_content_combo = null;
}
}
// Add preview element
function addPreviewModule(params, element) {
if (!PreviewImageVideoCombo) return;
const [name, value, options] = params;
if (value instanceof Object) return;
if (!this.root?.preview_content_combo) {
const preview_content_combo = $el(
"div.preview_content_combo",
{
style: {
position: "absolute",
maxWidth: "160px",
zIndex: 9999999,
},
parent: document.body,
},
[
$el("canvas.preview_img", {
style: { width: "100%" },
}),
$el("video.preview_vid", {
style: { opacity: 0, width: 0, height: 0 },
crossOrigin: "anonymous",
autoplay: false,
muted: false,
preload: "auto",
}),
$el("audio.preview_audio", {
style: { opacity: 0, position: "absolute", left: 0 },
crossOrigin: "anonymous",
autoplay: false,
muted: false,
preload: "auto",
controls: true,
}),
]
);
this.root.preview_content_combo = preview_content_combo;
}
LiteGraph.pointerListenerAdd(element, "enter", (e) => {
if (element?.dataset?.value) {
const body_rect = document.body.getBoundingClientRect();
const root_rect = this.root.getBoundingClientRect();
const { x, y } = element.getBoundingClientRect();
//const preview_rect = this.root.preview_content_combo.getBoundingClientRect();
//const scale = app.graph.extra.ds.scale;
const canvas = this.root.preview_content_combo.children[0];
const ctx = canvas.getContext("2d");
loadingContent(
api.apiURL(
`/view?filename=${encodeURIComponent(
element.dataset.value
)}&type=input`
)
).then(({ raw, type, src }) => {
this.root.preview_content_combo.style.maxWidth =
type === "audio" ? "300px" : "160px";
const previewWidth = parseInt(
this.root.preview_content_combo.style.maxWidth
);
// if (scale >= 1) {
// this.root.preview_content_combo.style.top = `${
// (y - root_rect.top) / scale
// }px`;
// } else {
// this.root.preview_content_combo.style.top = `${
// y - root_rect.top
// }px`;
// }
this.root.preview_content_combo.style.top = `${y}px`;
if (
body_rect.width &&
root_rect.right + previewWidth > body_rect.width
) {
this.root.preview_content_combo.style.left = `${
root_rect.left - previewWidth - 10
}px`;
} else {
this.root.preview_content_combo.style.left = `${
root_rect.left + root_rect.width + 10
}px`;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
const video = document.querySelector(".preview_vid");
const audio = document.querySelector(".preview_audio");
if (type === "image") {
canvas.width = raw.naturalWidth;
canvas.height = raw.naturalHeight;
if (!video.paused) {
video.pause();
video.currentTime = 0;
}
cancelAnimationFrame(canvas.requanim);
} else if (type === "video") {
canvas.width = raw.videoWidth;
canvas.height = raw.videoHeight;
video.play();
} else if (type === "audio") {
Object.assign(audio.style, {
opacity: 1,
display: "",
});
audio.play();
}
if (type === "video" || type === "image") {
Object.assign(audio.style, {
opacity: 0,
display: "none",
});
!audio.paused && audio.pause();
ctx.drawImage(raw, 0, 0, canvas.width, canvas.height);
}
if (type === "video")
canvas.requanim = requestAnimationFrame(
drawVid.bind(this, raw, canvas, ctx, type)
);
});
}
});
LiteGraph.pointerListenerAdd(element, "leave", (e) => {
if (element?.dataset?.value) {
const canvas = this.root.preview_content_combo.children[0];
const video = document.querySelector(".preview_vid");
if (!video.paused) {
video.pause();
video.currentTime = 0;
}
cancelAnimationFrame(canvas.requanim);
}
});
}
// AddItem
const addItem = ContextMenu.prototype.addItem;
ContextMenu.prototype.addItem = function () {
const element = addItem?.apply(this, arguments);
addPreviewModule.call(this, arguments, element);
};
// CloseItem
const closeItem = ContextMenu.prototype.close;
ContextMenu.prototype.close = function () {
closeItem?.apply(this, arguments);
closePreviewContextClick.call(this);
};
// Old verison comfyui, support context menu LiteGraph path...
// AddItem
const addItemLitegraph = LiteGraph.ContextMenu.prototype.addItem;
LiteGraph.ContextMenu.prototype.addItem = function () {
const element = addItemLitegraph?.apply(this, arguments);
addPreviewModule.call(this, arguments, element);
};
// CloseItem
const closeItemLitegraph = LiteGraph.ContextMenu.prototype.close;
LiteGraph.ContextMenu.prototype.close = function () {
closeItemLitegraph?.apply(this, arguments);
closePreviewContextClick.call(this);
};
const originalCloseAllContextMenus = LiteGraph.closeAllContextMenus;
LiteGraph.closeAllContextMenus = function () {
originalCloseAllContextMenus?.apply(this, arguments);
const previewCombo = document.querySelector(".preview_content_combo");
if (previewCombo) {
previewCombo.remove();
}
};
// -- end - Preview image, audio, video --
},
});
@@ -0,0 +1,234 @@
/*
* Title: Extras extension - Speech And Recognition
* Author: AlekPet
* Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode
*/
import { app } from "../../scripts/app.js";
import { $el } from "../../scripts/ui.js";
import { comfyuiDesktopConfirm } from "./utils.js";
import { addStylesheet } from "../../scripts/utils.js";
import {
SpeechWidget,
speechRect,
SpeechSynthesis,
checkboxLSCheckedByKey,
} from "./lib/extrasnode/extras_speech_and_recognation_widget.js";
import { RecognationSpeechDialog } from "./lib/extrasnode/extras_speech_and_recognation_dialog.js";
const convertIdClass = (text) => text.replaceAll(".", "_");
const idExt = "alekpet.ExtrasNode";
// Recognation & speech localStorage settings
const SpeechAndRecognationSpeechLS = localStorage.getItem(
`Comfy.Settings.${idExt}.SpeechAndRecognationSpeech`
);
// Settings set values from LS or default for Speech & Recognition widget settings
let SpeechAndRecognationSpeech = SpeechAndRecognationSpeechLS
? JSON.parse(SpeechAndRecognationSpeechLS)
: true,
SpeechAndRecognationSpeechSaveAs = JSON.parse(
localStorage.getItem(`${idExt}.SpeechAndRecognationSpeechSaveAs`),
false
);
// -- Extension: Speak text & Recognition speech --
app.registerExtension({
name: `${idExt}.SpeechAndRecognationSpeech`,
init() {
addStylesheet(
"css/extrasnode/extras_node_speech_recognition_style.css",
import.meta.url
);
// Speech & Recognition speech settings ui
app.ui.settings.addSetting({
id: `${idExt}.SpeechAndRecognationSpeech`,
name: "🔸 Speak text & Recognition speech",
defaultValue: true,
type: (name, sett, val) => {
const newUI = document.querySelector(".p-dialog-header");
return $el("tr", [
!newUI
? $el("td", [
$el("label", {
textContent: name,
for: convertIdClass(
`${idExt}.SpeechAndRecognationSpeech_show`
),
}),
])
: "",
$el("td", [
$el(
"label",
{
style: { display: "flex", alignItems: "center" },
textContent: "Enabled: ",
for: convertIdClass(`${idExt}.SpeechAndRecognationSpeech_show`),
},
[
$el("input", {
id: convertIdClass(
`${idExt}.SpeechAndRecognationSpeech_show`
),
type: "checkbox",
checked: val,
onchange: async (e) => {
const checked = !!e.target.checked;
SpeechAndRecognationSpeech = checked;
localStorage.setItem(
`Comfy.Settings.${idExt}.SpeechAndRecognationSpeech`,
checked
);
if (await comfyuiDesktopConfirm("Reload page?")) {
location.reload();
}
sett(checked);
},
}),
$el("span", {
textContent: "(Then reload the page)",
style: { fontSize: "0.6rem", color: "yellow" },
}),
]
),
$el(
"div",
{
style: {
display: "flex",
gap: "5px",
margin: "5px 0",
},
title: "Show modal window when saving recorded audio.",
},
[
$el("span", { textContent: "Output Save as?" }),
$el("input", {
type: "checkbox",
checked: SpeechAndRecognationSpeechSaveAs,
onchange: (e) => {
localStorage.setItem(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
!!e.target.checked
);
SpeechAndRecognationSpeechSaveAs = !!e.target.checked;
checkboxLSCheckedByKey(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
".alekpet_extras_node_recognition_saveAs"
);
},
}),
]
),
$el("button", {
textContent: "Speech settings",
onclick: () => {
new RecognationSpeechDialog().show();
},
style: {
display: "block",
},
}),
]),
]);
},
});
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// -- Speech & Recognition speech widget
// If ui settings is true and SpeechSynthesis or speechRecognition is not undefined
if (SpeechAndRecognationSpeech && (speechRect || SpeechSynthesis)) {
let nodeIsMultiString = false;
let outputNode = false;
if (nodeData?.input && nodeData?.input?.required) {
for (const inp of Object.keys(nodeData.input.required)) {
if (nodeData.input.required[inp][1]?.multiline) {
const type = nodeData.input.required[inp][0];
if (["STRING"].includes(type)) {
nodeIsMultiString = true;
break;
}
}
}
}
if (nodeData?.output) {
for (const out of nodeData.output) {
if (["STRING"].includes(out)) {
nodeIsMultiString = true;
outputNode = true;
break;
}
}
}
if (nodeIsMultiString) {
// Node Created
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
const ret = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
// Find all widget type customtext
const widgetsTextMulti = this?.widgets?.filter((w) =>
!outputNode
? ["customtext", "converted-widget"].includes(w.type)
: ["customtext"].includes(w.type)
);
const isIncludesSpeech = this?.widgets?.some(
(w) => w.type === "speak_and_recognation_type"
);
await new Promise((res) =>
setTimeout(() => {
res();
}, 16 * this.widgets?.length ?? 1)
);
if (!isIncludesSpeech && widgetsTextMulti?.length) {
widgetsTextMulti.forEach(async (w) => {
this.addCustomWidget(
SpeechWidget(this, "speak_and_recognation", [false, true], w)
);
});
}
return ret;
};
// onConfigure
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = async function (w) {
onConfigure?.apply(this, arguments);
if (w?.widgets_values?.length) {
await new Promise((res) =>
setTimeout(() => {
res();
}, 16 * this.widgets?.length ?? 1)
);
const ids_speech_clear = this.widgets.reduce(function (
arr,
el,
idx
) {
if (el.type === "speak_and_recognation_type") arr.push(idx);
return arr;
},
[]);
for (const id of ids_speech_clear)
this?.widgets[id]?.callback(w.widgets_values[id]);
}
};
}
}
// -- end - Speech & Recognition speech widget
},
});
+2 -685
View File
@@ -4,687 +4,12 @@
* Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode
*/
import { api } from "../../../../scripts/api.js";
import { app } from "../../../../scripts/app.js";
import { $el } from "../../../../scripts/ui.js";
import {
rgbToHex,
isValidStyle,
createWindowModal,
animateClick,
comfyuiDesktopAlert,
} from "../../utils.js";
import { RecognationSpeechDialog } from "./extras_node_dialogs.js";
import { rgbToHex, isValidStyle } from "../../utils.js";
const idExt = "alekpet.ExtrasNode";
const CONVERTED_TYPE = "converted-widget";
/* ~~~ Speech & Recognition speech Widget ~~~ */
const spRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
const SpeechSynthesis = window.speechSynthesis;
const regExpFileName = /[^\p{L}\d]+/gu;
const maxLenAudioFileName = 50;
let speechRect;
let mediaRecorder = null;
let audioChunks = [];
if (spRecognition) {
speechRect = new spRecognition();
speechRect.elements = null;
speechRect.isRecognition = false;
speechRect.isRecognitionAbort = false;
speechRect.lastText = "";
speechRect.addEventListener("result", (event) => {
const results = event?.results[0][0]?.transcript;
if (results && speechRect?.elements?.length) {
const clear_text = speechRect.elements[0].querySelector(
".alekpet_extras_node_recognition_clear"
);
if (clear_text.checked) {
speechRect.elements[1].value = results;
} else {
speechRect.elements[1].value += " " + results;
}
}
});
const resetSpeechRecognition = () => {
if (speechRect?.elements?.length) {
const info = speechRect.elements[0].querySelector(
".alekpet_extras_node_info span"
);
const icon_rec = speechRect.elements[0].querySelector(
".alekpet_extras_node_recognition_icon"
);
speechRect.elements[0].querySelector(
".alekpet_extras_node_recognition_aborted"
).style.display = "none";
icon_rec.classList.remove("alekpet_extras_node_recognition_icon_active");
setTimeout(() => (info.textContent = ""), 1000);
}
setStylesAllElements(".alekpet_extras_node_recognition_icon", null, {
display: "inline-block",
});
if (mediaRecorder) {
speechRect.lastText = speechRect?.elements[1].value;
mediaRecorder.stop();
} else {
speechRect.lastText = "";
speechRect.isRecognitionAbort = false;
}
mediaRecorder = null;
audioChunks = [];
speechRect.isRecognition = false;
speechRect.elements = null;
};
speechRect.addEventListener("audiostart", (e) => {
speechRect.isRecognition = true;
});
speechRect.addEventListener("end", () => {
resetSpeechRecognition();
speechRect.stop();
});
speechRect.addEventListener("speechend", () => {
resetSpeechRecognition();
speechRect.stop();
});
speechRect.addEventListener("error", (event) => {
if (speechRect?.elements?.length) {
const info = speechRect.elements[0].querySelector(
".alekpet_extras_node_info span"
);
info.textContent = event.error;
}
resetSpeechRecognition();
console.log(">> Error recognition: " + event.error);
});
} else {
console.warn(
"Your browser does not support: SpeechRecognition.\nIf Firefox: go to about:config > media.webspeech.recognition.enable and media.webspeech.recognition.force_enable > set true"
);
}
if (!SpeechSynthesis) {
console.warn("Your browser does not support: speechSynthesis");
}
// Get position speech SpeechRecognition widget
function getPostition(ctx, w_width, y, n_height, wInput) {
const MARGIN = 10;
const rect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(rect.width / ctx.canvas.width, rect.height / ctx.canvas.height)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN);
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
return {
transformOrigin: "0 0",
transform: scale,
transform: transform,
left: `${transform.a * w_width - 70 * scale.a + rect.left}px`,
top: `${(wInput.last_y - 15) * scale.d + scale.f + rect.top}px`,
maxWidth: `${w_width - MARGIN * 2}px`,
maxHeight: `${n_height - MARGIN * 2}px`,
zIndex: wInput?.inputEl?.style?.zIndex
? +wInput?.inputEl.style.zIndex + 1
: 20,
};
}
function getVoiceAndSettings() {
const voices = speechSynthesis.getVoices();
const { voice, volume, pitch, rate } =
RecognationSpeechDialog.getSettingsRecSpeechLS();
const voiceSelected = voices.filter((v) => v.name === voice);
return {
voice: voiceSelected.length ? voiceSelected[0] : null,
volume,
pitch,
rate,
};
}
// Function return utterance
function speakSynthesisUtterance(text, options = {}) {
const utterance = new SpeechSynthesisUtterance(text);
Object.assign(utterance, { ...options, ...getVoiceAndSettings() });
return utterance;
}
// Check premissions
async function checkPremissions(
device = { name: "microphone" },
update = null
) {
return navigator.permissions
.query(device)
.then((result) => {
const state = result.state;
if (state == "granted") {
return { device, state, status: true };
} else if (state == "prompt") {
return { device, state, status: false };
} else if (state == "denied") {
return { device, state, status: false };
}
result.onchange = update;
})
.catch((e) => ({ device, state: "error", status: false }));
}
// Check checkbox
function checkboxLSCheckedByKey(lsKey, selector, defaultVal = false) {
const currValue = JSON.parse(localStorage.getItem(lsKey, defaultVal));
Array.from(document.querySelectorAll(selector)).forEach(
(saveAs) => (saveAs.checked = currValue)
);
}
// Set styles
const setStylesAllElements = (selector, exclude = null, styles = {}) => {
let elements = Array.from(document.querySelectorAll(selector));
if (exclude) elements = elements.filter((r) => r !== exclude);
elements = elements.map((r) => Object.assign(r.style, styles));
};
function SpeechWidget(node, inputName, inputData, widgetsText) {
const widget = {
type: "speak_and_recognation_type",
name: inputName,
value: inputData,
size: [22, 1],
options: { hideOnZoom: true },
text_element: widgetsText?.inputEl,
draw(ctx, node, widget_width, y, widget_height) {
const hidden = widgetsText?.element?.hidden || widgetsText.hidden;
widget.element.style.display =
hidden || app.canvas.ds.scale < 0.6 ? "none" : "flex";
if (hidden) {
widget.options.onHide?.(widget);
}
if (hidden) {
return;
}
Object.assign(
widget.element.style,
getPostition(ctx, widget_width, y, node.size[1], widgetsText)
);
},
computeSize(...args) {
return [22, 1];
},
async callback(v) {
if (widgetsText?.element?.hasAttribute("readonly")) return;
widget.value = v ?? inputData ?? [false, true];
const checkboxSave = widget.element.querySelector(
".alekpet_extras_node_recognition_save"
);
const checkboxClear = widget.element.querySelector(
".alekpet_extras_node_recognition_clear"
);
if (checkboxClear) checkboxClear.checked = widget.value[1] ?? false;
if (checkboxSave) checkboxSave.checked = widget.value[0] ?? false;
},
onRemove() {
widget.element?.remove();
},
};
const buttons = [];
if (speechRect && !widgetsText?.element?.hasAttribute("readonly")) {
buttons.push(
$el("div.alekpet_extras_node_recognition_icon_box", [
$el("span.alekpet_extras_node_recognition_aborted", {
title: "Speech recognition abort",
textContent: "✖",
style: { display: "none" },
onclick: async function (e) {
const info = widget.element.querySelector(
".alekpet_extras_node_info span"
);
if (speechRect.isRecognition) {
e.currentTarget.style.display = "none";
speechRect.isRecognitionAbort = true;
speechRect.abort();
info.textContent = "aborted";
this.classList.remove(
"alekpet_extras_node_recognition_icon_active"
);
setTimeout(() => (info.textContent = ""), 2000);
}
},
}),
$el("span.alekpet_extras_node_recognition_icon", {
title: "Speech recognition",
onclick: async function (e) {
const info = widget.element.querySelector(
".alekpet_extras_node_info span"
);
const abort = widget.element.querySelector(
".alekpet_extras_node_recognition_aborted"
);
const checkboxSave = widget.element.querySelector(
".alekpet_extras_node_recognition_save"
);
// Hide other recognitions buttons
setStylesAllElements(
".alekpet_extras_node_recognition_icon",
e.currentTarget,
{
display: "none",
}
);
// Recognition
if (speechRect.elements === null) {
// Record audio
if (checkboxSave.checked) {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = async () => {
if (speechRect.isRecognitionAbort) {
speechRect.lastText = "";
speechRect.isRecognitionAbort = false;
return;
}
const saveAsWindow = JSON.parse(
localStorage.getItem(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
false
)
);
// Filename
let nameFile = "recording.webm";
if (speechRect?.lastText?.length) {
nameFile = `${speechRect.lastText
.slice(0, maxLenAudioFileName)
.replaceAll(regExpFileName, "_")}.webm`;
}
// Get audio
const audioBlob = new Blob(audioChunks, {
type: "audio/webm",
});
if (!saveAsWindow) {
const body = new FormData();
body.append("image", audioBlob, nameFile);
body.append("overwrite", "true");
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status !== 200) {
console.error(
"[ExtrasNode] Recording audio not saved!"
);
return;
}
console.log(
`[ExtrasNode] Recording audio "${nameFile}" saved successfully!`
);
} else {
const audioUrl = URL.createObjectURL(audioBlob);
const linkDown = document.createElement("a");
linkDown.href = audioUrl;
linkDown.download = nameFile;
linkDown.click();
}
speechRect.lastText = "";
};
} catch (err) {
comfyuiDesktopAlert(
`Device "Microphone" - ${err.message}!\n\nCheck device or allow access!`
);
}
}
// end - Record audio
speechRect.elements = [widget.element, widgetsText.inputEl];
info.textContent = "recognition";
this.classList.add("alekpet_extras_node_recognition_icon_active");
speechRect.start();
mediaRecorder && mediaRecorder.start();
abort.style.display = "inline-block";
} else {
speechRect.stop();
abort.style.display = "none";
info.textContent = "stoped";
this.classList.remove(
"alekpet_extras_node_recognition_icon_active"
);
setTimeout(() => (info.textContent = ""), 2000);
}
},
}),
// Settings elements
$el("div.alekpet_extras_node_speech_recognition_settings", [
$el(
"div.alekpet_extras_node_speech_recognition_settings_button.pi.pi-cog",
{
title: "Settings",
onclick: (e) => {
checkboxLSCheckedByKey(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
".alekpet_extras_node_recognition_saveAs"
);
animateClick(e.currentTarget.nextElementSibling);
},
}
),
createWindowModal({
textTitle: null,
stylesClose: {
top: "-4px",
right: "4px",
width: "auto",
height: "auto",
padding: "2px",
fontSize: "0.4rem",
lineHeight: 1,
},
stylesBox: {
background: "transparent",
border: 0,
padding: 0,
boxShadow: "none",
},
stylesBody: {
display: "flex",
flexDirection: "column",
alignItems: "stretch",
gap: "3px",
textWrap: "wrap",
background: "rgb(131, 131, 131)",
color: "white",
padding: "2px",
marginTop: "2px",
borderRadius: "6px",
fontSize: "0.4rem",
minWidth: "85px",
},
stylesWrapper: {
minWidth: "100px",
transform: "translate(0%, 0%)",
},
textBody: [
$el(
"label.alekpet_extras_node_speech_recognition_settings_props_label",
[
$el(
"span.alekpet_extras_node_speech_recognition_settings_props_name",
{
textContent: "Recoding audio",
title: "Save in audio file after recognition",
}
),
$el(
"input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_save",
{
type: "checkbox",
checked: widget.value[0] ?? false,
onchange: (e) =>
widget?.callback([!!e.target.checked, widget.value[1]]),
}
),
]
),
$el(
"label.alekpet_extras_node_speech_recognition_settings_props_label",
[
$el(
"span.alekpet_extras_node_speech_recognition_settings_props_name",
{
textContent: "Clear text",
title: "Clear text after recognition",
}
),
$el(
"input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_clear",
{
type: "checkbox",
checked: widget.value[1] ?? true,
onchange: (e) =>
widget?.callback([widget.value[0], !!e.target.checked]),
}
),
]
),
$el("hr", { style: { padding: 0, margin: 0 } }),
$el(
"label.alekpet_extras_node_speech_recognition_settings_props_label",
[
$el(
"span.alekpet_extras_node_speech_recognition_settings_props_name",
{
textContent: "'Save as' window?",
title: "Show modal window when saving recorded audio.",
}
),
$el(
"input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_saveAs",
{
type: "checkbox",
checked: JSON.parse(
localStorage.getItem(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
false
)
),
onchange: (e) => {
const check = !!e.target.checked;
const settCheck = document.body.querySelector(
"[id$='.SpeechAndRecognationSpeech'] div input"
);
settCheck && (settCheck.checked = check);
localStorage.setItem(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
check
);
checkboxLSCheckedByKey(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
".alekpet_extras_node_recognition_saveAs"
);
},
}
),
]
),
],
}),
]),
// end -- Settings elements
])
);
}
if (SpeechSynthesis && speechSynthesis) {
function buttonsStyles(
speechesButtons,
action = "add",
className = "alekpet_extras_node_speech_icon_playing"
) {
speechesButtons?.forEach((speechButton) =>
speechButton?.classList[action](className)
);
const settingTestSpeech = document.querySelector(
".panel_settings_recognation_speech_voice_test"
);
if (settingTestSpeech) {
if (action === "add") {
settingTestSpeech.style.opacity = 0.7;
settingTestSpeech.style.color = "var(--error-text)";
settingTestSpeech.title = "Cancel test speech";
settingTestSpeech.textContent = "Cancel test";
} else {
settingTestSpeech.style.opacity = 1;
settingTestSpeech.title = "Run test speech";
settingTestSpeech.textContent = "Test speech";
settingTestSpeech.style.color = "limegreen";
}
}
}
buttons.push(
$el("span.alekpet_extras_node_speech_icon", {
onclick: function () {
try {
const info = widget.element.querySelector(
".alekpet_extras_node_info span"
);
const speechesButtons = Array.from(
document.querySelectorAll(".alekpet_extras_node_speech_icon")
);
// Already playing
if (speechSynthesis.speaking) {
SpeechSynthesis.cancel();
info.textContent = "canceled";
this.title = "Speak text";
buttonsStyles(speechesButtons, "remove");
setTimeout(() => (info.textContent = ""), 2000);
return;
}
// Start playing text
const text = widgetsText?.element?.value;
if (text.trim() !== "") {
const utterance = speakSynthesisUtterance(text, {
onend: (e) => {
this.title = "Speak text";
info.textContent = "";
buttonsStyles(speechesButtons, "remove");
},
});
if (utterance) {
this.title = "Cancel speech";
info.textContent = "saying now";
buttonsStyles(speechesButtons);
SpeechSynthesis.speak(utterance);
}
} else {
const utterance = speakSynthesisUtterance(
`${widgetsText?.name || "This"}}, field is empty!`,
{
onend: (e) => {
this.title = "Speak text";
info.textContent = "";
buttonsStyles(speechesButtons, "remove");
},
}
);
if (utterance) {
this.title = "Cancel speech";
info.textContent = "empty text!";
buttonsStyles(speechesButtons);
SpeechSynthesis.speak(utterance);
}
}
} catch (err) {
console.log(err);
}
},
textContent: "🔊",
title: "Speak text",
})
);
}
widget.element = $el(
"div.alekpet_extras_node_speechrecognition_box",
{ style: { display: "none" } },
[
$el("div.alekpet_extras_node_speechrecognition_row", [...buttons]),
$el("div.alekpet_extras_node_info", [
$el("span", {
textContent: "",
style: { fontSize: "0.4em" },
}),
]),
]
);
const collapse = node.collapse;
node.collapse = function () {
collapse.apply(this, arguments);
if (this.flags?.collapsed) {
widget.element.hidden = true;
widget.element.style.display = "none";
}
widget.element.dataset.collapsed = this.flags?.collapsed ? "true" : "false";
};
const onRemovedOrig = node.onRemoved;
node.onRemoved = function () {
node?.widgets?.forEach((w) => {
if (w.type === "speak_and_recognation_type") {
w?.onRemove();
}
});
onRemovedOrig?.apply(node, arguments);
};
document.body.appendChild(widget.element);
return widget;
}
/* ~~~ end - Speech & Recognition speech Widget ~~~ */
/* ~~~ Color Widget ~~~ */
function makeColorWidget(node, inputName, inputData, widget) {
const color_hex = $el("input", {
@@ -781,12 +106,4 @@ function createPreiviewSize(node, name, options) {
}
/* ~~~ end - Preview Image Size Widget ~~~ */
export {
SpeechWidget,
makeColorWidget,
createPreiviewSize,
speechRect,
SpeechSynthesis,
speakSynthesisUtterance,
checkboxLSCheckedByKey,
};
export { makeColorWidget, createPreiviewSize };
@@ -1,6 +1,12 @@
/*
* Title: Extras extension - Speech And Recognition Dialog
* Author: AlekPet
* Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode
*/
import { ComfyDialog } from "../../../../../scripts/ui.js";
import { makeElement } from "../../utils.js";
import { speakSynthesisUtterance } from "./extras_node_widgets.js";
import { speakSynthesisUtterance } from "./extras_speech_and_recognation_widget.js";
const idExt = "alekpet.ExtrasNode";
@@ -34,7 +40,7 @@ export class RecognationSpeechDialog extends ComfyDialog {
class: ["panel_settings_recognation_speech"],
style: {
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateColumns: "0.2fr 1fr 0.2fr",
alignItems: "center",
justifyContent: "center",
gap: "5px",
@@ -200,6 +206,7 @@ export class RecognationSpeechDialog extends ComfyDialog {
style: {
width: "80vw",
color: "var(--input-text)",
padding: "20px",
},
children: [await this.createPanelSettings()],
});
@@ -0,0 +1,691 @@
/*
* Title: Extras Speech And Recognation Widget
* Author: AlekPet
* Github: https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/tree/master/ExtrasNode
*/
import { api } from "../../../../scripts/api.js";
import { app } from "../../../../scripts/app.js";
import { $el } from "../../../../scripts/ui.js";
import {
createWindowModal,
animateClick,
comfyuiDesktopAlert,
} from "../../utils.js";
import { RecognationSpeechDialog } from "./extras_speech_and_recognation_dialog.js";
const idExt = "alekpet.ExtrasNode";
/* ~~~ Speech & Recognition speech Widget ~~~ */
const spRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition;
const SpeechSynthesis = window.speechSynthesis;
const regExpFileName = /[^\p{L}\d]+/gu;
const maxLenAudioFileName = 50;
let speechRect;
let mediaRecorder = null;
let audioChunks = [];
if (spRecognition) {
speechRect = new spRecognition();
speechRect.elements = null;
speechRect.isRecognition = false;
speechRect.isRecognitionAbort = false;
speechRect.lastText = "";
speechRect.addEventListener("result", (event) => {
const results = event?.results[0][0]?.transcript;
if (results && speechRect?.elements?.length) {
const clear_text = speechRect.elements[0].querySelector(
".alekpet_extras_node_recognition_clear"
);
if (clear_text.checked) {
speechRect.elements[1].value = results;
} else {
speechRect.elements[1].value += " " + results;
}
}
});
const resetSpeechRecognition = () => {
if (speechRect?.elements?.length) {
const info = speechRect.elements[0].querySelector(
".alekpet_extras_node_info span"
);
const icon_rec = speechRect.elements[0].querySelector(
".alekpet_extras_node_recognition_icon"
);
speechRect.elements[0].querySelector(
".alekpet_extras_node_recognition_aborted"
).style.display = "none";
icon_rec.classList.remove("alekpet_extras_node_recognition_icon_active");
setTimeout(() => (info.textContent = ""), 1000);
}
setStylesAllElements(".alekpet_extras_node_recognition_icon", null, {
display: "inline-block",
});
if (mediaRecorder) {
speechRect.lastText = speechRect?.elements[1].value;
mediaRecorder.stop();
} else {
speechRect.lastText = "";
speechRect.isRecognitionAbort = false;
}
mediaRecorder = null;
audioChunks = [];
speechRect.isRecognition = false;
speechRect.elements = null;
};
speechRect.addEventListener("audiostart", (e) => {
speechRect.isRecognition = true;
});
speechRect.addEventListener("end", () => {
resetSpeechRecognition();
speechRect.stop();
});
speechRect.addEventListener("speechend", () => {
resetSpeechRecognition();
speechRect.stop();
});
speechRect.addEventListener("error", (event) => {
if (speechRect?.elements?.length) {
const info = speechRect.elements[0].querySelector(
".alekpet_extras_node_info span"
);
info.textContent = event.error;
}
resetSpeechRecognition();
console.log(">> Error recognition: " + event.error);
});
} else {
console.warn(
"Your browser does not support: SpeechRecognition.\nIf Firefox: go to about:config > media.webspeech.recognition.enable and media.webspeech.recognition.force_enable > set true"
);
}
if (!SpeechSynthesis) {
console.warn("Your browser does not support: speechSynthesis");
}
// Get position speech SpeechRecognition widget
function getPostition(ctx, w_width, y, n_height, wInput) {
const MARGIN = 10;
const rect = ctx.canvas.getBoundingClientRect();
const transform = new DOMMatrix()
.scaleSelf(rect.width / ctx.canvas.width, rect.height / ctx.canvas.height)
.multiplySelf(ctx.getTransform())
.translateSelf(MARGIN, MARGIN);
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
return {
transformOrigin: "0 0",
transform: scale,
transform: transform,
left: `${transform.a * w_width - 70 * scale.a + rect.left}px`,
top: `${(wInput.last_y - 15) * scale.d + scale.f + rect.top}px`,
maxWidth: `${w_width - MARGIN * 2}px`,
maxHeight: `${n_height - MARGIN * 2}px`,
zIndex: wInput?.inputEl?.style?.zIndex
? +wInput?.inputEl.style.zIndex + 1
: 20,
};
}
function getVoiceAndSettings() {
const voices = speechSynthesis.getVoices();
const { voice, volume, pitch, rate } =
RecognationSpeechDialog.getSettingsRecSpeechLS();
const voiceSelected = voices.filter((v) => v.name === voice);
return {
voice: voiceSelected.length ? voiceSelected[0] : null,
volume,
pitch,
rate,
};
}
// Function return utterance
function speakSynthesisUtterance(text, options = {}) {
const utterance = new SpeechSynthesisUtterance(text);
Object.assign(utterance, { ...options, ...getVoiceAndSettings() });
return utterance;
}
// Check premissions
async function checkPremissions(
device = { name: "microphone" },
update = null
) {
return navigator.permissions
.query(device)
.then((result) => {
const state = result.state;
if (state == "granted") {
return { device, state, status: true };
} else if (state == "prompt") {
return { device, state, status: false };
} else if (state == "denied") {
return { device, state, status: false };
}
result.onchange = update;
})
.catch((e) => ({ device, state: "error", status: false }));
}
// Check checkbox
function checkboxLSCheckedByKey(lsKey, selector, defaultVal = false) {
const currValue = JSON.parse(localStorage.getItem(lsKey, defaultVal));
Array.from(document.querySelectorAll(selector)).forEach(
(saveAs) => (saveAs.checked = currValue)
);
}
// Set styles
const setStylesAllElements = (selector, exclude = null, styles = {}) => {
let elements = Array.from(document.querySelectorAll(selector));
if (exclude) elements = elements.filter((r) => r !== exclude);
elements = elements.map((r) => Object.assign(r.style, styles));
};
function SpeechWidget(node, inputName, inputData, widgetsText) {
const widget = {
type: "speak_and_recognation_type",
name: inputName,
value: inputData,
size: [22, 1],
options: { hideOnZoom: true },
text_element: widgetsText?.inputEl,
draw(ctx, node, widget_width, y, widget_height) {
const hidden = widgetsText?.element?.hidden || widgetsText.hidden;
widget.element.style.display =
hidden || app.canvas.ds.scale < 0.6 ? "none" : "flex";
if (hidden) {
widget.options.onHide?.(widget);
}
if (hidden) {
return;
}
Object.assign(
widget.element.style,
getPostition(ctx, widget_width, y, node.size[1], widgetsText)
);
},
computeSize(...args) {
return [22, 1];
},
async callback(v) {
if (widgetsText?.element?.hasAttribute("readonly")) return;
widget.value = v ?? inputData ?? [false, true];
const checkboxSave = widget.element.querySelector(
".alekpet_extras_node_recognition_save"
);
const checkboxClear = widget.element.querySelector(
".alekpet_extras_node_recognition_clear"
);
if (checkboxClear) checkboxClear.checked = widget.value[1] ?? false;
if (checkboxSave) checkboxSave.checked = widget.value[0] ?? false;
},
onRemove() {
widget.element?.remove();
},
};
const buttons = [];
if (speechRect && !widgetsText?.element?.hasAttribute("readonly")) {
buttons.push(
$el("div.alekpet_extras_node_recognition_icon_box", [
$el("span.alekpet_extras_node_recognition_aborted", {
title: "Speech recognition abort",
textContent: "✖",
style: { display: "none" },
onclick: async function (e) {
const info = widget.element.querySelector(
".alekpet_extras_node_info span"
);
if (speechRect.isRecognition) {
e.currentTarget.style.display = "none";
speechRect.isRecognitionAbort = true;
speechRect.abort();
info.textContent = "aborted";
this.classList.remove(
"alekpet_extras_node_recognition_icon_active"
);
setTimeout(() => (info.textContent = ""), 2000);
}
},
}),
$el("span.alekpet_extras_node_recognition_icon", {
title: "Speech recognition",
onclick: async function (e) {
const info = widget.element.querySelector(
".alekpet_extras_node_info span"
);
const abort = widget.element.querySelector(
".alekpet_extras_node_recognition_aborted"
);
const checkboxSave = widget.element.querySelector(
".alekpet_extras_node_recognition_save"
);
// Hide other recognitions buttons
setStylesAllElements(
".alekpet_extras_node_recognition_icon",
e.currentTarget,
{
display: "none",
}
);
// Recognition
if (speechRect.elements === null) {
// Record audio
if (checkboxSave.checked) {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = async () => {
if (speechRect.isRecognitionAbort) {
speechRect.lastText = "";
speechRect.isRecognitionAbort = false;
return;
}
const saveAsWindow = JSON.parse(
localStorage.getItem(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
false
)
);
// Filename
let nameFile = "recording.webm";
if (speechRect?.lastText?.length) {
nameFile = `${speechRect.lastText
.slice(0, maxLenAudioFileName)
.replaceAll(regExpFileName, "_")}.webm`;
}
// Get audio
const audioBlob = new Blob(audioChunks, {
type: "audio/webm",
});
if (!saveAsWindow) {
const body = new FormData();
body.append("image", audioBlob, nameFile);
body.append("overwrite", "true");
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status !== 200) {
console.error(
"[ExtrasNode] Recording audio not saved!"
);
return;
}
console.log(
`[ExtrasNode] Recording audio "${nameFile}" saved successfully!`
);
} else {
const audioUrl = URL.createObjectURL(audioBlob);
const linkDown = document.createElement("a");
linkDown.href = audioUrl;
linkDown.download = nameFile;
linkDown.click();
}
speechRect.lastText = "";
};
} catch (err) {
comfyuiDesktopAlert(
`Device "Microphone" - ${err.message}!\n\nCheck device or allow access!`
);
}
}
// end - Record audio
speechRect.elements = [widget.element, widgetsText.inputEl];
info.textContent = "recognition";
this.classList.add("alekpet_extras_node_recognition_icon_active");
speechRect.start();
mediaRecorder && mediaRecorder.start();
abort.style.display = "inline-block";
} else {
speechRect.stop();
abort.style.display = "none";
info.textContent = "stoped";
this.classList.remove(
"alekpet_extras_node_recognition_icon_active"
);
setTimeout(() => (info.textContent = ""), 2000);
}
},
}),
// Settings elements
$el("div.alekpet_extras_node_speech_recognition_settings", [
$el(
"div.alekpet_extras_node_speech_recognition_settings_button.pi.pi-cog",
{
title: "Settings",
onclick: (e) => {
checkboxLSCheckedByKey(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
".alekpet_extras_node_recognition_saveAs"
);
animateClick(e.currentTarget.nextElementSibling);
},
}
),
createWindowModal({
textTitle: null,
stylesClose: {
top: "-4px",
right: "4px",
width: "auto",
height: "auto",
padding: "2px",
fontSize: "0.4rem",
lineHeight: 1,
},
stylesBox: {
background: "transparent",
border: 0,
padding: 0,
boxShadow: "none",
},
stylesBody: {
display: "flex",
flexDirection: "column",
alignItems: "stretch",
gap: "3px",
textWrap: "wrap",
background: "rgb(131, 131, 131)",
color: "white",
padding: "2px",
marginTop: "2px",
borderRadius: "6px",
fontSize: "0.4rem",
minWidth: "85px",
},
stylesWrapper: {
minWidth: "100px",
transform: "translate(0%, 0%)",
},
textBody: [
$el(
"label.alekpet_extras_node_speech_recognition_settings_props_label",
[
$el(
"span.alekpet_extras_node_speech_recognition_settings_props_name",
{
textContent: "Recoding audio",
title: "Save in audio file after recognition",
}
),
$el(
"input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_save",
{
type: "checkbox",
checked: widget.value[0] ?? false,
onchange: (e) =>
widget?.callback([!!e.target.checked, widget.value[1]]),
}
),
]
),
$el(
"label.alekpet_extras_node_speech_recognition_settings_props_label",
[
$el(
"span.alekpet_extras_node_speech_recognition_settings_props_name",
{
textContent: "Clear text",
title: "Clear text after recognition",
}
),
$el(
"input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_clear",
{
type: "checkbox",
checked: widget.value[1] ?? true,
onchange: (e) =>
widget?.callback([widget.value[0], !!e.target.checked]),
}
),
]
),
$el("hr", { style: { padding: 0, margin: 0 } }),
$el(
"label.alekpet_extras_node_speech_recognition_settings_props_label",
[
$el(
"span.alekpet_extras_node_speech_recognition_settings_props_name",
{
textContent: "'Save as' window?",
title: "Show modal window when saving recorded audio.",
}
),
$el(
"input.alekpet_extras_node_speech_recognition_settings_props_input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_saveAs",
{
type: "checkbox",
checked: JSON.parse(
localStorage.getItem(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
false
)
),
onchange: (e) => {
const check = !!e.target.checked;
const settCheck = document.body.querySelector(
"[id$='.SpeechAndRecognationSpeech'] div input"
);
settCheck && (settCheck.checked = check);
localStorage.setItem(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
check
);
checkboxLSCheckedByKey(
`${idExt}.SpeechAndRecognationSpeechSaveAs`,
".alekpet_extras_node_recognition_saveAs"
);
},
}
),
]
),
],
}),
]),
// end -- Settings elements
])
);
}
if (SpeechSynthesis && speechSynthesis) {
function buttonsStyles(
speechesButtons,
action = "add",
className = "alekpet_extras_node_speech_icon_playing"
) {
speechesButtons?.forEach((speechButton) =>
speechButton?.classList[action](className)
);
const settingTestSpeech = document.querySelector(
".panel_settings_recognation_speech_voice_test"
);
if (settingTestSpeech) {
if (action === "add") {
settingTestSpeech.style.opacity = 0.7;
settingTestSpeech.style.color = "var(--error-text)";
settingTestSpeech.title = "Cancel test speech";
settingTestSpeech.textContent = "Cancel test";
} else {
settingTestSpeech.style.opacity = 1;
settingTestSpeech.title = "Run test speech";
settingTestSpeech.textContent = "Test speech";
settingTestSpeech.style.color = "limegreen";
}
}
}
buttons.push(
$el("span.alekpet_extras_node_speech_icon", {
onclick: function () {
try {
const info = widget.element.querySelector(
".alekpet_extras_node_info span"
);
const speechesButtons = Array.from(
document.querySelectorAll(".alekpet_extras_node_speech_icon")
);
// Already playing
if (speechSynthesis.speaking) {
SpeechSynthesis.cancel();
info.textContent = "canceled";
this.title = "Speak text";
buttonsStyles(speechesButtons, "remove");
setTimeout(() => (info.textContent = ""), 2000);
return;
}
// Start playing text
const text = widgetsText?.element?.value;
if (text.trim() !== "") {
const utterance = speakSynthesisUtterance(text, {
onend: (e) => {
this.title = "Speak text";
info.textContent = "";
buttonsStyles(speechesButtons, "remove");
},
});
if (utterance) {
this.title = "Cancel speech";
info.textContent = "saying now";
buttonsStyles(speechesButtons);
SpeechSynthesis.speak(utterance);
}
} else {
const utterance = speakSynthesisUtterance(
`${widgetsText?.name || "This"}}, field is empty!`,
{
onend: (e) => {
this.title = "Speak text";
info.textContent = "";
buttonsStyles(speechesButtons, "remove");
},
}
);
if (utterance) {
this.title = "Cancel speech";
info.textContent = "empty text!";
buttonsStyles(speechesButtons);
SpeechSynthesis.speak(utterance);
}
}
} catch (err) {
console.log(err);
}
},
textContent: "🔊",
title: "Speak text",
})
);
}
widget.element = $el(
"div.alekpet_extras_node_speechrecognition_box",
{ style: { display: "none" } },
[
$el("div.alekpet_extras_node_speechrecognition_row", [...buttons]),
$el("div.alekpet_extras_node_info", [
$el("span", {
textContent: "",
style: { fontSize: "0.4em" },
}),
]),
]
);
const collapse = node.collapse;
node.collapse = function () {
collapse.apply(this, arguments);
if (this.flags?.collapsed) {
widget.element.hidden = true;
widget.element.style.display = "none";
}
widget.element.dataset.collapsed = this.flags?.collapsed ? "true" : "false";
};
const onRemovedOrig = node.onRemoved;
node.onRemoved = function () {
node?.widgets?.forEach((w) => {
if (w.type === "speak_and_recognation_type") {
w?.onRemove();
}
});
onRemovedOrig?.apply(node, arguments);
};
document.body.appendChild(widget.element);
return widget;
}
/* ~~~ end - Speech & Recognition speech Widget ~~~ */
export {
SpeechWidget,
speechRect,
SpeechSynthesis,
speakSynthesisUtterance,
checkboxLSCheckedByKey,
};