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
251 lines
7.9 KiB
JavaScript
251 lines
7.9 KiB
JavaScript
import { ComfyDialog } from "../../../../../scripts/ui.js";
|
|
import { makeElement } from "../../utils.js";
|
|
import { speakSynthesisUtterance } from "./extras_node_widgets.js";
|
|
|
|
const idExt = "alekpet.ExtrasNode";
|
|
|
|
export class RecognationSpeechDialog extends ComfyDialog {
|
|
static getSettingsRecSpeechLS() {
|
|
const SpeechVoiceLS = localStorage.getItem(
|
|
`Comfy.Settings.${idExt}.SpeechVoice`
|
|
);
|
|
|
|
const SpeechVolumeLS = localStorage.getItem(
|
|
`Comfy.Settings.${idExt}.SpeechVolume`
|
|
);
|
|
const SpeechPitchLS = localStorage.getItem(
|
|
`Comfy.Settings.${idExt}.SpeechPitch`
|
|
);
|
|
const SpeechRateLS = localStorage.getItem(
|
|
`Comfy.Settings.${idExt}.SpeechRate`
|
|
);
|
|
|
|
const voice =
|
|
SpeechVoiceLS && SpeechVoiceLS.trim() !== "" ? SpeechVoiceLS : null,
|
|
volume =
|
|
SpeechVolumeLS && SpeechVolumeLS.trim() !== "" ? +SpeechVolumeLS : 1,
|
|
pitch = SpeechPitchLS && SpeechPitchLS.trim() !== "" ? +SpeechPitchLS : 1,
|
|
rate = SpeechRateLS && SpeechRateLS.trim() !== "" ? +SpeechRateLS : 1;
|
|
return { voice, volume, pitch, rate };
|
|
}
|
|
|
|
async createPanelSettings() {
|
|
const panelSettings = makeElement("div", {
|
|
class: ["panel_settings_recognation_speech"],
|
|
style: {
|
|
display: "grid",
|
|
gridTemplateColumns: "1fr 1fr 1fr",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: "5px",
|
|
},
|
|
});
|
|
|
|
const voicesList = makeElement("select", {
|
|
class: ["panel_settings_recognation_speech_voice"],
|
|
onchange: (e) => {
|
|
const value = e.target.options[e.target.selectedIndex].dataset.name;
|
|
this.settings.voice = value;
|
|
localStorage.setItem(`Comfy.Settings.${idExt}.SpeechVoice`, value);
|
|
},
|
|
});
|
|
|
|
const synth = window.speechSynthesis;
|
|
if (synth) {
|
|
const voices = synth.getVoices();
|
|
|
|
for (let i = 0; i < voices.length; i++) {
|
|
const voice = voices[i];
|
|
const option = makeElement("option", {
|
|
textContent: `${voices[i].name} (${voices[i].lang})`,
|
|
dataset: { lang: voice.lang, name: voice.name },
|
|
parent: voicesList,
|
|
});
|
|
|
|
if (voice.default) option.textContent += " — DEFAULT";
|
|
if (voice.name === this.settings.voice) option.selected = true;
|
|
}
|
|
}
|
|
|
|
panelSettings.append(
|
|
makeElement("div", {
|
|
class: ["panel_settings_recognation_speech_voice_label"],
|
|
textContent: "Select voice: ",
|
|
style: { fontWeight: "800" },
|
|
}),
|
|
voicesList,
|
|
makeElement("button", {
|
|
textContent: "Test speech",
|
|
style: { color: "limegreen", fontSize: "1rem" },
|
|
class: ["panel_settings_recognation_speech_voice_test"],
|
|
onclick: (e) => {
|
|
const target = e.target;
|
|
const SpeechSynthesis = window.speechSynthesis;
|
|
|
|
if (SpeechSynthesis && speechSynthesis) {
|
|
if (SpeechSynthesis.speaking) {
|
|
this.cancelSpeech();
|
|
target.style.opacity = 1;
|
|
target.title = "Run test speech";
|
|
target.textContent = "Test speech";
|
|
target.style.color = "limegreen";
|
|
return;
|
|
}
|
|
|
|
const utterance = speakSynthesisUtterance(
|
|
"Hello! This is testing speech, on the ComfyUI!",
|
|
{
|
|
onend: (e) => {
|
|
target.style.opacity = 1;
|
|
target.title = "Run test speech";
|
|
target.textContent = "Test speech";
|
|
target.style.color = "limegreen";
|
|
this.cancelSpeech();
|
|
},
|
|
}
|
|
);
|
|
|
|
if (utterance) {
|
|
target.style.opacity = 0.7;
|
|
target.style.color = "var(--error-text)";
|
|
target.title = "Cancel test speech";
|
|
target.textContent = "Cancel test";
|
|
Array.from(
|
|
document.querySelectorAll(".alekpet_extras_node_speech_icon")
|
|
).forEach((speechButton) => {
|
|
speechButton.classList.add(
|
|
"alekpet_extras_node_speech_icon_playing"
|
|
);
|
|
});
|
|
SpeechSynthesis.speak(utterance);
|
|
}
|
|
} else {
|
|
target.textContent = "Not support!";
|
|
target.style.color = "var(--error-text)";
|
|
setTimeout(() => {
|
|
target.textContent = "Test speech";
|
|
target.style.color = "limegreen";
|
|
}, 500);
|
|
}
|
|
},
|
|
})
|
|
);
|
|
|
|
for (const opt in this.defaultSettings) {
|
|
const option = this.defaultSettings[opt];
|
|
const label = makeElement("div", {
|
|
class: [`panel_settings_recognation_speech_${opt}_label`],
|
|
textContent: `${opt[0].toUpperCase() + opt.slice(1)}: `,
|
|
style: { fontWeight: "800" },
|
|
});
|
|
|
|
const element = makeElement("input", {
|
|
class: [`panel_settings_recognation_speech_${opt}_input`],
|
|
type: "range",
|
|
min: option.min,
|
|
max: option.max,
|
|
step: 0.1,
|
|
dataset: { name: opt },
|
|
oninput: (e) => {
|
|
e.target.nextElementSibling.textContent = e.target.value;
|
|
},
|
|
onchange: (e) => {
|
|
this.settings[opt] = e.target.value;
|
|
localStorage.setItem(
|
|
`Comfy.Settings.${idExt}.Speech${
|
|
opt[0].toUpperCase() + opt.slice(1)
|
|
}`,
|
|
e.target.value
|
|
);
|
|
},
|
|
});
|
|
|
|
element.value = this.settings[opt] ?? option.default;
|
|
|
|
const valueEl = makeElement("span", {
|
|
class: [`panel_settings_recognation_speech_${opt}_value`],
|
|
textContent: element.value,
|
|
style: { justifySelf: "center" },
|
|
});
|
|
panelSettings.append(label, element, valueEl);
|
|
}
|
|
|
|
return panelSettings;
|
|
}
|
|
|
|
cancelSpeech() {
|
|
if (window.speechSynthesis && window.speechSynthesis.speak) {
|
|
Array.from(
|
|
document.querySelectorAll(".alekpet_extras_node_speech_icon")
|
|
).forEach((speechButton) => {
|
|
speechButton.classList.remove(
|
|
"alekpet_extras_node_speech_icon_playing"
|
|
);
|
|
});
|
|
window.speechSynthesis.cancel();
|
|
}
|
|
}
|
|
|
|
async show() {
|
|
this.cancelSpeech();
|
|
this.settings = RecognationSpeechDialog.getSettingsRecSpeechLS();
|
|
this.defaultSettings = {
|
|
volume: { min: 0, max: 1, default: 1 },
|
|
rate: { min: 0.1, max: 10, default: 1 },
|
|
pitch: { min: 0, max: 2, default: 1 },
|
|
};
|
|
|
|
const box = makeElement("div", {
|
|
class: ["box_recognation_speech"],
|
|
style: {
|
|
width: "25vw",
|
|
color: "var(--input-text)",
|
|
},
|
|
children: [await this.createPanelSettings()],
|
|
});
|
|
|
|
super.show(box);
|
|
this.element.style.zIndex = 9999;
|
|
|
|
const close = this.element.children[0].querySelectorAll("button")[1];
|
|
close.style.width = "50%";
|
|
|
|
const divButtons = makeElement("div", {
|
|
class: ["box_recognation_speech_buttons"],
|
|
style: { display: "flex" },
|
|
children: [
|
|
makeElement("button", {
|
|
textContent: "Reset to default",
|
|
style: { width: "50%" },
|
|
onclick: (e) => {
|
|
const ranges = box.querySelectorAll("[type='range']");
|
|
|
|
// Reset voice
|
|
const selectElement = box.querySelector(
|
|
".panel_settings_recognation_speech_voice"
|
|
);
|
|
const indexDefault = Array.from(selectElement.options).findIndex(
|
|
(o) => o.textContent.includes(" — DEFAULT")
|
|
);
|
|
selectElement.selectedIndex = indexDefault;
|
|
localStorage.removeItem(`Comfy.Settings.${idExt}.SpeechVoice`);
|
|
|
|
// Reset settings
|
|
for (const r of ranges) {
|
|
r.value = this.defaultSettings[r.dataset.name].default ?? 1;
|
|
r.nextElementSibling.textContent = r.value;
|
|
r.dispatchEvent(new Event("change"));
|
|
}
|
|
},
|
|
}),
|
|
close,
|
|
],
|
|
parent: this.element.children[0],
|
|
});
|
|
}
|
|
close() {
|
|
this.cancelSpeech();
|
|
this.element.remove();
|
|
}
|
|
}
|