diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 45994d690..d77e59eac 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -1,4 +1,3 @@ -body.tts .mes[is_user="true"] .mes_narrate, body.tts .mes[is_system="true"] .mes_narrate { display: none; } @@ -364,4 +363,4 @@ body.movingUI #groupMemberListPopout { body.noShadows * { text-shadow: none !important; -} \ No newline at end of file +} diff --git a/public/scripts/extensions/tts/coqui.js b/public/scripts/extensions/tts/coqui.js index 7f006c8b0..c287da7ca 100644 --- a/public/scripts/extensions/tts/coqui.js +++ b/public/scripts/extensions/tts/coqui.js @@ -5,13 +5,16 @@ TODO: */ import { doExtrasFetch, extension_settings, getApiUrl, getContext, modules, ModuleWorkerWrapper } from "../../extensions.js" +import { callPopup } from "../../../script.js" +import { initVoiceMap } from "./index.js" export { CoquiTtsProvider } const DEBUG_PREFIX = " "; const UPDATE_INTERVAL = 1000; -let charactersList = []; // Updated with module worker +let inApiCall = false; +let voiceIdList = []; // Updated with module worker let coquiApiModels = {}; // Initialized only once let coquiApiModelsFull = {}; // Initialized only once let coquiLocalModels = []; // Initialized only once @@ -39,16 +42,11 @@ const languageLabels = { "ja": "Japanese" } - -const defaultSettings = { - voiceMap: "", - voiceMapDict: {} -} - 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); } } @@ -57,46 +55,18 @@ function resetModelSettings() { $("#coqui_api_model_settings_speaker").val("none"); } -function updateCharactersList() { - let currentcharacters = new Set(); - const context = getContext(); - for (const i of context.characters) { - currentcharacters.add(i.name); - } - - currentcharacters = Array.from(currentcharacters); - currentcharacters.unshift(context.name1); - - 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 // //#############################// - static instance; - settings = {}; + settings - // Singleton to allow acces to instance in event functions - constructor() { - if (CoquiTtsProvider.instance === undefined) - CoquiTtsProvider.instance = this; + defaultSettings = { + voiceMap: {}, + customVoices: {}, + voiceIds: [], + voiceMapDict: {} } get settingsHtml() { @@ -104,13 +74,15 @@ class CoquiTtsProvider {
- - - - - +
+ + +
- +
- - - - - +
+ + + + + + + + +
` return html } @@ -44,39 +46,49 @@ 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') + saveTtsProviderSettings() } - loadSettings(settings) { + async loadSettings(settings) { // Pupulate Provider UI given input settings - if (!settings || Object.keys(settings).length == 0) { + if (Object.keys(settings).length == 0) { console.info("Using default TTS Provider settings") } // Only accept keys defined in defaultSettings - this.settings = deepClone(this.defaultSettings); + this.settings = this.defaultSettings - if (settings) { - for (const key in settings) { - if (key in this.settings) { - this.settings[key] = settings[key] - } else { - throw `Invalid setting passed to TTS Provider: ${key}` - } + for (const key in settings){ + if (key in this.settings){ + this.settings[key] = settings[key] + } else { + 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()}) + $('#elevenlabs_tts_settings').on('input',this.onSettingsChange) + + await this.checkReady() console.info("Settings loaded") } - async onApplyClick() { + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady(){ + await this.fetchTtsVoiceObjects() + } + + async onRefreshClick() { + } + + async onConnectClick() { // Update on Apply click return await this.updateApiKey().catch( (error) => { - throw error + toastr.error(`ElevenLabs: ${error}`) }) } @@ -85,11 +97,12 @@ 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 console.debug(`Saved new API_KEY: ${this.settings.apiKey}`) + this.onSettingsChange() } //#################// @@ -98,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 @@ -145,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/index.js b/public/scripts/extensions/tts/index.js index d45542380..e4d6b6d3a 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, @@ -285,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 += ` @@ -486,6 +487,12 @@ 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 @@ -513,59 +520,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() { +function onRefreshClick() { Promise.all([ - ttsProvider.onApplyClick(), - updateVoiceMap() + 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) @@ -608,13 +573,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,38 +592,210 @@ 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]) + await ttsProvider.loadSettings(extension_settings.tts[ttsProviderName]) + await initVoiceMap() } function onTtsProviderChange() { const ttsProviderSelection = $('#tts_provider').val() + extension_settings.tts.currentProvider = ttsProviderSelection loadTtsProvider(ttsProviderSelection) } -function onTtsProviderSettingsInput() { - ttsProvider.onSettingsChange() - - // Persist changes to SillyTavern tts extension settings - +// Ensure that TTS provider settings are saved to extension settings. +export function saveTtsProviderSettings() { + updateVoiceMap() extension_settings.tts[ttsProviderName] = ttsProvider.settings saveSettingsDebounced() console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`) } +//###################// +// 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)}`) + } + Object.assign(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. + * + */ +export 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.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 { + voiceId = 'disabled' + } + const voiceMapEntry = new VoiceMapEntry(character, voiceId) + voiceMapEntry.addUI(voiceIdsFromProvider) + voiceMapEntries.push(voiceMapEntry) + } + updateVoiceMap() +} $(document).ready(function () { function addExtensionControls() { @@ -669,10 +807,13 @@ $(document).ready(function () {
-
- Select TTS Provider
- +
- - - -
+
+
-
@@ -714,14 +851,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($("`) + }) + } + + async loadSettings(settings) { // Populate Provider UI given input settings 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,11 +90,18 @@ class NovelTtsProvider { } } + this.populateCustomVoices() + await 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(){ + await this.fetchTtsVoiceObjects() + } - async onApplyClick() { + async onRefreshClick() { return } @@ -72,8 +125,8 @@ class NovelTtsProvider { //###########// // API CALLS // //###########// - async fetchTtsVoiceIds() { - const voices = [ + 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 }, { name: 'Orea', voice_id: 'Orea', lang: 'en-US', preview_url: false }, @@ -89,6 +142,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; } 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 diff --git a/public/scripts/extensions/tts/silerotts.js b/public/scripts/extensions/tts/silerotts.js index c70d73ff6..857d5ddc6 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 { saveTtsProviderSettings } from "./index.js" export { SileroTtsProvider } @@ -8,6 +9,7 @@ class SileroTtsProvider { //########// settings + ready = false voices = [] separator = ' .. ' @@ -29,9 +31,10 @@ class SileroTtsProvider { onSettingsChange() { // Used when provider settings are updated from UI this.settings.provider_endpoint = $('#silero_tts_endpoint').val() + saveTtsProviderSettings() } - loadSettings(settings) { + async loadSettings(settings) { // Pupulate Provider UI given input settings if (Object.keys(settings).length == 0) { console.info("Using default TTS Provider settings") @@ -60,11 +63,19 @@ class SileroTtsProvider { }, 2000); $('#silero_tts_endpoint').val(this.settings.provider_endpoint) + $('#silero_tts_endpoint').on("input", () => {this.onSettingsChange()}) + + await this.checkReady() + console.info("Settings loaded") } + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady(){ + await this.fetchTtsVoiceObjects() + } - async onApplyClick() { + async onRefreshClick() { return } @@ -74,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 @@ -93,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/style.css b/public/scripts/extensions/tts/style.css index 81bd0727d..0f4a2c70c 100644 --- a/public/scripts/extensions/tts/style.css +++ b/public/scripts/extensions/tts/style.css @@ -50,4 +50,41 @@ .voice_preview .fa-play { cursor: pointer; -} \ No newline at end of file +} + +.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; +} + +.tts_block { + display: flex; + align-items: baseline; + column-gap: 5px; + flex-wrap: wrap; +} + +.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; +} diff --git a/public/scripts/extensions/tts/system.js b/public/scripts/extensions/tts/system.js index 5e8c5f2b0..0a9b237ee 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 { saveTtsProviderSettings } from "./index.js" export { SystemTtsProvider } /** @@ -80,6 +80,7 @@ class SystemTtsProvider { //########// settings + ready = false voices = [] separator = ' ... ' @@ -106,10 +107,10 @@ 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'); + saveTtsProviderSettings() } - loadSettings(settings) { + async loadSettings(settings) { // Populate Provider UI given input settings if (Object.keys(settings).length == 0) { console.info("Using default TTS Provider settings"); @@ -143,19 +144,29 @@ 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"); } - async onApplyClick() { + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady(){ + await this.fetchTtsVoiceObjects() + } + + async onRefreshClick() { return } //#################// // TTS Interfaces // //#################// - fetchTtsVoiceIds() { + fetchTtsVoiceObjects() { if (!('speechSynthesis' in window)) { return []; } diff --git a/server.js b/server.js index 241951c81..dca2303f5 100644 --- a/server.js +++ b/server.js @@ -4894,7 +4894,7 @@ async function readAllChunks(readableStream) { }); readableStream.on('end', () => { - console.log('Finished reading the stream.'); + //console.log('Finished reading the stream.'); resolve(chunks); });