From 24b6261f468d2716d9c0114c10e678b149d12a8a Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Tue, 22 Aug 2023 08:30:17 -0500 Subject: [PATCH 01/14] add ready flag, add custom voice feature --- public/scripts/extensions/tts/novel.js | 74 +++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/public/scripts/extensions/tts/novel.js b/public/scripts/extensions/tts/novel.js index f3393c96a..05c6effa7 100644 --- a/public/scripts/extensions/tts/novel.js +++ b/public/scripts/extensions/tts/novel.js @@ -1,4 +1,4 @@ -import { getRequestHeaders } from "../../../script.js" +import { getRequestHeaders, callPopup } from "../../../script.js" import { getPreviewString } from "./index.js" export { NovelTtsProvider } @@ -9,22 +9,63 @@ class NovelTtsProvider { //########// settings + ready = false voices = [] separator = ' . ' audioElement = document.createElement('audio') defaultSettings = { - voiceMap: {} + voiceMap: {}, + customVoices: [] } get settingsHtml() { - let html = `Use NovelAI's TTS engine.
- The Voice IDs in the preview list are only examples, as it can be any string of text. Feel free to try different options!
- Hint: Save an API key in the NovelAI API settings to use it here.`; + let html = ` +
+ + Use NovelAI's TTS engine.
+ The default Voice IDs are only examples. Add custom voices and Novel will create a new random voice for it. Feel free to try different options!
+
+ Hint: Save an API key in the NovelAI API settings to use it here.
+ +
+ + + +
+ `; return html; } onSettingsChange() { + + } + + // Add a new Novel custom voice to provider + async addCustomVoice(){ + const voiceName = await callPopup('

Custom Voice name:

', 'input') + this.settings.customVoices.push(voiceName) + this.populateCustomVoices() + } + + // Delete selected custom voice from provider + deleteCustomVoice() { + const selected = $("#tts-novel-custom-voices-select").find(':selected').val(); + const voiceIndex = this.settings.customVoices.indexOf(selected); + + if (voiceIndex !== -1) { + this.settings.customVoices.splice(voiceIndex, 1); + } + this.populateCustomVoices() + } + + // Create the UI dropdown list of voices in provider + populateCustomVoices(){ + let voiceSelect = $("#tts-novel-custom-voices-select") + voiceSelect.empty() + this.settings.customVoices.forEach(voice => { + voiceSelect.append(``) + }) } loadSettings(settings) { @@ -32,6 +73,8 @@ class NovelTtsProvider { if (Object.keys(settings).length == 0) { console.info("Using default TTS Provider settings") } + $("#tts-novel-custom-voices-add").on('click', () => (this.addCustomVoice())) + $("#tts-novel-custom-voices-delete").on('click',() => (this.deleteCustomVoice())) // Only accept keys defined in defaultSettings this.settings = this.defaultSettings @@ -44,9 +87,22 @@ class NovelTtsProvider { } } + this.populateCustomVoices() + this.checkReady() console.info("Settings loaded") } + // Perform a simple readiness check by trying to fetch voiceIds + // Doesnt really do much for Novel, not seeing a good way to test this at the moment. + async checkReady(){ + try { + await this.fetchTtsVoiceIds() + this.ready = true + + } catch { + this.ready = false + } + } async onApplyClick() { return @@ -73,7 +129,7 @@ class NovelTtsProvider { // API CALLS // //###########// async fetchTtsVoiceIds() { - const voices = [ + let voices = [ { name: 'Ligeia', voice_id: 'Ligeia', lang: 'en-US', preview_url: false }, { name: 'Aini', voice_id: 'Aini', lang: 'en-US', preview_url: false }, { name: 'Orea', voice_id: 'Orea', lang: 'en-US', preview_url: false }, @@ -89,6 +145,12 @@ class NovelTtsProvider { { name: 'Lam', voice_id: 'Lam', lang: 'en-US', preview_url: false }, ]; + // Add in custom voices to the map + let addVoices = this.settings.customVoices.map(voice => + ({ name: voice, voice_id: voice, lang: 'en-US', preview_url: false }) + ) + voices = voices.concat(addVoices) + return voices; } From 56fcf1cbb8ee9e75cd0a51a4a7dbc53c58c553c4 Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Tue, 22 Aug 2023 08:30:33 -0500 Subject: [PATCH 02/14] add ready flag --- public/scripts/extensions/tts/coqui.js | 17 +++++++++++++++++ public/scripts/extensions/tts/edge.js | 18 ++++++++++++++++++ public/scripts/extensions/tts/elevenlabs.js | 14 ++++++++++++++ public/scripts/extensions/tts/silerotts.js | 18 ++++++++++++++++++ public/scripts/extensions/tts/system.js | 13 +++++++++++++ 5 files changed, 80 insertions(+) diff --git a/public/scripts/extensions/tts/coqui.js b/public/scripts/extensions/tts/coqui.js index cfaf83fec..c30b30ca1 100644 --- a/public/scripts/extensions/tts/coqui.js +++ b/public/scripts/extensions/tts/coqui.js @@ -84,6 +84,7 @@ class CoquiTtsProvider { //#############################// settings + ready defaultSettings = { voiceMap: "", @@ -147,6 +148,7 @@ class CoquiTtsProvider { loadSettings(settings) { // Only accept keys defined in defaultSettings this.settings = this.defaultSettings + this.ready = false for (const key in settings) { if (key in this.settings) { @@ -227,6 +229,21 @@ class CoquiTtsProvider { }); } + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady(){ + try { + if (!modules.includes('coqui-tts')){ + this.ready = false + return + } + await this.fetchTtsVoiceIds() + this.ready = true + + } catch { + this.ready = false + } + } + updateVoiceMap() { this.settings.voiceMap = ""; for (let i in this.settings.voiceMapDict) { diff --git a/public/scripts/extensions/tts/edge.js b/public/scripts/extensions/tts/edge.js index e0537a6c5..493f6eafd 100644 --- a/public/scripts/extensions/tts/edge.js +++ b/public/scripts/extensions/tts/edge.js @@ -11,6 +11,7 @@ class EdgeTtsProvider { //########// settings + ready = false voices = [] separator = ' . ' audioElement = document.createElement('audio') @@ -52,10 +53,27 @@ class EdgeTtsProvider { $('#edge_tts_rate').val(this.settings.rate || 0); $('#edge_tts_rate_output').text(this.settings.rate || 0); + this.checkReady() + console.info("Settings loaded") } + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady(){ + try { + if (!modules.includes('edge-tts')){ + this.ready = false + return + } + await this.fetchTtsVoiceIds() + this.ready = true + + } catch { + this.ready = false + } + } + async onApplyClick() { return } diff --git a/public/scripts/extensions/tts/elevenlabs.js b/public/scripts/extensions/tts/elevenlabs.js index 72c180cee..5a00e92d6 100644 --- a/public/scripts/extensions/tts/elevenlabs.js +++ b/public/scripts/extensions/tts/elevenlabs.js @@ -6,6 +6,7 @@ class ElevenLabsTtsProvider { //########// settings + ready = false voices = [] separator = ' ... ... ... ' @@ -66,9 +67,22 @@ class ElevenLabsTtsProvider { $('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost) $('#elevenlabs_tts_api_key').val(this.settings.apiKey) $('#tts_auto_generation').prop('checked', this.settings.multilingual) + + this.checkReady() console.info("Settings loaded") } + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady(){ + try { + await this.fetchTtsVoiceIds() + this.ready = true + + } catch { + this.ready = false + } + } + async onApplyClick() { // Update on Apply click return await this.updateApiKey().catch( (error) => { diff --git a/public/scripts/extensions/tts/silerotts.js b/public/scripts/extensions/tts/silerotts.js index c70d73ff6..17c8f2e41 100644 --- a/public/scripts/extensions/tts/silerotts.js +++ b/public/scripts/extensions/tts/silerotts.js @@ -8,6 +8,7 @@ class SileroTtsProvider { //########// settings + ready = false voices = [] separator = ' .. ' @@ -60,9 +61,26 @@ class SileroTtsProvider { }, 2000); $('#silero_tts_endpoint').val(this.settings.provider_endpoint) + + this.checkReady() + console.info("Settings loaded") } + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady(){ + try { + if (!modules.includes('silero-tts')){ + this.ready = false + return + } + await this.fetchTtsVoiceIds() + this.ready = true + + } catch { + this.ready = false + } + } async onApplyClick() { return diff --git a/public/scripts/extensions/tts/system.js b/public/scripts/extensions/tts/system.js index 5e8c5f2b0..ae104b7b8 100644 --- a/public/scripts/extensions/tts/system.js +++ b/public/scripts/extensions/tts/system.js @@ -80,6 +80,7 @@ class SystemTtsProvider { //########// settings + ready = false voices = [] separator = ' ... ' @@ -145,9 +146,21 @@ class SystemTtsProvider { $('#system_tts_pitch').val(this.settings.pitch || this.defaultSettings.pitch); $('#system_tts_pitch_output').text(this.settings.pitch); $('#system_tts_rate_output').text(this.settings.rate); + this.checkReady() console.info("Settings loaded"); } + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady(){ + try { + await this.fetchTtsVoiceIds() + this.ready = true + + } catch { + this.ready = false + } + } + async onApplyClick() { return } From 5b43fe25e8642c29b895a0b63399a2e51d4e5826 Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Wed, 23 Aug 2023 08:27:53 -0500 Subject: [PATCH 03/14] update checkReady. add voiceMap ui select --- public/scripts/extensions/tts/coqui.js | 20 +- public/scripts/extensions/tts/edge.js | 19 +- public/scripts/extensions/tts/elevenlabs.js | 9 +- public/scripts/extensions/tts/index.js | 271 +++++++++++++++----- public/scripts/extensions/tts/novel.js | 9 +- public/scripts/extensions/tts/silerotts.js | 12 +- public/scripts/extensions/tts/style.css | 16 ++ public/scripts/extensions/tts/system.js | 9 +- 8 files changed, 234 insertions(+), 131 deletions(-) diff --git a/public/scripts/extensions/tts/coqui.js b/public/scripts/extensions/tts/coqui.js index c30b30ca1..27e261f9c 100644 --- a/public/scripts/extensions/tts/coqui.js +++ b/public/scripts/extensions/tts/coqui.js @@ -42,8 +42,9 @@ const languageLabels = { function throwIfModuleMissing() { if (!modules.includes('coqui-tts')) { - toastr.error(`Add coqui-tts to enable-modules and restart the Extras API.`, "Coqui TTS module not loaded.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - throw new Error(DEBUG_PREFIX, `Coqui TTS module not loaded.`); + const message = `Coqui TTS module not loaded. Add coqui-tts to enable-modules and restart the Extras API.` + // toastr.error(message, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); + throw new Error(DEBUG_PREFIX, message); } } @@ -84,7 +85,6 @@ class CoquiTtsProvider { //#############################// settings - ready defaultSettings = { voiceMap: "", @@ -148,7 +148,6 @@ class CoquiTtsProvider { loadSettings(settings) { // Only accept keys defined in defaultSettings this.settings = this.defaultSettings - this.ready = false for (const key in settings) { if (key in this.settings) { @@ -231,17 +230,8 @@ class CoquiTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ - try { - if (!modules.includes('coqui-tts')){ - this.ready = false - return - } - await this.fetchTtsVoiceIds() - this.ready = true - - } catch { - this.ready = false - } + throwIfModuleMissing() + await this.fetchTtsVoiceIds() } updateVoiceMap() { diff --git a/public/scripts/extensions/tts/edge.js b/public/scripts/extensions/tts/edge.js index 493f6eafd..7a0b92e87 100644 --- a/public/scripts/extensions/tts/edge.js +++ b/public/scripts/extensions/tts/edge.js @@ -11,7 +11,6 @@ class EdgeTtsProvider { //########// settings - ready = false voices = [] separator = ' . ' audioElement = document.createElement('audio') @@ -61,17 +60,8 @@ class EdgeTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ - try { - if (!modules.includes('edge-tts')){ - this.ready = false - return - } - await this.fetchTtsVoiceIds() - this.ready = true - - } catch { - this.ready = false - } + throwIfModuleMissing() + await this.fetchTtsVoiceIds() } async onApplyClick() { @@ -162,8 +152,9 @@ class EdgeTtsProvider { } function throwIfModuleMissing() { if (!modules.includes('edge-tts')) { - toastr.error(`Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`) - throw new Error(`Edge TTS module not loaded.`) + const message = `Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.` + // toastr.error(message) + throw new Error(message) } } diff --git a/public/scripts/extensions/tts/elevenlabs.js b/public/scripts/extensions/tts/elevenlabs.js index 5a00e92d6..5bf964f18 100644 --- a/public/scripts/extensions/tts/elevenlabs.js +++ b/public/scripts/extensions/tts/elevenlabs.js @@ -6,7 +6,6 @@ class ElevenLabsTtsProvider { //########// settings - ready = false voices = [] separator = ' ... ... ... ' @@ -74,13 +73,7 @@ class ElevenLabsTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ - try { - await this.fetchTtsVoiceIds() - this.ready = true - - } catch { - this.ready = false - } + await this.fetchTtsVoiceIds() } async onApplyClick() { diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 65e3e5a4a..9f18262b8 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -13,6 +13,7 @@ export { talkingAnimation }; const UPDATE_INTERVAL = 1000 +let voiceMapEntries = [] let voiceMap = {} // {charName:voiceid, charName2:voiceid2} let audioControl let storedvalue = false; @@ -224,8 +225,8 @@ function debugTtsPlayback() { console.log(JSON.stringify( { "ttsProviderName": ttsProviderName, + "voiceMap": voiceMap, "currentMessageNumber": currentMessageNumber, - "isWorkerBusy": isWorkerBusy, "audioPaused": audioPaused, "audioJobQueue": audioJobQueue, "currentAudioJob": currentAudioJob, @@ -486,6 +487,7 @@ function loadSettings() { if (Object.keys(extension_settings.tts).length === 0) { Object.assign(extension_settings.tts, defaultSettings) } + $('#tts_provider').val(extension_settings.tts.currentProvider) $('#tts_enabled').prop( 'checked', extension_settings.tts.enabled @@ -513,59 +515,17 @@ function setTtsStatus(status, success) { } } -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 -} - -async function voicemapIsValid(parsedVoiceMap) { - let valid = true - for (const characterName in parsedVoiceMap) { - const parsedVoiceName = parsedVoiceMap[characterName] - try { - await ttsProvider.getVoice(parsedVoiceName) - } catch (error) { - console.error(error) - valid = false - } - } - return valid -} - -async function updateVoiceMap() { - let isValidResult = false - - const value = $('#tts_voice_map').val() - const parsedVoiceMap = parseVoiceMap(value) - - isValidResult = await voicemapIsValid(parsedVoiceMap) - if (isValidResult) { - ttsProvider.settings.voiceMap = String(value) - // console.debug(`ttsProvider.voiceMap: ${ttsProvider.settings.voiceMap}`) - voiceMap = parsedVoiceMap - console.debug(`Saved new voiceMap: ${value}`) - saveSettingsDebounced() - } else { - throw 'Voice map is invalid, check console for errors' - } -} - function onApplyClick() { Promise.all([ ttsProvider.onApplyClick(), - updateVoiceMap() + // 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) @@ -608,13 +568,14 @@ function onNarrateTranslatedOnlyClick() { // TTS Provider // //##############// -function loadTtsProvider(provider) { +async function loadTtsProvider(provider) { //Clear the current config and add new config $("#tts_provider_settings").html("") if (!provider) { - provider + return } + // Init provider references extension_settings.tts.currentProvider = provider ttsProviderName = provider @@ -626,27 +587,17 @@ function loadTtsProvider(provider) { console.warn(`Provider ${ttsProviderName} not in Extension Settings, initiatilizing provider in settings`) extension_settings.tts[ttsProviderName] = {} } - - // Load voicemap settings - let voiceMapFromSettings - if ("voiceMap" in extension_settings.tts[ttsProviderName]) { - voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap - voiceMap = parseVoiceMap(voiceMapFromSettings) - } else { - voiceMapFromSettings = "" - voiceMap = {} - } - $('#tts_voice_map').val(voiceMapFromSettings) - $('#tts_provider').val(ttsProviderName) - ttsProvider.loadSettings(extension_settings.tts[ttsProviderName]) + 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. function onTtsProviderSettingsInput() { ttsProvider.onSettingsChange() @@ -658,6 +609,191 @@ function onTtsProviderSettingsInput() { } +//###################// +// voiceMap Handling // +//###################// + +async function onChatChanged() { + await resetTtsPlayback() + await initVoiceMap() +} + +function getCharacters(){ + const context = getContext() + let characters = [] + if (context.groupId === null){ + // Single char chat + characters.push(context.name1) + characters.push(context.name2) + } else { + // Group chat + 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)}`) + } + extension_settings.tts[ttsProviderName].voiceMap = voiceMap + saveSettingsDebounced() +} + +class VoiceMapEntry { + name + voiceId + selectElement + constructor (name, voiceId='disabled') { + this.name = name + this.voiceId = voiceId + this.selectElement = null + } + + addUI(voiceIds){ + let sanitizedName = sanitizeId(this.name) + 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. Should only be called when character/chat is changed. + * + */ +async function initVoiceMap(){ + // Clear existing voiceMap state + $('#tts_voicemap_block').empty() + voiceMapEntries = [] + + // 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) + + // Get characters in current chat + const characters = getCharacters() + + // 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.fetchTtsVoiceIds() + } + 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 { + voiceId = 'disabled' + } + const voiceMapEntry = new VoiceMapEntry(character, voiceId) + voiceMapEntry.addUI(voiceIdsFromProvider) + voiceMapEntries.push(voiceMapEntry) + } + updateVoiceMap() +} $(document).ready(function () { function addExtensionControls() { @@ -669,6 +805,8 @@ $(document).ready(function () {
+
+
Select TTS Provider
- -
+
+
- +
@@ -735,4 +870,6 @@ $(document).ready(function () { const wrapper = new ModuleWorkerWrapper(moduleWorker); setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL) // Init depends on all the things eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback); + eventSource.on(event_types.CHAT_CHANGED, onChatChanged) + eventSource.on(event_types.GROUP_UPDATED, onChatChanged) }) diff --git a/public/scripts/extensions/tts/novel.js b/public/scripts/extensions/tts/novel.js index 05c6effa7..1b1d0c456 100644 --- a/public/scripts/extensions/tts/novel.js +++ b/public/scripts/extensions/tts/novel.js @@ -9,7 +9,6 @@ class NovelTtsProvider { //########// settings - ready = false voices = [] separator = ' . ' audioElement = document.createElement('audio') @@ -95,13 +94,7 @@ class NovelTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds // Doesnt really do much for Novel, not seeing a good way to test this at the moment. async checkReady(){ - try { - await this.fetchTtsVoiceIds() - this.ready = true - - } catch { - this.ready = false - } + await this.fetchTtsVoiceIds() } async onApplyClick() { diff --git a/public/scripts/extensions/tts/silerotts.js b/public/scripts/extensions/tts/silerotts.js index 17c8f2e41..3628fa2ee 100644 --- a/public/scripts/extensions/tts/silerotts.js +++ b/public/scripts/extensions/tts/silerotts.js @@ -69,17 +69,7 @@ class SileroTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ - try { - if (!modules.includes('silero-tts')){ - this.ready = false - return - } - await this.fetchTtsVoiceIds() - this.ready = true - - } catch { - this.ready = false - } + await this.fetchTtsVoiceIds() } async onApplyClick() { diff --git a/public/scripts/extensions/tts/style.css b/public/scripts/extensions/tts/style.css index 81bd0727d..a2f0d8057 100644 --- a/public/scripts/extensions/tts/style.css +++ b/public/scripts/extensions/tts/style.css @@ -50,4 +50,20 @@ .voice_preview .fa-play { cursor: pointer; +} + +.tts-button { + margin: 0; + outline: none; + border: none; + cursor: pointer; + transition: 0.3s; + opacity: 0.7; + align-items: center; + justify-content: center; + +} + +.tts-button:hover { + opacity: 1; } \ No newline at end of file diff --git a/public/scripts/extensions/tts/system.js b/public/scripts/extensions/tts/system.js index ae104b7b8..b97fdcd2d 100644 --- a/public/scripts/extensions/tts/system.js +++ b/public/scripts/extensions/tts/system.js @@ -146,19 +146,12 @@ class SystemTtsProvider { $('#system_tts_pitch').val(this.settings.pitch || this.defaultSettings.pitch); $('#system_tts_pitch_output').text(this.settings.pitch); $('#system_tts_rate_output').text(this.settings.rate); - this.checkReady() console.info("Settings loaded"); } // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ - try { - await this.fetchTtsVoiceIds() - this.ready = true - - } catch { - this.ready = false - } + await this.fetchTtsVoiceIds() } async onApplyClick() { From 44cd4287cbba2e35f51a2372a6c19e8854a76ffa Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Fri, 25 Aug 2023 08:27:43 -0500 Subject: [PATCH 04/14] coqui voices, change how provider settings save --- public/scripts/extensions/tts/coqui.js | 130 ++++++++------------ public/scripts/extensions/tts/edge.js | 4 +- public/scripts/extensions/tts/elevenlabs.js | 31 +++-- public/scripts/extensions/tts/index.js | 19 ++- public/scripts/extensions/tts/novel.js | 4 +- public/scripts/extensions/tts/silerotts.js | 3 + public/scripts/extensions/tts/style.css | 7 ++ public/scripts/extensions/tts/system.js | 9 +- 8 files changed, 105 insertions(+), 102 deletions(-) diff --git a/public/scripts/extensions/tts/coqui.js b/public/scripts/extensions/tts/coqui.js index 27e261f9c..a05291d25 100644 --- a/public/scripts/extensions/tts/coqui.js +++ b/public/scripts/extensions/tts/coqui.js @@ -5,6 +5,8 @@ TODO: */ import { doExtrasFetch, extension_settings, getApiUrl, getContext, modules, ModuleWorkerWrapper } from "../../extensions.js" +import { callPopup } from "../../../script.js" +import { onTtsProviderSettingsInput } from "./index.js" export { CoquiTtsProvider } @@ -12,7 +14,7 @@ const DEBUG_PREFIX = " "; const UPDATE_INTERVAL = 1000; let inApiCall = false; -let charactersList = []; // Updated with module worker +let voiceIdList = []; // Updated with module worker let coquiApiModels = {}; // Initialized only once let coquiApiModelsFull = {}; // Initialized only once let coquiLocalModels = []; // Initialized only once @@ -52,33 +54,7 @@ function resetModelSettings() { $("#coqui_api_model_settings_language").val("none"); $("#coqui_api_model_settings_speaker").val("none"); } - -function updateCharactersList() { - let currentcharacters = new Set(); - for (const i of getContext().characters) { - currentcharacters.add(i.name); - } - - currentcharacters = Array.from(currentcharacters) - - if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) { - charactersList = currentcharacters - - $('#coqui_character_select') - .find('option') - .remove() - .end() - .append('') - .val('none') - - for (const charName of charactersList) { - $("#coqui_character_select").append(new Option(charName, charName)); - } - - console.debug(DEBUG_PREFIX, "Updated character list to:", charactersList); - } -} - + class CoquiTtsProvider { //#############################// // Extension UI and Settings // @@ -88,6 +64,7 @@ class CoquiTtsProvider { defaultSettings = { voiceMap: "", + voiceIds: [], voiceMapDict: {} } @@ -96,13 +73,15 @@ class CoquiTtsProvider {
- - - - - +
+ + +
- - - - - +
+ + + + + + + + +
` return html } @@ -42,6 +46,7 @@ class ElevenLabsTtsProvider { this.settings.stability = $('#elevenlabs_tts_stability').val() this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val() this.settings.multilingual = $('#elevenlabs_tts_multilingual').prop('checked') + onTtsProviderSettingsInput() } @@ -66,6 +71,8 @@ class ElevenLabsTtsProvider { $('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost) $('#elevenlabs_tts_api_key').val(this.settings.apiKey) $('#tts_auto_generation').prop('checked', this.settings.multilingual) + $('#eleven_labs_connect').on('click',this.onConnectClick) + $('#elevenlabs_tts_settings').on('input',this.onSettingsChange) this.checkReady() console.info("Settings loaded") @@ -77,6 +84,9 @@ class ElevenLabsTtsProvider { } async onApplyClick() { + } + + async onConnectClick() { // Update on Apply click return await this.updateApiKey().catch( (error) => { throw error @@ -93,6 +103,7 @@ class ElevenLabsTtsProvider { }) this.settings.apiKey = this.settings.apiKey console.debug(`Saved new API_KEY: ${this.settings.apiKey}`) + this.onSettingsChange() } //#################// diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 9f18262b8..458c49b30 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -515,7 +515,7 @@ function setTtsStatus(status, success) { } } -function onApplyClick() { +function onRefreshClick() { Promise.all([ ttsProvider.onApplyClick(), // updateVoiceMap() @@ -598,11 +598,7 @@ function onTtsProviderChange() { } // Ensure that TTS provider settings are saved to extension settings. -function onTtsProviderSettingsInput() { - ttsProvider.onSettingsChange() - - // Persist changes to SillyTavern tts extension settings - +export function onTtsProviderSettingsInput() { extension_settings.tts[ttsProviderName] = ttsProvider.settings saveSettingsDebounced() console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`) @@ -807,10 +803,11 @@ $(document).ready(function () {
-
- Select TTS Provider
- +
@@ -849,14 +845,13 @@ $(document).ready(function () {
` $('#extensions_settings').append(settingsHtml) - $('#tts_apply').on('click', onApplyClick) + $('#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_auto_generation').on('click', onAutoGenerationClick); $('#tts_voices').on('click', onTtsVoicesClick) - $('#tts_provider_settings').on('input', onTtsProviderSettingsInput) for (const provider in ttsProviders) { $('#tts_provider').append($("`) }) + this.onSettingsChange() } loadSettings(settings) { diff --git a/public/scripts/extensions/tts/silerotts.js b/public/scripts/extensions/tts/silerotts.js index 3628fa2ee..0f2db6de8 100644 --- a/public/scripts/extensions/tts/silerotts.js +++ b/public/scripts/extensions/tts/silerotts.js @@ -1,4 +1,5 @@ import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js" +import { onTtsProviderSettingsInput } from "./index.js" export { SileroTtsProvider } @@ -30,6 +31,7 @@ class SileroTtsProvider { onSettingsChange() { // Used when provider settings are updated from UI this.settings.provider_endpoint = $('#silero_tts_endpoint').val() + onTtsProviderSettingsInput() } loadSettings(settings) { @@ -61,6 +63,7 @@ class SileroTtsProvider { }, 2000); $('#silero_tts_endpoint').val(this.settings.provider_endpoint) + $('#silero_tts_endpoint').on("input", this.onSettingsChange) this.checkReady() diff --git a/public/scripts/extensions/tts/style.css b/public/scripts/extensions/tts/style.css index a2f0d8057..5df2b0fc7 100644 --- a/public/scripts/extensions/tts/style.css +++ b/public/scripts/extensions/tts/style.css @@ -66,4 +66,11 @@ .tts-button:hover { opacity: 1; +} + +.tts_block { + display: flex; + align-items: center; + column-gap: 5px; + flex-wrap: wrap; } \ No newline at end of file diff --git a/public/scripts/extensions/tts/system.js b/public/scripts/extensions/tts/system.js index b97fdcd2d..102b0bf1b 100644 --- a/public/scripts/extensions/tts/system.js +++ b/public/scripts/extensions/tts/system.js @@ -1,7 +1,7 @@ import { isMobile } from "../../RossAscends-mods.js"; import { getPreviewString } from "./index.js"; import { talkingAnimation } from './index.js'; - +import { onTtsProviderSettingsInput } from "./index.js" export { SystemTtsProvider } /** @@ -107,7 +107,7 @@ class SystemTtsProvider { this.settings.pitch = Number($('#system_tts_pitch').val()); $('#system_tts_pitch_output').text(this.settings.pitch); $('#system_tts_rate_output').text(this.settings.rate); - console.log('Save changes'); + onTtsProviderSettingsInput() } loadSettings(settings) { @@ -144,6 +144,11 @@ class SystemTtsProvider { $('#system_tts_rate').val(this.settings.rate || this.defaultSettings.rate); $('#system_tts_pitch').val(this.settings.pitch || this.defaultSettings.pitch); + + // Trigger updates + $('#system_tts_rate').on("input", this.onSettingsChange) + $('#system_tts_rate').on("input", this.onSettingsChange) + $('#system_tts_pitch_output').text(this.settings.pitch); $('#system_tts_rate_output').text(this.settings.rate); console.info("Settings loaded"); From 3ab9aee1957d0a809e5898ed2b92bd25295197ec Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Fri, 25 Aug 2023 08:45:37 -0500 Subject: [PATCH 05/14] elevenlabs connect button --- public/scripts/extensions/tts/elevenlabs.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/scripts/extensions/tts/elevenlabs.js b/public/scripts/extensions/tts/elevenlabs.js index fb7ae461a..86ecfa1c4 100644 --- a/public/scripts/extensions/tts/elevenlabs.js +++ b/public/scripts/extensions/tts/elevenlabs.js @@ -66,12 +66,11 @@ class ElevenLabsTtsProvider { throw `Invalid setting passed to TTS Provider: ${key}` } } - $('#elevenlabs_tts_stability').val(this.settings.stability) $('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost) $('#elevenlabs_tts_api_key').val(this.settings.apiKey) $('#tts_auto_generation').prop('checked', this.settings.multilingual) - $('#eleven_labs_connect').on('click',this.onConnectClick) + $('#eleven_labs_connect').on('click', () => {this.onConnectClick()}) $('#elevenlabs_tts_settings').on('input',this.onSettingsChange) this.checkReady() From 1417aa12f17b4cff7431a066923fde9d15ef692d Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Fri, 25 Aug 2023 08:51:35 -0500 Subject: [PATCH 06/14] fix bug with function call context --- public/scripts/extensions/tts/edge.js | 2 +- public/scripts/extensions/tts/elevenlabs.js | 2 +- public/scripts/extensions/tts/silerotts.js | 2 +- public/scripts/extensions/tts/system.js | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/scripts/extensions/tts/edge.js b/public/scripts/extensions/tts/edge.js index adb296f5a..eadfd6bee 100644 --- a/public/scripts/extensions/tts/edge.js +++ b/public/scripts/extensions/tts/edge.js @@ -53,7 +53,7 @@ class EdgeTtsProvider { $('#edge_tts_rate').val(this.settings.rate || 0); $('#edge_tts_rate_output').text(this.settings.rate || 0); - $('#edge_tts_rate').on("input",this.onSettingsChange) + $('#edge_tts_rate').on("input", () => {this.onSettingsChange()}) this.checkReady() console.info("Settings loaded") diff --git a/public/scripts/extensions/tts/elevenlabs.js b/public/scripts/extensions/tts/elevenlabs.js index 86ecfa1c4..d282c1512 100644 --- a/public/scripts/extensions/tts/elevenlabs.js +++ b/public/scripts/extensions/tts/elevenlabs.js @@ -88,7 +88,7 @@ class ElevenLabsTtsProvider { async onConnectClick() { // Update on Apply click return await this.updateApiKey().catch( (error) => { - throw error + toastr.error(`ElevenLabs: ${error}`) }) } diff --git a/public/scripts/extensions/tts/silerotts.js b/public/scripts/extensions/tts/silerotts.js index 0f2db6de8..3d0f3e101 100644 --- a/public/scripts/extensions/tts/silerotts.js +++ b/public/scripts/extensions/tts/silerotts.js @@ -63,7 +63,7 @@ class SileroTtsProvider { }, 2000); $('#silero_tts_endpoint').val(this.settings.provider_endpoint) - $('#silero_tts_endpoint').on("input", this.onSettingsChange) + $('#silero_tts_endpoint').on("input", () => {this.onSettingsChange()}) this.checkReady() diff --git a/public/scripts/extensions/tts/system.js b/public/scripts/extensions/tts/system.js index 102b0bf1b..473523aff 100644 --- a/public/scripts/extensions/tts/system.js +++ b/public/scripts/extensions/tts/system.js @@ -146,8 +146,8 @@ class SystemTtsProvider { $('#system_tts_pitch').val(this.settings.pitch || this.defaultSettings.pitch); // Trigger updates - $('#system_tts_rate').on("input", this.onSettingsChange) - $('#system_tts_rate').on("input", this.onSettingsChange) + $('#system_tts_rate').on("input", () =>{this.onSettingsChange()}) + $('#system_tts_rate').on("input", () => {this.onSettingsChange()}) $('#system_tts_pitch_output').text(this.settings.pitch); $('#system_tts_rate_output').text(this.settings.rate); From d03af9b41dbde04fa7dbb5e96fff652957314688 Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Fri, 25 Aug 2023 22:51:58 -0500 Subject: [PATCH 07/14] name updates, complete custom voices --- public/scripts/extensions/tts/coqui.js | 114 +++++++++++++------------ 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/public/scripts/extensions/tts/coqui.js b/public/scripts/extensions/tts/coqui.js index a05291d25..2c8cc5045 100644 --- a/public/scripts/extensions/tts/coqui.js +++ b/public/scripts/extensions/tts/coqui.js @@ -6,7 +6,7 @@ TODO: import { doExtrasFetch, extension_settings, getApiUrl, getContext, modules, ModuleWorkerWrapper } from "../../extensions.js" import { callPopup } from "../../../script.js" -import { onTtsProviderSettingsInput } from "./index.js" +import { initVoiceMap } from "./index.js" export { CoquiTtsProvider } @@ -63,7 +63,8 @@ class CoquiTtsProvider { settings defaultSettings = { - voiceMap: "", + voiceMap: {}, + customVoices: {}, voiceIds: [], voiceMapDict: {} } @@ -74,8 +75,8 @@ class CoquiTtsProvider {
To use CoquiTTS, select the origin, language, and model, then click Add Voice. The voice will then be available to add to a character. Voices are saved globally.
- -
@@ -137,7 +138,7 @@ class CoquiTtsProvider { } initLocalModels(); - this.updateVoiceMap(); // Overide any manual modification + this.updateCustomVoices(); // Overide any manual modification $("#coqui_api_model_div").hide(); $("#coqui_local_model_div").hide(); @@ -200,48 +201,44 @@ class CoquiTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ throwIfModuleMissing() - await this.fetchTtsVoiceIds() + await this.fetchTtsVoiceObjects() } - updateVoiceMap() { + updateCustomVoices() { // Takes voiceMapDict and converts it to a string to save to voiceMap - this.settings.voiceMap = ""; - for (let i in this.settings.voiceMapDict) { - const voice_settings = this.settings.voiceMapDict[i]; - this.settings.voiceMap += i + ":" + voice_settings["model_id"]; + this.settings.customVoices = {}; + for (let voiceName in this.settings.voiceMapDict) { + const voiceId = this.settings.voiceMapDict[voiceName]; + this.settings.customVoices[voiceName] = voiceId["model_id"]; - if (voice_settings["model_language"] != null) - this.settings.voiceMap += "[" + voice_settings["model_language"] + "]"; + if (voiceId["model_language"] != null) + this.settings.customVoices[voiceName] += "[" + voiceId["model_language"] + "]"; - if (voice_settings["model_speaker"] != null) - this.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]"; - - this.settings.voiceMap += ","; + if (voiceId["model_speaker"] != null) + this.settings.customVoices[voiceName] += "[" + voiceId["model_speaker"] + "]"; } // Update UI select list with voices - $("#coqui_voiceid_select").empty() - $('#coqui_voiceid_select') + $("#coqui_voicename_select").empty() + $('#coqui_voicename_select') .find('option') .remove() .end() - .append('') + .append('') .val('none') - for (const voiceId in this.settings.voiceMapDict) { - $("#coqui_voiceid_select").append(new Option(voiceId, voiceId)); + for (const voiceName in this.settings.voiceMapDict) { + $("#coqui_voicename_select").append(new Option(voiceName, voiceName)); } - extension_settings.tts.Coqui = this.settings; this.onSettingsChange() } onSettingsChange() { console.debug(DEBUG_PREFIX, "Settings changes", this.settings); extension_settings.tts.Coqui = this.settings; - onTtsProviderSettingsInput() } - async onApplyClick() { + async onRefreshClick() { this.checkReady() } @@ -251,7 +248,7 @@ class CoquiTtsProvider { } // Ask user for voiceId name to save voice - const voiceId = await callPopup('

Name of Coqui voice to add to voice select dropdown:

', 'input') + const voiceName = await callPopup('

Name of Coqui voice to add to voice select dropdown:

', 'input') const model_origin = $("#coqui_model_origin").val(); const model_language = $("#coqui_api_language").val(); @@ -260,15 +257,15 @@ class CoquiTtsProvider { let model_setting_speaker = $("#coqui_api_model_settings_speaker").val(); - if (!voiceId) { - toastr.error(`VoiceId empty, please enter one.`, DEBUG_PREFIX + " voice mapping voiceId", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + if (!voiceName) { + toastr.error(`Voice name empty, please enter one.`, DEBUG_PREFIX + " voice mapping voice name", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); + this.updateCustomVoices(); // Overide any manual modification return; } if (model_origin == "none") { toastr.error(`Origin not selected, please select one.`, DEBUG_PREFIX + " voice mapping origin", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + this.updateCustomVoices(); // Overide any manual modification return; } @@ -277,25 +274,25 @@ class CoquiTtsProvider { if (model_name == "none") { toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + this.updateCustomVoices(); // Overide any manual modification return; } - this.settings.voiceMapDict[voiceId] = { model_type: "local", model_id: "local/" + model_id }; - console.debug(DEBUG_PREFIX, "Registered new voice map: ", voiceId, ":", this.settings.voiceMapDict[voiceId]); - this.updateVoiceMap(); // Overide any manual modification + this.settings.voiceMapDict[voiceName] = { model_type: "local", model_id: "local/" + model_id }; + console.debug(DEBUG_PREFIX, "Registered new voice map: ", voiceName, ":", this.settings.voiceMapDict[voiceName]); + this.updateCustomVoices(); // Overide any manual modification return; } if (model_language == "none") { toastr.error(`Language not selected, please select one.`, DEBUG_PREFIX + " voice mapping language", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + this.updateCustomVoices(); // Overide any manual modification return; } if (model_name == "none") { toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); - this.updateVoiceMap(); // Overide any manual modification + this.updateCustomVoices(); // Overide any manual modification return; } @@ -324,42 +321,48 @@ class CoquiTtsProvider { return; } - console.debug(DEBUG_PREFIX, "Current voice map: ", this.settings.voiceMap); + console.debug(DEBUG_PREFIX, "Current custom voices: ", this.settings.customVoices); - this.settings.voiceMapDict[voiceId] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker }; + this.settings.voiceMapDict[voiceName] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker }; - console.debug(DEBUG_PREFIX, "Registered new voice map: ", voiceId, ":", this.settings.voiceMapDict[voiceId]); + console.debug(DEBUG_PREFIX, "Registered new voice map: ", voiceName, ":", this.settings.voiceMapDict[voiceName]); - this.updateVoiceMap(); + this.updateCustomVoices(); + initVoiceMap() // Update TTS extension voiceMap - let successMsg = voiceId + ":" + model_id; + let successMsg = voiceName + ":" + model_id; if (model_setting_language != null) successMsg += "[" + model_setting_language + "]"; if (model_setting_speaker != null) successMsg += "[" + model_setting_speaker + "]"; toastr.info(successMsg, DEBUG_PREFIX + " voice map updated", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); + return } - // DBG: assume voiceName is correct - // TODO: check voice is correct async getVoice(voiceName) { - console.log(DEBUG_PREFIX, "getVoice", voiceName); - const output = { voice_id: voiceName }; - return output; + let match = await this.fetchTtsVoiceObjects() + match = match.filter( + voice => voice.name == voiceName + )[0] + if (!match) { + throw `TTS Voice name ${voiceName} not found in CoquiTTS Provider voice list` + } + return match; } async onRemoveClick() { - const voiceId = $("#coqui_voiceid_select").val(); + const voiceName = $("#coqui_voicename_select").val(); - if (voiceId === "none") { - toastr.error(`VoiceId not selected, please select one.`, DEBUG_PREFIX + " voice mapping voiceId", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); + if (voiceName === "none") { + toastr.error(`Voice not selected, please select one.`, DEBUG_PREFIX + " voice mapping voiceId", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); return; } // Todo erase from voicemap - delete (this.settings.voiceMapDict[voiceId]); - this.updateVoiceMap(); // TODO + delete (this.settings.voiceMapDict[voiceName]); + this.updateCustomVoices(); + initVoiceMap() // Update TTS extension voiceMap } async onModelOriginChange() { @@ -677,6 +680,8 @@ class CoquiTtsProvider { // ts_models/ja/kokoro/tacotron2-DDC async generateTts(text, voiceId) { throwIfModuleMissing() + voiceId = this.settings.customVoices[voiceId] + const url = new URL(getApiUrl()); url.pathname = '/api/text-to-speech/coqui/generate-tts'; @@ -724,8 +729,11 @@ class CoquiTtsProvider { } // Dirty hack to say not implemented - async fetchTtsVoiceIds() { - return [{ name: "Voice samples not implemented for coqui TTS yet, search for the model samples online", voice_id: "", lang: "", }] + async fetchTtsVoiceObjects() { + const voiceIds = Object + .keys(this.settings.voiceMapDict) + .map(voice => ({ name: voice, voice_id: voice, preview_url: false })); + return voiceIds } // Do nothing From b3a4787db6db7e792842c8cdf50032239ac3a7ab Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Fri, 25 Aug 2023 22:52:26 -0500 Subject: [PATCH 08/14] name changes --- public/scripts/extensions/tts/edge.js | 12 ++++++------ public/scripts/extensions/tts/elevenlabs.js | 14 +++++++------- public/scripts/extensions/tts/novel.js | 14 ++++++-------- public/scripts/extensions/tts/silerotts.js | 12 ++++++------ public/scripts/extensions/tts/system.js | 10 +++++----- 5 files changed, 30 insertions(+), 32 deletions(-) diff --git a/public/scripts/extensions/tts/edge.js b/public/scripts/extensions/tts/edge.js index eadfd6bee..015509f5c 100644 --- a/public/scripts/extensions/tts/edge.js +++ b/public/scripts/extensions/tts/edge.js @@ -2,7 +2,7 @@ import { getRequestHeaders } from "../../../script.js" import { getApiUrl } from "../../extensions.js" import { doExtrasFetch, modules } from "../../extensions.js" import { getPreviewString } from "./index.js" -import { onTtsProviderSettingsInput } from "./index.js" +import { saveTtsProviderSettings } from "./index.js" export { EdgeTtsProvider } @@ -31,7 +31,7 @@ class EdgeTtsProvider { onSettingsChange() { this.settings.rate = Number($('#edge_tts_rate').val()); $('#edge_tts_rate_output').text(this.settings.rate); - onTtsProviderSettingsInput() + saveTtsProviderSettings() } loadSettings(settings) { @@ -63,10 +63,10 @@ class EdgeTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ throwIfModuleMissing() - await this.fetchTtsVoiceIds() + await this.fetchTtsVoiceObjects() } - async onApplyClick() { + async onRefreshClick() { return } @@ -76,7 +76,7 @@ class EdgeTtsProvider { async getVoice(voiceName) { if (this.voices.length == 0) { - this.voices = await this.fetchTtsVoiceIds() + this.voices = await this.fetchTtsVoiceObjects() } const match = this.voices.filter( voice => voice.name == voiceName @@ -95,7 +95,7 @@ class EdgeTtsProvider { //###########// // API CALLS // //###########// - async fetchTtsVoiceIds() { + async fetchTtsVoiceObjects() { throwIfModuleMissing() const url = new URL(getApiUrl()); diff --git a/public/scripts/extensions/tts/elevenlabs.js b/public/scripts/extensions/tts/elevenlabs.js index d282c1512..6397c0f12 100644 --- a/public/scripts/extensions/tts/elevenlabs.js +++ b/public/scripts/extensions/tts/elevenlabs.js @@ -1,4 +1,4 @@ -import { onTtsProviderSettingsInput } from "./index.js" +import { saveTtsProviderSettings } from "./index.js" export { ElevenLabsTtsProvider } class ElevenLabsTtsProvider { @@ -46,7 +46,7 @@ class ElevenLabsTtsProvider { this.settings.stability = $('#elevenlabs_tts_stability').val() this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val() this.settings.multilingual = $('#elevenlabs_tts_multilingual').prop('checked') - onTtsProviderSettingsInput() + saveTtsProviderSettings() } @@ -79,10 +79,10 @@ class ElevenLabsTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ - await this.fetchTtsVoiceIds() + await this.fetchTtsVoiceObjects() } - async onApplyClick() { + async onRefreshClick() { } async onConnectClick() { @@ -97,7 +97,7 @@ class ElevenLabsTtsProvider { // Using this call to validate API key this.settings.apiKey = $('#elevenlabs_tts_api_key').val() - await this.fetchTtsVoiceIds().catch(error => { + await this.fetchTtsVoiceObjects().catch(error => { throw `TTS API key validation failed` }) this.settings.apiKey = this.settings.apiKey @@ -111,7 +111,7 @@ class ElevenLabsTtsProvider { async getVoice(voiceName) { if (this.voices.length == 0) { - this.voices = await this.fetchTtsVoiceIds() + this.voices = await this.fetchTtsVoiceObjects() } const match = this.voices.filter( elevenVoice => elevenVoice.name == voiceName @@ -158,7 +158,7 @@ class ElevenLabsTtsProvider { //###########// // API CALLS // //###########// - async fetchTtsVoiceIds() { + async fetchTtsVoiceObjects() { const headers = { 'xi-api-key': this.settings.apiKey } diff --git a/public/scripts/extensions/tts/novel.js b/public/scripts/extensions/tts/novel.js index 5dbde0226..58fd73abc 100644 --- a/public/scripts/extensions/tts/novel.js +++ b/public/scripts/extensions/tts/novel.js @@ -1,6 +1,6 @@ import { getRequestHeaders, callPopup } from "../../../script.js" import { getPreviewString } from "./index.js" -import { onTtsProviderSettingsInput } from "./index.js" +import { initVoiceMap } from "./index.js" export { NovelTtsProvider } @@ -37,15 +37,13 @@ class NovelTtsProvider { return html; } - onSettingsChange() { - onTtsProviderSettingsInput() - } // Add a new Novel custom voice to provider async addCustomVoice(){ const voiceName = await callPopup('

Custom Voice name:

', 'input') this.settings.customVoices.push(voiceName) this.populateCustomVoices() + initVoiceMap() // Update TTS extension voiceMap } // Delete selected custom voice from provider @@ -57,6 +55,7 @@ class NovelTtsProvider { this.settings.customVoices.splice(voiceIndex, 1); } this.populateCustomVoices() + initVoiceMap() // Update TTS extension voiceMap } // Create the UI dropdown list of voices in provider @@ -66,7 +65,6 @@ class NovelTtsProvider { this.settings.customVoices.forEach(voice => { voiceSelect.append(``) }) - this.onSettingsChange() } loadSettings(settings) { @@ -96,10 +94,10 @@ class NovelTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds // Doesnt really do much for Novel, not seeing a good way to test this at the moment. async checkReady(){ - await this.fetchTtsVoiceIds() + await this.fetchTtsVoiceObjects() } - async onApplyClick() { + async onRefreshClick() { return } @@ -123,7 +121,7 @@ class NovelTtsProvider { //###########// // API CALLS // //###########// - async fetchTtsVoiceIds() { + async fetchTtsVoiceObjects() { let voices = [ { name: 'Ligeia', voice_id: 'Ligeia', lang: 'en-US', preview_url: false }, { name: 'Aini', voice_id: 'Aini', lang: 'en-US', preview_url: false }, diff --git a/public/scripts/extensions/tts/silerotts.js b/public/scripts/extensions/tts/silerotts.js index 3d0f3e101..a7a1b8873 100644 --- a/public/scripts/extensions/tts/silerotts.js +++ b/public/scripts/extensions/tts/silerotts.js @@ -1,5 +1,5 @@ import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js" -import { onTtsProviderSettingsInput } from "./index.js" +import { saveTtsProviderSettings } from "./index.js" export { SileroTtsProvider } @@ -31,7 +31,7 @@ class SileroTtsProvider { onSettingsChange() { // Used when provider settings are updated from UI this.settings.provider_endpoint = $('#silero_tts_endpoint').val() - onTtsProviderSettingsInput() + saveTtsProviderSettings() } loadSettings(settings) { @@ -72,10 +72,10 @@ class SileroTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ - await this.fetchTtsVoiceIds() + await this.fetchTtsVoiceObjects() } - async onApplyClick() { + async onRefreshClick() { return } @@ -85,7 +85,7 @@ class SileroTtsProvider { async getVoice(voiceName) { if (this.voices.length == 0) { - this.voices = await this.fetchTtsVoiceIds() + this.voices = await this.fetchTtsVoiceObjects() } const match = this.voices.filter( sileroVoice => sileroVoice.name == voiceName @@ -104,7 +104,7 @@ class SileroTtsProvider { //###########// // API CALLS // //###########// - async fetchTtsVoiceIds() { + async fetchTtsVoiceObjects() { const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${await response.json()}`) diff --git a/public/scripts/extensions/tts/system.js b/public/scripts/extensions/tts/system.js index 473523aff..1b6ea07d4 100644 --- a/public/scripts/extensions/tts/system.js +++ b/public/scripts/extensions/tts/system.js @@ -1,7 +1,7 @@ import { isMobile } from "../../RossAscends-mods.js"; import { getPreviewString } from "./index.js"; import { talkingAnimation } from './index.js'; -import { onTtsProviderSettingsInput } from "./index.js" +import { saveTtsProviderSettings } from "./index.js" export { SystemTtsProvider } /** @@ -107,7 +107,7 @@ class SystemTtsProvider { this.settings.pitch = Number($('#system_tts_pitch').val()); $('#system_tts_pitch_output').text(this.settings.pitch); $('#system_tts_rate_output').text(this.settings.rate); - onTtsProviderSettingsInput() + saveTtsProviderSettings() } loadSettings(settings) { @@ -156,17 +156,17 @@ class SystemTtsProvider { // Perform a simple readiness check by trying to fetch voiceIds async checkReady(){ - await this.fetchTtsVoiceIds() + await this.fetchTtsVoiceObjects() } - async onApplyClick() { + async onRefreshClick() { return } //#################// // TTS Interfaces // //#################// - fetchTtsVoiceIds() { + fetchTtsVoiceObjects() { if (!('speechSynthesis' in window)) { return []; } From 765751aae07781a75846d0ba683c9306d8e5074e Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Fri, 25 Aug 2023 22:52:55 -0500 Subject: [PATCH 09/14] fix voice map, name changes, add readme --- public/scripts/extensions/tts/index.js | 13 ++--- public/scripts/extensions/tts/readme.md | 71 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 public/scripts/extensions/tts/readme.md diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 458c49b30..7c088abd4 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -286,7 +286,7 @@ async function onTtsVoicesClick() { let popupText = '' try { - const voiceIds = await ttsProvider.fetchTtsVoiceIds() + const voiceIds = await ttsProvider.fetchTtsVoiceObjects() for (const voice of voiceIds) { popupText += ` @@ -517,7 +517,7 @@ function setTtsStatus(status, success) { function onRefreshClick() { Promise.all([ - ttsProvider.onApplyClick(), + ttsProvider.onRefreshClick(), // updateVoiceMap() ]).then(() => { extension_settings.tts[ttsProviderName] = ttsProvider.settings @@ -598,7 +598,8 @@ function onTtsProviderChange() { } // Ensure that TTS provider settings are saved to extension settings. -export function onTtsProviderSettingsInput() { +export function saveTtsProviderSettings() { + updateVoiceMap() extension_settings.tts[ttsProviderName] = ttsProvider.settings saveSettingsDebounced() console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`) @@ -723,10 +724,10 @@ class VoiceMapEntry { } /** - * Init voiceMapEntries for character select list. Should only be called when character/chat is changed. + * Init voiceMapEntries for character select list. * */ -async function initVoiceMap(){ +export async function initVoiceMap(){ // Clear existing voiceMap state $('#tts_voicemap_block').empty() voiceMapEntries = [] @@ -766,7 +767,7 @@ async function initVoiceMap(){ // Get voiceIds from provider let voiceIdsFromProvider try { - voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceIds() + voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects() } catch { toastr.error("TTS Provider failed to return voice ids.") diff --git a/public/scripts/extensions/tts/readme.md b/public/scripts/extensions/tts/readme.md new file mode 100644 index 000000000..fb48e116b --- /dev/null +++ b/public/scripts/extensions/tts/readme.md @@ -0,0 +1,71 @@ +# Provider Requirements. +Because I don't know how, or if you can, and/or maybe I am just too lazy to implement interfaces in JS, here's the requirements of a provider that the extension needs to operate. + +### class YourTtsProvider +#### Required +Exported for use in extension index.js, and added to providers list in index.js +1. generateTts(text, voiceId) +2. fetchTtsVoiceObjects() +3. onRefreshClick() +4. checkReady() +5. loadSettings(settingsObject) +6. settings field +7. settingsHtml field + +#### Optional +1. previewTtsVoice() +2. separator field + +# Requirement Descriptions +### generateTts(text, voiceId) +Must return `audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/webm']` +Must take text to be rendered and the voiceId to identify the voice to be used + +### fetchTtsVoiceObjects() +Required. +Used by the TTS extension to get a list of voice objects from the provider. +Must return an list of voice objects representing the available voices. +1. name: a friendly user facing name to assign to characters. Shows in dropdown list next to user. +2. voice_id: the provider specific id of the voice used in fetchTtsGeneration() call +3. preview_url: a URL to a local audio file that will be used to sample voices +4. lang: OPTIONAL language string + +### getVoice(voiceName) +Required. +Must return a single voice object matching the provided voiceName. The voice object must have the following at least: +1. name: a friendly user facing name to assign to characters. Shows in dropdown list next to user. +2. voice_id: the provider specific id of the voice used in fetchTtsGeneration() call +3. preview_url: a URL to a local audio file that will be used to sample voices +4. lang: OPTIONAL language indicator + +### onRefreshClick() +Required. +Users click this button to reconnect/reinit the selected provider. +Responds to the user clicking the refresh button, which is intended to re-initialize the Provider into a working state, like retrying connections or checking if everything is loaded. + +### checkReady() +Required. +Return without error to let TTS extension know that the provider is ready. +Return an error to block the main TTS extension for initializing the provider and UI. The error will be put in the TTS extension UI directly. + +### loadSettings(settingsObject) +Required. +Handle the input settings from the TTS extension on provider load. +Put code in here to load your provider settings. + +### settings field +Required, used for storing any provider state that needs to be saved. +Anything stored in this field is automatically persisted under extension_settings[providerName] by the main extension in `saveTtsProviderSettings()`, as well as loaded when the provider is selected in `loadTtsProvider(provider)`. +TTS extension doesn't expect any specific contents. + +### settingsHtml field +Required, injected into the TTS extension UI. Besides adding it, not relied on by TTS extension directly. + +### previewTtsVoice() +Optional. +Function to handle playing previews of voice samples if no direct preview_url is available in fetchTtsVoiceObjects() response + +### separator field +Optional. +Used when narrate quoted text is enabled. +Defines the string of characters used to introduce separation between between the groups of extracted quoted text sent to the provider. The provider will use this to introduce pauses by default using `...` \ No newline at end of file From d8843274b1969a993bc03b7ca7fabcfbd0672ada Mon Sep 17 00:00:00 2001 From: ouoertheo Date: Sun, 27 Aug 2023 20:47:44 -0500 Subject: [PATCH 10/14] merge voicemap to settings rather than overwrite --- public/scripts/extensions/tts/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 7c088abd4..dd2fdcf6e 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -678,7 +678,7 @@ function updateVoiceMap() { voiceMap = tempVoiceMap console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`) } - extension_settings.tts[ttsProviderName].voiceMap = voiceMap + Object.assign(extension_settings.tts[ttsProviderName].voiceMap, voiceMap) saveSettingsDebounced() } From b51511b99ffebaeae4880bf6aac8b7b37eb26799 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:46:41 +0300 Subject: [PATCH 11/14] Fixed Novel custom voices not saving --- public/scripts/extensions/tts/index.js | 11 +++++++--- public/scripts/extensions/tts/novel.js | 28 ++++++++++++++----------- public/scripts/extensions/tts/style.css | 18 ++++++++++++++-- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 5d00d2ddc..3a41ac6e7 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -487,6 +487,11 @@ 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', @@ -575,7 +580,7 @@ async function loadTtsProvider(provider) { if (!provider) { return } - + // Init provider references extension_settings.tts.currentProvider = provider ttsProviderName = provider @@ -725,7 +730,7 @@ class VoiceMapEntry { /** * Init voiceMapEntries for character select list. - * + * */ export async function initVoiceMap(){ // Clear existing voiceMap state @@ -751,7 +756,7 @@ export async function initVoiceMap(){ // Get characters in current chat const characters = getCharacters() - + // Get saved voicemap from provider settings, handling new and old representations let voiceMapFromSettings = {} if ("voiceMap" in extension_settings.tts[ttsProviderName]) { diff --git a/public/scripts/extensions/tts/novel.js b/public/scripts/extensions/tts/novel.js index 58fd73abc..3f88d665e 100644 --- a/public/scripts/extensions/tts/novel.js +++ b/public/scripts/extensions/tts/novel.js @@ -1,5 +1,5 @@ import { getRequestHeaders, callPopup } from "../../../script.js" -import { getPreviewString } from "./index.js" +import { getPreviewString, saveTtsProviderSettings } from "./index.js" import { initVoiceMap } from "./index.js" export { NovelTtsProvider } @@ -21,17 +21,19 @@ class NovelTtsProvider { get settingsHtml() { let html = ` -
- - Use NovelAI's TTS engine.
- The default Voice IDs are only examples. Add custom voices and Novel will create a new random voice for it. Feel free to try different options!
-
- Hint: Save an API key in the NovelAI API settings to use it here.
+
+
Use NovelAI's TTS engine.
+
+ The default Voice IDs are only examples. Add custom voices and Novel will create a new random voice for it. + Feel free to try different options! +
+ Hint: Save an API key in the NovelAI API settings to use it here. +
-
+
- - + +
`; return html; @@ -44,18 +46,20 @@ class NovelTtsProvider { this.settings.customVoices.push(voiceName) this.populateCustomVoices() initVoiceMap() // Update TTS extension voiceMap + saveTtsProviderSettings() } // Delete selected custom voice from provider deleteCustomVoice() { const selected = $("#tts-novel-custom-voices-select").find(':selected').val(); const voiceIndex = this.settings.customVoices.indexOf(selected); - + if (voiceIndex !== -1) { this.settings.customVoices.splice(voiceIndex, 1); } this.populateCustomVoices() initVoiceMap() // Update TTS extension voiceMap + saveTtsProviderSettings() } // Create the UI dropdown list of voices in provider @@ -139,7 +143,7 @@ class NovelTtsProvider { ]; // Add in custom voices to the map - let addVoices = this.settings.customVoices.map(voice => + let addVoices = this.settings.customVoices.map(voice => ({ name: voice, voice_id: voice, lang: 'en-US', preview_url: false }) ) voices = voices.concat(addVoices) diff --git a/public/scripts/extensions/tts/style.css b/public/scripts/extensions/tts/style.css index 5df2b0fc7..0f4a2c70c 100644 --- a/public/scripts/extensions/tts/style.css +++ b/public/scripts/extensions/tts/style.css @@ -70,7 +70,21 @@ .tts_block { display: flex; - align-items: center; + align-items: baseline; column-gap: 5px; flex-wrap: wrap; -} \ No newline at end of file +} + +.tts_custom_voices { + display: flex; + align-items: baseline; + gap: 5px; +} + +.novel_tts_hints { + font-size: calc(0.9 * var(--mainFontSize)); + display: flex; + flex-direction: column; + gap: 5px; + margin-bottom: 5px; +} From ac78d51d5918899acaab602e2f78be1379fee79b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:58:46 +0300 Subject: [PATCH 12/14] Change all TTS providers loading to async --- public/scripts/extensions/tts/coqui.js | 20 ++++++++++---------- public/scripts/extensions/tts/edge.js | 4 ++-- public/scripts/extensions/tts/elevenlabs.js | 6 +++--- public/scripts/extensions/tts/index.js | 4 ++-- public/scripts/extensions/tts/novel.js | 4 ++-- public/scripts/extensions/tts/silerotts.js | 4 ++-- public/scripts/extensions/tts/system.js | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/public/scripts/extensions/tts/coqui.js b/public/scripts/extensions/tts/coqui.js index 2c8cc5045..c287da7ca 100644 --- a/public/scripts/extensions/tts/coqui.js +++ b/public/scripts/extensions/tts/coqui.js @@ -54,7 +54,7 @@ function resetModelSettings() { $("#coqui_api_model_settings_language").val("none"); $("#coqui_api_model_settings_speaker").val("none"); } - + class CoquiTtsProvider { //#############################// // Extension UI and Settings // @@ -111,7 +111,7 @@ class CoquiTtsProvider { Model installed on extras server
- +