Move request to backend

1883095e39a210e5cb5cde0c4882bb7cd395c18a

Cohee <18619528+Cohee1207@users.noreply.github.com>

5 files changed, +104 -19Showing whitespace changes
public/scripts/extensions/tts/index.js+10 -10
@@ -83,21 +83,21 @@ export function getPreviewString(lang) {
8383}
8484
8585const ttsProviders = {
8686 ElevenLabsAllTalk: ElevenLabsTtsProviderAllTalkTtsProvider,
8787 SileroAzure: SileroTtsProviderAzureTtsProvider,
88- XTTSv2: XTTSTtsProvider,
89- VITS: VITSTtsProvider,
90- GSVI: GSVITtsProvider,
91- SBVits2: SBVits2TtsProvider,
92- System: SystemTtsProvider,
9388 Coqui: CoquiTtsProvider,
9489 Edge: EdgeTtsProvider,
90+ ElevenLabs: ElevenLabsTtsProvider,
91+ GSVI: GSVITtsProvider,
9592 Novel: NovelTtsProvider,
9693 OpenAI: OpenAITtsProvider,
9794 AllTalk'OpenAI Compatible': AllTalkTtsProviderOpenAICompatibleTtsProvider,
95+ SBVits2: SBVits2TtsProvider,
96+ Silero: SileroTtsProvider,
9897 SpeechT5: SpeechT5TtsProvider,
9998 AzureSystem: AzureTtsProviderSystemTtsProvider,
10099 OpenAICompatibleVITS: OpenAICompatibleTtsProviderVITSTtsProvider,
100+ XTTSv2: XTTSTtsProvider,
101101};
102102let ttsProvider;
103103let ttsProviderName;
public/scripts/extensions/tts/openai-compatible.js+47 -8
@@ -1,4 +1,6 @@
11import { 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';
24import { getPreviewString, saveTtsProviderSettings } from './index.js';
35
46export { OpenAICompatibleTtsProvider };
@@ -21,7 +23,15 @@ class OpenAICompatibleTtsProvider {
2123 get settingsHtml() {
2224 let html = `
2325 <label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
26+ <div class="flex-container alignItemsCenter">
27+ <div class="flex1">
2428 <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>
2535 <label for="openai_compatible_model">Model:</label>
2636 <input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/>
2737 <label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
@@ -64,7 +74,35 @@ class OpenAICompatibleTtsProvider {
6474
6575 $('#openai_compatible_tts_speed_output').text(this.settings.speed);
6676
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+ });
68106
69107 await this.checkReady();
70108
@@ -75,7 +113,7 @@ class OpenAICompatibleTtsProvider {
75113 // Update dynamically
76114 this.settings.provider_endpoint = String($('#openai_compatible_tts_endpoint').val());
77115 this.settings.model = String($('#openai_compatible_model').val());
78116 this.settings.available_voices = String($('#openai_compatible_tts_voices').val()).split(',');
79117 this.settings.speed = Number($('#openai_compatible_tts_speed').val());
80118 $('#openai_compatible_tts_speed_output').text(this.settings.speed);
81119 saveTtsProviderSettings();
@@ -132,15 +170,16 @@ class OpenAICompatibleTtsProvider {
132170
133171 async fetchTtsGeneration(inputText, voiceId) {
134172 console.info(`Generating new TTS for voice_id ${voiceId}`);
135173 const response = await fetch(this.settings.provider_endpoint'/api/openai/custom/generate-voice', {
136174 method: 'POST',
137175 headers: getRequestHeaders(),
138176 body: JSON.stringify({
139177 'model'provider_endpoint: this.settings.modelprovider_endpoint,
140- 'input': inputText,
178+ model: this.settings.model,
141179 'voice'input: voiceIdinputText,
142180 'response_format'voice: 'mp3'voiceId,
143- 'speed': this.settings.speed,
181+ response_format: 'mp3',
182+ speed: this.settings.speed,
144183 }),
145184 });
146185
public/scripts/secrets.js+2 -1
@@ -33,6 +33,7 @@ export const SECRET_KEYS = {
3333 HUGGINGFACE: 'api_key_huggingface',
3434 STABILITY: 'api_key_stability',
3535 BLOCKENTROPY: 'api_key_blockentropy',
36+ CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
3637};
3738
3839const INPUT_MAP = {
@@ -127,7 +128,7 @@ export async function writeSecret(key, value) {
127128 const text = await response.text();
128129
129130 if (text == 'ok') {
130131 secret_state[key] = true!!value;
131132 updateSecretDisplay();
132133 }
133134 }
src/endpoints/openai.js+44 -0
@@ -282,4 +282,48 @@ router.post('/generate-image', jsonParser, async (request, response) => {
282282 }
283283});
284284
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+
285329module.exports = { router };
src/endpoints/secrets.js+1 -0
@@ -45,6 +45,7 @@ const SECRET_KEYS = {
4545 HUGGINGFACE: 'api_key_huggingface',
4646 STABILITY: 'api_key_stability',
4747 BLOCKENTROPY: 'api_key_blockentropy',
48+ CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
4849};
4950
5051// These are the keys that are safe to expose, even if allowKeysExposure is false