Refactor ElevenLabs TTS API key handling (#4906) * Refactor ElevenLabs TTS API key handling #4483 * Remove unused connection button and related event handler from ElevenLabs TTS provider * Add ElevenLabs STT endpoint

c75df956c598f995d543e383f55917d50ce2eaf5

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

Signed
4 files changed, +395 -106Ignore whitespace
public/scripts/extensions/tts/elevenlabs.js+131 -106
@@ -1,23 +1,20 @@
11import { saveTtsProviderSettings } from './index.js';
2+import { event_types, eventSource, getRequestHeaders } from '/script.js';
3+import { SECRET_KEYS, secret_state, writeSecret } from '/scripts/secrets.js';
4+import { getBase64Async } from '/scripts/utils.js';
25export { ElevenLabsTtsProvider };
36
47class ElevenLabsTtsProvider {
5- //########//
6- // Config //
7- //########//
8-
98 settings;
109 voices = [];
1110 separator = ' ... ... ... ';
1211
13-
1412 defaultSettings = {
1513 stability: 0.75,
1614 similarity_boost: 0.75,
1715 style_exaggeration: 0.00,
1816 speaker_boost: true,
1917 speed: 1.0,
20- apiKey: '',
2118 model: 'eleven_turbo_v2_5',
2219 voiceMap: {},
2320 };
@@ -25,8 +22,15 @@ class ElevenLabsTtsProvider {
2522 get settingsHtml() {
2623 let html = `
2724 <div class="elevenlabs_tts_settings">
28- <label for="elevenlabs_tts_api_key">API Key</label>
25+ <div class="flex-container alignItemsBaseline">
29- <input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>
26+ <h4 for="elevenlabs_tts_key" class="flex1 margin0">
27+ <a href="https://elevenlabs.io/app/developers/api-keys" target="_blank">ElevenLabs TTS Key</a>
28+ </h4>
29+ <div id="elevenlabs_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_elevenlabs">
30+ <i class="fa-solid fa-key"></i>
31+ <span>Click to set</span>
32+ </div>
33+ </div>
3034 <label for="elevenlabs_tts_model">Model</label>
3135 <select id="elevenlabs_tts_model" class="text_pole">
3236 <option value="eleven_v3">Eleven v3</option>
@@ -39,7 +43,6 @@ class ElevenLabsTtsProvider {
3943 <option value="eleven_multilingual_v1">Multilingual v1 (Old)</option>
4044 <option value="eleven_turbo_v2">Turbo v2 (Old)</option>
4145 </select>
42- <input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
4346 <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
4447 <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />
4548 <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
@@ -72,6 +75,20 @@ class ElevenLabsTtsProvider {
7275 return html;
7376 }
7477
78+ constructor() {
79+ this.handler = async function (/** @type {string} */ key) {
80+ if (key !== SECRET_KEYS.ELEVENLABS) return;
81+ $('#elevenlabs_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELEVENLABS]);
82+ await this.fetchTtsVoiceObjects();
83+ }.bind(this);
84+ }
85+
86+ dispose() {
87+ [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
88+ eventSource.removeListener(event, this.handler);
89+ });
90+ }
91+
7592 shouldInvolveExtendedSettings() {
7693 // Models that support extended settings (style_exaggeration, speaker_boost)
7794 const modelsWithExtendedSettings = [
@@ -114,6 +131,18 @@ class ElevenLabsTtsProvider {
114131 delete settings['multilingual'];
115132 }
116133
134+ if (Object.hasOwn(settings, 'apiKey')) {
135+ if (settings.apiKey && !secret_state[SECRET_KEYS.ELEVENLABS]){
136+ await writeSecret(SECRET_KEYS.ELEVENLABS, settings.apiKey);
137+ }
138+ delete settings['apiKey'];
139+ }
140+
141+ $('#elevenlabs_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.ELEVENLABS]);
142+ [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
143+ eventSource.on(event, this.handler);
144+ });
145+
117146 for (const key in settings) {
118147 if (key in this.settings) {
119148 this.settings[key] = settings[key];
@@ -127,9 +156,7 @@ class ElevenLabsTtsProvider {
127156 $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);
128157 $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);
129158 $('#elevenlabs_tts_speed').val(this.settings.speed);
130- $('#elevenlabs_tts_api_key').val(this.settings.apiKey);
131159 $('#elevenlabs_tts_model').val(this.settings.model);
132- $('#eleven_labs_connect').on('click', () => { this.onConnectClick(); });
133160 $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this));
134161 $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));
135162 $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));
@@ -157,23 +184,17 @@ class ElevenLabsTtsProvider {
157184 }
158185
159186 async onRefreshClick() {
160- }
187+ await this.fetchTtsVoiceObjects();
161-
162- async onConnectClick() {
163- // Update on Apply click
164- return await this.updateApiKey().catch((error) => {
165- toastr.error(`ElevenLabs: ${error}`);
166- });
167188 }
168189
169190 setupVoiceCloningMenu() {
170191 const audioFilesInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_audio_files'));
171192 const selectedFilesListElement = document.getElementById('elevenlabs_tts_selected_files_list');
172193 const cloneVoiceButton = document.getElementById('elevenlabs_tts_clone_voice_button');
173194 const uploadAudioFileButton = document.getElementById('upload_audio_file');
174195 const voiceCloningNameInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_name'));
175196 const voiceCloningDescriptionInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_description'));
176197 const voiceCloningLabelsInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_labels'));
177198
178199 const updateCloneVoiceButtonVisibility = () => {
179200 cloneVoiceButton.style.display = audioFilesInput.files.length > 0 ? 'inline-block' : 'none';
@@ -224,22 +245,11 @@ class ElevenLabsTtsProvider {
224245 updateCloneVoiceButtonVisibility();
225246 }
226247
227- async updateApiKey() {
248+ /**
228- // Using this call to validate API key
249+ * Get voice object by name
229- this.settings.apiKey = $('#elevenlabs_tts_api_key').val();
250+ * @param {string} voiceName Voice name to look up
230-
251+ * @returns {Promise<Object>} Voice object
231- await this.fetchTtsVoiceObjects().catch(error => {
252+ */
232- throw 'TTS API key validation failed';
233- });
234- console.debug(`Saved new API_KEY: ${this.settings.apiKey}`);
235- $('#tts_status').text('');
236- this.onSettingsChange();
237- }
238-
239- //#################//
240- // TTS Interfaces //
241- //#################//
242-
243253 async getVoice(voiceName) {
244254 if (this.voices.length == 0) {
245255 this.voices = await this.fetchTtsVoiceObjects();
@@ -253,25 +263,30 @@ class ElevenLabsTtsProvider {
253263 return match;
254264 }
255265
256-
266+ /**
267+ * Generate TTS audio
268+ * @param {string} text Text to synthesize
269+ * @param {string} voiceId Voice ID to use for synthesis
270+ * @returns {Promise<Response>} Response object containing audio data
271+ */
257272 async generateTts(text, voiceId) {
258273 const historyId = await this.findTtsGenerationInHistory(text, voiceId);
259274
260- let response;
261275 if (historyId) {
262276 console.debug(`Found existing TTS generation with id ${historyId}`);
263277 response =return await this.fetchTtsFromHistory(historyId);
264278 } else {
265279 console.debug('No existing TTS generation found, requesting new generation');
266280 response =return await this.fetchTtsGeneration(text, voiceId);
267281 }
268- return response;
269282 }
270283
271- //###################//
284+ /**
272- // Helper Functions //
285+ * Find existing TTS generation in history
273- //###################//
286+ * @param {string} message Message text used for TTS generation
274-
287+ * @param {string} voiceId Voice ID used for TTS generation
288+ * @returns {Promise<string>} History item ID if found, empty string otherwise
289+ */
275290 async findTtsGenerationInHistory(message, voiceId) {
276291 const ttsHistory = await this.fetchTtsHistory();
277292 for (const history of ttsHistory) {
@@ -285,40 +300,35 @@ class ElevenLabsTtsProvider {
285300 return '';
286301 }
287302
288-
289- //###########//
290- // API CALLS //
291- //###########//
292303 async fetchTtsVoiceObjects() {
293- const headers = {
304+ const response = await fetch('/api/speech/elevenlabs/voices', {
294- 'xi-api-key': this.settings.apiKey,
305+ method: 'POST',
295- };
306+ headers: getRequestHeaders({ omitContentType: true }),
296- const response = await fetch('https://api.elevenlabs.io/v1/voices', {
297- headers: headers,
298307 });
299308 if (!response.ok) {
300309 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
301310 }
302311 const responseJson = await response.json();
303312 return responseJson.voices;
304313 }
305314
306315 async fetchTtsVoiceSettings() {
307- const headers = {
316+ const response = await fetch('/api/speech/elevenlabs/voice-settings', {
308- 'xi-api-key': this.settings.apiKey,
317+ method: 'POST',
309- };
318+ headers: getRequestHeaders({ omitContentType: true }),
310- const response = await fetch(
319+ });
311- 'https://api.elevenlabs.io/v1/voices/settings/default',
312- {
313- headers: headers,
314- },
315- );
316320 if (!response.ok) {
317321 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
318322 }
319323 return response.json();
320324 }
321325
326+ /**
327+ * Fetch new TTS generation from ElevenLabs API
328+ * @param {string} text Text to synthesize
329+ * @param {string} voiceId Voice ID to use for synthesis
330+ * @returns {Promise<Response>} Response object containing audio data
331+ */
322332 async fetchTtsGeneration(text, voiceId) {
323333 let model = this.settings.model ?? 'eleven_monolingual_v1';
324334 console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`);
@@ -335,75 +345,90 @@ class ElevenLabsTtsProvider {
335345 request.voice_settings.style = Number(this.settings.style_exaggeration);
336346 request.voice_settings.use_speaker_boost = Boolean(this.settings.speaker_boost);
337347 }
338348 const response = await fetch(`https:/'/api.elevenlabs.io/v1/text-to-speech/${voiceId}`elevenlabs/synthesize', {
339349 method: 'POST',
340350 headers: {getRequestHeaders(),
341- 'xi-api-key': this.settings.apiKey,
351+ body: JSON.stringify({
342- 'Content-Type': 'application/json',
352+ voiceId: voiceId,
343- },
353+ request: request,
344- body: JSON.stringify(request),
354+ }),
345355 });
346356 if (!response.ok) {
347357 toastr.error(response.statusText, 'TTS Generation Failed');
348358 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
349359 }
350360 return response;
351361 }
352362
353- async fetchTtsFromHistory(history_item_id) {
363+ /**
354- console.info(`Fetched existing TTS with history_item_id ${history_item_id}`);
364+ * Fetch existing TTS audio from history
355- const response = await fetch(
365+ * @param {string} historyItemId History item ID to fetch audio for
356- `https://api.elevenlabs.io/v1/history/${history_item_id}/audio`,
366+ * @returns {Promise<Response>} Response object containing audio data
357- {
367+ */
358- headers: {
368+ async fetchTtsFromHistory(historyItemId) {
359- 'xi-api-key': this.settings.apiKey,
369+ console.info(`Fetched existing TTS with history_item_id ${historyItemId}`);
360- },
370+ const response = await fetch('/api/speech/elevenlabs/history-audio', {
361- },
371+ method: 'POST',
362- );
372+ headers: getRequestHeaders(),
373+ body: JSON.stringify({
374+ historyItemId: historyItemId,
375+ }),
376+ });
363377 if (!response.ok) {
364378 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
365379 }
366380 return response;
367381 }
368382
383+ /**
384+ * Fetch TTS generation history
385+ * @returns {Promise<Array>} Array of TTS history items
386+ */
369387 async fetchTtsHistory() {
370- const headers = {
388+ const response = await fetch('/api/speech/elevenlabs/history', {
371- 'xi-api-key': this.settings.apiKey,
389+ method: 'POST',
372- };
390+ headers: getRequestHeaders({ omitContentType: true }),
373- const response = await fetch('https://api.elevenlabs.io/v1/history', {
374- headers: headers,
375391 });
376392 if (!response.ok) {
377393 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
378394 }
379395 const responseJson = await response.json();
380396 return responseJson.history;
381397 }
382398
399+ /**
400+ * Add a new voice via ElevenLabs API
401+ * @param {string} name Voice name
402+ * @param {string} description Voice description
403+ * @param {string} labels Voice labels
404+ * @returns {Promise<Object>} Newly created voice object
405+ */
383406 async addVoice(name, description, labels) {
384- const selected_files = document.querySelectorAll('input[type="file"][name="audio_files"]');
407+ const audioFilesInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_audio_files'));
385- const formData = new FormData();
408+ if (!(audioFilesInput instanceof HTMLInputElement) || audioFilesInput.files.length === 0) {
409+ throw new Error('No audio files selected for voice cloning.');
410+ }
386411
387- formData.append('name', name);
412+ const data = {
388- formData.append('description', description);
413+ name: name,
389- formData.append('labels', labels);
414+ description: description,
415+ labels: labels,
416+ files: [],
417+ };
390418
391419 for (const file of selected_filesaudioFilesInput.files) {
392- if (file.files.length > 0) {
420+ const base64Data = await getBase64Async(file);
393- formData.append('files', file.files[0]);
421+ data.files.push(base64Data);
394- }
395422 }
396423
397424 const response = await fetch('https://api.elevenlabs.io/v1speech/elevenlabs/voices/add', {
398425 method: 'POST',
399426 headers: {getRequestHeaders(),
400- 'xi-api-key': this.settings.apiKey,
427+ body: JSON.stringify(data),
401- },
402- body: formData,
403428 });
404429
405430 if (!response.ok) {
406431 throw new Error(`HTTP ${response.status}:. ${awaitSee responseserver console for details.text()}`);
407432 }
408433
409434 return await response.json();
public/scripts/secrets.js+2 -0
@@ -71,6 +71,7 @@ export const SECRET_KEYS = {
7171 COMETAPI: 'api_key_cometapi',
7272 ZAI: 'api_key_zai',
7373 SILICONFLOW: 'api_key_siliconflow',
74+ ELEVENLABS: 'api_key_elevenlabs',
7475};
7576
7677const FRIENDLY_NAMES = {
@@ -130,6 +131,7 @@ const FRIENDLY_NAMES = {
130131 [SECRET_KEYS.AZURE_OPENAI]: 'Azure OpenAI',
131132 [SECRET_KEYS.ZAI]: 'Z.AI',
132133 [SECRET_KEYS.SILICONFLOW]: 'SiliconFlow',
134+ [SECRET_KEYS.ELEVENLABS]: 'ElevenLabs TTS',
133135};
134136
135137const INPUT_MAP = {
src/endpoints/secrets.js+1 -0
@@ -64,6 +64,7 @@ export const SECRET_KEYS = {
6464 AZURE_OPENAI: 'api_key_azure_openai',
6565 ZAI: 'api_key_zai',
6666 SILICONFLOW: 'api_key_siliconflow',
67+ ELEVENLABS: 'api_key_elevenlabs',
6768};
6869
6970/**
src/endpoints/speech.js+261 -0
@@ -1,9 +1,13 @@
11import { Buffer } from 'node:buffer';
2+import fs from 'node:fs';
23import express from 'express';
34import wavefile from 'wavefile';
45import fetch from 'node-fetch';
6+import FormData from 'form-data';
7+import mime from 'mime-types';
58import { getPipeline } from '../transformers.js';
69import { forwardFetchResponse } from '../util.js';
10+import { readSecret, SECRET_KEYS } from './secrets.js';
711
812export const router = express.Router();
913
@@ -138,3 +142,260 @@ pollinations.post('/generate', async (req, res) => {
138142});
139143
140144router.use('/pollinations', pollinations);
145+
146+const elevenlabs = express.Router();
147+
148+elevenlabs.post('/voices', async (req, res) => {
149+ try {
150+ const apiKey = readSecret(req.user.directories, SECRET_KEYS.ELEVENLABS);
151+ if (!apiKey) {
152+ console.warn('ElevenLabs API key not found');
153+ return res.sendStatus(400);
154+ }
155+
156+ const response = await fetch('https://api.elevenlabs.io/v1/voices', {
157+ headers: {
158+ 'xi-api-key': apiKey,
159+ },
160+ });
161+
162+ if (!response.ok) {
163+ const text = await response.text();
164+ console.warn(`ElevenLabs voices fetch failed: HTTP ${response.status} - ${text}`);
165+ return res.sendStatus(500);
166+ }
167+
168+ const responseJson = await response.json();
169+ return res.json(responseJson);
170+ } catch (error) {
171+ console.error(error);
172+ return res.sendStatus(500);
173+ }
174+});
175+
176+elevenlabs.post('/voice-settings', async (req, res) => {
177+ try {
178+ const apiKey = readSecret(req.user.directories, SECRET_KEYS.ELEVENLABS);
179+ if (!apiKey) {
180+ console.warn('ElevenLabs API key not found');
181+ return res.sendStatus(400);
182+ }
183+
184+ const response = await fetch('https://api.elevenlabs.io/v1/voices/settings/default', {
185+ headers: {
186+ 'xi-api-key': apiKey,
187+ },
188+ });
189+
190+ if (!response.ok) {
191+ const text = await response.text();
192+ console.warn(`ElevenLabs voice settings fetch failed: HTTP ${response.status} - ${text}`);
193+ return res.sendStatus(500);
194+ }
195+ const responseJson = await response.json();
196+ return res.json(responseJson);
197+ } catch (error) {
198+ console.error(error);
199+ return res.sendStatus(500);
200+ }
201+});
202+
203+elevenlabs.post('/synthesize', async (req, res) => {
204+ try {
205+ const apiKey = readSecret(req.user.directories, SECRET_KEYS.ELEVENLABS);
206+ if (!apiKey) {
207+ console.warn('ElevenLabs API key not found');
208+ return res.sendStatus(400);
209+ }
210+
211+ const { voiceId, request } = req.body;
212+
213+ if (!voiceId || !request) {
214+ console.warn('ElevenLabs synthesis request missing voiceId or request body');
215+ return res.sendStatus(400);
216+ }
217+
218+ console.debug('ElevenLabs TTS request:', request);
219+
220+ const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
221+ method: 'POST',
222+ headers: {
223+ 'xi-api-key': apiKey,
224+ 'Content-Type': 'application/json',
225+ },
226+ body: JSON.stringify(request),
227+ });
228+
229+ if (!response.ok) {
230+ const text = await response.text();
231+ console.warn(`ElevenLabs synthesis failed: HTTP ${response.status} - ${text}`);
232+ return res.sendStatus(500);
233+ }
234+
235+ res.set('Content-Type', 'audio/mpeg');
236+ forwardFetchResponse(response, res);
237+ } catch (error) {
238+ console.error(error);
239+ return res.sendStatus(500);
240+ }
241+});
242+
243+elevenlabs.post('/history', async (req, res) => {
244+ try {
245+ const apiKey = readSecret(req.user.directories, SECRET_KEYS.ELEVENLABS);
246+ if (!apiKey) {
247+ console.warn('ElevenLabs API key not found');
248+ return res.sendStatus(400);
249+ }
250+
251+ const response = await fetch('https://api.elevenlabs.io/v1/history', {
252+ headers: {
253+ 'xi-api-key': apiKey,
254+ },
255+ });
256+
257+ if (!response.ok) {
258+ const text = await response.text();
259+ console.warn(`ElevenLabs history fetch failed: HTTP ${response.status} - ${text}`);
260+ return res.sendStatus(500);
261+ }
262+
263+ const responseJson = await response.json();
264+ return res.json(responseJson);
265+ } catch (error) {
266+ console.error(error);
267+ return res.sendStatus(500);
268+ }
269+});
270+
271+elevenlabs.post('/history-audio', async (req, res) => {
272+ try {
273+ const apiKey = readSecret(req.user.directories, SECRET_KEYS.ELEVENLABS);
274+ if (!apiKey) {
275+ console.warn('ElevenLabs API key not found');
276+ return res.sendStatus(400);
277+ }
278+
279+ const { historyItemId } = req.body;
280+ if (!historyItemId) {
281+ console.warn('ElevenLabs history audio request missing historyItemId');
282+ return res.sendStatus(400);
283+ }
284+
285+ console.debug('ElevenLabs history audio request for ID:', historyItemId);
286+
287+ const response = await fetch(`https://api.elevenlabs.io/v1/history/${historyItemId}/audio`, {
288+ headers: {
289+ 'xi-api-key': apiKey,
290+ },
291+ });
292+
293+ if (!response.ok) {
294+ const text = await response.text();
295+ console.warn(`ElevenLabs history audio fetch failed: HTTP ${response.status} - ${text}`);
296+ return res.sendStatus(500);
297+ }
298+
299+ res.set('Content-Type', 'audio/mpeg');
300+ forwardFetchResponse(response, res);
301+ } catch (error) {
302+ console.error(error);
303+ return res.sendStatus(500);
304+ }
305+});
306+
307+elevenlabs.post('/voices/add', async (req, res) => {
308+ try {
309+ const apiKey = readSecret(req.user.directories, SECRET_KEYS.ELEVENLABS);
310+ if (!apiKey) {
311+ console.warn('ElevenLabs API key not found');
312+ return res.sendStatus(400);
313+ }
314+
315+ const { name, description, labels, files } = req.body;
316+
317+ const formData = new FormData();
318+ formData.append('name', name || 'Custom Voice');
319+ formData.append('description', description || 'Uploaded via SillyTavern');
320+ formData.append('labels', labels || '');
321+
322+ for (const fileData of (files || [])) {
323+ const [mimeType, base64Data] = /^data:(.+);base64,(.+)$/.exec(fileData)?.slice(1) || [];
324+ if (!mimeType || !base64Data) {
325+ console.warn('Invalid audio file data provided for ElevenLabs voice upload');
326+ continue;
327+ }
328+ const buffer = Buffer.from(base64Data, 'base64');
329+ formData.append('files', buffer, {
330+ filename: `audio.${mime.extension(mimeType) || 'wav'}`,
331+ contentType: mimeType,
332+ });
333+ }
334+
335+ console.debug('ElevenLabs voice upload request:', { name, description, labels, files: files?.length || 0 });
336+
337+ const response = await fetch('https://api.elevenlabs.io/v1/voices/add', {
338+ method: 'POST',
339+ headers: {
340+ 'xi-api-key': apiKey,
341+ },
342+ body: formData,
343+ });
344+
345+ if (!response.ok) {
346+ const text = await response.text();
347+ console.warn(`ElevenLabs voice upload failed: HTTP ${response.status} - ${text}`);
348+ return res.sendStatus(500);
349+ }
350+
351+ const responseJson = await response.json();
352+ return res.json(responseJson);
353+ } catch (error) {
354+ console.error(error);
355+ return res.sendStatus(500);
356+ }
357+});
358+
359+elevenlabs.post('/recognize', async (req, res) => {
360+ try {
361+ const apiKey = readSecret(req.user.directories, SECRET_KEYS.ELEVENLABS);
362+ if (!apiKey) {
363+ console.warn('ElevenLabs API key not found');
364+ return res.sendStatus(400);
365+ }
366+
367+ if (!req.file) {
368+ console.warn('No audio file found');
369+ return res.sendStatus(400);
370+ }
371+
372+ console.info('Processing audio file with ElevenLabs', req.file.path);
373+ const formData = new FormData();
374+ formData.append('file', fs.createReadStream(req.file.path), { filename: 'audio.wav', contentType: 'audio/wav' });
375+ formData.append('model_id', req.body.model);
376+
377+ const response = await fetch('https://api.elevenlabs.io/v1/speech-to-text', {
378+ method: 'POST',
379+ headers: {
380+ 'xi-api-key': apiKey,
381+ },
382+ body: formData,
383+ });
384+
385+ if (!response.ok) {
386+ const text = await response.text();
387+ console.warn(`ElevenLabs speech recognition failed: HTTP ${response.status} - ${text}`);
388+ return res.sendStatus(500);
389+ }
390+
391+ fs.unlinkSync(req.file.path);
392+ const responseJson = await response.json();
393+ console.debug('ElevenLabs speech recognition response:', responseJson);
394+ return res.json(responseJson);
395+ } catch (error) {
396+ console.error(error);
397+ return res.sendStatus(500);
398+ }
399+});
400+
401+router.use('/elevenlabs', elevenlabs);