diff --git a/templates/STfiles/alltalk.js b/templates/STfiles/alltalk.js index e44b312..f70daf5 100644 --- a/templates/STfiles/alltalk.js +++ b/templates/STfiles/alltalk.js @@ -3,92 +3,33 @@ import { saveTtsProviderSettings } from './index.js'; export { AllTalkTtsProvider }; -const css = ` -.settings-row { - display: flex; - justify-content: space-between; /* Distribute space evenly between the children */ - align-items: center; - width: 100%; /* Full width to spread out the children */ -} - -.settings-option { - flex: 1; /* Each child will take equal space */ - margin: 0 10px; /* Adjust spacing as needed */ -} - -.endpoint-option { - flex: 1; /* Each child will take equal space */ - margin: 0 10px; /* Adjust spacing as needed */ - margin-right: 25px; - width: 38%; /* Full width to spread out the children */ -} - -.website-row { - display: flex; - justify-content: start; /* Aligns items to the start of the row */ - align-items: center; - margin-top: 10px; /* Top margin */ - margin-bottom: 10px; /* Bottom margin */ -} - -.website-option { - flex: 1; /* Allows each option to take equal space */ - margin-right: 10px; /* Spacing between the two options, adjust as needed */ - margin-left: 10px; /* Adjust this value to align as desired */ - /* Remove the individual margin top and bottom declarations if they are set globally on the .settings-row */ -} - -.settings-separator { - margin-top: 10px; - margin-bottom: 10px; - padding: 18x; - font-weight: bold; - border-top: 1px solid #e1e1e1; /* Grey line */ - border-bottom: 1px solid #e1e1e1; /* Grey line */ - text-align: center; -} - -.status-message { - flex: 1; /* Adjust as needed */ - margin: 0 10px; - /* Additional styling */ -} - -.model-endpoint-row { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; -} - -.model-option, .endpoint-option { - flex: 1; - margin: 0 10px; - margin-left: 10px; /* Adjust this value to align as desired */ - /* Adjust width as needed to fit in one line */ -} - -.endpoint-option { - /* Adjust width to align with model dropdown */ - width: 38%; -} - -#status_info { - color: lightgreen; /* Or any specific shade of green you prefer */ -} -`; - -const style = document.createElement('style'); -style.type = 'text/css'; -style.appendChild(document.createTextNode(css)); -document.head.appendChild(style); - class AllTalkTtsProvider { //########// // Config // //########// - settings; + settings = {}; + constructor() { + // Initialize with default settings if they are not already set + this.settings = { + provider_endpoint: this.settings.provider_endpoint || 'http://localhost:7851', + language: this.settings.language || 'en', + voiceMap: this.settings.voiceMap || {}, + at_generation_method: this.settings.at_generation_method || 'standard_generation', + narrator_enabled: this.settings.narrator_enabled || 'false', + at_narrator_text_not_inside: this.settings.at_narrator_text_not_inside || 'narrator', + narrator_voice_gen: this.settings.narrator_voice_gen || 'female_01.wav', + finetuned_model: this.settings.finetuned_model || 'false' + }; + // Separate property for dynamically updated settings from the server + this.dynamicSettings = { + modelsAvailable: [], + currentModel: '', + deepspeed_available: false, + deepSpeedEnabled: false, + lowVramEnabled: false, + }; + } ready = false; voices = []; separator = '. '; @@ -114,23 +55,12 @@ class AllTalkTtsProvider { 'Hindi': 'hi', }; - Settings = { - provider_endpoint: 'http://localhost:7851', - language: 'en', - voiceMap: {}, - at_generation_method: 'standard_generation', - narrator_enabled: 'false', - at_narrator_text_not_inside: 'narrator', - narrator_voice_gen: 'female_01.wav', - finetuned_model: 'false' - }; - get settingsHtml() { - let html = `
AllTalk Settings
`; + let html = `
AllTalk Settings
`; - html += `
+ html += `
-
+
@@ -149,7 +79,7 @@ class AllTalkTtsProvider {
-
+
`; if (this.voices) { @@ -170,7 +100,7 @@ class AllTalkTtsProvider { } html += `
-
+
@@ -191,49 +121,47 @@ class AllTalkTtsProvider {
-
+
- +
`; - html += `
-
+ html += `
+
-
+
-
+
Status: Ready
-
+
`; - html += `
-
- AllTalk Config & Docs. + html += `
+
+ AllTalk Config & Docs.
-
+
AllTalk Website.
`; -html += `
-
+ html += `
+
Text-generation-webui users - Uncheck Enable TTS in Text-generation-webui.
`; - - return html; } @@ -244,14 +172,35 @@ html += `
async loadSettings(settings) { updateStatus('Offline'); - // Use default settings if no settings are provided - this.settings = Object.keys(settings).length === 0 ? this.Settings : settings; + if (Object.keys(settings).length === 0) { + console.info('Using default AllTalk TTS Provider settings'); + } else { + // Populate settings with provided values, ignoring server-provided settings + for (const key in settings) { + if (key in this.settings) { + this.settings[key] = settings[key]; + } else { + console.debug(`Ignoring non-user-configurable setting: ${key}`); + } + } + } + + // Update UI elements to reflect the loaded settings + $('#at_server').val(this.settings.provider_endpoint); + $('#language_options').val(this.settings.language); + //$('#voicemap').val(this.settings.voiceMap); + $('#at_generation_method').val(this.settings.at_generation_method); + $('#at_narrator_enabled').val(this.settings.narrator_enabled); + $('#at_narrator_text_not_inside').val(this.settings.at_narrator_text_not_inside); + $('#narrator_voice').val(this.settings.narrator_voice_gen); + + console.debug('AllTalkTTS: Settings loaded'); try { // Check if TTS provider is ready await this.checkReady(); + await this.updateSettingsFromServer(); // Fetch dynamic settings from the TTS server await this.fetchTtsVoiceObjects(); // Fetch voices only if service is ready - await this.updateSettingsFromServer(); this.updateNarratorVoicesDropdown(); this.updateLanguageDropdown(); this.setupEventListeners(); @@ -259,29 +208,23 @@ html += `
updateStatus('Ready'); } catch (error) { console.error("Error loading settings:", error); - // Set status to Error if there is an issue in loading settings updateStatus('Offline'); } } applySettingsToHTML() { - // Load AllTalk specific settings first - const loadedSettings = loadAllTalkSettings(); // Apply loaded settings or use defaults - this.settings = loadedSettings ? { ...this.Settings, ...loadedSettings } : this.Settings; const narratorVoiceSelect = document.getElementById('narrator_voice'); const atNarratorSelect = document.getElementById('at_narrator_enabled'); const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside'); const generationMethodSelect = document.getElementById('at_generation_method'); + this.settings.narrator_voice = this.settings.narrator_voice_gen; // Apply settings to Narrator Voice dropdown if (narratorVoiceSelect && this.settings.narrator_voice) { narratorVoiceSelect.value = this.settings.narrator_voice.replace('.wav', ''); - this.settings.narrator_voice_gen = this.settings.narrator_voice; - //console.log(this.settings.narrator_voice_gen) } // Apply settings to AT Narrator Enabled dropdown if (atNarratorSelect) { - //console.log(this.settings.narrator_enabled) // Sync the state with the checkbox in index.js const ttsPassAsterisksCheckbox = document.getElementById('tts_pass_asterisks'); // Access the checkbox from index.js const ttsNarrateQuotedCheckbox = document.getElementById('tts_narrate_quoted'); // Access the checkbox from index.js @@ -314,18 +257,15 @@ html += `
const languageSelect = document.getElementById('language_options'); if (languageSelect && this.settings.language) { languageSelect.value = this.settings.language; - //console.log(this.settings.language) } // Apply settings to Text Not Inside dropdown if (textNotInsideSelect && this.settings.text_not_inside) { textNotInsideSelect.value = this.settings.text_not_inside; this.settings.at_narrator_text_not_inside = this.settings.text_not_inside; - //console.log(this.settings.at_narrator_text_not_inside) } // Apply settings to Generation Method dropdown if (generationMethodSelect && this.settings.at_generation_method) { generationMethodSelect.value = this.settings.at_generation_method; - //console.log(this.settings.at_generation_method) } // Additional logic to disable/enable dropdowns based on the selected generation method const isStreamingEnabled = this.settings.at_generation_method === 'streaming_enabled'; @@ -347,7 +287,6 @@ html += `
ftOption.textContent = 'XTTSv2 FT'; modelSelect.appendChild(ftOption); } - //console.log("Settings applied to HTML."); } //##############################// @@ -486,7 +425,7 @@ html += `
const languageSelect = document.getElementById('language_options'); if (languageSelect) { // Ensure default language is set - this.settings.language = this.Settings.language; + this.settings.language = this.settings.language; languageSelect.innerHTML = ''; for (let language in this.languageLabels) { @@ -610,7 +549,7 @@ html += `
if (narratorVoiceSelect) { narratorVoiceSelect.addEventListener('change', (event) => { this.settings.narrator_voice_gen = `${event.target.value}.wav`; - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } @@ -618,7 +557,7 @@ html += `
if (textNotInsideSelect) { textNotInsideSelect.addEventListener('change', (event) => { this.settings.text_not_inside = event.target.value; - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } @@ -656,12 +595,11 @@ html += `
$('#tts_narrate_dialogues').click(); $('#tts_narrate_dialogues').trigger('change'); } - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } - // Event Listener for AT Generation Method Dropdown const atGenerationMethodSelect = document.getElementById('at_generation_method'); const atNarratorEnabledSelect = document.getElementById('at_narrator_enabled'); @@ -680,7 +618,7 @@ html += `
atNarratorEnabledSelect.disabled = false; } this.settings.at_generation_method = selectedMethod; // Update the setting here - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } @@ -689,9 +627,19 @@ html += `
if (languageSelect) { languageSelect.addEventListener('change', (event) => { this.settings.language = event.target.value; - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } + + // Listener for AllTalk Endpoint Input + const atServerInput = document.getElementById('at_server'); + if (atServerInput) { + atServerInput.addEventListener('input', (event) => { + this.settings.provider_endpoint = event.target.value; + this.onSettingsChange(); // Save the settings after change + }); + } + } //#############################// @@ -699,30 +647,16 @@ html += `
//#############################// onSettingsChange() { - // Update settings that SillyTavern will save - this.settings.provider_endpoint = $('#at_server').val(); + // Update settings based on the UI elements + //this.settings.provider_endpoint = $('#at_server').val(); this.settings.language = $('#language_options').val(); - saveTtsProviderSettings(); // This function should save settings handled by SillyTavern - // Call the function to handle AllTalk specific settings - this.onSettingsChangeAllTalk(); // Save the settings after change - } - - onSettingsChangeAllTalk() { - // Update AllTalk specific settings and save to localStorage - this.settings.narrator_enabled = $('#at_narrator_enabled').val() === 'true'; - this.settings.narrator_voice = $('#narrator_voice').val() + '.wav'; // assuming you need to append .wav - this.settings.text_not_inside = $('#at_narrator_text_not_inside').val(); // "character" or "narrator" - this.settings.at_generation_method = $('#at_generation_method').val(); // Streaming or standard - this.settings.language = $('#language_options').val(); // Streaming or standard - - // Call the save function with the current settings - saveAllTalkSettings({ - narrator_voice: this.settings.narrator_voice, - narrator_enabled: this.settings.narrator_enabled, - text_not_inside: this.settings.text_not_inside, - at_generation_method: this.settings.at_generation_method, - language: this.settings.language - }); + //this.settings.voiceMap = $('#voicemap').val(); + this.settings.at_generation_method = $('#at_generation_method').val(); + this.settings.narrator_enabled = $('#at_narrator_enabled').val(); + this.settings.at_narrator_text_not_inside = $('#at_narrator_text_not_inside').val(); + this.settings.narrator_voice_gen = $('#narrator_voice').val(); + // Save the updated settings + saveTtsProviderSettings(); } //#########################// @@ -802,38 +736,20 @@ html += `
async generateTts(inputText, voiceId) { try { if (this.settings.at_generation_method === 'streaming_enabled') { - // For streaming method + // Construct the streaming URL const streamingUrl = `${this.settings.provider_endpoint}/api/tts-generate-streaming?text=${encodeURIComponent(inputText)}&voice=${encodeURIComponent(voiceId)}.wav&language=${encodeURIComponent(this.settings.language)}&output_file=stream_output.wav`; - const audioElement = new Audio(streamingUrl); - audioElement.play(); // Play the audio stream directly console.log("Streaming URL:", streamingUrl); - return new Response(null, { - status: 200, - statusText: "OK", - headers: { - "Content-Type": "audio/wav", - "Content-Location": streamingUrl - } - }); + + // Return the streaming URL directly + return streamingUrl; } else { // For standard method const outputUrl = await this.fetchTtsGeneration(inputText, voiceId); - // Fetch the audio data as a blob from the URL const audioResponse = await fetch(outputUrl); if (!audioResponse.ok) { throw new Error(`HTTP ${audioResponse.status}: Failed to fetch audio data`); } - const audioBlob = await audioResponse.blob(); - if (!audioBlob.type.startsWith('audio/')) { - throw new Error(`Invalid audio data format. Expecting audio/*, got ${audioBlob.type}`); - } - return new Response(audioBlob, { - status: 200, - statusText: "OK", - headers: { - "Content-Type": audioBlob.type - } - }); + return audioResponse; // Return the fetch response directly } } catch (error) { console.error("Error in generateTts:", error); @@ -841,6 +757,7 @@ html += `
} } + //####################// // Generate Standard // //####################// @@ -852,8 +769,8 @@ html += `
'text_filtering': "standard", 'character_voice_gen': voiceId + ".wav", 'narrator_enabled': this.settings.narrator_enabled, - 'narrator_voice_gen': this.settings.narrator_voice_gen, - 'text_not_inside': this.settings.text_not_inside, + 'narrator_voice_gen': this.settings.narrator_voice_gen + ".wav", + 'text_not_inside': this.settings.at_narrator_text_not_inside, 'language': this.settings.language, 'output_file_name': "st_output", 'output_file_timestamp': "true", @@ -877,7 +794,6 @@ html += `
if (!response.ok) { const errorText = await response.text(); console.error(`[fetchTtsGeneration] Error Response Text:`, errorText); - // Uncomment the following line if you have a UI element for displaying errors // toastr.error(response.statusText, 'TTS Generation Failed'); throw new Error(`HTTP ${response.status}: ${errorText}`); } @@ -912,27 +828,6 @@ function updateStatus(message) { case 'Error': statusElement.style.color = 'red'; break; - // Add more cases as needed } } -} - -//########################// -// Save/load AT Settings // -//########################// - -function saveAllTalkSettings(settingsToSave) { - // Save the specific settings to localStorage - console.log("settings", settingsToSave) - localStorage.setItem('AllTalkSettings', JSON.stringify(settingsToSave)); -} - -function loadAllTalkSettings() { - // Retrieve the settings from localStorage - const settings = localStorage.getItem('AllTalkSettings'); - // If settings exist, parse them back into an object - if (settings) { - return JSON.parse(settings); - } - return null; -} +} \ No newline at end of file diff --git a/templates/STfiles/index.js b/templates/STfiles/index.js index d07aea6..dd84a22 100644 --- a/templates/STfiles/index.js +++ b/templates/STfiles/index.js @@ -1,1028 +1,1046 @@ -import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced } from '../../../script.js'; -import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js'; -import { delay, escapeRegex, getStringHash } from '../../utils.js'; -import { EdgeTtsProvider } from './edge.js'; -import { ElevenLabsTtsProvider } from './elevenlabs.js'; -import { SileroTtsProvider } from './silerotts.js'; -import { CoquiTtsProvider } from './coqui.js'; -import { SystemTtsProvider } from './system.js'; -import { NovelTtsProvider } from './novel.js'; -import { power_user } from '../../power-user.js'; -import { registerSlashCommand } from '../../slash-commands.js'; -import { OpenAITtsProvider } from './openai.js'; -import { XTTSTtsProvider } from './xtts.js'; -import { AllTalkTtsProvider } from './alltalk.js'; -export { talkingAnimation }; - -const UPDATE_INTERVAL = 1000; - -let voiceMapEntries = []; -let voiceMap = {}; // {charName:voiceid, charName2:voiceid2} -let storedvalue = false; -let lastChatId = null; -let lastMessageHash = null; - -const DEFAULT_VOICE_MARKER = '[Default Voice]'; -const DISABLED_VOICE_MARKER = 'disabled'; - -export function getPreviewString(lang) { - const previewStrings = { - 'en-US': 'The quick brown fox jumps over the lazy dog', - 'en-GB': 'Sphinx of black quartz, judge my vow', - 'fr-FR': 'Portez ce vieux whisky au juge blond qui fume', - 'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich', - 'it-IT': 'Pranzo d\'acqua fa volti sghembi', - 'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón', - 'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky', - 'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!', - 'pt-BR': 'Vejo xá gritando que fez show sem playback.', - 'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.', - 'uk-UA': 'Фабрикуймо гідність, лящім їжею, ґав хапаймо, з\'єднавці чаш!', - 'pl-PL': 'Pchnąć w tę łódź jeża lub ośm skrzyń fig', - 'cs-CZ': 'Příliš žluťoučký kůň úpěl ďábelské ódy', - 'sk-SK': 'Vyhŕňme si rukávy a vyprážajme čínske ryžové cestoviny', - 'hu-HU': 'Árvíztűrő tükörfúrógép', - 'tr-TR': 'Pijamalı hasta yağız şoföre çabucak güvendi', - 'nl-NL': 'De waard heeft een kalfje en een pinkje opgegeten', - 'sv-SE': 'Yxskaftbud, ge vårbygd, zinkqvarn', - 'da-DK': 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon', - 'ja-JP': 'いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす', - 'ko-KR': '가나다라마바사아자차카타파하', - 'zh-CN': '我能吞下玻璃而不伤身体', - 'ro-RO': 'Muzicologă în bej vând whisky și tequila, preț fix', - 'bg-BG': 'Щъркелите се разпръснаха по цялото небе', - 'el-GR': 'Ταχίστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός', - 'fi-FI': 'Voi veljet, miksi juuri teille myin nämä vehkeet?', - 'he-IL': 'הקצינים צעקו: "כל הכבוד לצבא הצבאות!"', - 'id-ID': 'Jangkrik itu memang enak, apalagi kalau digoreng', - 'ms-MY': 'Muzik penyanyi wanita itu menggambarkan kehidupan yang penuh dengan duka nestapa', - 'th-TH': 'เป็นไงบ้างครับ ผมชอบกินข้าวผัดกระเพราหมูกรอบ', - 'vi-VN': 'Cô bé quàng khăn đỏ đang ngồi trên bãi cỏ xanh', - 'ar-SA': 'أَبْجَدِيَّة عَرَبِيَّة', - 'hi-IN': 'श्वेता ने श्वेता के श्वेते हाथों में श्वेता का श्वेता चावल पकड़ा', - }; - const fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'; - - return previewStrings[lang] ?? fallbackPreview; -} - -let ttsProviders = { - ElevenLabs: ElevenLabsTtsProvider, - Silero: SileroTtsProvider, - XTTSv2: XTTSTtsProvider, - System: SystemTtsProvider, - Coqui: CoquiTtsProvider, - Edge: EdgeTtsProvider, - Novel: NovelTtsProvider, - OpenAI: OpenAITtsProvider, - AllTalk: AllTalkTtsProvider, -}; -let ttsProvider; -let ttsProviderName; - -let ttsLastMessage = null; - -async function onNarrateOneMessage() { - audioElement.src = '/sounds/silence.mp3'; - const context = getContext(); - const id = $(this).closest('.mes').attr('mesid'); - const message = context.chat[id]; - - if (!message) { - return; - } - - resetTtsPlayback(); - ttsJobQueue.push(message); - moduleWorker(); -} - -async function onNarrateText(args, text) { - if (!text) { - return; - } - - audioElement.src = '/sounds/silence.mp3'; - - // To load all characters in the voice map, set unrestricted to true - await initVoiceMap(true); - - const baseName = args?.voice || name2; - const name = (baseName === 'SillyTavern System' ? DEFAULT_VOICE_MARKER : baseName) || DEFAULT_VOICE_MARKER; - - const voiceMapEntry = voiceMap[name] === DEFAULT_VOICE_MARKER - ? voiceMap[DEFAULT_VOICE_MARKER] - : voiceMap[name]; - - if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) { - toastr.info(`Specified voice for ${name} was not found. Check the TTS extension settings.`); - return; - } - - resetTtsPlayback(); - ttsJobQueue.push({ mes: text, name: name }); - await moduleWorker(); - - // Return back to the chat voices - await initVoiceMap(false); -} - -async function moduleWorker() { - // Primarily determining when to add new chat to the TTS queue - const enabled = $('#tts_enabled').is(':checked'); - $('body').toggleClass('tts', enabled); - if (!enabled) { - return; - } - - const context = getContext(); - const chat = context.chat; - - processTtsQueue(); - processAudioJobQueue(); - updateUiAudioPlayState(); - - // Auto generation is disabled - if (extension_settings.tts.auto_generation == false) { - return; - } - - // no characters or group selected - if (!context.groupId && context.characterId === undefined) { - return; - } - - // Chat changed - if ( - context.chatId !== lastChatId - ) { - currentMessageNumber = context.chat.length ? context.chat.length : 0; - saveLastValues(); - - // Force to speak on the first message in the new chat - if (context.chat.length === 1) { - lastMessageHash = -1; - } - - return; - } - - // take the count of messages - let lastMessageNumber = context.chat.length ? context.chat.length : 0; - - // There's no new messages - let diff = lastMessageNumber - currentMessageNumber; - let hashNew = getStringHash((chat.length && chat[chat.length - 1].mes) ?? ''); - - // if messages got deleted, diff will be < 0 - if (diff < 0) { - // necessary actions will be taken by the onChatDeleted() handler - return; - } - - // if no new messages, or same message, or same message hash, do nothing - if (diff == 0 && hashNew === lastMessageHash) { - return; - } - - // If streaming, wait for streaming to finish before processing new messages - if (context.streamingProcessor && !context.streamingProcessor.isFinished) { - return; - } - - // clone message object, as things go haywire if message object is altered below (it's passed by reference) - const message = structuredClone(chat[chat.length - 1]); - - // if last message within current message, message got extended. only send diff to TTS. - if (ttsLastMessage !== null && message.mes.indexOf(ttsLastMessage) !== -1) { - let tmp = message.mes; - message.mes = message.mes.replace(ttsLastMessage, ''); - ttsLastMessage = tmp; - } else { - ttsLastMessage = message.mes; - } - - // We're currently swiping. Don't generate voice - if (!message || message.mes === '...' || message.mes === '') { - return; - } - - // Don't generate if message doesn't have a display text - if (extension_settings.tts.narrate_translated_only && !(message?.extra?.display_text)) { - return; - } - - // Don't generate if message is a user message and user message narration is disabled - if (message.is_user && !extension_settings.tts.narrate_user) { - return; - } - - // New messages, add new chat to history - lastMessageHash = hashNew; - currentMessageNumber = lastMessageNumber; - - console.debug( - `Adding message from ${message.name} for TTS processing: "${message.mes}"`, - ); - ttsJobQueue.push(message); -} - -function talkingAnimation(switchValue) { - if (!modules.includes('talkinghead')) { - console.debug('Talking Animation module not loaded'); - return; - } - - const apiUrl = getApiUrl(); - const animationType = switchValue ? 'start' : 'stop'; - - if (switchValue !== storedvalue) { - try { - console.log(animationType + ' Talking Animation'); - doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`); - storedvalue = switchValue; // Update the storedvalue to the current switchValue - } catch (error) { - // Handle the error here or simply ignore it to prevent logging - } - } - updateUiAudioPlayState(); -} - -function resetTtsPlayback() { - // Stop system TTS utterance - cancelTtsPlay(); - - // Clear currently processing jobs - currentTtsJob = null; - currentAudioJob = null; - - // Reset audio element - audioElement.currentTime = 0; - audioElement.src = ''; - - // Clear any queue items - ttsJobQueue.splice(0, ttsJobQueue.length); - audioJobQueue.splice(0, audioJobQueue.length); - - // Set audio ready to process again - audioQueueProcessorReady = true; -} - -function isTtsProcessing() { - let processing = false; - - // Check job queues - if (ttsJobQueue.length > 0 || audioJobQueue.length > 0) { - processing = true; - } - // Check current jobs - if (currentTtsJob != null || currentAudioJob != null) { - processing = true; - } - return processing; -} - -function debugTtsPlayback() { - console.log(JSON.stringify( - { - 'ttsProviderName': ttsProviderName, - 'voiceMap': voiceMap, - 'currentMessageNumber': currentMessageNumber, - 'audioPaused': audioPaused, - 'audioJobQueue': audioJobQueue, - 'currentAudioJob': currentAudioJob, - 'audioQueueProcessorReady': audioQueueProcessorReady, - 'ttsJobQueue': ttsJobQueue, - 'currentTtsJob': currentTtsJob, - 'ttsConfig': extension_settings.tts, - }, - )); -} -window.debugTtsPlayback = debugTtsPlayback; - -//##################// -// Audio Control // -//##################// - -let audioElement = new Audio(); -audioElement.id = 'tts_audio'; -audioElement.autoplay = true; - -let audioJobQueue = []; -let currentAudioJob; -let audioPaused = false; -let audioQueueProcessorReady = true; - -async function playAudioData(audioBlob) { - // Since current audio job can be cancelled, don't playback if it is null - if (currentAudioJob == null) { - console.log('Cancelled TTS playback because currentAudioJob was null'); - } - const reader = new FileReader(); - reader.onload = function (e) { - const srcUrl = e.target.result; - audioElement.src = srcUrl; - }; - reader.readAsDataURL(audioBlob); - audioElement.addEventListener('ended', completeCurrentAudioJob); - audioElement.addEventListener('canplay', () => { - console.debug('Starting TTS playback'); - audioElement.play(); - }); -} - -window['tts_preview'] = function (id) { - const audio = document.getElementById(id); - - if (audio && !$(audio).data('disabled')) { - audio.play(); - } - else { - ttsProvider.previewTtsVoice(id); - } -}; - -async function onTtsVoicesClick() { - let popupText = ''; - - try { - const voiceIds = await ttsProvider.fetchTtsVoiceObjects(); - - for (const voice of voiceIds) { - popupText += ` -
- ${voice.lang || ''} - ${voice.name} - -
`; - if (voice.preview_url) { - popupText += ``; - } - } - } catch { - popupText = 'Could not load voices list. Check your API key.'; - } - - callPopup(popupText, 'text'); -} - -function updateUiAudioPlayState() { - if (extension_settings.tts.enabled == true) { - $('#ttsExtensionMenuItem').show(); - let img; - // Give user feedback that TTS is active by setting the stop icon if processing or playing - if (!audioElement.paused || isTtsProcessing()) { - img = 'fa-solid fa-stop-circle extensionsMenuExtensionButton'; - } else { - img = 'fa-solid fa-circle-play extensionsMenuExtensionButton'; - } - $('#tts_media_control').attr('class', img); - } else { - $('#ttsExtensionMenuItem').hide(); - } -} - -function onAudioControlClicked() { - audioElement.src = '/sounds/silence.mp3'; - let context = getContext(); - // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful - if (!audioElement.paused || isTtsProcessing()) { - resetTtsPlayback(); - talkingAnimation(false); - } else { - // Default play behavior if not processing or playing is to play the last message. - ttsJobQueue.push(context.chat[context.chat.length - 1]); - } - updateUiAudioPlayState(); -} - -function addAudioControl() { - - $('#extensionsMenu').prepend(` -
-
- TTS Playback -
`); - $('#ttsExtensionMenuItem').attr('title', 'TTS play/pause').on('click', onAudioControlClicked); - updateUiAudioPlayState(); -} - -function completeCurrentAudioJob() { - audioQueueProcessorReady = true; - currentAudioJob = null; - talkingAnimation(false); //stop lip animation - // updateUiPlayState(); -} - -/** - * Accepts an HTTP response containing audio/mpeg data, and puts the data as a Blob() on the queue for playback - * @param {Response} response - */ -async function addAudioJob(response) { - const audioData = await response.blob(); - if (!audioData.type.startsWith('audio/')) { - throw `TTS received HTTP response with invalid data format. Expecting audio/*, got ${audioData.type}`; - } - audioJobQueue.push(audioData); - console.debug('Pushed audio job to queue.'); -} - -async function processAudioJobQueue() { - // Nothing to do, audio not completed, or audio paused - stop processing. - if (audioJobQueue.length == 0 || !audioQueueProcessorReady || audioPaused) { - return; - } - try { - audioQueueProcessorReady = false; - currentAudioJob = audioJobQueue.pop(); - playAudioData(currentAudioJob); - talkingAnimation(true); - } catch (error) { - console.error(error); - audioQueueProcessorReady = true; - } -} - -//################// -// TTS Control // -//################// - -let ttsJobQueue = []; -let currentTtsJob; // Null if nothing is currently being processed -let currentMessageNumber = 0; - -function completeTtsJob() { - console.info(`Current TTS job for ${currentTtsJob?.name} completed.`); - currentTtsJob = null; -} - -function saveLastValues() { - const context = getContext(); - lastChatId = context.chatId; - lastMessageHash = getStringHash( - (context.chat.length && context.chat[context.chat.length - 1].mes) ?? '', - ); -} - -async function tts(text, voiceId, char) { - let response = await ttsProvider.generateTts(text, voiceId); - - // RVC injection - if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function') - response = await window['rvcVoiceConversion'](response, char, text); - - addAudioJob(response); - completeTtsJob(); -} - -async function processTtsQueue() { - // Called each moduleWorker iteration to pull chat messages from queue - if (currentTtsJob || ttsJobQueue.length <= 0 || audioPaused) { - return; - } - - console.debug('New message found, running TTS'); - currentTtsJob = ttsJobQueue.shift(); - let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes; - - if (extension_settings.tts.skip_codeblocks) { - text = text.replace(/^\s{4}.*$/gm, '').trim(); - text = text.replace(/```.*?```/gs, '').trim(); - } - - if (!extension_settings.tts.pass_asterisks) { - text = extension_settings.tts.narrate_dialogues_only - ? text.replace(/\*[^*]*?(\*|$)/g, '').trim() // remove asterisks content - : text.replaceAll('*', '').trim(); // remove just the asterisks - } - - if (extension_settings.tts.narrate_quoted_only) { - const special_quotes = /[“”]/g; // Extend this regex to include other special quotes - text = text.replace(special_quotes, '"'); - const matches = text.match(/".*?"/g); // Matches text inside double quotes, non-greedily - const partJoiner = (ttsProvider?.separator || ' ... '); - text = matches ? matches.join(partJoiner) : text; - } - - if (typeof ttsProvider?.processText === 'function') { - text = await ttsProvider.processText(text); - } - - // Collapse newlines and spaces into single space - text = text.replace(/\s+/g, ' ').trim(); - - console.log(`TTS: ${text}`); - const char = currentTtsJob.name; - - // Remove character name from start of the line if power user setting is disabled - if (char && !power_user.allow_name2_display) { - const escapedChar = escapeRegex(char); - text = text.replace(new RegExp(`^${escapedChar}:`, 'gm'), ''); - } - - try { - if (!text) { - console.warn('Got empty text in TTS queue job.'); - completeTtsJob(); - return; - } - - const voiceMapEntry = voiceMap[char] === DEFAULT_VOICE_MARKER ? voiceMap[DEFAULT_VOICE_MARKER] : voiceMap[char]; - - if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) { - throw `${char} not in voicemap. Configure character in extension settings voice map`; - } - const voice = await ttsProvider.getVoice(voiceMapEntry); - const voiceId = voice.voice_id; - if (voiceId == null) { - toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`); - throw `Unable to attain voiceId for ${char}`; - } - tts(text, voiceId, char); - } catch (error) { - console.error(error); - currentTtsJob = null; - } -} - -// Secret function for now -async function playFullConversation() { - const context = getContext(); - const chat = context.chat; - ttsJobQueue = chat; -} -window.playFullConversation = playFullConversation; - -//#############################// -// Extension UI and Settings // -//#############################// - -function loadSettings() { - if (Object.keys(extension_settings.tts).length === 0) { - Object.assign(extension_settings.tts, defaultSettings); - } - for (const key in defaultSettings) { - if (!(key in extension_settings.tts)) { - extension_settings.tts[key] = defaultSettings[key]; - } - } - $('#tts_provider').val(extension_settings.tts.currentProvider); - $('#tts_enabled').prop( - 'checked', - extension_settings.tts.enabled, - ); - $('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only); - $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only); - $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation); - $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only); - $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user); - $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks); - $('body').toggleClass('tts', extension_settings.tts.enabled); -} - -const defaultSettings = { - voiceMap: '', - ttsEnabled: false, - currentProvider: 'ElevenLabs', - auto_generation: true, - narrate_user: false, -}; - -function setTtsStatus(status, success) { - $('#tts_status').text(status); - if (success) { - $('#tts_status').removeAttr('style'); - } else { - $('#tts_status').css('color', 'red'); - } -} - -function onRefreshClick() { - Promise.all([ - ttsProvider.onRefreshClick(), - // updateVoiceMap() - ]).then(() => { - extension_settings.tts[ttsProviderName] = ttsProvider.settings; - saveSettingsDebounced(); - setTtsStatus('Successfully applied settings', true); - console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`); - initVoiceMap(); - updateVoiceMap(); - }).catch(error => { - console.error(error); - setTtsStatus(error, false); - }); -} - -function onEnableClick() { - extension_settings.tts.enabled = $('#tts_enabled').is( - ':checked', - ); - updateUiAudioPlayState(); - saveSettingsDebounced(); -} - - -function onAutoGenerationClick() { - extension_settings.tts.auto_generation = !!$('#tts_auto_generation').prop('checked'); - saveSettingsDebounced(); -} - - -function onNarrateDialoguesClick() { - extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked'); - saveSettingsDebounced(); -} - -function onNarrateUserClick() { - extension_settings.tts.narrate_user = !!$('#tts_narrate_user').prop('checked'); - saveSettingsDebounced(); -} - -function onNarrateQuotedClick() { - extension_settings.tts.narrate_quoted_only = !!$('#tts_narrate_quoted').prop('checked'); - saveSettingsDebounced(); -} - - -function onNarrateTranslatedOnlyClick() { - extension_settings.tts.narrate_translated_only = !!$('#tts_narrate_translated_only').prop('checked'); - saveSettingsDebounced(); -} - -function onSkipCodeblocksClick() { - extension_settings.tts.skip_codeblocks = !!$('#tts_skip_codeblocks').prop('checked'); - saveSettingsDebounced(); -} - -function onPassAsterisksClick() { - extension_settings.tts.pass_asterisks = !!$('#tts_pass_asterisks').prop('checked'); - saveSettingsDebounced(); -} - -//##############// -// TTS Provider // -//##############// - -async function loadTtsProvider(provider) { - //Clear the current config and add new config - $('#tts_provider_settings').html(''); - - if (!provider) { - return; - } - - // Init provider references - extension_settings.tts.currentProvider = provider; - ttsProviderName = provider; - ttsProvider = new ttsProviders[provider]; - - // Init provider settings - $('#tts_provider_settings').append(ttsProvider.settingsHtml); - if (!(ttsProviderName in extension_settings.tts)) { - console.warn(`Provider ${ttsProviderName} not in Extension Settings, initiatilizing provider in settings`); - extension_settings.tts[ttsProviderName] = {}; - } - await ttsProvider.loadSettings(extension_settings.tts[ttsProviderName]); - await initVoiceMap(); -} - -function onTtsProviderChange() { - const ttsProviderSelection = $('#tts_provider').val(); - extension_settings.tts.currentProvider = ttsProviderSelection; - loadTtsProvider(ttsProviderSelection); -} - -// Ensure that TTS provider settings are saved to extension settings. -export function saveTtsProviderSettings() { - extension_settings.tts[ttsProviderName] = ttsProvider.settings; - updateVoiceMap(); - saveSettingsDebounced(); - console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`); -} - - -//###################// -// voiceMap Handling // -//###################// - -async function onChatChanged() { - await resetTtsPlayback(); - const voiceMapInit = initVoiceMap(); - await Promise.race([voiceMapInit, delay(1000)]); - ttsLastMessage = null; -} - -async function onChatDeleted() { - const context = getContext(); - - // update internal references to new last message - lastChatId = context.chatId; - currentMessageNumber = context.chat.length ? context.chat.length : 0; - - // compare against lastMessageHash. If it's the same, we did not delete the last chat item, so no need to reset tts queue - let messageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? ''); - if (messageHash === lastMessageHash) { - return; - } - lastMessageHash = messageHash; - ttsLastMessage = (context.chat.length && context.chat[context.chat.length - 1].mes) ?? ''; - - // stop any tts playback since message might not exist anymore - await resetTtsPlayback(); -} - -/** - * Get characters in current chat - * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat. - * @returns {string[]} - Array of character names - */ -function getCharacters(unrestricted) { - const context = getContext(); - - if (unrestricted) { - const names = context.characters.map(char => char.name); - names.unshift(DEFAULT_VOICE_MARKER); - return names; - } - - let characters = []; - if (context.groupId === null) { - // Single char chat - characters.push(DEFAULT_VOICE_MARKER); - characters.push(context.name1); - characters.push(context.name2); - } else { - // Group chat - characters.push(DEFAULT_VOICE_MARKER); - characters.push(context.name1); - const group = context.groups.find(group => context.groupId == group.id); - for (let member of group.members) { - // Remove suffix - if (member.endsWith('.png')) { - member = member.slice(0, -4); - } - characters.push(member); - } - } - return characters; -} - -function sanitizeId(input) { - // Remove any non-alphanumeric characters except underscore (_) and hyphen (-) - let sanitized = input.replace(/[^a-zA-Z0-9-_]/g, ''); - - // Ensure first character is always a letter - if (!/^[a-zA-Z]/.test(sanitized)) { - sanitized = 'element_' + sanitized; - } - - return sanitized; -} - -function parseVoiceMap(voiceMapString) { - let parsedVoiceMap = {}; - for (const [charName, voiceId] of voiceMapString - .split(',') - .map(s => s.split(':'))) { - if (charName && voiceId) { - parsedVoiceMap[charName.trim()] = voiceId.trim(); - } - } - return parsedVoiceMap; -} - - - -/** - * Apply voiceMap based on current voiceMapEntries - */ -function updateVoiceMap() { - const tempVoiceMap = {}; - for (const voice of voiceMapEntries) { - if (voice.voiceId === null) { - continue; - } - tempVoiceMap[voice.name] = voice.voiceId; - } - if (Object.keys(tempVoiceMap).length !== 0) { - voiceMap = tempVoiceMap; - console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`); - } - if (!extension_settings.tts[ttsProviderName].voiceMap) { - extension_settings.tts[ttsProviderName].voiceMap = {}; - } - Object.assign(extension_settings.tts[ttsProviderName].voiceMap, voiceMap); - saveSettingsDebounced(); -} - -class VoiceMapEntry { - name; - voiceId; - selectElement; - constructor(name, voiceId = DEFAULT_VOICE_MARKER) { - this.name = name; - this.voiceId = voiceId; - this.selectElement = null; - } - - addUI(voiceIds) { - let sanitizedName = sanitizeId(this.name); - let defaultOption = this.name === DEFAULT_VOICE_MARKER ? - `` : - ``; - let template = ` -
- ${this.name} - -
- `; - $('#tts_voicemap_block').append(template); - - // Populate voice ID select list - for (const voiceId of voiceIds) { - const option = document.createElement('option'); - option.innerText = voiceId.name; - option.value = voiceId.name; - $(`#tts_voicemap_char_${sanitizedName}_voice`).append(option); - } - - this.selectElement = $(`#tts_voicemap_char_${sanitizedName}_voice`); - this.selectElement.on('change', args => this.onSelectChange(args)); - this.selectElement.val(this.voiceId); - } - - onSelectChange(args) { - this.voiceId = this.selectElement.find(':selected').val(); - updateVoiceMap(); - } -} - -/** - * Init voiceMapEntries for character select list. - * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat. - */ -export async function initVoiceMap(unrestricted = false) { - // Gate initialization if not enabled or TTS Provider not ready. Prevents error popups. - const enabled = $('#tts_enabled').is(':checked'); - if (!enabled) { - return; - } - - // Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying. - try { - await ttsProvider.checkReady(); - } catch (error) { - const message = `TTS Provider not ready. ${error}`; - setTtsStatus(message, false); - return; - } - - setTtsStatus('TTS Provider Loaded', true); - - // Clear existing voiceMap state - $('#tts_voicemap_block').empty(); - voiceMapEntries = []; - - // Get characters in current chat - const characters = getCharacters(unrestricted); - - // Get saved voicemap from provider settings, handling new and old representations - let voiceMapFromSettings = {}; - if ('voiceMap' in extension_settings.tts[ttsProviderName]) { - // Handle previous representation - if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'string') { - voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap); - // Handle new representation - } else if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'object') { - voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap; - } - } - - // Get voiceIds from provider - let voiceIdsFromProvider; - try { - voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects(); - } - catch { - toastr.error('TTS Provider failed to return voice ids.'); - } - - // Build UI using VoiceMapEntry objects - for (const character of characters) { - if (character === 'SillyTavern System') { - continue; - } - // Check provider settings for voiceIds - let voiceId; - if (character in voiceMapFromSettings) { - voiceId = voiceMapFromSettings[character]; - } else if (character === DEFAULT_VOICE_MARKER) { - voiceId = DISABLED_VOICE_MARKER; - } else { - voiceId = DEFAULT_VOICE_MARKER; - } - const voiceMapEntry = new VoiceMapEntry(character, voiceId); - voiceMapEntry.addUI(voiceIdsFromProvider); - voiceMapEntries.push(voiceMapEntry); - } - updateVoiceMap(); -} - -$(document).ready(function () { - function addExtensionControls() { - const settingsHtml = ` -
-
-
- TTS -
-
-
-
-
- Select TTS Provider
-
- - -
-
- - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
- `; - $('#extensions_settings').append(settingsHtml); - $('#tts_refresh').on('click', onRefreshClick); - $('#tts_enabled').on('click', onEnableClick); - $('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick); - $('#tts_narrate_quoted').on('click', onNarrateQuotedClick); - $('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick); - $('#tts_skip_codeblocks').on('click', onSkipCodeblocksClick); - $('#tts_pass_asterisks').on('click', onPassAsterisksClick); - $('#tts_auto_generation').on('click', onAutoGenerationClick); - $('#tts_narrate_user').on('click', onNarrateUserClick); - $('#tts_voices').on('click', onTtsVoicesClick); - for (const provider in ttsProviders) { - $('#tts_provider').append($('` : + ``; + let template = ` +
+ ${this.name} + +
+ `; + $('#tts_voicemap_block').append(template); + + // Populate voice ID select list + for (const voiceId of voiceIds) { + const option = document.createElement('option'); + option.innerText = voiceId.name; + option.value = voiceId.name; + $(`#tts_voicemap_char_${sanitizedName}_voice`).append(option); + } + + this.selectElement = $(`#tts_voicemap_char_${sanitizedName}_voice`); + this.selectElement.on('change', args => this.onSelectChange(args)); + this.selectElement.val(this.voiceId); + } + + onSelectChange(args) { + this.voiceId = this.selectElement.find(':selected').val(); + updateVoiceMap(); + } +} + +/** + * Init voiceMapEntries for character select list. + * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat. + */ +export async function initVoiceMap(unrestricted = false) { + // Gate initialization if not enabled or TTS Provider not ready. Prevents error popups. + const enabled = $('#tts_enabled').is(':checked'); + if (!enabled) { + return; + } + + // Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying. + try { + await ttsProvider.checkReady(); + } catch (error) { + const message = `TTS Provider not ready. ${error}`; + setTtsStatus(message, false); + return; + } + + setTtsStatus('TTS Provider Loaded', true); + + // Clear existing voiceMap state + $('#tts_voicemap_block').empty(); + voiceMapEntries = []; + + // Get characters in current chat + const characters = getCharacters(unrestricted); + + // Get saved voicemap from provider settings, handling new and old representations + let voiceMapFromSettings = {}; + if ('voiceMap' in extension_settings.tts[ttsProviderName]) { + // Handle previous representation + if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'string') { + voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap); + // Handle new representation + } else if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'object') { + voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap; + } + } + + // Get voiceIds from provider + let voiceIdsFromProvider; + try { + voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects(); + } + catch { + toastr.error('TTS Provider failed to return voice ids.'); + } + + // Build UI using VoiceMapEntry objects + for (const character of characters) { + if (character === 'SillyTavern System') { + continue; + } + // Check provider settings for voiceIds + let voiceId; + if (character in voiceMapFromSettings) { + voiceId = voiceMapFromSettings[character]; + } else if (character === DEFAULT_VOICE_MARKER) { + voiceId = DISABLED_VOICE_MARKER; + } else { + voiceId = DEFAULT_VOICE_MARKER; + } + const voiceMapEntry = new VoiceMapEntry(character, voiceId); + voiceMapEntry.addUI(voiceIdsFromProvider); + voiceMapEntries.push(voiceMapEntry); + } + updateVoiceMap(); +} + +$(document).ready(function () { + function addExtensionControls() { + const settingsHtml = ` +
+
+
+ TTS +
+
+
+
+
+ Select TTS Provider
+
+ + +
+
+ + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ `; + $('#extensions_settings').append(settingsHtml); + $('#tts_refresh').on('click', onRefreshClick); + $('#tts_enabled').on('click', onEnableClick); + $('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick); + $('#tts_narrate_quoted').on('click', onNarrateQuotedClick); + $('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick); + $('#tts_skip_codeblocks').on('click', onSkipCodeblocksClick); + $('#tts_pass_asterisks').on('click', onPassAsterisksClick); + $('#tts_auto_generation').on('click', onAutoGenerationClick); + $('#tts_narrate_user').on('click', onNarrateUserClick); + $('#tts_voices').on('click', onTtsVoicesClick); + for (const provider in ttsProviders) { + $('#tts_provider').append($('