From 5fb4bd7bd1453f769dc49fad77338ae8e6259adf Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Sat, 25 Mar 2023 22:20:44 +0700 Subject: [PATCH] feat: add nuance on-premise (#131) * feat: add nuance on-premise * feat: update fetch nuance credential * fix: update * fix nuance tts test against on-prem, refactor aws/google tts testing to use speech-utils package --------- Co-authored-by: Quan HL Co-authored-by: Dave Horton --- lib/routes/api/speech-credentials.js | 37 +++++++++++++++++++--------- lib/utils/speech-utils.js | 24 ++++++------------ package-lock.json | 14 +++++------ package.json | 4 +-- test/speech-credentials.js | 27 +++++++++++++++++++- 5 files changed, 66 insertions(+), 40 deletions(-) diff --git a/lib/routes/api/speech-credentials.js b/lib/routes/api/speech-credentials.js index b54b239..98b1743 100644 --- a/lib/routes/api/speech-credentials.js +++ b/lib/routes/api/speech-credentials.js @@ -44,6 +44,8 @@ const encryptCredential = (obj) => { region, client_id, secret, + nuance_tts_uri, + nuance_stt_uri, use_custom_tts, custom_tts_endpoint, use_custom_stt, @@ -97,9 +99,10 @@ const encryptCredential = (obj) => { return encrypt(wsData); case 'nuance': - assert(client_id, 'invalid nuance speech credential: client_id is required'); - assert(secret, 'invalid nuance speech credential: secret is required'); - const nuanceData = JSON.stringify({client_id, secret}); + const checked = (client_id && secret) || (nuance_tts_uri || nuance_stt_uri); + assert(checked, 'invalid nuance speech credential: either entered client id and\ + secret or entered a nuance_tts_uri or nuance_stt_uri'); + const nuanceData = JSON.stringify({client_id, secret, nuance_tts_uri, nuance_stt_uri}); return encrypt(nuanceData); case 'deepgram': @@ -216,7 +219,7 @@ router.get('/', async(req, res) => { else if ('nuance' === obj.vendor) { const o = JSON.parse(decrypt(credential)); obj.client_id = o.client_id; - obj.secret = obscureKey(o.secret); + obj.secret = o.secret ? obscureKey(o.secret) : null; } else if ('deepgram' === obj.vendor) { const o = JSON.parse(decrypt(credential)); @@ -291,7 +294,9 @@ router.get('/:sid', async(req, res) => { else if ('nuance' === obj.vendor) { const o = JSON.parse(decrypt(credential)); obj.client_id = o.client_id; - obj.secret = obscureKey(o.secret); + obj.secret = o.secret ? obscureKey(o.secret) : null; + obj.nuance_tts_uri = o.nuance_tts_uri; + obj.nuance_stt_uri = o.nuance_stt_uri; } else if ('deepgram' === obj.vendor) { const o = JSON.parse(decrypt(credential)); @@ -347,7 +352,8 @@ router.put('/:sid', async(req, res) => { const sid = req.params.sid; const logger = req.app.locals.logger; try { - const {use_for_tts, use_for_stt, region, aws_region, stt_region, tts_region, riva_server_uri} = req.body; + const {use_for_tts, use_for_stt, region, aws_region, stt_region, tts_region, + riva_server_uri, nuance_tts_uri, nuance_stt_uri} = req.body; if (typeof use_for_tts === 'undefined' && typeof use_for_stt === 'undefined') { throw new DbErrorUnprocessableRequest('use_for_tts and use_for_stt are the only updateable fields'); } @@ -383,7 +389,9 @@ router.put('/:sid', async(req, res) => { custom_stt_endpoint, stt_region, tts_region, - riva_server_uri + riva_server_uri, + nuance_stt_uri, + nuance_tts_uri }; logger.info({o, newCred}, 'updating speech credential with this new credential'); obj.credential = encryptCredential(newCred); @@ -435,7 +443,8 @@ router.get('/:sid/test', async(req, res) => { if (cred.use_for_tts) { try { - await testGoogleTts(logger, credential); + const {getTtsVoices} = req.app.locals; + await testGoogleTts(logger, getTtsVoices, credential); results.tts.status = 'ok'; SpeechCredential.ttsTestResult(sid, true); } catch (err) { @@ -457,8 +466,9 @@ router.get('/:sid/test', async(req, res) => { } else if (cred.vendor === 'aws') { if (cred.use_for_tts) { + const {getTtsVoices} = req.app.locals; try { - await testAwsTts(logger, { + await testAwsTts(logger, getTtsVoices, { accessKeyId: credential.access_key_id, secretAccessKey: credential.secret_access_key, region: credential.aws_region || process.env.AWS_REGION @@ -540,13 +550,16 @@ router.get('/:sid/test', async(req, res) => { const { client_id, - secret + secret, + nuance_tts_uri, + nuance_stt_uri } = credential; if (cred.use_for_tts) { try { await testNuanceTts(logger, getTtsVoices, { client_id, - secret + secret, + nuance_tts_uri }); results.tts.status = 'ok'; SpeechCredential.ttsTestResult(sid, true); @@ -561,7 +574,7 @@ router.get('/:sid/test', async(req, res) => { } if (cred.use_for_stt) { try { - await testNuanceStt(logger, {client_id, secret}); + await testNuanceStt(logger, {client_id, secret, nuance_stt_uri}); results.stt.status = 'ok'; SpeechCredential.sttTestResult(sid, true); } catch (err) { diff --git a/lib/utils/speech-utils.js b/lib/utils/speech-utils.js index 1672eff..0c6f8c2 100644 --- a/lib/utils/speech-utils.js +++ b/lib/utils/speech-utils.js @@ -1,6 +1,4 @@ -const ttsGoogle = require('@google-cloud/text-to-speech'); const sttGoogle = require('@google-cloud/speech').v1p1beta1; -const { PollyClient, DescribeVoicesCommand } = require('@aws-sdk/client-polly'); const { TranscribeClient, ListVocabulariesCommand } = require('@aws-sdk/client-transcribe'); const { Deepgram } = require('@deepgram/sdk'); const sdk = require('microsoft-cognitiveservices-speech-sdk'); @@ -34,9 +32,10 @@ const testNuanceStt = async(logger, credentials) => { return true; }; -const testGoogleTts = async(logger, credentials) => { - const client = new ttsGoogle.TextToSpeechClient({credentials}); - await client.listVoices(); +const testGoogleTts = async(logger, getTtsVoices, credentials) => { + const voices = await getTtsVoices({vendor: 'google', credentials}); + return voices; + }; const testGoogleStt = async(logger, credentials) => { @@ -120,19 +119,10 @@ const testMicrosoftStt = async(logger, credentials) => { }); }; -const testAwsTts = async(logger, credentials) => { +const testAwsTts = async(logger, getTtsVoices, credentials) => { try { - const {region, accessKeyId, secretAccessKey} = credentials; - const client = new PollyClient({ - region, - credentials: { - accessKeyId, - secretAccessKey - } - }); - const command = new DescribeVoicesCommand({LanguageCode: 'en-US'}); - const response = await client.send(command); - return response; + const voices = await getTtsVoices({vendor: 'aws', credentials}); + return voices; } catch (err) { logger.info({err}, 'testMicrosoftTts - failed to list voices for region ${region}'); throw err; diff --git a/package-lock.json b/package-lock.json index f379c5d..7036929 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@google-cloud/text-to-speech": "^4.0.3", "@jambonz/db-helpers": "^0.7.3", "@jambonz/realtimedb-helpers": "^0.7.0", - "@jambonz/speech-utils": "^0.0.6", + "@jambonz/speech-utils": "^0.0.8", "@jambonz/time-series": "^0.2.5", "@jambonz/verb-specifications": "^0.0.3", "@soniox/soniox-node": "^1.1.0", @@ -1721,9 +1721,9 @@ } }, "node_modules/@jambonz/speech-utils": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.0.6.tgz", - "integrity": "sha512-cZckiUe2tKUB93aXjvZAXSeyuMbLp/fvT4GQqRh5u7BriA/CS2r+hTFMxzSpboNlnni30M/pM0/Jjta6gzFPgw==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.0.8.tgz", + "integrity": "sha512-OuJUk2D1JT1oZlWS7mCFWY3VbNS34MpIRRjwqm/wKTSuzfpvKezXHp4/9eddmuP+mjAC2OwFxphTn08yiApvHQ==", "dependencies": { "@aws-sdk/client-polly": "^3.276.0", "@google-cloud/text-to-speech": "^4.2.0", @@ -10227,9 +10227,9 @@ } }, "@jambonz/speech-utils": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.0.6.tgz", - "integrity": "sha512-cZckiUe2tKUB93aXjvZAXSeyuMbLp/fvT4GQqRh5u7BriA/CS2r+hTFMxzSpboNlnni30M/pM0/Jjta6gzFPgw==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.0.8.tgz", + "integrity": "sha512-OuJUk2D1JT1oZlWS7mCFWY3VbNS34MpIRRjwqm/wKTSuzfpvKezXHp4/9eddmuP+mjAC2OwFxphTn08yiApvHQ==", "requires": { "@aws-sdk/client-polly": "^3.276.0", "@google-cloud/text-to-speech": "^4.2.0", diff --git a/package.json b/package.json index d4b5297..226a5b7 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,12 @@ "url": "https://github.com/jambonz/jambonz-api-server.git" }, "dependencies": { - "@aws-sdk/client-polly": "^3.290.0", "@aws-sdk/client-transcribe": "^3.290.0", "@deepgram/sdk": "^1.10.2", "@google-cloud/speech": "^5.1.0", - "@google-cloud/text-to-speech": "^4.0.3", "@jambonz/db-helpers": "^0.7.3", "@jambonz/realtimedb-helpers": "^0.7.0", - "@jambonz/speech-utils": "^0.0.6", + "@jambonz/speech-utils": "^0.0.8", "@jambonz/time-series": "^0.2.5", "@jambonz/verb-specifications": "^0.0.3", "@soniox/soniox-node": "^1.1.0", diff --git a/test/speech-credentials.js b/test/speech-credentials.js index 42f3058..b7b08b6 100644 --- a/test/speech-credentials.js +++ b/test/speech-credentials.js @@ -392,9 +392,34 @@ test('speech credentials tests', async(t) => { }); t.ok(result.statusCode === 204, 'successfully deleted speech credential'); + /* add a credential for nuance */ + result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + vendor: 'nuance', + use_for_stt: true, + use_for_tts: true, + client_id: 'client_id', + secret: 'secret', + nuance_tts_uri: "192.168.1.2:5060", + nuance_stt_uri: "192.168.1.2:5061" + } + }); + t.ok(result.statusCode === 201, 'successfully added speech credential for nuance'); + const nuance_sid = result.body.sid; + + /* delete the credential */ + result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${nuance_sid}`, { + auth: authUser, + resolveWithFullResponse: true, + }); + t.ok(result.statusCode === 204, 'successfully deleted speech credential'); + await deleteObjectBySid(request, '/Accounts', account_sid); await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid); - //t.end(); + t.end(); } catch (err) { console.error(err);