Files
AlekPet-ComfyUI_Custom_Node…/ExtrasNode/js/extras_node.js
T
AlekPet 8f9388b28d implemented save recognition to audio file
Add support save recognition to file
Add option to settings show modal window "Save as"
Add checkbox enable save audio or not.
2024-11-21 18:51:23 +03:00

790 lines
25 KiB
JavaScript

/*
* Title: Extras extensions
* 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 { ComfyWidgets } from "../../scripts/widgets.js";
import { $el } from "../../scripts/ui.js";
import { isValidStyle } from "./utils.js";
import { addStylesheet } from "../../scripts/utils.js";
import {
SpeechWidget,
makeColorWidget,
createPreiviewSize,
speechRect,
SpeechSynthesis,
} from "./lib/extrasnode/extras_node_widgets.js";
import { RecognationSpeechDialog } from "./lib/extrasnode/extras_node_dialogs.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"],
};
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;
} else if (SUPPORTS_FORMAT.video.includes(ext)) {
const video = document.querySelector(".preview_vid");
video.onerror = (err) => rej(err);
video.addEventListener("canplay", (e) => {
res({ raw: video, type: "video", src });
});
video.src = src;
} else 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, video and audio select list combo
const addItem = LiteGraph.ContextMenu.prototype.addItem;
LiteGraph.ContextMenu.prototype.addItem = function () {
const element = addItem?.apply(this, arguments);
if (!PreviewImageVideoCombo) return;
const [name, value, options] = arguments;
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",
right: "calc(-1% - 160px)",
},
parent: this.root,
},
[
$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 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`;
}
if (
body_rect.width &&
root_rect.right + previewWidth > body_rect.width
) {
this.root.preview_content_combo.style.left = `${
-previewWidth - 10
}px`;
this.root.preview_content_combo.style.right = "auto";
} else {
this.root.preview_content_combo.style.left = "auto";
this.root.preview_content_combo.style.right = `calc(-1% - ${this.root.preview_content_combo.style.maxWidth})`;
}
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,
});
audio.play();
}
if (type === "video" || type === "image") {
Object.assign(audio.style, {
opacity: 0,
});
!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);
}
});
};
// 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;
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: (e) => {
const checked = !!e.target.checked;
SpeechAndRecognationSpeech = checked;
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;
},
}),
]
),
$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
switch (nodeData.name) {
case "PreviewTextNode": {
// Node Created
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const ret = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
let PreviewTextNode = app.graph._nodes.filter(
(wi) => wi.type == nodeData.name
),
nodeName = `${nodeData.name}_${PreviewTextNode.length}`;
console.log(`Create ${nodeData.name}: ${nodeName}`);
const wi = ComfyWidgets.STRING(
this,
nodeName,
[
"STRING",
{
default: "",
placeholder: "Text message output...",
multiline: true,
},
],
app
);
wi.widget.inputEl.readOnly = true;
this.setSize(this.computeSize(this.size));
app.graph.setDirtyCanvas(true, false);
return ret;
};
// Function set value
const outSet = function (texts) {
if (texts.length > 0) {
let widget_id = this?.widgets.findIndex(
(w) => w.type == "customtext"
);
if (Array.isArray(texts)) {
texts = texts.map((v) =>
typeof v === "object" ? JSON.stringify(v) : v.toString()
);
texts = texts
.filter((word) => word.trim() !== "")
.map((word) => word.trim())
.join(" ");
}
this.widgets[widget_id].value = texts;
app.graph.setDirtyCanvas(true);
}
};
// onExecuted
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (texts) {
onExecuted?.apply(this, arguments);
outSet.call(this, texts?.string);
};
break;
}
// --- Preview Text Node
// -- Colors nodes
case "ColorsCorrectNode": {
// Node Created
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const ret = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
let ColorsCorrectNode = app.graph._nodes.filter(
(wi) => wi.type == nodeData.name
),
nodeName = `${nodeData.name}_${ColorsCorrectNode.length}`;
console.log(`Create ${nodeData.name}: ${nodeName}`);
return ret;
};
break;
}
case "HexToHueNode": {
// Node Created
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const ret = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
let HexToHueNode = app.graph._nodes.filter(
(wi) => wi.type == nodeData.name
),
nodeName = `${nodeData.name}_${HexToHueNode.length}`;
console.log(`Create ${nodeData.name}: ${nodeName}`);
let widgetColor = null;
for (let w of this.widgets) {
if (w.name === "color_hex") {
widgetColor = makeColorWidget(
this,
nodeData.name,
nodeData?.input?.required?.color_hex,
w
);
}
}
api.addEventListener("alekpet_get_color_hex", async ({ detail }) => {
const { color_hex, unique_id } = detail;
if (+unique_id !== this.id || !color_hex) {
return;
}
widgetColor.widget.w_color_hex.value = color_hex;
});
return ret;
};
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)
);
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)
);
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
},
});