Files
AlekPet-ComfyUI_Custom_Node…/ExtrasNode/lib/extras_node_widgets.js
T

686 lines
20 KiB
JavaScript

/*
* Title: Extras widgets
* 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 { rgbToHex, isValidStyle } from "../../utils.js";
import { RecognationSpeechDialog } from "./extras_node_dialogs.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 }));
}
// 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;
widget.element.dataset.shouldHide = hidden ? "true" : "false";
const isInVisibleNodes =
widget.element.dataset.isInVisibleNodes === "true";
const isCollapsed = widget.element.dataset.collapsed === "true";
const actualHidden = hidden || !isInVisibleNodes || isCollapsed;
const wasHidden = widget.element.hidden;
widget.element.hidden = actualHidden;
widget.element.style.display = actualHidden ? "none" : "flex";
if (actualHidden && !wasHidden) {
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) {
const isCheckedSave = widget.value[0] ?? false;
if (isCheckedSave) {
const premission = await checkPremissions();
checkboxSave.checked =
premission?.status && isCheckedSave ? true : false;
} else {
checkboxSave.checked = isCheckedSave;
}
}
},
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) {
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 = "";
};
}
// 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);
}
},
}),
$el(
"input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_save",
{
type: "checkbox",
checked: widget.value[0] ?? false,
title: "Save in audio file after recognition",
onchange: async (e) => {
const premission = await checkPremissions();
let checkValue = !!e.target.checked;
if (!premission?.status && premission.state != "prompt") {
alert(
`Access to the device "${premission.device.name}" is denied!\nAllow access to the device!`
);
checkValue = false;
}
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(() => widget?.callback([checkValue, widget.value[1]]))
.catch((e) => {
widget?.callback([false, widget.value[1]]);
alert(
`Access to the device "${premission.device.name}" is denied!\nAllow access to the device!`
);
});
},
}
),
$el(
"input.alekpet_extras_node_speech_recognition_checkbox.alekpet_extras_node_recognition_clear",
{
type: "checkbox",
checked: widget.value[1] ?? true,
title: "Clear text after recognition",
onchange: (e) => {
widget?.callback([widget.value[0], !!e.target.checked]);
},
}
),
])
);
}
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",
{ hidden: true, 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", {
type: "color",
value: inputData[1]?.default || "#00ff33",
oninput: () => widget.callback?.(color_hex.value),
});
const color_text = $el("div", {
title: "Click to copy color to clipboard",
style: {
textAlign: "center",
fontSize: "20px",
height: "20px",
fontWeight: "600",
lineHeight: 1.5,
background: "var(--comfy-menu-bg)",
border: "dotted 2px white",
fontFamily: "sans-serif",
letterSpacing: "0.5rem",
borderRadius: "8px",
textShadow: "0 0 4px #fff",
cursor: "pointer",
},
onclick: () => navigator.clipboard.writeText(color_hex.value),
});
const w_color_hex = node.addDOMWidget(inputName, "color_hex", color_hex, {
getValue() {
color_text.style.color = color_hex.value;
color_text.textContent = color_hex.value;
return color_hex.value;
},
setValue(v) {
widget.value = v;
color_hex.value = v;
},
});
widget.callback = (v) => {
let color = isValidStyle("color", v).result ? v : "#00ff33";
if (color.includes("#") && color.length === 4) {
const opt_color = new Option().style;
opt_color["color"] = color;
color = rgbToHex(opt_color["color"]);
}
color_hex.value = color;
widget.value = color;
};
const w_color_text = node.addDOMWidget(
inputName + "_box",
"color_hex_box",
color_text
);
w_color_hex.color_hex = color_hex;
widget.w_color_hex = w_color_hex;
widget.w_color_text = w_color_text;
return { widget };
}
/* ~~~ end - Color Widget ~~~ */
/* ~~~ Preview Image Size Widget ~~~ */
function createPreiviewSize(node, name, options) {
const { color } = options;
const res = $el("div", {
style: {
fontSize: "0.8rem",
color: color,
fontFamily: "monospace",
padding: 0,
margin: 0,
outline: 0,
},
});
const widget = node.addDOMWidget(name, "show_resolution", res, {
getValue() {
return res.innerHTML;
},
setValue(v) {
res.innerHTML = v;
},
});
widget.computeSize = () => [node.size[0], 40];
return widget;
}
/* ~~~ end - Preview Image Size Widget ~~~ */
export {
SpeechWidget,
makeColorWidget,
createPreiviewSize,
speechRect,
SpeechSynthesis,
speakSynthesisUtterance,
};