From 6f84c334420bf8efe7b49240c64fd02ebeb07e70 Mon Sep 17 00:00:00 2001 From: AlekPet Date: Thu, 26 Jun 2025 23:05:30 +0300 Subject: [PATCH] Changed the structure of extensions in the ExtrasNode folder, moved them to extensions in separate files, for convenience --- .../extras_node_speech_recognition_style.css | 130 ++++ ExtrasNode/css/extras_node_styles.css | 131 ---- ExtrasNode/js/extras_node.js | 711 +----------------- ExtrasNode/js/extras_preview_image_size.js | 241 ++++++ ExtrasNode/js/extras_preview_media.js | 294 ++++++++ .../js/extras_speech_and_recognition.js | 234 ++++++ ExtrasNode/lib/extras_node_widgets.js | 687 +---------------- ...> extras_speech_and_recognation_dialog.js} | 11 +- .../extras_speech_and_recognation_widget.js | 691 +++++++++++++++++ 9 files changed, 1602 insertions(+), 1528 deletions(-) create mode 100644 ExtrasNode/css/extras_node_speech_recognition_style.css create mode 100644 ExtrasNode/js/extras_preview_image_size.js create mode 100644 ExtrasNode/js/extras_preview_media.js create mode 100644 ExtrasNode/js/extras_speech_and_recognition.js rename ExtrasNode/lib/{extras_node_dialogs.js => extras_speech_and_recognation_dialog.js} (95%) create mode 100644 ExtrasNode/lib/extras_speech_and_recognation_widget.js diff --git a/ExtrasNode/css/extras_node_speech_recognition_style.css b/ExtrasNode/css/extras_node_speech_recognition_style.css new file mode 100644 index 0000000..12bcc86 --- /dev/null +++ b/ExtrasNode/css/extras_node_speech_recognition_style.css @@ -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; +} diff --git a/ExtrasNode/css/extras_node_styles.css b/ExtrasNode/css/extras_node_styles.css index 35d1690..6b719f4 100644 --- a/ExtrasNode/css/extras_node_styles.css +++ b/ExtrasNode/css/extras_node_styles.css @@ -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; diff --git a/ExtrasNode/js/extras_node.js b/ExtrasNode/js/extras_node.js index a169438..a4b8b5c 100644 --- a/ExtrasNode/js/extras_node.js +++ b/ExtrasNode/js/extras_node.js @@ -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 = `
Width: ${image.naturalWidth}, Height: ${ - image.naturalHeight - }
`; - }; - 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 }, }); diff --git a/ExtrasNode/js/extras_preview_image_size.js b/ExtrasNode/js/extras_preview_image_size.js new file mode 100644 index 0000000..654f8db --- /dev/null +++ b/ExtrasNode/js/extras_preview_image_size.js @@ -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 = `
Width: ${image.naturalWidth}, Height: ${ + image.naturalHeight + }
`; + }; + image.src = api.apiURL( + "/view?" + + new URLSearchParams(images[0]).toString() + + app.getPreviewFormatParam() + + app.getRandParam() + ); + } + } + } + }; + } + }, +}); diff --git a/ExtrasNode/js/extras_preview_media.js b/ExtrasNode/js/extras_preview_media.js new file mode 100644 index 0000000..df3a264 --- /dev/null +++ b/ExtrasNode/js/extras_preview_media.js @@ -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 -- + }, +}); diff --git a/ExtrasNode/js/extras_speech_and_recognition.js b/ExtrasNode/js/extras_speech_and_recognition.js new file mode 100644 index 0000000..c792d91 --- /dev/null +++ b/ExtrasNode/js/extras_speech_and_recognition.js @@ -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 + }, +}); diff --git a/ExtrasNode/lib/extras_node_widgets.js b/ExtrasNode/lib/extras_node_widgets.js index d7f930e..92ce2fc 100644 --- a/ExtrasNode/lib/extras_node_widgets.js +++ b/ExtrasNode/lib/extras_node_widgets.js @@ -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 }; diff --git a/ExtrasNode/lib/extras_node_dialogs.js b/ExtrasNode/lib/extras_speech_and_recognation_dialog.js similarity index 95% rename from ExtrasNode/lib/extras_node_dialogs.js rename to ExtrasNode/lib/extras_speech_and_recognation_dialog.js index 9fcb256..e092ed2 100644 --- a/ExtrasNode/lib/extras_node_dialogs.js +++ b/ExtrasNode/lib/extras_speech_and_recognation_dialog.js @@ -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()], }); diff --git a/ExtrasNode/lib/extras_speech_and_recognation_widget.js b/ExtrasNode/lib/extras_speech_and_recognation_widget.js new file mode 100644 index 0000000..da37e60 --- /dev/null +++ b/ExtrasNode/lib/extras_speech_and_recognation_widget.js @@ -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, +};