Move request to backend
| @@ -83,21 +83,21 @@ export function getPreviewString(lang) { | ||
| 83 | 83 | } |
| 84 | 84 | |
| 85 | 85 | const ttsProviders = { |
| 86 | 86 | ElevenLabsAllTalk: ElevenLabsTtsProviderAllTalkTtsProvider, |
| 87 | 87 | SileroAzure: SileroTtsProviderAzureTtsProvider, |
| 88 | - XTTSv2: XTTSTtsProvider, | |
| 89 | - VITS: VITSTtsProvider, | |
| 90 | - GSVI: GSVITtsProvider, | |
| 91 | - SBVits2: SBVits2TtsProvider, | |
| 92 | - System: SystemTtsProvider, | |
| 93 | 88 | Coqui: CoquiTtsProvider, |
| 94 | 89 | Edge: EdgeTtsProvider, |
| 90 | + ElevenLabs: ElevenLabsTtsProvider, | |
| 91 | + GSVI: GSVITtsProvider, | |
| 95 | 92 | Novel: NovelTtsProvider, |
| 96 | 93 | OpenAI: OpenAITtsProvider, |
| 97 | 94 | AllTalk'OpenAI Compatible': AllTalkTtsProviderOpenAICompatibleTtsProvider, |
| 95 | + SBVits2: SBVits2TtsProvider, | |
| 96 | + Silero: SileroTtsProvider, | |
| 98 | 97 | SpeechT5: SpeechT5TtsProvider, |
| 99 | 98 | AzureSystem: AzureTtsProviderSystemTtsProvider, |
| 100 | 99 | OpenAICompatibleVITS: OpenAICompatibleTtsProviderVITSTtsProvider, |
| 100 | + XTTSv2: XTTSTtsProvider, | |
| 101 | 101 | }; |
| 102 | 102 | let ttsProvider; |
| 103 | 103 | let ttsProviderName; |
| @@ -1,4 +1,6 @@ | ||
| 1 | 1 | import { getRequestHeaders } from '../../../script.js'; |
| 2 | +import { callGenericPopup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; | |
| 3 | +import { findSecret, SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; | |
| 2 | 4 | import { getPreviewString, saveTtsProviderSettings } from './index.js'; |
| 3 | 5 | |
| 4 | 6 | export { OpenAICompatibleTtsProvider }; |
| @@ -21,7 +23,15 @@ class OpenAICompatibleTtsProvider { | ||
| 21 | 23 | get settingsHtml() { |
| 22 | 24 | let html = ` |
| 23 | 25 | <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label> |
| 24 | - <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/> | |
| 26 | + <div class="flex-container alignItemsCenter"> | |
| 27 | + <div class="flex1"> | |
| 28 | + <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/> | |
| 29 | + </div> | |
| 30 | + <div id="openai_compatible_tts_key" class="menu_button menu_button_icon"> | |
| 31 | + <i class="fa-solid fa-key"></i> | |
| 32 | + <span>API Key</span> | |
| 33 | + </div> | |
| 34 | + </div> | |
| 25 | 35 | <label for="openai_compatible_model">Model:</label> |
| 26 | 36 | <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/> |
| 27 | 37 | <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label> |
| @@ -64,7 +74,35 @@ class OpenAICompatibleTtsProvider { | ||
| 64 | 74 | |
| 65 | 75 | $('#openai_compatible_tts_speed_output').text(this.settings.speed); |
| 66 | 76 | |
| 67 | - this.refreshSession(); | |
| 77 | + $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]); | |
| 78 | + $('#openai_compatible_tts_key').on('click', async () => { | |
| 79 | + const popupText = 'OpenAI-compatible TTS API Key'; | |
| 80 | + const savedKey = secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS] ? await findSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS) : ''; | |
| 81 | + | |
| 82 | + const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey, { | |
| 83 | + customButtons: [{ | |
| 84 | + text: 'Remove Key', | |
| 85 | + appendAtEnd: true, | |
| 86 | + result: POPUP_RESULT.CANCELLED, | |
| 87 | + action: async () => { | |
| 88 | + await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, ''); | |
| 89 | + $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]); | |
| 90 | + toastr.success('API Key removed'); | |
| 91 | + await this.onRefreshClick(); | |
| 92 | + }, | |
| 93 | + }], | |
| 94 | + }); | |
| 95 | + | |
| 96 | + if (key == false || key == '' || key == null) { | |
| 97 | + return; | |
| 98 | + } | |
| 99 | + | |
| 100 | + await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, String(key)); | |
| 101 | + | |
| 102 | + toastr.success('API Key saved'); | |
| 103 | + $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]); | |
| 104 | + await this.onRefreshClick(); | |
| 105 | + }); | |
| 68 | 106 | |
| 69 | 107 | await this.checkReady(); |
| 70 | 108 | |
| @@ -75,7 +113,7 @@ class OpenAICompatibleTtsProvider { | ||
| 75 | 113 | // Update dynamically |
| 76 | 114 | this.settings.provider_endpoint = String($('#openai_compatible_tts_endpoint').val()); |
| 77 | 115 | this.settings.model = String($('#openai_compatible_model').val()); |
| 78 | 116 | this.settings.available_voices = String($('#openai_compatible_tts_voices').val()).split(','); |
| 79 | 117 | this.settings.speed = Number($('#openai_compatible_tts_speed').val()); |
| 80 | 118 | $('#openai_compatible_tts_speed_output').text(this.settings.speed); |
| 81 | 119 | saveTtsProviderSettings(); |
| @@ -132,15 +170,16 @@ class OpenAICompatibleTtsProvider { | ||
| 132 | 170 | |
| 133 | 171 | async fetchTtsGeneration(inputText, voiceId) { |
| 134 | 172 | console.info(`Generating new TTS for voice_id ${voiceId}`); |
| 135 | 173 | const response = await fetch(this.settings.provider_endpoint'/api/openai/custom/generate-voice', { |
| 136 | 174 | method: 'POST', |
| 137 | 175 | headers: getRequestHeaders(), |
| 138 | 176 | body: JSON.stringify({ |
| 139 | 177 | 'model'provider_endpoint: this.settings.modelprovider_endpoint, |
| 140 | - 'input': inputText, | |
| 178 | + model: this.settings.model, | |
| 141 | 179 | 'voice'input: voiceIdinputText, |
| 142 | 180 | 'response_format'voice: 'mp3'voiceId, |
| 143 | - 'speed': this.settings.speed, | |
| 181 | + response_format: 'mp3', | |
| 182 | + speed: this.settings.speed, | |
| 144 | 183 | }), |
| 145 | 184 | }); |
| 146 | 185 | |
| @@ -33,6 +33,7 @@ export const SECRET_KEYS = { | ||
| 33 | 33 | HUGGINGFACE: 'api_key_huggingface', |
| 34 | 34 | STABILITY: 'api_key_stability', |
| 35 | 35 | BLOCKENTROPY: 'api_key_blockentropy', |
| 36 | + CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts', | |
| 36 | 37 | }; |
| 37 | 38 | |
| 38 | 39 | const INPUT_MAP = { |
| @@ -127,7 +128,7 @@ export async function writeSecret(key, value) { | ||
| 127 | 128 | const text = await response.text(); |
| 128 | 129 | |
| 129 | 130 | if (text == 'ok') { |
| 130 | 131 | secret_state[key] = true!!value; |
| 131 | 132 | updateSecretDisplay(); |
| 132 | 133 | } |
| 133 | 134 | } |
| @@ -282,4 +282,48 @@ router.post('/generate-image', jsonParser, async (request, response) => { | ||
| 282 | 282 | } |
| 283 | 283 | }); |
| 284 | 284 | |
| 285 | +const custom = express.Router(); | |
| 286 | + | |
| 287 | +custom.post('/generate-voice', jsonParser, async (request, response) => { | |
| 288 | + try { | |
| 289 | + const key = readSecret(request.user.directories, SECRET_KEYS.CUSTOM_OPENAI_TTS); | |
| 290 | + const { input, provider_endpoint, response_format, voice, speed, model } = request.body; | |
| 291 | + | |
| 292 | + if (!provider_endpoint) { | |
| 293 | + console.log('No OpenAI-compatible TTS provider endpoint provided'); | |
| 294 | + return response.sendStatus(400); | |
| 295 | + } | |
| 296 | + | |
| 297 | + const result = await fetch(provider_endpoint, { | |
| 298 | + method: 'POST', | |
| 299 | + headers: { | |
| 300 | + 'Content-Type': 'application/json', | |
| 301 | + Authorization: `Bearer ${key ?? ''}`, | |
| 302 | + }, | |
| 303 | + body: JSON.stringify({ | |
| 304 | + input: input ?? '', | |
| 305 | + response_format: response_format ?? 'mp3', | |
| 306 | + voice: voice ?? 'alloy', | |
| 307 | + speed: speed ?? 1, | |
| 308 | + model: model ?? 'tts-1', | |
| 309 | + }), | |
| 310 | + }); | |
| 311 | + | |
| 312 | + if (!result.ok) { | |
| 313 | + const text = await result.text(); | |
| 314 | + console.log('OpenAI request failed', result.statusText, text); | |
| 315 | + return response.status(500).send(text); | |
| 316 | + } | |
| 317 | + | |
| 318 | + const buffer = await result.arrayBuffer(); | |
| 319 | + response.setHeader('Content-Type', 'audio/mpeg'); | |
| 320 | + return response.send(Buffer.from(buffer)); | |
| 321 | + } catch (error) { | |
| 322 | + console.error('OpenAI TTS generation failed', error); | |
| 323 | + response.status(500).send('Internal server error'); | |
| 324 | + } | |
| 325 | +}); | |
| 326 | + | |
| 327 | +router.use('/custom', custom); | |
| 328 | + | |
| 285 | 329 | module.exports = { router }; |
| @@ -45,6 +45,7 @@ const SECRET_KEYS = { | ||
| 45 | 45 | HUGGINGFACE: 'api_key_huggingface', |
| 46 | 46 | STABILITY: 'api_key_stability', |
| 47 | 47 | BLOCKENTROPY: 'api_key_blockentropy', |
| 48 | + CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts', | |
| 48 | 49 | }; |
| 49 | 50 | |
| 50 | 51 | // These are the keys that are safe to expose, even if allowKeysExposure is false |