Support set voice (install on system), volume, pitch, rate Add support speech text output nodes contains string type Small changes utils.js Update version pyproject.toml
547 lines
17 KiB
JavaScript
547 lines
17 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
|
|
|
|
// 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;
|
|
|
|
// Register Extension
|
|
app.registerExtension({
|
|
name: idExt,
|
|
init() {
|
|
addStylesheet("css/extrasnode/extras_node_styles.css", import.meta.url);
|
|
|
|
// 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",
|
|
},
|
|
}),
|
|
]),
|
|
]);
|
|
},
|
|
});
|
|
},
|
|
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);
|
|
};
|
|
// onConfigure
|
|
const onConfigure = nodeType.prototype.onConfigure;
|
|
nodeType.prototype.onConfigure = function (w) {
|
|
onConfigure?.apply(this, arguments);
|
|
if (w?.widgets_values?.length) {
|
|
outSet.call(this, w.widgets_values);
|
|
}
|
|
};
|
|
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;
|
|
|
|
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;
|
|
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) =>
|
|
["customtext", "converted-widget"].includes(w.type)
|
|
);
|
|
|
|
await new Promise((res) =>
|
|
setTimeout(() => {
|
|
res();
|
|
}, 16 * this.widgets.length)
|
|
);
|
|
|
|
if (widgetsTextMulti.length) {
|
|
widgetsTextMulti.forEach(async (w) => {
|
|
this.addCustomWidget(
|
|
SpeechWidget(this, "speak_and_recognation", 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
|
|
},
|
|
});
|