Move request to backend
| @@ -83,21 +83,21 @@ export function getPreviewString(lang) { | |||
| 83 | } | 83 | } |
| 84 | 84 | ||
| 85 | const ttsProviders = { | 85 | const ttsProviders = { |
| 86 | ElevenLabs: ElevenLabsTtsProvider, | 86 | AllTalk: AllTalkTtsProvider, |
| 87 | Silero: SileroTtsProvider, | 87 | Azure: AzureTtsProvider, |
| 88 | XTTSv2: XTTSTtsProvider, | ||
| 89 | VITS: VITSTtsProvider, | ||
| 90 | GSVI: GSVITtsProvider, | ||
| 91 | SBVits2: SBVits2TtsProvider, | ||
| 92 | System: SystemTtsProvider, | ||
| 93 | Coqui: CoquiTtsProvider, | 88 | Coqui: CoquiTtsProvider, |
| 94 | Edge: EdgeTtsProvider, | 89 | Edge: EdgeTtsProvider, |
| 90 | ElevenLabs: ElevenLabsTtsProvider, | ||
| 91 | GSVI: GSVITtsProvider, | ||
| 95 | Novel: NovelTtsProvider, | 92 | Novel: NovelTtsProvider, |
| 96 | OpenAI: OpenAITtsProvider, | 93 | OpenAI: OpenAITtsProvider, |
| 97 | AllTalk: AllTalkTtsProvider, | 94 | 'OpenAI Compatible': OpenAICompatibleTtsProvider, |
| 95 | SBVits2: SBVits2TtsProvider, | ||
| 96 | Silero: SileroTtsProvider, | ||
| 98 | SpeechT5: SpeechT5TtsProvider, | 97 | SpeechT5: SpeechT5TtsProvider, |
| 99 | Azure: AzureTtsProvider, | 98 | System: SystemTtsProvider, |
| 100 | OpenAICompatible: OpenAICompatibleTtsProvider, | 99 | VITS: VITSTtsProvider, |
| 100 | XTTSv2: XTTSTtsProvider, | ||
| 101 | }; | 101 | }; |
| 102 | let ttsProvider; | 102 | let ttsProvider; |
| 103 | let ttsProviderName; | 103 | let ttsProviderName; |
| @@ -1,4 +1,6 @@ | |||
| 1 | import { getRequestHeaders } from '../../../script.js'; | 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 | import { getPreviewString, saveTtsProviderSettings } from './index.js'; | 4 | import { getPreviewString, saveTtsProviderSettings } from './index.js'; |
| 3 | 5 | ||
| 4 | export { OpenAICompatibleTtsProvider }; | 6 | export { OpenAICompatibleTtsProvider }; |
| @@ -21,7 +23,15 @@ class OpenAICompatibleTtsProvider { | |||
| 21 | get settingsHtml() { | 23 | get settingsHtml() { |
| 22 | let html = ` | 24 | let html = ` |
| 23 | <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label> | 25 | <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label> |
| 26 | <div class="flex-container alignItemsCenter"> | ||
| 27 | <div class="flex1"> | ||
| 24 | <input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/> | 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 | <label for="openai_compatible_model">Model:</label> | 35 | <label for="openai_compatible_model">Model:</label> |
| 26 | <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/> | 36 | <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/> |
| 27 | <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label> | 37 | <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label> |
| @@ -64,7 +74,35 @@ class OpenAICompatibleTtsProvider { | |||
| 64 | 74 | ||
| 65 | $('#openai_compatible_tts_speed_output').text(this.settings.speed); | 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 | await this.checkReady(); | 107 | await this.checkReady(); |
| 70 | 108 | ||
| @@ -75,7 +113,7 @@ class OpenAICompatibleTtsProvider { | |||
| 75 | // Update dynamically | 113 | // Update dynamically |
| 76 | this.settings.provider_endpoint = String($('#openai_compatible_tts_endpoint').val()); | 114 | this.settings.provider_endpoint = String($('#openai_compatible_tts_endpoint').val()); |
| 77 | this.settings.model = String($('#openai_compatible_model').val()); | 115 | this.settings.model = String($('#openai_compatible_model').val()); |
| 78 | this.settings.available_voices = $('#openai_compatible_tts_voices').val().split(','); | 116 | this.settings.available_voices = String($('#openai_compatible_tts_voices').val()).split(','); |
| 79 | this.settings.speed = Number($('#openai_compatible_tts_speed').val()); | 117 | this.settings.speed = Number($('#openai_compatible_tts_speed').val()); |
| 80 | $('#openai_compatible_tts_speed_output').text(this.settings.speed); | 118 | $('#openai_compatible_tts_speed_output').text(this.settings.speed); |
| 81 | saveTtsProviderSettings(); | 119 | saveTtsProviderSettings(); |
| @@ -132,15 +170,16 @@ class OpenAICompatibleTtsProvider { | |||
| 132 | 170 | ||
| 133 | async fetchTtsGeneration(inputText, voiceId) { | 171 | async fetchTtsGeneration(inputText, voiceId) { |
| 134 | console.info(`Generating new TTS for voice_id ${voiceId}`); | 172 | console.info(`Generating new TTS for voice_id ${voiceId}`); |
| 135 | const response = await fetch(this.settings.provider_endpoint, { | 173 | const response = await fetch('/api/openai/custom/generate-voice', { |
| 136 | method: 'POST', | 174 | method: 'POST', |
| 137 | headers: getRequestHeaders(), | 175 | headers: getRequestHeaders(), |
| 138 | body: JSON.stringify({ | 176 | body: JSON.stringify({ |
| 139 | 'model': this.settings.model, | 177 | provider_endpoint: this.settings.provider_endpoint, |
| 140 | 'input': inputText, | 178 | model: this.settings.model, |
| 141 | 'voice': voiceId, | 179 | input: inputText, |
| 142 | 'response_format': 'mp3', | 180 | voice: 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 | HUGGINGFACE: 'api_key_huggingface', | 33 | HUGGINGFACE: 'api_key_huggingface', |
| 34 | STABILITY: 'api_key_stability', | 34 | STABILITY: 'api_key_stability', |
| 35 | BLOCKENTROPY: 'api_key_blockentropy', | 35 | BLOCKENTROPY: 'api_key_blockentropy', |
| 36 | CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts', | ||
| 36 | }; | 37 | }; |
| 37 | 38 | ||
| 38 | const INPUT_MAP = { | 39 | const INPUT_MAP = { |
| @@ -127,7 +128,7 @@ export async function writeSecret(key, value) { | |||
| 127 | const text = await response.text(); | 128 | const text = await response.text(); |
| 128 | 129 | ||
| 129 | if (text == 'ok') { | 130 | if (text == 'ok') { |
| 130 | secret_state[key] = true; | 131 | secret_state[key] = !!value; |
| 131 | updateSecretDisplay(); | 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 | module.exports = { router }; | 329 | module.exports = { router }; |
| @@ -45,6 +45,7 @@ const SECRET_KEYS = { | |||
| 45 | HUGGINGFACE: 'api_key_huggingface', | 45 | HUGGINGFACE: 'api_key_huggingface', |
| 46 | STABILITY: 'api_key_stability', | 46 | STABILITY: 'api_key_stability', |
| 47 | BLOCKENTROPY: 'api_key_blockentropy', | 47 | BLOCKENTROPY: 'api_key_blockentropy', |
| 48 | CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts', | ||
| 48 | }; | 49 | }; |
| 49 | 50 | ||
| 50 | // These are the keys that are safe to expose, even if allowKeysExposure is false | 51 | // These are the keys that are safe to expose, even if allowKeysExposure is false |