Files
AlekPet-ComfyUI_Custom_Node…/ExtrasNode/js/extras_node.js
T

761 lines
24 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,
// 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("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
},
});