Changed the structure of extensions in the ExtrasNode folder, moved them to extensions in separate files, for convenience
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
@@ -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 };
|
||||
|
||||
+9
-2
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user