From 1629f5296230e887f5efecf0b4bfae11c92ed366 Mon Sep 17 00:00:00 2001 From: Hoan HL Date: Tue, 18 Nov 2025 16:06:09 +0700 Subject: [PATCH] support elevenlabs stt --- lib/routes/api/speech-credentials.js | 15 ++- lib/utils/speech-data/stt-elevenlabs.js | 102 ++++++++++++++++++ lib/utils/speech-data/stt-model-elevenlabs.js | 6 ++ lib/utils/speech-utils.js | 45 +++++++- package-lock.json | 28 ++++- package.json | 1 + test/serve-integration.js | 34 +++--- 7 files changed, 208 insertions(+), 23 deletions(-) create mode 100644 lib/utils/speech-data/stt-elevenlabs.js create mode 100644 lib/utils/speech-data/stt-model-elevenlabs.js diff --git a/lib/routes/api/speech-credentials.js b/lib/routes/api/speech-credentials.js index ead1e2f..f3512a2 100644 --- a/lib/routes/api/speech-credentials.js +++ b/lib/routes/api/speech-credentials.js @@ -18,7 +18,8 @@ const {decryptCredential, testWhisper, testDeepgramTTS, testInworld, testResembleTTS, testHoundifyStt, - testGladiaStt} = require('../../utils/speech-utils'); + testGladiaStt, + testElevenlabsStt} = require('../../utils/speech-utils'); const {DbErrorUnprocessableRequest, DbErrorForbidden, DbErrorBadRequest} = require('../../utils/errors'); const { testGoogleTts, @@ -273,9 +274,11 @@ const encryptCredential = (obj) => { case 'elevenlabs': assert(api_key, 'invalid elevenLabs speech credential: api_key is required'); assert(model_id, 'invalid elevenLabs speech credential: model_id is required'); + assert(stt_model_id, 'invalid elevenLabs speech credential: stt_model_id is required'); const elevenlabsData = JSON.stringify({ api_key, model_id, + stt_model_id, ...(api_uri && {api_uri}), options}); return encrypt(elevenlabsData); @@ -909,6 +912,16 @@ router.get('/:sid/test', async(req, res) => { SpeechCredential.ttsTestResult(sid, false); } } + if (cred.use_for_stt && api_key) { + try { + await testElevenlabsStt(logger, credential); + results.stt.status = 'ok'; + SpeechCredential.sttTestResult(sid, true); + } catch (err) { + results.stt = {status: 'fail', reason: err.message}; + SpeechCredential.sttTestResult(sid, false); + } + } } else if (cred.vendor === 'speechmatics') { const {api_key} = credential; if (cred.use_for_stt) { diff --git a/lib/utils/speech-data/stt-elevenlabs.js b/lib/utils/speech-data/stt-elevenlabs.js new file mode 100644 index 0000000..44d67ee --- /dev/null +++ b/lib/utils/speech-data/stt-elevenlabs.js @@ -0,0 +1,102 @@ +module.exports = [ + // Major languages with ISO 639-1 codes + { name: 'Arabic', value: 'ar' }, + { name: 'Bengali', value: 'bn' }, + { name: 'Bulgarian', value: 'bg' }, + { name: 'Catalan', value: 'ca' }, + { name: 'Chinese (Simplified)', value: 'zh' }, + { name: 'Chinese (Traditional)', value: 'zh-TW' }, + { name: 'Croatian', value: 'hr' }, + { name: 'Czech', value: 'cs' }, + { name: 'Danish', value: 'da' }, + { name: 'Dutch', value: 'nl' }, + { name: 'English', value: 'en' }, + { name: 'English (US)', value: 'en-US' }, + { name: 'English (UK)', value: 'en-GB' }, + { name: 'English (Australia)', value: 'en-AU' }, + { name: 'English (Canada)', value: 'en-CA' }, + { name: 'English (India)', value: 'en-IN' }, + { name: 'Estonian', value: 'et' }, + { name: 'Finnish', value: 'fi' }, + { name: 'French', value: 'fr' }, + { name: 'French (Canada)', value: 'fr-CA' }, + { name: 'German', value: 'de' }, + { name: 'German (Austria)', value: 'de-AT' }, + { name: 'German (Switzerland)', value: 'de-CH' }, + { name: 'Greek', value: 'el' }, + { name: 'Gujarati', value: 'gu' }, + { name: 'Hebrew', value: 'he' }, + { name: 'Hindi', value: 'hi' }, + { name: 'Hungarian', value: 'hu' }, + { name: 'Indonesian', value: 'id' }, + { name: 'Italian', value: 'it' }, + { name: 'Japanese', value: 'ja' }, + { name: 'Kannada', value: 'kn' }, + { name: 'Korean', value: 'ko' }, + { name: 'Latvian', value: 'lv' }, + { name: 'Lithuanian', value: 'lt' }, + { name: 'Malay', value: 'ms' }, + { name: 'Malayalam', value: 'ml' }, + { name: 'Marathi', value: 'mr' }, + { name: 'Norwegian', value: 'no' }, + { name: 'Norwegian Bokmål', value: 'nb' }, + { name: 'Persian', value: 'fa' }, + { name: 'Polish', value: 'pl' }, + { name: 'Portuguese', value: 'pt' }, + { name: 'Portuguese (Brazil)', value: 'pt-BR' }, + { name: 'Portuguese (Portugal)', value: 'pt-PT' }, + { name: 'Punjabi', value: 'pa' }, + { name: 'Romanian', value: 'ro' }, + { name: 'Russian', value: 'ru' }, + { name: 'Serbian', value: 'sr' }, + { name: 'Slovak', value: 'sk' }, + { name: 'Slovenian', value: 'sl' }, + { name: 'Spanish', value: 'es' }, + { name: 'Spanish (Spain)', value: 'es-ES' }, + { name: 'Spanish (Mexico)', value: 'es-MX' }, + { name: 'Spanish (Argentina)', value: 'es-AR' }, + { name: 'Spanish (Colombia)', value: 'es-CO' }, + { name: 'Spanish (Latin America)', value: 'es-419' }, + { name: 'Swahili', value: 'sw' }, + { name: 'Swedish', value: 'sv' }, + { name: 'Tamil', value: 'ta' }, + { name: 'Telugu', value: 'te' }, + { name: 'Thai', value: 'th' }, + { name: 'Turkish', value: 'tr' }, + { name: 'Ukrainian', value: 'uk' }, + { name: 'Urdu', value: 'ur' }, + { name: 'Vietnamese', value: 'vi' }, + { name: 'Welsh', value: 'cy' }, + + // Additional languages with ISO 639-3 codes where needed + { name: 'Afrikaans', value: 'af' }, + { name: 'Albanian', value: 'sq' }, + { name: 'Amharic', value: 'am' }, + { name: 'Armenian', value: 'hy' }, + { name: 'Azerbaijani', value: 'az' }, + { name: 'Basque', value: 'eu' }, + { name: 'Belarusian', value: 'be' }, + { name: 'Bosnian', value: 'bs' }, + { name: 'Burmese', value: 'my' }, + { name: 'Filipino', value: 'fil' }, + { name: 'Galician', value: 'gl' }, + { name: 'Georgian', value: 'ka' }, + { name: 'Icelandic', value: 'is' }, + { name: 'Irish', value: 'ga' }, + { name: 'Kazakh', value: 'kk' }, + { name: 'Khmer', value: 'km' }, + { name: 'Lao', value: 'lo' }, + { name: 'Luxembourgish', value: 'lb' }, + { name: 'Macedonian', value: 'mk' }, + { name: 'Maltese', value: 'mt' }, + { name: 'Mongolian', value: 'mn' }, + { name: 'Nepali', value: 'ne' }, + { name: 'Pashto', value: 'ps' }, + { name: 'Sinhala', value: 'si' }, + { name: 'Somali', value: 'so' }, + { name: 'Sundanese', value: 'su' }, + { name: 'Tagalog', value: 'tl' }, + { name: 'Uzbek', value: 'uz' }, + { name: 'Yiddish', value: 'yi' }, + { name: 'Zulu', value: 'zu' } +]; diff --git a/lib/utils/speech-data/stt-model-elevenlabs.js b/lib/utils/speech-data/stt-model-elevenlabs.js new file mode 100644 index 0000000..5255f21 --- /dev/null +++ b/lib/utils/speech-data/stt-model-elevenlabs.js @@ -0,0 +1,6 @@ +module.exports = [ + { name: 'Scribe_v1', value: 'scribe_v1' }, + { name: 'Scribe_v1_experimental', value: 'scribe_v1_experimental' }, + { name: 'Scribe_v2', value: 'scribe_v2' }, +]; + diff --git a/lib/utils/speech-utils.js b/lib/utils/speech-utils.js index e27eaa2..153c80f 100644 --- a/lib/utils/speech-utils.js +++ b/lib/utils/speech-utils.js @@ -9,6 +9,7 @@ const Houndify = require('houndify'); const { GladiaClient } = require('@gladiaio/sdk'); const {decrypt, obscureKey} = require('./encrypt-decrypt'); const { RealtimeSession } = require('speechmatics'); +const {ElevenLabsClient} = require('@elevenlabs/elevenlabs-js'); const TtsGoogleLanguagesVoices = require('./speech-data/tts-google'); const TtsAwsLanguagesVoices = require('./speech-data/tts-aws'); @@ -52,11 +53,13 @@ const SttVoxistLanguagesVoices = require('./speech-data/stt-voxist'); const SttVerbioLanguagesVoices = require('./speech-data/stt-verbio'); const SttOpenaiLanguagesVoices = require('./speech-data/stt-openai'); const SttGladiaLanguagesVoices = require('./speech-data/stt-gladia'); +const SttElevenlabsLanguagesVoices = require('./speech-data/stt-elevenlabs'); const SttModelOpenai = require('./speech-data/stt-model-openai'); const sttModelDeepgram = require('./speech-data/stt-model-deepgram'); const sttModelCartesia = require('./speech-data/stt-model-cartesia'); +const sttModelElevenlabs = require('./speech-data/stt-model-elevenlabs'); function capitalizeFirst(str) { if (!str) return str; @@ -402,6 +405,42 @@ const testElevenlabs = async(logger, credentials) => { return response.body; }; +const testElevenlabsStt = async(logger, credentials) => { + const {api_key, stt_model_id} = credentials; + + // Validate API key + if (!api_key) { + throw new Error('ElevenLabs API key is required'); + } + + const client = new ElevenLabsClient({ + environment: 'https://api.elevenlabs.io', + apiKey: api_key + }); + + return new Promise(async(resolve, reject) => { + try { + const audioBuffer = fs.readFileSync(`${__dirname}/../../data/test_audio.wav`); + + const result = await client.speechToText.convert({ + file: audioBuffer, + modelId: stt_model_id + }); + + // Check if we got a valid response with text + if (result && result.text && result.text.length > 0) { + logger.debug({result}, 'ElevenLabs STT successful'); + resolve(result); + } else { + reject(new Error('no transcript returned from ElevenLabs STT')); + } + } catch (error) { + logger.info({error}, 'failed to get ElevenLabs STT transcript'); + reject(error); + } + }); +}; + const testPlayHT = async(logger, synthAudio, credentials) => { try { await synthAudio( @@ -849,6 +888,7 @@ function decryptCredential(obj, credential, logger, isObscureKey = true) { obj.model_id = o.model_id; obj.api_uri = o.api_uri; obj.options = o.options; + obj.stt_model_id = o.stt_model_id; } else if ('playht' === obj.vendor) { const o = JSON.parse(decrypt(credential)); obj.api_key = isObscureKey ? obscureKey(o.api_key) : o.api_key; @@ -1218,13 +1258,13 @@ async function getLanguagesVoicesForElevenlabs(credential) { language.voices = voices; } } - return tranform(languages, undefined, models); + return tranform(languages, SttElevenlabsLanguagesVoices, models, sttModelElevenlabs); } else { const voices = TtsElevenlabsLanguagesVoices[0].voices; for (const language of TtsElevenlabsLanguagesVoices) { language.voices = voices; } - return tranform(TtsElevenlabsLanguagesVoices, undefined, TtsModelElevenLabs); + return tranform(TtsElevenlabsLanguagesVoices, SttElevenlabsLanguagesVoices, TtsModelElevenLabs, sttModelElevenlabs); } } @@ -1783,6 +1823,7 @@ module.exports = { testIbmStt, testSonioxStt, testElevenlabs, + testElevenlabsStt, testPlayHT, testRimelabs, testInworld, diff --git a/package-lock.json b/package-lock.json index 449e5f6..b05a9ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@aws-sdk/client-transcribe": "^3.549.0", "@azure/storage-blob": "^12.17.0", "@deepgram/sdk": "^1.21.0", + "@elevenlabs/elevenlabs-js": "^2.24.1", "@gladiaio/sdk": "^0.5.2", "@google-cloud/speech": "^6.5.0", "@google-cloud/storage": "^7.9.0", @@ -3714,6 +3715,20 @@ } } }, + "node_modules/@elevenlabs/elevenlabs-js": { + "version": "2.24.1", + "resolved": "https://registry.npmjs.org/@elevenlabs/elevenlabs-js/-/elevenlabs-js-2.24.1.tgz", + "integrity": "sha512-i6bDExgK9lYne1vLhy85JJ3O8bNi5vPTfcgq8kT3HG4+3rgkUJtg5UP29Mn1KONc4ZOeYUomzxJ820uLkT9z6g==", + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.9", + "node-fetch": "^2.7.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -6404,6 +6419,12 @@ "node": ">= 0.8" } }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "license": "MIT" + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -12444,9 +12465,10 @@ } }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index a811a74..52c86ac 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@aws-sdk/client-transcribe": "^3.549.0", "@azure/storage-blob": "^12.17.0", "@deepgram/sdk": "^1.21.0", + "@elevenlabs/elevenlabs-js": "^2.24.1", "@gladiaio/sdk": "^0.5.2", "@google-cloud/speech": "^6.5.0", "@google-cloud/storage": "^7.9.0", diff --git a/test/serve-integration.js b/test/serve-integration.js index f3d828d..3971b2b 100644 --- a/test/serve-integration.js +++ b/test/serve-integration.js @@ -6,7 +6,7 @@ process.on('SIGINT', async() => { if (stopping) return; stopping = true; console.log('shutting down'); - await stopDocker(); + // await stopDocker(); process.exit(0); }); @@ -84,19 +84,19 @@ const stopDocker = () => { }); }) }; - -startDocker() - .then(createDb) - .then(createSchema) - .then(seedDb) - .then(resetAdminPassword) - .then(generateSipTrace) - .then(() => { - console.log('ready for testing!'); - require('..'); - }) - .catch(async(err) => { - console.error({err}, 'Error running integration test'); - await stopDocker(); - process.exit(-1); - }); +require('..'); +// startDocker() +// .then(createDb) +// .then(createSchema) +// .then(seedDb) +// .then(resetAdminPassword) +// .then(generateSipTrace) +// .then(() => { +// console.log('ready for testing!'); +// require('..'); +// }) +// .catch(async(err) => { +// console.error({err}, 'Error running integration test'); +// await stopDocker(); +// process.exit(-1); +// });