diff --git a/.gitignore b/.gitignore index b6af9e3..91c0275 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Logs logs *.log +run-tests.sh # Runtime data pids diff --git a/lib/auth/index.js b/lib/auth/index.js index d1ba4f5..a169a8a 100644 --- a/lib/auth/index.js +++ b/lib/auth/index.js @@ -11,7 +11,7 @@ const sql = ` function makeStrategy(logger, retrieveKey) { return new Strategy( async function(token, done) { - logger.debug(`validating with token ${token}`); + //logger.debug(`validating with token ${token}`); jwt.verify(token, process.env.JWT_SECRET, async(err, decoded) => { if (err) { if (err.name === 'TokenExpiredError') { diff --git a/lib/routes/api/speech-credentials.js b/lib/routes/api/speech-credentials.js index 630a249..afde1a9 100644 --- a/lib/routes/api/speech-credentials.js +++ b/lib/routes/api/speech-credentials.js @@ -14,7 +14,8 @@ const { testMicrosoftTts, testWellSaidTts, testNuanceStt, - testNuanceTts + testNuanceTts, + testDeepgramStt } = require('../../utils/speech-utils'); const obscureKey = (key) => { @@ -88,6 +89,11 @@ const encryptCredential = (obj) => { const nuanceData = JSON.stringify({client_id, secret}); return encrypt(nuanceData); + case 'deepgram': + assert(api_key, 'invalid deepgram speech credential: api_key is required'); + const deepgramData = JSON.stringify({api_key}); + return encrypt(deepgramData); + default: assert(false, `invalid or missing vendor: ${vendor}`); } @@ -175,6 +181,10 @@ router.get('/', async(req, res) => { obj.client_id = o.client_id; obj.secret = obscureKey(o.secret); } + else if ('deepgram' === obj.vendor) { + const o = JSON.parse(decrypt(credential)); + obj.api_key = obscureKey(o.api_key); + } return obj; })); } catch (err) { @@ -225,6 +235,10 @@ router.get('/:sid', async(req, res) => { obj.client_id = o.client_id; obj.secret = obscureKey(o.secret); } + else if ('deepgram' === obj.vendor) { + const o = JSON.parse(decrypt(credential)); + obj.api_key = obscureKey(o.api_key); + } res.status(200).json(obj); } catch (err) { sysError(logger, res, err); @@ -474,6 +488,19 @@ router.get('/:sid/test', async(req, res) => { } } } + else if (cred.vendor === 'deepgram') { + const {api_key} = credential; + if (cred.use_for_stt) { + try { + await testDeepgramStt(logger, {api_key}); + results.stt.status = 'ok'; + SpeechCredential.sttTestResult(sid, true); + } catch (err) { + results.stt = {status: 'fail', reason: err.message}; + SpeechCredential.sttTestResult(sid, false); + } + } + } res.status(200).json(results); } catch (err) { sysError(logger, res, err); diff --git a/lib/utils/speech-utils.js b/lib/utils/speech-utils.js index b9f5f9b..8f456c2 100644 --- a/lib/utils/speech-utils.js +++ b/lib/utils/speech-utils.js @@ -2,6 +2,7 @@ const ttsGoogle = require('@google-cloud/text-to-speech'); const sttGoogle = require('@google-cloud/speech').v1p1beta1; const Polly = require('aws-sdk/clients/polly'); const AWS = require('aws-sdk'); +const { Deepgram } = require('@deepgram/sdk'); const bent = require('bent'); const fs = require('fs'); @@ -42,6 +43,33 @@ const testGoogleStt = async(logger, credentials) => { } }; +const testDeepgramStt = async(logger, credentials) => { + const {api_key} = credentials; + const deepgram = new Deepgram(api_key); + + const mimetype = 'audio/wav'; + const source = { + buffer: fs.readFileSync(`${__dirname}/../../data/test_audio.wav`), + mimetype: mimetype + }; + + return new Promise((resolve, reject) => { + // Send the audio to Deepgram and get the response + deepgram.transcription + .preRecorded(source, {punctuate: true}) + .then((response) => { + //logger.debug({response}, 'got transcript'); + if (response?.results?.channels[0]?.alternatives?.length > 0) resolve(response); + else reject(new Error('no transcript returned')); + return; + }) + .catch((err) => { + logger.info({err}, 'failed to get deepgram transcript'); + reject(err); + }); + }); +}; + const testAwsTts = (logger, credentials) => { const polly = new Polly(credentials); return new Promise((resolve, reject) => { @@ -138,5 +166,6 @@ module.exports = { testMicrosoftStt, testWellSaidStt, testNuanceTts, - testNuanceStt + testNuanceStt, + testDeepgramStt }; diff --git a/package-lock.json b/package-lock.json index df69bd2..cfacacb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,11 @@ "version": "v0.7.7", "license": "MIT", "dependencies": { + "@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.5.7", + "@jambonz/realtimedb-helpers": "^0.5.9", "@jambonz/time-series": "^0.2.5", "argon2-ffi": "^2.0.0", "aws-sdk": "^2.1152.0", @@ -478,6 +479,16 @@ "node": ">=6.9.0" } }, + "node_modules/@deepgram/sdk": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@deepgram/sdk/-/sdk-1.10.2.tgz", + "integrity": "sha512-7f/Uya1Tu0NBcxYSTbbmDnvAQ+YvzuzipVNa1uwfcMyiQZgBVZv+E7ToJhhC7KRr/tmQjniW29RsPqhOMBN99Q==", + "dependencies": { + "bufferutil": "^4.0.6", + "utf-8-validate": "^5.0.9", + "ws": "^7.5.5" + } + }, "node_modules/@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -676,9 +687,9 @@ } }, "node_modules/@jambonz/realtimedb-helpers": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.5.7.tgz", - "integrity": "sha512-TOTnFWSa4ronCdQTWfB8c5VI6DXcBEyDA4vbZnzkVAzSP90NpRPOPrvo2tEZxcGSlVIjBZew7rWgWyqkSwUT/Q==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.5.9.tgz", + "integrity": "sha512-1DdEL+Zy3vcgJNXeGaiAdIe5k+3NdRdtTikJMiACrKUF1GVpnEjv/NKapr5u9FdOODblJ2bgFjktLpmSsVK/9Q==", "dependencies": { "@google-cloud/text-to-speech": "^4.0.3", "@grpc/grpc-js": "^1.7.3", @@ -1320,6 +1331,18 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, + "node_modules/bufferutil": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", + "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4497,9 +4520,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", - "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", + "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -6404,6 +6427,18 @@ "resolved": "https://registry.npmjs.org/username-sync/-/username-sync-1.0.3.tgz", "integrity": "sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA==" }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -7046,6 +7081,16 @@ "to-fast-properties": "^2.0.0" } }, + "@deepgram/sdk": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@deepgram/sdk/-/sdk-1.10.2.tgz", + "integrity": "sha512-7f/Uya1Tu0NBcxYSTbbmDnvAQ+YvzuzipVNa1uwfcMyiQZgBVZv+E7ToJhhC7KRr/tmQjniW29RsPqhOMBN99Q==", + "requires": { + "bufferutil": "^4.0.6", + "utf-8-validate": "^5.0.9", + "ws": "^7.5.5" + } + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -7203,9 +7248,9 @@ } }, "@jambonz/realtimedb-helpers": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.5.7.tgz", - "integrity": "sha512-TOTnFWSa4ronCdQTWfB8c5VI6DXcBEyDA4vbZnzkVAzSP90NpRPOPrvo2tEZxcGSlVIjBZew7rWgWyqkSwUT/Q==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@jambonz/realtimedb-helpers/-/realtimedb-helpers-0.5.9.tgz", + "integrity": "sha512-1DdEL+Zy3vcgJNXeGaiAdIe5k+3NdRdtTikJMiACrKUF1GVpnEjv/NKapr5u9FdOODblJ2bgFjktLpmSsVK/9Q==", "requires": { "@google-cloud/text-to-speech": "^4.0.3", "@grpc/grpc-js": "^1.7.3", @@ -7728,6 +7773,14 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, + "bufferutil": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", + "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -10136,9 +10189,9 @@ "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, "node-gyp-build": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", - "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==" + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", + "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==" }, "node-object-hash": { "version": "2.3.10", @@ -11600,6 +11653,14 @@ "resolved": "https://registry.npmjs.org/username-sync/-/username-sync-1.0.3.tgz", "integrity": "sha512-m/7/FSqjJNAzF2La448c/aEom0gJy7HY7Y509h6l0ePvEkFictAGptwWaj1msWJ38JbfEDOUoE8kqFee9EHKdA==" }, + "utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/package.json b/package.json index b0dd4ea..3c6440a 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,11 @@ "url": "https://github.com/jambonz/jambonz-api-server.git" }, "dependencies": { + "@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.5.7", + "@jambonz/realtimedb-helpers": "^0.5.9", "@jambonz/time-series": "^0.2.5", "argon2-ffi": "^2.0.0", "aws-sdk": "^2.1152.0", diff --git a/test/speech-credentials.js b/test/speech-credentials.js index 1eaec3d..ca0cf84 100644 --- a/test/speech-credentials.js +++ b/test/speech-credentials.js @@ -135,6 +135,7 @@ test('speech credentials tests', async(t) => { json: true, }); console.log(JSON.stringify(result)); + t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for deepgram'); } /* add a credential for wellsaid */ @@ -159,6 +160,7 @@ test('speech credentials tests', async(t) => { json: true, }); console.log(JSON.stringify(result)); + t.ok(result.statusCode === 200 && result.body.tts.status === 'ok', 'successfully tested speech credential for wellsaid'); /* delete the credential */ result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}`, { @@ -168,6 +170,39 @@ test('speech credentials tests', async(t) => { t.ok(result.statusCode === 204, 'successfully deleted speech credential'); } + /* add a credential for deepgram */ + if (process.env.DEEPGRAM_API_KEY) { + result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + vendor: 'deepgram', + use_for_stt: true, + api_key: process.env.DEEPGRAM_API_KEY + } + }); + t.ok(result.statusCode === 201, 'successfully added speech credential for deepgram'); + const ms_sid = result.body.sid; + + /* test the speech credential */ + result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${ms_sid}/test`, { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + console.log(JSON.stringify(result)); + t.ok(result.statusCode === 200 && result.body.stt.status === 'ok', 'successfully tested speech credential for deepgram'); + + /* delete the credential */ + result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${ms_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();