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 @@
1import { saveTtsProviderSettings } from './index.js';1import { saveTtsProviderSettings } from './index.js';
2import { event_types, eventSource, getRequestHeaders } from '/script.js';
3import { SECRET_KEYS, secret_state, writeSecret } from '/scripts/secrets.js';
4import { getBase64Async } from '/scripts/utils.js';
2export { ElevenLabsTtsProvider };5export { ElevenLabsTtsProvider };
36
4class ElevenLabsTtsProvider {7class ElevenLabsTtsProvider {
5 //########//
6 // Config //
7 //########//
8
9 settings;8 settings;
10 voices = [];9 voices = [];
11 separator = ' ... ... ... ';10 separator = ' ... ... ... ';
1211
13
14 defaultSettings = {12 defaultSettings = {
15 stability: 0.75,13 stability: 0.75,
16 similarity_boost: 0.75,14 similarity_boost: 0.75,
17 style_exaggeration: 0.00,15 style_exaggeration: 0.00,
18 speaker_boost: true,16 speaker_boost: true,
19 speed: 1.0,17 speed: 1.0,
20 apiKey: '',
21 model: 'eleven_turbo_v2_5',18 model: 'eleven_turbo_v2_5',
22 voiceMap: {},19 voiceMap: {},
23 };20 };
@@ -25,8 +22,15 @@ class ElevenLabsTtsProvider {
25 get settingsHtml() {22 get settingsHtml() {
26 let html = `23 let html = `
27 <div class="elevenlabs_tts_settings">24 <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>
30 <label for="elevenlabs_tts_model">Model</label>34 <label for="elevenlabs_tts_model">Model</label>
31 <select id="elevenlabs_tts_model" class="text_pole">35 <select id="elevenlabs_tts_model" class="text_pole">
32 <option value="eleven_v3">Eleven v3</option>36 <option value="eleven_v3">Eleven v3</option>
@@ -39,7 +43,6 @@ class ElevenLabsTtsProvider {
39 <option value="eleven_multilingual_v1">Multilingual v1 (Old)</option>43 <option value="eleven_multilingual_v1">Multilingual v1 (Old)</option>
40 <option value="eleven_turbo_v2">Turbo v2 (Old)</option>44 <option value="eleven_turbo_v2">Turbo v2 (Old)</option>
41 </select>45 </select>
42 <input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
43 <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>46 <label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
44 <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />47 <input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.01" />
45 <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>48 <label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
@@ -72,6 +75,20 @@ class ElevenLabsTtsProvider {
72 return html;75 return html;
73 }76 }
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
75 shouldInvolveExtendedSettings() {92 shouldInvolveExtendedSettings() {
76 // Models that support extended settings (style_exaggeration, speaker_boost)93 // Models that support extended settings (style_exaggeration, speaker_boost)
77 const modelsWithExtendedSettings = [94 const modelsWithExtendedSettings = [
@@ -114,6 +131,18 @@ class ElevenLabsTtsProvider {
114 delete settings['multilingual'];131 delete settings['multilingual'];
115 }132 }
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
117 for (const key in settings) {146 for (const key in settings) {
118 if (key in this.settings) {147 if (key in this.settings) {
119 this.settings[key] = settings[key];148 this.settings[key] = settings[key];
@@ -127,9 +156,7 @@ class ElevenLabsTtsProvider {
127 $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);156 $('#elevenlabs_tts_style_exaggeration').val(this.settings.style_exaggeration);
128 $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);157 $('#elevenlabs_tts_speaker_boost').prop('checked', this.settings.speaker_boost);
129 $('#elevenlabs_tts_speed').val(this.settings.speed);158 $('#elevenlabs_tts_speed').val(this.settings.speed);
130 $('#elevenlabs_tts_api_key').val(this.settings.apiKey);
131 $('#elevenlabs_tts_model').val(this.settings.model);159 $('#elevenlabs_tts_model').val(this.settings.model);
132 $('#eleven_labs_connect').on('click', () => { this.onConnectClick(); });
133 $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this));160 $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this));
134 $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));161 $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));
135 $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));162 $('#elevenlabs_tts_style_exaggeration').on('input', this.onSettingsChange.bind(this));
@@ -157,23 +184,17 @@ class ElevenLabsTtsProvider {
157 }184 }
158185
159 async onRefreshClick() {186 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 });
167 }188 }
168189
169 setupVoiceCloningMenu() {190 setupVoiceCloningMenu() {
170 const audioFilesInput = document.getElementById('elevenlabs_tts_audio_files');191 const audioFilesInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_audio_files'));
171 const selectedFilesListElement = document.getElementById('elevenlabs_tts_selected_files_list');192 const selectedFilesListElement = document.getElementById('elevenlabs_tts_selected_files_list');
172 const cloneVoiceButton = document.getElementById('elevenlabs_tts_clone_voice_button');193 const cloneVoiceButton = document.getElementById('elevenlabs_tts_clone_voice_button');
173 const uploadAudioFileButton = document.getElementById('upload_audio_file');194 const uploadAudioFileButton = document.getElementById('upload_audio_file');
174 const voiceCloningNameInput = document.getElementById('elevenlabs_tts_voice_cloning_name');195 const voiceCloningNameInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_name'));
175 const voiceCloningDescriptionInput = document.getElementById('elevenlabs_tts_voice_cloning_description');196 const voiceCloningDescriptionInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_description'));
176 const voiceCloningLabelsInput = document.getElementById('elevenlabs_tts_voice_cloning_labels');197 const voiceCloningLabelsInput = /** @type {HTMLInputElement} */ (document.getElementById('elevenlabs_tts_voice_cloning_labels'));
177198
178 const updateCloneVoiceButtonVisibility = () => {199 const updateCloneVoiceButtonVisibility = () => {
179 cloneVoiceButton.style.display = audioFilesInput.files.length > 0 ? 'inline-block' : 'none';200 cloneVoiceButton.style.display = audioFilesInput.files.length > 0 ? 'inline-block' : 'none';
@@ -224,22 +245,11 @@ class ElevenLabsTtsProvider {
224 updateCloneVoiceButtonVisibility();245 updateCloneVoiceButtonVisibility();
225 }246 }
226247
227 async updateApiKey() {248 /**
228 // Using this call to validate API key249 * Get voice object by name
229 this.settings.apiKey = $('#elevenlabs_tts_api_key').val();250 * @param {string} voiceName Voice name to look up
230251 * @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
243 async getVoice(voiceName) {253 async getVoice(voiceName) {
244 if (this.voices.length == 0) {254 if (this.voices.length == 0) {
245 this.voices = await this.fetchTtsVoiceObjects();255 this.voices = await this.fetchTtsVoiceObjects();
@@ -253,25 +263,30 @@ class ElevenLabsTtsProvider {
253 return match;263 return match;
254 }264 }
255265
256266 /**
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 */
257 async generateTts(text, voiceId) {272 async generateTts(text, voiceId) {
258 const historyId = await this.findTtsGenerationInHistory(text, voiceId);273 const historyId = await this.findTtsGenerationInHistory(text, voiceId);
259274
260 let response;
261 if (historyId) {275 if (historyId) {
262 console.debug(`Found existing TTS generation with id ${historyId}`);276 console.debug(`Found existing TTS generation with id ${historyId}`);
263 response = await this.fetchTtsFromHistory(historyId);277 return await this.fetchTtsFromHistory(historyId);
264 } else {278 } else {
265 console.debug('No existing TTS generation found, requesting new generation');279 console.debug('No existing TTS generation found, requesting new generation');
266 response = await this.fetchTtsGeneration(text, voiceId);280 return await this.fetchTtsGeneration(text, voiceId);
267 }281 }
268 return response;
269 }282 }
270283
271 //###################//284 /**
272 // Helper Functions //285 * Find existing TTS generation in history
273 //###################//286 * @param {string} message Message text used for TTS generation
274287 * @param {string} voiceId Voice ID used for TTS generation
288 * @returns {Promise<string>} History item ID if found, empty string otherwise
289 */
275 async findTtsGenerationInHistory(message, voiceId) {290 async findTtsGenerationInHistory(message, voiceId) {
276 const ttsHistory = await this.fetchTtsHistory();291 const ttsHistory = await this.fetchTtsHistory();
277 for (const history of ttsHistory) {292 for (const history of ttsHistory) {
@@ -285,40 +300,35 @@ class ElevenLabsTtsProvider {
285 return '';300 return '';
286 }301 }
287302
288
289 //###########//
290 // API CALLS //
291 //###########//
292 async fetchTtsVoiceObjects() {303 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,
298 });307 });
299 if (!response.ok) {308 if (!response.ok) {
300 throw new Error(`HTTP ${response.status}: ${await response.text()}`);309 throw new Error(`HTTP ${response.status}. See server console for details.`);
301 }310 }
302 const responseJson = await response.json();311 const responseJson = await response.json();
303 return responseJson.voices;312 return responseJson.voices;
304 }313 }
305314
306 async fetchTtsVoiceSettings() {315 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 );
316 if (!response.ok) {320 if (!response.ok) {
317 throw new Error(`HTTP ${response.status}: ${await response.text()}`);321 throw new Error(`HTTP ${response.status}. See server console for details.`);
318 }322 }
319 return response.json();323 return response.json();
320 }324 }
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 */
322 async fetchTtsGeneration(text, voiceId) {332 async fetchTtsGeneration(text, voiceId) {
323 let model = this.settings.model ?? 'eleven_monolingual_v1';333 let model = this.settings.model ?? 'eleven_monolingual_v1';
324 console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`);334 console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`);
@@ -335,75 +345,90 @@ class ElevenLabsTtsProvider {
335 request.voice_settings.style = Number(this.settings.style_exaggeration);345 request.voice_settings.style = Number(this.settings.style_exaggeration);
336 request.voice_settings.use_speaker_boost = Boolean(this.settings.speaker_boost);346 request.voice_settings.use_speaker_boost = Boolean(this.settings.speaker_boost);
337 }347 }
338 const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {348 const response = await fetch('/api/speech/elevenlabs/synthesize', {
339 method: 'POST',349 method: 'POST',
340 headers: {350 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 }),
345 });355 });
346 if (!response.ok) {356 if (!response.ok) {
347 toastr.error(response.statusText, 'TTS Generation Failed');357 toastr.error(response.statusText, 'TTS Generation Failed');
348 throw new Error(`HTTP ${response.status}: ${await response.text()}`);358 throw new Error(`HTTP ${response.status}. See server console for details.`);
349 }359 }
350 return response;360 return response;
351 }361 }
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 });
363 if (!response.ok) {377 if (!response.ok) {
364 throw new Error(`HTTP ${response.status}: ${await response.text()}`);378 throw new Error(`HTTP ${response.status}. See server console for details.`);
365 }379 }
366 return response;380 return response;
367 }381 }
368382
383 /**
384 * Fetch TTS generation history
385 * @returns {Promise<Array>} Array of TTS history items
386 */
369 async fetchTtsHistory() {387 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,
375 });391 });
376 if (!response.ok) {392 if (!response.ok) {
377 throw new Error(`HTTP ${response.status}: ${await response.text()}`);393 throw new Error(`HTTP ${response.status}. See server console for details.`);
378 }394 }
379 const responseJson = await response.json();395 const responseJson = await response.json();
380 return responseJson.history;396 return responseJson.history;
381 }397 }
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 */
383 async addVoice(name, description, labels) {406 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
391 for (const file of selected_files) {419 for (const file of audioFilesInput.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 }
395 }422 }
396423
397 const response = await fetch('https://api.elevenlabs.io/v1/voices/add', {424 const response = await fetch('/api/speech/elevenlabs/voices/add', {
398 method: 'POST',425 method: 'POST',
399 headers: {426 headers: getRequestHeaders(),
400 'xi-api-key': this.settings.apiKey,427 body: JSON.stringify(data),
401 },
402 body: formData,
403 });428 });
404429
405 if (!response.ok) {430 if (!response.ok) {
406 throw new Error(`HTTP ${response.status}: ${await response.text()}`);431 throw new Error(`HTTP ${response.status}. See server console for details.`);
407 }432 }
408433
409 return await response.json();434 return await response.json();
public/scripts/secrets.js+2 -0
@@ -71,6 +71,7 @@ export const SECRET_KEYS = {
71 COMETAPI: 'api_key_cometapi',71 COMETAPI: 'api_key_cometapi',
72 ZAI: 'api_key_zai',72 ZAI: 'api_key_zai',
73 SILICONFLOW: 'api_key_siliconflow',73 SILICONFLOW: 'api_key_siliconflow',
74 ELEVENLABS: 'api_key_elevenlabs',
74};75};
7576
76const FRIENDLY_NAMES = {77const FRIENDLY_NAMES = {
@@ -130,6 +131,7 @@ const FRIENDLY_NAMES = {
130 [SECRET_KEYS.AZURE_OPENAI]: 'Azure OpenAI',131 [SECRET_KEYS.AZURE_OPENAI]: 'Azure OpenAI',
131 [SECRET_KEYS.ZAI]: 'Z.AI',132 [SECRET_KEYS.ZAI]: 'Z.AI',
132 [SECRET_KEYS.SILICONFLOW]: 'SiliconFlow',133 [SECRET_KEYS.SILICONFLOW]: 'SiliconFlow',
134 [SECRET_KEYS.ELEVENLABS]: 'ElevenLabs TTS',
133};135};
134136
135const INPUT_MAP = {137const INPUT_MAP = {
src/endpoints/secrets.js+1 -0
@@ -64,6 +64,7 @@ export const SECRET_KEYS = {
64 AZURE_OPENAI: 'api_key_azure_openai',64 AZURE_OPENAI: 'api_key_azure_openai',
65 ZAI: 'api_key_zai',65 ZAI: 'api_key_zai',
66 SILICONFLOW: 'api_key_siliconflow',66 SILICONFLOW: 'api_key_siliconflow',
67 ELEVENLABS: 'api_key_elevenlabs',
67};68};
6869
69/**70/**
src/endpoints/speech.js+261 -0
@@ -1,9 +1,13 @@
1import { Buffer } from 'node:buffer';1import { Buffer } from 'node:buffer';
2import fs from 'node:fs';
2import express from 'express';3import express from 'express';
3import wavefile from 'wavefile';4import wavefile from 'wavefile';
4import fetch from 'node-fetch';5import fetch from 'node-fetch';
6import FormData from 'form-data';
7import mime from 'mime-types';
5import { getPipeline } from '../transformers.js';8import { getPipeline } from '../transformers.js';
6import { forwardFetchResponse } from '../util.js';9import { forwardFetchResponse } from '../util.js';
10import { readSecret, SECRET_KEYS } from './secrets.js';
711
8export const router = express.Router();12export const router = express.Router();
913
@@ -138,3 +142,260 @@ pollinations.post('/generate', async (req, res) => {
138});142});
139143
140router.use('/pollinations', pollinations);144router.use('/pollinations', pollinations);
145
146const elevenlabs = express.Router();
147
148elevenlabs.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
176elevenlabs.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
203elevenlabs.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
243elevenlabs.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
271elevenlabs.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
307elevenlabs.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
359elevenlabs.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
401router.use('/elevenlabs', elevenlabs);