Compare commits

...

51 Commits

Author SHA1 Message Date
Dave Horton
45045fd39b improve deprecated syntax 2023-04-10 09:12:41 -04:00
Dave Horton
92da5518eb try 5 2023-04-10 09:08:54 -04:00
Dave Horton
1126ea827f try 4 2023-04-10 09:02:39 -04:00
Dave Horton
fcc4c0a5ce try 3 2023-04-10 09:00:19 -04:00
Dave Horton
ba3ff42987 try 2 2023-04-10 08:56:44 -04:00
Dave Horton
765311c491 interpolate tags 2023-04-10 08:53:57 -04:00
Dave Horton
dcd8b378b2 use docker/build-push-action@v4 2023-04-10 08:49:44 -04:00
Quan HL
552a4e9fd1 feat: upload docker image to docker hub 2023-04-10 15:12:50 +07:00
Snyk bot
04003a709e fix: package.json & package-lock.json to reduce vulnerabilities (#305)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-XML2JS-5414874
2023-04-09 12:41:47 -04:00
Dave Horton
565ee609ef based on more testing default google to command_and_search for gather and latest_long for transcribe 2023-04-07 07:34:03 -04:00
Vinod Dharashive
9587465e85 Support for Cisco NBR for Agentassist (#303)
* NBR Support

* NBR Support

* NBR Support

* re-invite Ok sdp should be sendonly

* NBR Support

* sendrecv sdp correction

* Update siprec-utils.js

* Updated comments

* Siprec participants details added to hook

* Bugfix siprec

* Update call-info.js
2023-04-07 07:33:24 -04:00
Dave Horton
845d80a23d change population of test data 2023-04-05 13:10:01 -04:00
Hoan Luu Huu
3109db7861 feat: update stat collector version (#302) 2023-04-05 12:02:41 -04:00
Hoan Luu Huu
11c5047465 fix: Re-invite sip rec does not update media (#300)
* fix: Re-invite sip rec does not update media

* fix: Re-invite sip rec does not update media
2023-04-05 09:46:32 -04:00
Dave Horton
e19ea629f0 response to siprec invite should have a:recvonly if offer had a:sendonly (#298) 2023-04-04 21:02:21 -04:00
Antony Jukes
fe529c6bfb removed incorrect "this" from this.target.auth as it actually a local const. (#296) 2023-04-03 11:13:12 -04:00
Dave Horton
e980b82ec4 update to speech utils with improved microsoft tts 2023-04-01 13:20:59 -04:00
Hoan Luu Huu
318ca19791 fix: update speech utils version (#295)
* fix: update speech utils version

* update package-lock.json

---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-04-01 11:35:13 -04:00
Dave Horton
e2bd211346 update to latest speech-utils 2023-03-31 16:50:46 -04:00
Dave Horton
410c07fae6 further fix for google model 2023-03-31 12:37:04 -04:00
Dave Horton
2ebfbfb3d8 google STT: when altLanguges are used default to a model that supports it 2023-03-31 12:31:14 -04:00
Dave Horton
a29795839d Bugfix/bot mode restart (#292)
* restart background gather if we get a new config with bargein=enable and changes to input types

* stop background gather properly before restarting

* fix: sticky background gather tasks must not be restarted if we have a new background gather

* fix undefined reference

* safety
2023-03-31 09:35:23 -04:00
Hoan Luu Huu
28088a4cdd feat: queue play audio (#290)
* feat: queue play audio

* fix: typo

* fix: typo

* fix: typo

* fix: typo

* fix: typo

* fix: typo

* fix: typo

* fix: typo

* fix: typo

* fix: typo
2023-03-30 15:31:54 -04:00
Dave Horton
afb381eec9 bugfix: setting altLanguages on Azure once left it turned on 2023-03-29 08:49:34 -04:00
Dave Horton
ed00ccb681 bump version 2023-03-28 14:14:25 -04:00
Dave Horton
6e945dde9a google stt fixes, including defaulting to phone_call model based on c… (#288)
* google stt fixes, including defaulting to phone_call model based on comparative model testing

* lint error
2023-03-28 10:02:03 -04:00
Dave Horton
efdea3e514 gather defaults to multiple utterances 2023-03-27 15:53:01 -04:00
Dave Horton
5131d524ce bugfix: allow for empty transcripts that nuance returns 2023-03-27 14:13:50 -04:00
Anton Voylenko
c0114015ea check encryption env on start (#286) 2023-03-26 15:45:20 -04:00
Anton Voylenko
a293ec09d0 add ENCRYPTION_SECRET variable (#283)
* add ENCRYPTION_SECRET variable

* add env for tests
2023-03-26 14:52:58 -04:00
Dave Horton
f71ae83ce4 bugfix: nuance on-prem stt 2023-03-26 14:26:36 -04:00
Hoan Luu Huu
0dd161913c fix: gather task should clear dtmf event before resolve (#284)
Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-03-26 12:32:51 -04:00
Dave Horton
63ab554908 google STT: default to command_and_search for Gather, as latest_short seems to have issues, various other fixes (#285) 2023-03-26 12:20:03 -04:00
Dave Horton
e1bd075ebc support for nuance on-prem stt/tts 2023-03-25 12:08:54 -04:00
Dave Horton
9de89258a1 update speech-utils@0.0.8 2023-03-24 14:50:08 -04:00
Dave Horton
145ed488db make the feature committed in dd4d9aa enabled only if JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS is set, as it is a behavior change 2023-03-23 07:54:39 -04:00
Dave Horton
c06a43adfa Gather: bugfix for alternate languages with Azure 2023-03-22 14:32:25 -04:00
Dave Horton
bebc82d194 bugfix: gather with google STT does not need to restart transcribing after end of utterance 2023-03-21 15:46:00 -04:00
Dave Horton
cdc82e99ff add minor logging 2023-03-21 12:35:02 -04:00
Dave Horton
dd4d9aa261 Gather: if an empty array of hints are supplied, this signals we should mask global hints for this collection 2023-03-21 12:16:12 -04:00
Dave Horton
1dcf9ee5a2 update to speech-utils@0.0.6 2023-03-21 08:27:25 -04:00
Dave Horton
4b28db0946 update to speech-utils@.0.0.5 2023-03-21 08:00:52 -04:00
Dave Horton
e7ff76b938 update to speech-utils with AWS tts bugfix 2023-03-20 15:35:20 -04:00
Dave Horton
f245275983 gather: remove duplicate and null hints, restart timeout on interim transcripts 2023-03-20 15:34:55 -04:00
Dave Horton
690deed89d prune unused logging 2023-03-19 12:04:02 -04:00
Dave Horton
26053ec709 update speech-utils with support for more audio formats for custom tts 2023-03-15 09:14:41 -04:00
Dave Horton
34e8203338 update to realtime-dbhelpers that factored out speech-utils 2023-03-14 10:07:29 -04:00
Hoan Luu Huu
7be3c64116 feat: update speech-ultil version 1.0.1 (#275)
* feat: update speech-ultil version 1.0.1

* feat: update speech-ultil version 1.0.1

* more fixes for custom stt

* more fixes

* fixes

* update drachtio-fsmrf

* pass url to mod_jambonz_transcribe

* transcription utils: handle custom results

* handle custom speech vendor errors

* add support for hints to custom speech

* change to custom speech options

* send hints as an array for custom speech

* update latest speech-utils

* transcribe: changes to support soniox

* bugfix: soniox transcribe

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-03-12 19:38:36 -04:00
Hoan Luu Huu
f71d3aed8b feat: forward PAI from inbound call to dial outbound call (#280)
* feat: forward PAI from inbound call to dial outbound call

* fix: review comment

---------

Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-03-09 08:58:19 -05:00
Hoan Luu Huu
5ab24337b2 fix: use TTS_FAILURE alert type for synthAudio (#278)
Co-authored-by: Quan HL <quanluuhoang8@gmail.com>
2023-03-08 07:42:06 -05:00
Dave Horton
2af76d94a6 bugfix: repeated ws failures should stop eventually 2023-03-07 16:29:00 -05:00
25 changed files with 2642 additions and 513 deletions

View File

@@ -2,13 +2,8 @@ name: Docker
on: on:
push: push:
# Publish `main` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags: tags:
- v* - '*'
env: env:
IMAGE_NAME: feature-server IMAGE_NAME: feature-server
@@ -20,32 +15,41 @@ jobs:
if: github.event_name == 'push' if: github.event_name == 'push'
steps: steps:
- uses: actions/checkout@v3 - name: Checkout code
uses: actions/checkout@v3
- name: Build image - name: prepare tag
run: docker build . --file Dockerfile --tag $IMAGE_NAME id: prepare_tag
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
run: | run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME IMAGE_ID=jambonz/$IMAGE_NAME
# Change all uppercase to lowercase # Strip git ref prefix from version
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip git ref prefix from version # Strip "v" prefix from tag name
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Strip "v" prefix from tag name # Use Docker `latest` tag convention
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') [ "$VERSION" == "main" ] && VERSION=latest
# Use Docker `latest` tag convention echo IMAGE_ID=$IMAGE_ID
[ "$VERSION" == "main" ] && VERSION=latest echo VERSION=$VERSION
echo IMAGE_ID=$IMAGE_ID echo "{image_id}={$IMAGE_ID}" >> $GITHUB_OUTPUT
echo VERSION=$VERSION echo "{version}={$VERSION}" >> $GITHUB_OUTPUT
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - name: Login to Docker Hub
docker push $IMAGE_ID:$VERSION uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.prepare_tag.outputs.image_id }}:${{ steps.prepare_tag.outputs.version }}
build-args: |
GITHUB_REPOSITORY=$GITHUB_REPOSITORY
GITHUB_REF=$GITHUB_REF

View File

@@ -18,6 +18,7 @@ Configuration is provided via environment variables:
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes| |DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|DRACHTIO_SECRET| shared secret|yes| |DRACHTIO_SECRET| shared secret|yes|
|ENABLE_METRICS| if 1, metrics will be generated|no| |ENABLE_METRICS| if 1, metrics will be generated|no|
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes|
|GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes| |GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes|
|HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes| |HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes|
|JAMBONES_GATHER_EARLY_HINTS_MATCH| if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency |no| |JAMBONES_GATHER_EARLY_HINTS_MATCH| if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency |no|

1
app.js
View File

@@ -8,6 +8,7 @@ assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var'); assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var'); assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var'); assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var');
assert.ok(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET, 'missing ENCRYPTION_SECRET env var');
const Srf = require('drachtio-srf'); const Srf = require('drachtio-srf');
const srf = new Srf(); const srf = new Srf();

View File

@@ -104,7 +104,7 @@ router.post('/', async(req, res) => {
proxy: `sip:${sbcAddress}`, proxy: `sip:${sbcAddress}`,
localSdp: ep.local.sdp localSdp: ep.local.sdp
}); });
if (target.auth) opts.auth = this.target.auth; if (target.auth) opts.auth = target.auth;
/** /**

View File

@@ -27,7 +27,11 @@ module.exports = function(srf, logger) {
function initLocals(req, res, next) { function initLocals(req, res, next) {
const callId = req.get('Call-ID'); const callId = req.get('Call-ID');
logger.info({callId}, 'new incoming call'); logger.info({
callId,
callingNumber: req.callingNumber,
calledNumber: req.calledNumber
}, 'new incoming call');
if (!req.has('X-Account-Sid')) { if (!req.has('X-Account-Sid')) {
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header'); logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
return res.send(500); return res.send(500);
@@ -42,7 +46,16 @@ module.exports = function(srf, logger) {
} }
if (req.has('X-Authenticated-User')) req.locals.originatingUser = req.get('X-Authenticated-User'); if (req.has('X-Authenticated-User')) req.locals.originatingUser = req.get('X-Authenticated-User');
if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN'); if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN');
if (req.has('X-Cisco-Recording-Participant')) {
const ciscoParticipants = req.get('X-Cisco-Recording-Participant');
const regex = /sip:[\d]+@[\d]+\.[\d]+\.[\d]+\.[\d]+/g;
const sipURIs = ciscoParticipants.match(regex);
logger.info(`X-Cisco-Recording-Participant : ${sipURIs} `);
if (sipURIs && sipURIs.length > 0) {
req.locals.calledNumber = sipURIs[0];
req.locals.callingNumber = sipURIs[1];
}
}
next(); next();
} }
@@ -90,8 +103,10 @@ module.exports = function(srf, logger) {
.find((p) => p.type === 'application/sdp') .find((p) => p.type === 'application/sdp')
.content; .content;
const {sdp1, sdp2, ...metadata} = await parseSiprecPayload(req, logger); const {sdp1, sdp2, ...metadata} = await parseSiprecPayload(req, logger);
req.locals.calledNumber = metadata.caller.number; if (!req.locals.calledNumber && !req.locals.calledNumber) {
req.locals.callingNumber = metadata.callee.number; req.locals.calledNumber = metadata.caller.number;
req.locals.callingNumber = metadata.callee.number;
}
req.locals = { req.locals = {
...req.locals, ...req.locals,
siprec: { siprec: {

View File

@@ -33,6 +33,23 @@ class CallInfo {
this.callStatus = CallStatus.Trying; this.callStatus = CallStatus.Trying;
this.originatingSipIp = req.get('X-Forwarded-For'); this.originatingSipIp = req.get('X-Forwarded-For');
this.originatingSipTrunkName = req.get('X-Originating-Carrier'); this.originatingSipTrunkName = req.get('X-Originating-Carrier');
const {siprec} = req.locals;
if (siprec) {
const caller = parseUri(req.locals.callingNumber);
const callee = parseUri(req.locals.calledNumber);
this.participants = [
{
participant: 'caller',
uriUser: caller.user,
uriHost: caller.host
},
{
participant: 'callee',
uriUser: callee.user,
uriHost: callee.host
}
];
}
} }
else if (opts.parentCallInfo) { else if (opts.parentCallInfo) {
// outbound call that is a child of an existing call // outbound call that is a child of an existing call

View File

@@ -511,12 +511,24 @@ class CallSession extends Emitter {
async enableBotMode(gather, autoEnable) { async enableBotMode(gather, autoEnable) {
try { try {
if (this.backgroundGatherTask) {
this.logger.info('CallSession:enableBotMode - bot mode currently enabled, ignoring request to start again');
return;
}
const t = normalizeJambones(this.logger, [gather]); const t = normalizeJambones(this.logger, [gather]);
this.backgroundGatherTask = makeTask(this.logger, t[0]); const task = makeTask(this.logger, t[0]);
if (this.isBotModeEnabled) {
const currInput = this.backgroundGatherTask.input;
const newInput = task.input;
if (JSON.stringify(currInput) === JSON.stringify(newInput)) {
this.logger.info('CallSession:enableBotMode - bot mode currently enabled, ignoring request to start again');
return;
}
else {
this.logger.info({currInput, newInput},
'CallSession:enableBotMode - restarting background gather to apply new input type');
this.backgroundGatherTask.sticky = false;
this.disableBotMode();
}
}
this.backgroundGatherTask = task;
this._bargeInEnabled = true; this._bargeInEnabled = true;
this.backgroundGatherTask this.backgroundGatherTask
.once('dtmf', this._clearTasks.bind(this, this.backgroundGatherTask)) .once('dtmf', this._clearTasks.bind(this, this.backgroundGatherTask))
@@ -528,13 +540,15 @@ class CallSession extends Emitter {
const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundGatherTask.summary}`); const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundGatherTask.summary}`);
this.backgroundGatherTask.span = span; this.backgroundGatherTask.span = span;
this.backgroundGatherTask.ctx = ctx; this.backgroundGatherTask.ctx = ctx;
this.backgroundGatherTask.sticky = autoEnable;
this.backgroundGatherTask.exec(this, resources) this.backgroundGatherTask.exec(this, resources)
.then(() => { .then(() => {
this.logger.info('CallSession:enableBotMode: gather completed'); this.logger.info('CallSession:enableBotMode: gather completed');
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners(); this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
this.backgroundGatherTask && this.backgroundGatherTask.span.end(); this.backgroundGatherTask && this.backgroundGatherTask.span.end();
const sticky = this.backgroundGatherTask?.sticky;
this.backgroundGatherTask = null; this.backgroundGatherTask = null;
if (autoEnable && !this.callGone && !this._stopping && this._bargeInEnabled) { if (sticky && !this.callGone && !this._stopping && this._bargeInEnabled) {
this.logger.info('CallSession:enableBotMode: restarting background gather'); this.logger.info('CallSession:enableBotMode: restarting background gather');
setImmediate(() => this.enableBotMode(gather, true)); setImmediate(() => this.enableBotMode(gather, true));
} }
@@ -636,7 +650,9 @@ class CallSession extends Emitter {
return { return {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
client_id: credential.client_id, client_id: credential.client_id,
secret: credential.secret secret: credential.secret,
nuance_tts_uri: credential.nuance_tts_uri,
nuance_stt_uri: credential.nuance_stt_uri
}; };
} }
else if ('deepgram' === vendor) { else if ('deepgram' === vendor) {
@@ -660,6 +676,14 @@ class CallSession extends Emitter {
stt_region: credential.stt_region stt_region: credential.stt_region
}; };
} }
else if (vendor.startsWith('custom:')) {
return {
speech_credential_sid: credential.speech_credential_sid,
auth_token: credential.auth_token,
custom_stt_url: credential.custom_stt_url,
custom_tts_url: credential.custom_tts_url
};
}
} }
else { else {
writeAlerts({ writeAlerts({
@@ -690,7 +714,7 @@ class CallSession extends Emitter {
let skip = false; let skip = false;
this.currentTask = task; this.currentTask = task;
if (TaskName.Gather === task.name && this.isBotModeEnabled) { if (TaskName.Gather === task.name && this.isBotModeEnabled) {
if (this.backgroundGatherTask.updateTaskInProgress(task)) { if (this.backgroundGatherTask.updateTaskInProgress(task) !== false) {
this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`);
skip = true; skip = true;
} }
@@ -754,7 +778,6 @@ class CallSession extends Emitter {
trackTmpFile(path) { trackTmpFile(path) {
// TODO: don't add if its already in the list (should we make it a set?) // TODO: don't add if its already in the list (should we make it a set?)
this.logger.debug(`adding tmp file to track ${path}`);
this.tmpFiles.add(path); this.tmpFiles.add(path);
} }
@@ -1129,14 +1152,14 @@ class CallSession extends Emitter {
_injectTasks(newTasks) { _injectTasks(newTasks) {
const gatherPos = this.tasks.map((t) => t.name).indexOf(TaskName.Gather); const gatherPos = this.tasks.map((t) => t.name).indexOf(TaskName.Gather);
const currentlyExecutingGather = this.currentTask?.name === TaskName.Gather; const currentlyExecutingGather = this.currentTask?.name === TaskName.Gather;
/*
this.logger.debug({ this.logger.debug({
currentTaskList: listTaskNames(this.tasks), currentTaskList: listTaskNames(this.tasks),
newContent: listTaskNames(newTasks), newContent: listTaskNames(newTasks),
currentlyExecutingGather, currentlyExecutingGather,
gatherPos gatherPos
}, 'CallSession:_injectTasks - starting'); }, 'CallSession:_injectTasks - starting');
*/
const killGather = () => { const killGather = () => {
this.logger.debug('CallSession:_injectTasks - killing current gather because we have new content'); this.logger.debug('CallSession:_injectTasks - killing current gather because we have new content');
this.currentTask.kill(this); this.currentTask.kill(this);
@@ -1145,10 +1168,11 @@ class CallSession extends Emitter {
if (-1 === gatherPos) { if (-1 === gatherPos) {
/* no gather in the stack simply append tasks */ /* no gather in the stack simply append tasks */
this.tasks.push(...newTasks); this.tasks.push(...newTasks);
/*
this.logger.debug({ this.logger.debug({
updatedTaskList: listTaskNames(this.tasks) updatedTaskList: listTaskNames(this.tasks)
}, 'CallSession:_injectTasks - completed (simple append)'); }, 'CallSession:_injectTasks - completed (simple append)');
*/
/* we do need to kill the current gather if we are executing one */ /* we do need to kill the current gather if we are executing one */
if (currentlyExecutingGather) killGather(); if (currentlyExecutingGather) killGather();
return; return;
@@ -1176,12 +1200,10 @@ class CallSession extends Emitter {
this.replaceApplication(t); this.replaceApplication(t);
} }
else if (process.env.JAMBONES_INJECT_CONTENT) { else if (process.env.JAMBONES_INJECT_CONTENT) {
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks (injecting content)');
this._injectTasks(t); this._injectTasks(t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list'); this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
} }
else { else {
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks');
this.tasks.push(...t); this.tasks.push(...t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list'); this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
} }
@@ -1225,7 +1247,7 @@ class CallSession extends Emitter {
this.logger.info(`CallSession:_onCommand - invalid command ${command}`); this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
} }
if (this.wakeupResolver) { if (this.wakeupResolver) {
this.logger.debug({resolution}, 'CallSession:_onCommand - got commands, waking up..'); //this.logger.debug({resolution}, 'CallSession:_onCommand - got commands, waking up..');
this.wakeupResolver(resolution); this.wakeupResolver(resolution);
this.wakeupResolver = null; this.wakeupResolver = null;
} }

View File

@@ -1,6 +1,7 @@
const InboundCallSession = require('./inbound-call-session'); const InboundCallSession = require('./inbound-call-session');
const {createSipRecPayload} = require('../utils/siprec-utils'); const {createSipRecPayload} = require('../utils/siprec-utils');
const {CallStatus} = require('../utils/constants'); const {CallStatus} = require('../utils/constants');
const {parseSiprecPayload} = require('../utils/siprec-utils');
/** /**
* @classdesc Subclass of InboundCallSession. This represents a CallSession that is * @classdesc Subclass of InboundCallSession. This represents a CallSession that is
* established for an inbound SIPREC call. * established for an inbound SIPREC call.
@@ -16,6 +17,32 @@ class SipRecCallSession extends InboundCallSession {
this.metadata = metadata; this.metadata = metadata;
} }
async _onReinvite(req, res) {
try {
this.logger.info(req.payload, 'SipRec Re-INVITE payload');
const {sdp1: reSdp1, sdp2: reSdp2, metadata: reMetadata} = await parseSiprecPayload(req, this.logger);
this.sdp1 = reSdp1;
this.sdp2 = reSdp2;
this.metadata = reMetadata;
if (this.ep && this.ep2) {
let remoteSdp = this.sdp1.replace(/sendonly/, 'sendrecv');
const newSdp1 = await this.ep.modify(remoteSdp);
remoteSdp = this.sdp2.replace(/sendonly/, 'sendrecv');
const newSdp2 = await this.ep2.modify(remoteSdp);
const combinedSdp = await createSipRecPayload(newSdp1, newSdp2, this.logger);
res.send(200, {body: combinedSdp});
this.logger.info({offer: req.body, answer: combinedSdp}, 'SipRec handling reINVITE');
}
else {
this.logger.info('got SipRec reINVITE but no endpoint and media has not been released');
res.send(488);
}
} catch (err) {
this.logger.error(err, 'Error handling reinvite');
}
}
async answerSipRecCall() { async answerSipRecCall() {
try { try {
this.ms = this.getMS(); this.ms = this.getMS();

View File

@@ -400,15 +400,19 @@ class TaskDial extends Task {
let fqdn; let fqdn;
if (!sbcAddress) throw new Error('no SBC found for outbound call'); if (!sbcAddress) throw new Error('no SBC found for outbound call');
this.headers = {
'X-Account-Sid': cs.accountSid,
...(req && req.has('X-CID') && {'X-CID': req.get('X-CID')}),
...(req && req.has('P-Asserted-Identity') && {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
// Put headers at the end to make sure opt.headers override all default behavior.
...this.headers
};
const opts = { const opts = {
headers: req && req.has('X-CID') ? Object.assign(this.headers, {'X-CID': req.get('X-CID')}) : this.headers, headers: this.headers,
proxy: `sip:${sbcAddress}`, proxy: `sip:${sbcAddress}`,
callingNumber: this.callerId || req.callingNumber callingNumber: this.callerId || req.callingNumber
}; };
opts.headers = {
...opts.headers,
'X-Account-Sid': cs.accountSid
};
const t = this.target.find((t) => t.type === 'teams'); const t = this.target.find((t) => t.type === 'teams');
if (t) { if (t) {

View File

@@ -9,7 +9,8 @@ const {
DeepgramTranscriptionEvents, DeepgramTranscriptionEvents,
SonioxTranscriptionEvents, SonioxTranscriptionEvents,
IbmTranscriptionEvents, IbmTranscriptionEvents,
NvidiaTranscriptionEvents NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('../utils/constants'); } = require('../utils/constants');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
@@ -53,7 +54,7 @@ class TaskGather extends Task {
/* timeout of zero means no timeout */ /* timeout of zero means no timeout */
this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000; this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000;
this.interim = !!this.partialResultHook || this.bargein; this.interim = !!this.partialResultHook || this.bargein || (this.timeout > 0);
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true; this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
this.minBargeinWordCount = this.data.minBargeinWordCount || 1; this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
if (this.data.recognizer) { if (this.data.recognizer) {
@@ -69,6 +70,11 @@ class TaskGather extends Task {
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit; if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit;
this.isContinuousAsr = this.asrTimeout > 0; this.isContinuousAsr = this.asrTimeout > 0;
if (Array.isArray(this.data.recognizer.hints) &&
0 == this.data.recognizer.hints.length && process.env.JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS) {
logger.debug('Gather: an empty hints array was supplied, so we will mask global hints');
this.maskGlobalSttHints = true;
}
this.data.recognizer.hints = this.data.recognizer.hints || []; this.data.recognizer.hints = this.data.recognizer.hints || [];
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || []; this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || [];
} }
@@ -92,12 +98,17 @@ class TaskGather extends Task {
this._sonioxTranscripts = []; this._sonioxTranscripts = [];
this.parentTask = parentTask; this.parentTask = parentTask;
this.partialTranscriptsCount = 0;
} }
get name() { return TaskName.Gather; } get name() { return TaskName.Gather; }
get needsStt() { return this.input.includes('speech'); } get needsStt() { return this.input.includes('speech'); }
get wantsSingleUtterance() {
return this.data.recognizer?.singleUtterance === true;
}
get earlyMedia() { get earlyMedia() {
return (this.sayTask && this.sayTask.earlyMedia) || return (this.sayTask && this.sayTask.earlyMedia) ||
(this.playTask && this.playTask.earlyMedia); (this.playTask && this.playTask.earlyMedia);
@@ -119,14 +130,17 @@ class TaskGather extends Task {
} }
async exec(cs, {ep}) { async exec(cs, {ep}) {
this.logger.debug('Gather:exec'); this.logger.debug({options: this.data}, 'Gather:exec');
await super.exec(cs); await super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers; const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
if (cs.hasGlobalSttHints) { if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints; const {hints, hintsBoost} = cs.globalSttHints;
this.data.recognizer.hints = this.data.recognizer.hints.concat(hints); const setOfHints = new Set(this.data.recognizer.hints
.concat(hints)
.filter((h) => typeof h === 'string' && h.length > 0));
this.data.recognizer.hints = [...setOfHints];
if (!this.data.recognizer.hintsBoost && hintsBoost) this.data.recognizer.hintsBoost = hintsBoost; if (!this.data.recognizer.hintsBoost && hintsBoost) this.data.recognizer.hintsBoost = hintsBoost;
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost}, this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
'Gather:exec - applying global sttHints'); 'Gather:exec - applying global sttHints');
@@ -187,7 +201,6 @@ class TaskGather extends Task {
throw new Error(`No speech-to-text service credentials for ${this.vendor} have been configured`); throw new Error(`No speech-to-text service credentials for ${this.vendor} have been configured`);
} }
this.logger.info({sttCredentials: this.sttCredentials}, 'Gather:exec - sttCredentials');
if (this.vendor === 'nuance' && this.sttCredentials.client_id) { if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
/* get nuance access token */ /* get nuance access token */
const {client_id, secret} = this.sttCredentials; const {client_id, secret} = this.sttCredentials;
@@ -206,7 +219,6 @@ class TaskGather extends Task {
this._startTimer(); this._startTimer();
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer(); if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
if (this.input.includes('speech') && !this.listenDuringPrompt) { if (this.input.includes('speech') && !this.listenDuringPrompt) {
this.logger.debug('Gather:exec - calling _initSpeech');
this._initSpeech(cs, ep) this._initSpeech(cs, ep)
.then(() => { .then(() => {
if (this.killed) { if (this.killed) {
@@ -308,6 +320,7 @@ class TaskGather extends Task {
const {timeout} = opts; const {timeout} = opts;
this.timeout = timeout; this.timeout = timeout;
this._startTimer(); this._startTimer();
return true;
} }
_onDtmf(cs, ep, evt) { _onDtmf(cs, ep, evt) {
@@ -347,7 +360,6 @@ class TaskGather extends Task {
async _initSpeech(cs, ep) { async _initSpeech(cs, ep) {
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer); const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
this.logger.debug(opts, 'TaskGather:_initSpeech - channel vars');
switch (this.vendor) { switch (this.vendor) {
case 'google': case 'google':
this.bugname = 'google_transcribe'; this.bugname = 'google_transcribe';
@@ -379,8 +391,6 @@ class TaskGather extends Task {
this._onTranscriptionComplete.bind(this, cs, ep)); this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NuanceTranscriptionEvents.VadDetected, ep.addCustomEventListener(NuanceTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep)); this._onVadDetected.bind(this, cs, ep));
ep.addCustomEventListener(NuanceTranscriptionEvents.Error,
this._onNuanceError.bind(this, cs, ep));
/* stall timers until prompt finishes playing */ /* stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) { if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
@@ -399,8 +409,6 @@ class TaskGather extends Task {
case 'soniox': case 'soniox':
this.bugname = 'soniox_transcribe'; this.bugname = 'soniox_transcribe';
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(SonioxTranscriptionEvents.Error,
this._onSonioxError.bind(this, cs, ep));
break; break;
case 'ibm': case 'ibm':
@@ -409,8 +417,6 @@ class TaskGather extends Task {
ep.addCustomEventListener(IbmTranscriptionEvents.Connect, this._onIbmConnect.bind(this, cs, ep)); ep.addCustomEventListener(IbmTranscriptionEvents.Connect, this._onIbmConnect.bind(this, cs, ep));
ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure, ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
this._onIbmConnectFailure.bind(this, cs, ep)); this._onIbmConnectFailure.bind(this, cs, ep));
ep.addCustomEventListener(IbmTranscriptionEvents.Error,
this._onIbmError.bind(this, cs, ep));
break; break;
case 'nvidia': case 'nvidia':
@@ -423,8 +429,6 @@ class TaskGather extends Task {
this._onTranscriptionComplete.bind(this, cs, ep)); this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected, ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep)); this._onVadDetected.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.Error,
this._onNvidiaError.bind(this, cs, ep));
/* I think nvidia has this (??) - stall timers until prompt finishes playing */ /* I think nvidia has this (??) - stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) { if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
@@ -433,11 +437,23 @@ class TaskGather extends Task {
break; break;
default: default:
this.notifyError({ msg: 'ASR error', details:`Invalid vendor ${this.vendor}`}); if (this.vendor.startsWith('custom:')) {
this.notifyTaskDone(); this.bugname = `${this.vendor}_transcribe`;
throw new Error(`Invalid vendor ${this.vendor}`); ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(JambonzTranscriptionEvents.Connect, this._onJambonzConnect.bind(this, cs, ep));
ep.addCustomEventListener(JambonzTranscriptionEvents.ConnectFailure,
this._onJambonzConnectFailure.bind(this, cs, ep));
break;
}
else {
this.notifyError({ msg: 'ASR error', details:`Invalid vendor ${this.vendor}`});
this.notifyTaskDone();
throw new Error(`Invalid vendor ${this.vendor}`);
}
} }
/* common handler for all stt engine errors */
ep.addCustomEventListener(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep));
await ep.set(opts) await ep.set(opts)
.catch((err) => this.logger.info(err, 'Error setting channel variables')); .catch((err) => this.logger.info(err, 'Error setting channel variables'));
} }
@@ -545,8 +561,13 @@ class TaskGather extends Task {
} }
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language); evt = this.normalizeTranscription(evt, this.vendor, 1, this.language);
if (evt.alternatives.length === 0) {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
return;
}
if (this.earlyHintsMatch && evt.is_final === false) { /* fast path: our first partial transcript exactly matches an early hint */
if (this.earlyHintsMatch && evt.is_final === false && this.partialTranscriptsCount++ === 0) {
const transcript = evt.alternatives[0].transcript?.toLowerCase(); const transcript = evt.alternatives[0].transcript?.toLowerCase();
const hints = this.data.recognizer?.hints || []; const hints = this.data.recognizer?.hints || [];
if (hints.find((h) => h.toLowerCase() === transcript)) { if (hints.find((h) => h.toLowerCase() === transcript)) {
@@ -620,6 +641,8 @@ class TaskGather extends Task {
others do not. others do not.
*/ */
//const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD; //const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
this._clearTimer();
this._startTimer();
if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) { if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) {
if (!this.playComplete) { if (!this.playComplete) {
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech'); this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
@@ -648,7 +671,16 @@ class TaskGather extends Task {
this._killAudio(cs); this._killAudio(cs);
} }
if (!this.resolved && !this.killed && !this._bufferedTranscripts.length) { /**
* By default, Gather asks google for multiple utterances.
* The reason is that we can sometimes get an 'end_of_utterance' event without
* getting a transcription. This can happen if someone coughs or mumbles.
* For that reason don't ask for a single utterance and we'll terminate the transcribe operation
* once we get a final transcript.
* However, if the usr has specified a singleUtterance, then we need to restart here
* since we dont have a final transcript yet.
*/
if (!this.resolved && !this.killed && !this._bufferedTranscripts.length && this.wantsSingleUtterance) {
this._startTranscribing(ep); this._startTranscribing(ep);
} }
} }
@@ -662,26 +694,30 @@ class TaskGather extends Task {
_onTranscriptionComplete(cs, ep) { _onTranscriptionComplete(cs, ep) {
this.logger.debug('TaskGather:_onTranscriptionComplete'); this.logger.debug('TaskGather:_onTranscriptionComplete');
} }
_onNuanceError(cs, ep, evt) {
const {code, error, details} = evt;
if (code === 404 && error === 'No speech') {
this.logger.debug({code, error, details}, 'TaskGather:_onNuanceError');
return this._resolve('timeout');
}
this.logger.info({code, error, details}, 'TaskGather:_onNuanceError');
if (code === 413 && error === 'Too much speech') {
return this._resolve('timeout');
}
}
_onSonioxError(cs, ep, evt) {
this.logger.info({evt}, 'TaskGather:_onSonioxError');
}
_onNvidiaError(cs, ep, evt) {
this.logger.info({evt}, 'TaskGather:_onNvidiaError');
}
_onDeepgramConnect(_cs, _ep) { _onDeepgramConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onDeepgramConnect'); this.logger.debug('TaskGather:_onDeepgramConnect');
} }
_onJambonzConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onJambonzConnect');
}
_onJambonzError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskGather:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') {
const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout');
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
}
this.logger.info({evt}, 'TaskGather:_onJambonzError');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
}
_onDeepGramConnectFailure(cs, _ep, evt) { _onDeepGramConnectFailure(cs, _ep, evt) {
const {reason} = evt; const {reason} = evt;
@@ -696,6 +732,19 @@ class TaskGather extends Task {
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor deepgram: ${reason}`}); this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor deepgram: ${reason}`});
this.notifyTaskDone(); this.notifyTaskDone();
} }
_onJambonzConnectFailure(cs, _ep, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onJambonzConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${reason}`});
this.notifyTaskDone();
}
_onIbmConnect(_cs, _ep) { _onIbmConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onIbmConnect'); this.logger.debug('TaskGather:_onIbmConnect');
@@ -745,6 +794,10 @@ class TaskGather extends Task {
if (this.resolved) return; if (this.resolved) return;
this.resolved = true; this.resolved = true;
// Clear dtmf event
if (this.dtmfBargein) {
this.ep.removeAllListeners('dtmf');
}
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearTimer(); this._clearTimer();

View File

@@ -2,6 +2,7 @@ const Task = require('./task');
const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants'); const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const moment = require('moment'); const moment = require('moment');
const MAX_PLAY_AUDIO_QUEUE_SIZE = 10;
class TaskListen extends Task { class TaskListen extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
@@ -20,6 +21,8 @@ class TaskListen extends Task {
this.nested = parentTask instanceof Task; this.nested = parentTask instanceof Task;
this.results = {}; this.results = {};
this.playAudioQueue = [];
this.isPlayingAudioFromQueue = false;
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this); if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
} }
@@ -58,6 +61,7 @@ class TaskListen extends Task {
super.kill(cs); super.kill(cs);
this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`); this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`);
this._clearTimer(); this._clearTimer();
this.playAudioQueue = [];
if (this.ep && this.ep.connected) { if (this.ep && this.ep.connected) {
this.logger.debug('TaskListen:kill closing websocket'); this.logger.debug('TaskListen:kill closing websocket');
try { try {
@@ -184,16 +188,36 @@ class TaskListen extends Task {
this.notifyTaskDone(); this.notifyTaskDone();
} }
async _onPlayAudio(ep, evt) { async _playAudio(ep, evt, logger) {
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
try { try {
const results = await ep.play(evt.file); const results = await ep.play(evt.file);
this.logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`); logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
ep.forkAudioSendText({type: 'playDone', data: Object.assign({id: evt.id}, results)}); ep.forkAudioSendText({type: 'playDone', data: Object.assign({id: evt.id}, results)});
} catch (err) {
logger.error({err}, 'Error playing file');
} }
catch (err) { }
this.logger.error({err}, 'Error playing file');
async _onPlayAudio(ep, evt) {
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
if (!evt.queuePlay) {
this.playAudioQueue = [];
this._playAudio(ep, evt, this.logger);
this.isPlayingAudioFromQueue = false;
return;
} }
if (this.playAudioQueue.length <= MAX_PLAY_AUDIO_QUEUE_SIZE) {
this.playAudioQueue.push(evt);
}
if (this.isPlayingAudioFromQueue) return;
this.isPlayingAudioFromQueue = true;
while (this.playAudioQueue.length > 0) {
await this._playAudio(ep, this.playAudioQueue.shift(), this.logger);
}
this.isPlayingAudioFromQueue = false;
} }
_onKillAudio(ep) { _onKillAudio(ep) {

View File

@@ -143,7 +143,7 @@ class TaskSay extends Task {
span.end(); span.end();
writeAlerts({ writeAlerts({
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.TTS_NOT_PROVISIONED, alert_type: AlertType.TTS_FAILURE,
vendor, vendor,
detail: err.message detail: err.message
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
@@ -154,7 +154,6 @@ class TaskSay extends Task {
const arr = this.text.map((t) => generateAudio(t)); const arr = this.text.map((t) => generateAudio(t));
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length); const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
this.logger.debug({filepath}, 'synthesized files for tts');
this.notifyStatus({event: 'start-playback'}); this.notifyStatus({event: 'start-playback'});
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) { while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {

View File

@@ -9,7 +9,8 @@ const {
DeepgramTranscriptionEvents, DeepgramTranscriptionEvents,
SonioxTranscriptionEvents, SonioxTranscriptionEvents,
IbmTranscriptionEvents, IbmTranscriptionEvents,
NvidiaTranscriptionEvents NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('../utils/constants'); } = require('../utils/constants');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const { normalizeJambones } = require('@jambonz/verb-specifications');
@@ -23,11 +24,13 @@ class TaskTranscribe extends Task {
setChannelVarsForStt, setChannelVarsForStt,
normalizeTranscription, normalizeTranscription,
removeSpeechListeners, removeSpeechListeners,
setSpeechCredentialsAtRuntime setSpeechCredentialsAtRuntime,
compileSonioxTranscripts
} = require('../utils/transcription-utils')(logger); } = require('../utils/transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt; this.setChannelVarsForStt = setChannelVarsForStt;
this.normalizeTranscription = normalizeTranscription; this.normalizeTranscription = normalizeTranscription;
this.removeSpeechListeners = removeSpeechListeners; this.removeSpeechListeners = removeSpeechListeners;
this.compileSonioxTranscripts = compileSonioxTranscripts;
this.transcriptionHook = this.data.transcriptionHook; this.transcriptionHook = this.data.transcriptionHook;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia); this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
@@ -41,6 +44,9 @@ class TaskTranscribe extends Task {
/* let credentials be supplied in the recognizer object at runtime */ /* let credentials be supplied in the recognizer object at runtime */
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer); this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
/* buffer for soniox transcripts */
this._sonioxTranscripts = [];
recognizer.hints = recognizer.hints || []; recognizer.hints = recognizer.hints || [];
recognizer.altLanguages = recognizer.altLanguages || []; recognizer.altLanguages = recognizer.altLanguages || [];
} }
@@ -184,8 +190,6 @@ class TaskTranscribe extends Task {
this._onStartOfSpeech.bind(this, cs, ep, channel)); this._onStartOfSpeech.bind(this, cs, ep, channel));
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete, ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep, channel)); this._onTranscriptionComplete.bind(this, cs, ep, channel));
ep.addCustomEventListener(AzureTranscriptionEvents.Error,
this._onNuanceError.bind(this, cs, ep, channel));
break; break;
case 'deepgram': case 'deepgram':
this.bugname = 'deepgram_transcribe'; this.bugname = 'deepgram_transcribe';
@@ -198,9 +202,8 @@ class TaskTranscribe extends Task {
break; break;
case 'soniox': case 'soniox':
this.bugname = 'soniox_transcribe'; this.bugname = 'soniox_transcribe';
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription,
ep.addCustomEventListener(SonioxTranscriptionEvents.Error, this._onTranscription.bind(this, cs, ep, channel));
this._onSonioxError.bind(this, cs, ep));
break; break;
case 'ibm': case 'ibm':
this.bugname = 'ibm_transcribe'; this.bugname = 'ibm_transcribe';
@@ -210,8 +213,6 @@ class TaskTranscribe extends Task {
this._onIbmConnect.bind(this, cs, ep, channel)); this._onIbmConnect.bind(this, cs, ep, channel));
ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure, ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
this._onIbmConnectFailure.bind(this, cs, ep, channel)); this._onIbmConnectFailure.bind(this, cs, ep, channel));
ep.addCustomEventListener(IbmTranscriptionEvents.Error,
this._onIbmError.bind(this, cs, ep, channel));
break; break;
case 'nvidia': case 'nvidia':
@@ -224,14 +225,13 @@ class TaskTranscribe extends Task {
this._onTranscriptionComplete.bind(this, cs, ep)); this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected, ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep)); this._onVadDetected.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.Error,
this._onNvidiaError.bind(this, cs, ep));
break; break;
default: default:
throw new Error(`Invalid vendor ${this.vendor}`); throw new Error(`Invalid vendor ${this.vendor}`);
} }
/* common handler for all stt engine errors */
ep.addCustomEventListener(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep));
await ep.set(opts) await ep.set(opts)
.catch((err) => this.logger.info(err, 'Error setting channel variables')); .catch((err) => this.logger.info(err, 'Error setting channel variables'));
@@ -259,8 +259,11 @@ class TaskTranscribe extends Task {
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization'); this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization');
evt = this.normalizeTranscription(evt, this.vendor, channel, this.language); evt = this.normalizeTranscription(evt, this.vendor, channel, this.language);
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription'); this.logger.debug({evt}, 'TaskTranscribe:_onTranscription');
if (evt.alternatives.length === 0) {
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, continue listening');
return;
}
if (evt.alternatives[0]?.transcript === '' && !cs.callGone && !this.killed) { if (evt.alternatives[0]?.transcript === '' && !cs.callGone && !this.killed) {
if (['microsoft', 'deepgram'].includes(this.vendor)) { if (['microsoft', 'deepgram'].includes(this.vendor)) {
@@ -273,6 +276,15 @@ class TaskTranscribe extends Task {
return; return;
} }
if (this.vendor === 'soniox') {
/* compile transcripts into one */
this._sonioxTranscripts.push(evt.vendor.finalWords);
if (evt.is_final) {
evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language);
this._sonioxTranscripts = [];
}
}
if (this.transcriptionHook) { if (this.transcriptionHook) {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
@@ -321,23 +333,6 @@ class TaskTranscribe extends Task {
this._timer = null; this._timer = null;
} }
} }
_onNuanceError(_cs, _ep, _channel, evt) {
const {code, error, details} = evt;
if (code === 404 && error === 'No speech') {
this.logger.debug({code, error, details}, 'TaskTranscribe:_onNuanceError');
return this._resolve('timeout');
}
this.logger.info({code, error, details}, 'TaskTranscribe:_onNuanceError');
if (code === 413 && error === 'Too much speech') {
return this._resolve('timeout');
}
}
_onSonioxError(cs, ep, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onSonioxError');
}
_onNvidiaError(cs, ep, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onNvidiaError');
}
_onDeepgramConnect(_cs, _ep) { _onDeepgramConnect(_cs, _ep) {
this.logger.debug('TaskTranscribe:_onDeepgramConnect'); this.logger.debug('TaskTranscribe:_onDeepgramConnect');
} }
@@ -376,6 +371,24 @@ class TaskTranscribe extends Task {
_onIbmError(cs, _ep, _channel, evt) { _onIbmError(cs, _ep, _channel, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onIbmError'); this.logger.info({evt}, 'TaskTranscribe:_onIbmError');
} }
_onJambonzError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') {
const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout');
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
}
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
}
} }

View File

@@ -267,7 +267,6 @@ module.exports = (logger) => {
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task)); ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, onNoSpeechDetected.bind(null, cs, ep, task)); ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, onNoSpeechDetected.bind(null, cs, ep, task));
} }
logger.debug({sttOpts}, 'startAmd: setting channel vars');
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables')); await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
amd amd

View File

@@ -110,6 +110,12 @@
"NoSpeechDetected": "azure_transcribe::no_speech_detected", "NoSpeechDetected": "azure_transcribe::no_speech_detected",
"VadDetected": "azure_transcribe::vad_detected" "VadDetected": "azure_transcribe::vad_detected"
}, },
"JambonzTranscriptionEvents": {
"Transcription": "jambonz_transcribe::transcription",
"ConnectFailure": "jambonz_transcribe::connect_failed",
"Connect": "jambonz_transcribe::connect",
"Error": "jambonz_transcribe::error"
},
"ListenEvents": { "ListenEvents": {
"Connect": "mod_audio_fork::connect", "Connect": "mod_audio_fork::connect",
"ConnectFailure": "mod_audio_fork::connect_failed", "ConnectFailure": "mod_audio_fork::connect_failed",

View File

@@ -50,6 +50,8 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id; obj.client_id = o.client_id;
obj.secret = o.secret; obj.secret = o.secret;
obj.nuance_tts_uri = o.nuance_tts_uri;
obj.nuance_stt_uri = o.nuance_stt_uri;
} }
else if ('ibm' === obj.vendor) { else if ('ibm' === obj.vendor) {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
@@ -66,6 +68,12 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key; obj.api_key = o.api_key;
} }
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = o.auth_token;
obj.custom_stt_url = o.custom_stt_url;
obj.custom_tts_url = o.custom_tts_url;
}
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
@@ -83,53 +91,13 @@ module.exports = (logger, srf) => {
const [r2] = await pp.query(sqlSpeechCredentials, account_sid); const [r2] = await pp.query(sqlSpeechCredentials, account_sid);
const speech = r2.map(speechMapper); const speech = r2.map(speechMapper);
/* search at the service provider level if we don't find it at the account level */ /* add service provider creds unless we have that vendor at the account level */
const haveGoogle = speech.find((s) => s.vendor === 'google'); const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
const haveAws = speech.find((s) => s.vendor === 'aws'); r3.forEach((s) => {
const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft'); if (!speech.find((s2) => s2.vendor === s.vendor)) {
const haveWellsaid = speech.find((s) => s.vendor === 'wellsaid'); speech.push(speechMapper(s));
const haveNuance = speech.find((s) => s.vendor === 'nuance');
const haveDeepgram = speech.find((s) => s.vendor === 'deepgram');
const haveSoniox = speech.find((s) => s.vendor === 'soniox');
const haveIbm = speech.find((s) => s.vendor === 'ibm');
if (!haveGoogle || !haveAws || !haveMicrosoft || !haveWellsaid ||
!haveNuance || !haveIbm || !haveDeepgram || !haveSoniox) {
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
if (r3.length) {
if (!haveGoogle) {
const google = r3.find((s) => s.vendor === 'google');
if (google) speech.push(speechMapper(google));
}
if (!haveAws) {
const aws = r3.find((s) => s.vendor === 'aws');
if (aws) speech.push(speechMapper(aws));
}
if (!haveMicrosoft) {
const ms = r3.find((s) => s.vendor === 'microsoft');
if (ms) speech.push(speechMapper(ms));
}
if (!haveWellsaid) {
const wellsaid = r3.find((s) => s.vendor === 'wellsaid');
if (wellsaid) speech.push(speechMapper(wellsaid));
}
if (!haveNuance) {
const nuance = r3.find((s) => s.vendor === 'nuance');
if (nuance) speech.push(speechMapper(nuance));
}
if (!haveDeepgram) {
const deepgram = r3.find((s) => s.vendor === 'deepgram');
if (deepgram) speech.push(speechMapper(deepgram));
}
if (!haveSoniox) {
const soniox = r3.find((s) => s.vendor === 'soniox');
if (soniox) speech.push(speechMapper(soniox));
}
if (!haveIbm) {
const ibm = r3.find((s) => s.vendor === 'ibm');
if (ibm) speech.push(speechMapper(ibm));
}
} }
} });
return { return {
...r[0], ...r[0],

View File

@@ -2,9 +2,9 @@ const crypto = require('crypto');
const algorithm = process.env.LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc'; const algorithm = process.env.LEGACY_CRYPTO ? 'aes-256-ctr' : 'aes-256-cbc';
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const secretKey = crypto.createHash('sha256') const secretKey = crypto.createHash('sha256')
.update(String(process.env.JWT_SECRET)) .update(process.env.ENCRYPTION_SECRET || process.env.JWT_SECRET)
.digest('base64') .digest('base64')
.substr(0, 32); .substring(0, 32);
const encrypt = (text) => { const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, secretKey, iv); const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
@@ -25,8 +25,8 @@ const decrypt = (data) => {
throw err; throw err;
} }
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex')); const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]); const decrypted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
return decrpyted.toString(); return decrypted.toString();
}; };
module.exports = { module.exports = {

View File

@@ -138,7 +138,6 @@ function installSrfLocals(srf, logger) {
retrieveCall, retrieveCall,
listCalls, listCalls,
deleteCall, deleteCall,
synthAudio,
createHash, createHash,
retrieveHash, retrieveHash,
deleteKey, deleteKey,
@@ -151,11 +150,17 @@ function installSrfLocals(srf, logger) {
pushBack, pushBack,
popFront, popFront,
removeFromList, removeFromList,
lengthOfList,
getListPosition, getListPosition,
lengthOfList,
} = require('@jambonz/realtimedb-helpers')({
host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger, tracer);
const {
synthAudio,
getNuanceAccessToken, getNuanceAccessToken,
getIbmAccessToken, getIbmAccessToken,
} = require('@jambonz/realtimedb-helpers')({ } = require('@jambonz/speech-utils')({
host: process.env.JAMBONES_REDIS_HOST, host: process.env.JAMBONES_REDIS_HOST,
port: process.env.JAMBONES_REDIS_PORT || 6379 port: process.env.JAMBONES_REDIS_PORT || 6379
}, logger, tracer); }, logger, tracer);

View File

@@ -47,8 +47,16 @@ const parseSiprecPayload = (req, logger) => {
} }
} }
if (!sdp || !meta) {
logger.info({payload: req.payload}, 'invalid SIPREC payload'); if (!meta && sdp) {
const arr = /^([^]+)(m=[^]+?)(m=[^]+?)$/.exec(sdp);
opts.sdp1 = `${arr[1]}${arr[2]}`;
opts.sdp2 = `${arr[1]}${arr[3]}\r\n`;
opts.sessionId = uuidv4();
logger.info({ payload: req.payload }, 'SIPREC payload with no metadata (e.g. Cisco NBR)');
resolve(opts);
} else if (!sdp || !meta) {
logger.info({ payload: req.payload }, 'invalid SIPREC payload');
return reject(new Error('expected multipart SIPREC body')); return reject(new Error('expected multipart SIPREC body'));
} }
@@ -242,7 +250,8 @@ const createSipRecPayload = (sdp1, sdp2, logger) => {
.replace(/a=sendonly\r\n/g, '') .replace(/a=sendonly\r\n/g, '')
.replace(/a=direction:both\r\n/g, ''); .replace(/a=direction:both\r\n/g, '');
*/ */
return combinedSdp;
return combinedSdp.replace(/sendrecv/g, 'recvonly');
}; };
module.exports = { parseSiprecPayload, createSipRecPayload } ; module.exports = { parseSiprecPayload, createSipRecPayload } ;

View File

@@ -6,7 +6,8 @@ const {
NuanceTranscriptionEvents, NuanceTranscriptionEvents,
DeepgramTranscriptionEvents, DeepgramTranscriptionEvents,
SonioxTranscriptionEvents, SonioxTranscriptionEvents,
NvidiaTranscriptionEvents NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('./constants'); } = require('./constants');
const stickyVars = { const stickyVars = {
@@ -28,6 +29,7 @@ const stickyVars = {
'AZURE_SERVICE_ENDPOINT_ID', 'AZURE_SERVICE_ENDPOINT_ID',
'AZURE_REQUEST_SNR', 'AZURE_REQUEST_SNR',
'AZURE_PROFANITY_OPTION', 'AZURE_PROFANITY_OPTION',
'AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES',
'AZURE_SERVICE_ENDPOINT', 'AZURE_SERVICE_ENDPOINT',
'AZURE_INITIAL_SPEECH_TIMEOUT_MS', 'AZURE_INITIAL_SPEECH_TIMEOUT_MS',
'AZURE_USE_OUTPUT_FORMAT_DETAILED', 'AZURE_USE_OUTPUT_FORMAT_DETAILED',
@@ -223,6 +225,15 @@ const normalizeGoogle = (evt, channel, language) => {
}; };
}; };
const normalizeCustom = (evt, channel, language) => {
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [evt.alternatives[0]]
};
};
const normalizeNuance = (evt, channel, language) => { const normalizeNuance = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt)); const copy = JSON.parse(JSON.stringify(evt));
return { return {
@@ -302,6 +313,9 @@ module.exports = (logger) => {
case 'soniox': case 'soniox':
return normalizeSoniox(evt, channel, language); return normalizeSoniox(evt, channel, language);
default: default:
if (vendor.startsWith('custom:')) {
return normalizeCustom(evt, channel, language);
}
logger.error(`Unknown vendor ${vendor}`); logger.error(`Unknown vendor ${vendor}`);
return evt; return evt;
} }
@@ -311,6 +325,7 @@ module.exports = (logger) => {
let opts = {}; let opts = {};
const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {}; const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {};
const vad = {enable, voiceMs, mode}; const vad = {enable, voiceMs, mode};
const vendor = rOpts.vendor;
/* voice activity detection works across vendors */ /* voice activity detection works across vendors */
opts = { opts = {
@@ -320,59 +335,42 @@ module.exports = (logger) => {
...(vad.enable && typeof vad.mode === 'number' && {RECOGNIZER_VAD_MODE: vad.mode}), ...(vad.enable && typeof vad.mode === 'number' && {RECOGNIZER_VAD_MODE: vad.mode}),
}; };
if ('google' === rOpts.vendor) { if ('google' === vendor) {
const model = task.name === TaskName.Gather ? 'command_and_search' : 'latest_long';
opts = { opts = {
...opts, ...opts,
...(sttCredentials && ...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),
{GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}), ...(rOpts.separateRecognitionPerChannel && {GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
...(rOpts.enhancedModel && ...(rOpts.separateRecognitionPerChanne === false && {GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 0}),
{GOOGLE_SPEECH_USE_ENHANCED: 1}), ...(rOpts.profanityFilter && {GOOGLE_SPEECH_PROFANITY_FILTER: 1}),
...(rOpts.separateRecognitionPerChannel && ...(rOpts.punctuation && {GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1}),
{GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 1}), ...(rOpts.words && {GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 1}),
...(rOpts.profanityFilter && ...(rOpts.singleUtterance && {GOOGLE_SPEECH_SINGLE_UTTERANCE: 1}),
{GOOGLE_SPEECH_PROFANITY_FILTER: 1}), ...(rOpts.diarization && {GOOGLE_SPEECH_SPEAKER_DIARIZATION: 1}),
...(rOpts.punctuation &&
{GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1}),
...(rOpts.words &&
{GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 1}),
...((rOpts.singleUtterance || task.name === TaskName.Gather) &&
{GOOGLE_SPEECH_SINGLE_UTTERANCE: 1}),
...(rOpts.diarization &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION: 1}),
...(rOpts.diarization && rOpts.diarizationMinSpeakers > 0 && ...(rOpts.diarization && rOpts.diarizationMinSpeakers > 0 &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT: rOpts.diarizationMinSpeakers}), {GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT: rOpts.diarizationMinSpeakers}),
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 && ...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}), {GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}),
...(rOpts.enhancedModel === false && ...(rOpts.enhancedModel !== false && {GOOGLE_SPEECH_USE_ENHANCED: 1}),
{GOOGLE_SPEECH_USE_ENHANCED: 0}), ...(rOpts.profanityFilter === false && {GOOGLE_SPEECH_PROFANITY_FILTER: 0}),
...(rOpts.separateRecognitionPerChannel === false && ...(rOpts.punctuation === false && {GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 0}),
{GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 0}), ...(rOpts.words == false && {GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 0}),
...(rOpts.profanityFilter === false && ...(rOpts.diarization === false && {GOOGLE_SPEECH_SPEAKER_DIARIZATION: 0}),
{GOOGLE_SPEECH_PROFANITY_FILTER: 0}),
...(rOpts.punctuation === false &&
{GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 0}),
...(rOpts.words == false &&
{GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 0}),
...((rOpts.singleUtterance === false || task.name === TaskName.Transcribe) &&
{GOOGLE_SPEECH_SINGLE_UTTERANCE: 0}),
...(rOpts.diarization === false &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION: 0}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{GOOGLE_SPEECH_HINTS: rOpts.hints.join(',')}), {GOOGLE_SPEECH_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{GOOGLE_SPEECH_HINTS: JSON.stringify(rOpts.hints)}), {GOOGLE_SPEECH_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' && ...(typeof rOpts.hintsBoost === 'number' && {GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}),
{GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}),
...(rOpts.altLanguages.length > 0 && ...(rOpts.altLanguages.length > 0 &&
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: rOpts.altLanguages.join(',')}), {GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
...(rOpts.interactionType && ...(rOpts.interactionType &&
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}), {GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
...{GOOGLE_SPEECH_MODEL: rOpts.model || (task.name === TaskName.Gather ? 'latest_short' : 'phone_call')}, ...{GOOGLE_SPEECH_MODEL: rOpts.model || model},
...(rOpts.naicsCode > 0 && ...(rOpts.naicsCode > 0 && {GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
{GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}), GOOGLE_SPEECH_METADATA_RECORDING_DEVICE_TYPE: 'phone_line',
}; };
} }
else if (['aws', 'polly'].includes(rOpts.vendor)) { else if (['aws', 'polly'].includes(vendor)) {
opts = { opts = {
...opts, ...opts,
...(rOpts.vocabularyName && {AWS_VOCABULARY_NAME: rOpts.vocabularyName}), ...(rOpts.vocabularyName && {AWS_VOCABULARY_NAME: rOpts.vocabularyName}),
@@ -385,7 +383,7 @@ module.exports = (logger) => {
}), }),
}; };
} }
else if ('microsoft' === rOpts.vendor) { else if ('microsoft' === vendor) {
opts = { opts = {
...opts, ...opts,
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
@@ -393,7 +391,7 @@ module.exports = (logger) => {
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.phrase).join(',')}), {AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.phrase).join(',')}),
...(rOpts.altLanguages && rOpts.altLanguages.length > 0 && ...(rOpts.altLanguages && rOpts.altLanguages.length > 0 &&
{AZURE_SERVICE_ENDPOINT_ID: rOpts.sttCredentials}), {AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}), ...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
...(rOpts.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}), ...(rOpts.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}),
...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}), ...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}),
@@ -410,7 +408,7 @@ module.exports = (logger) => {
{AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint}) {AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint})
}; };
} }
else if ('nuance' === rOpts.vendor) { else if ('nuance' === vendor) {
/** /**
* Note: all nuance options are in recognizer.nuanceOptions, should migrate * Note: all nuance options are in recognizer.nuanceOptions, should migrate
* other vendor settings to similar nested structure * other vendor settings to similar nested structure
@@ -418,12 +416,9 @@ module.exports = (logger) => {
const {nuanceOptions = {}} = rOpts; const {nuanceOptions = {}} = rOpts;
opts = { opts = {
...opts, ...opts,
...(sttCredentials.access_token) && ...(sttCredentials.access_token) && {NUANCE_ACCESS_TOKEN: sttCredentials.access_token},
{NUANCE_ACCESS_TOKEN: sttCredentials.access_token}, ...(sttCredentials.nuance_stt_uri) && {NUANCE_KRYPTON_ENDPOINT: sttCredentials.nuance_stt_uri},
...(sttCredentials.krypton_endpoint) && ...(nuanceOptions.topic) && {NUANCE_TOPIC: nuanceOptions.topic},
{NUANCE_KRYPTON_ENDPOINT: sttCredentials.krypton_endpoint},
...(nuanceOptions.topic) &&
{NUANCE_TOPIC: nuanceOptions.topic},
...(nuanceOptions.utteranceDetectionMode) && ...(nuanceOptions.utteranceDetectionMode) &&
{NUANCE_UTTERANCE_DETECTION_MODE: nuanceOptions.utteranceDetectionMode}, {NUANCE_UTTERANCE_DETECTION_MODE: nuanceOptions.utteranceDetectionMode},
...(nuanceOptions.punctuation || rOpts.punctuation) && {NUANCE_PUNCTUATION: nuanceOptions.punctuation}, ...(nuanceOptions.punctuation || rOpts.punctuation) && {NUANCE_PUNCTUATION: nuanceOptions.punctuation},
@@ -461,7 +456,7 @@ module.exports = (logger) => {
{NUANCE_RESOURCES: JSON.stringify(nuanceOptions.resources)}, {NUANCE_RESOURCES: JSON.stringify(nuanceOptions.resources)},
}; };
} }
else if ('deepgram' === rOpts.vendor) { else if ('deepgram' === vendor) {
const {deepgramOptions = {}} = rOpts; const {deepgramOptions = {}} = rOpts;
opts = { opts = {
...opts, ...opts,
@@ -505,7 +500,7 @@ module.exports = (logger) => {
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag} {DEEPGRAM_SPEECH_TAG: deepgramOptions.tag}
}; };
} }
else if ('soniox' === rOpts.vendor) { else if ('soniox' === vendor) {
const {sonioxOptions = {}} = rOpts; const {sonioxOptions = {}} = rOpts;
const {storage = {}} = sonioxOptions; const {storage = {}} = sonioxOptions;
opts = { opts = {
@@ -528,7 +523,7 @@ module.exports = (logger) => {
...(storage?.id && storage?.disableSearch && {SONIOX_STORAGE_DISABLE_SEARCH: 1}) ...(storage?.id && storage?.disableSearch && {SONIOX_STORAGE_DISABLE_SEARCH: 1})
}; };
} }
else if ('ibm' === rOpts.vendor) { else if ('ibm' === vendor) {
const {ibmOptions = {}} = rOpts; const {ibmOptions = {}} = rOpts;
opts = { opts = {
...opts, ...opts,
@@ -552,7 +547,7 @@ module.exports = (logger) => {
{IBM_SPEECH_WATSON_LEARNING_OPT_OUT: ibmOptions.watsonLearningOptOut} {IBM_SPEECH_WATSON_LEARNING_OPT_OUT: ibmOptions.watsonLearningOptOut}
}; };
} }
else if ('nvidia' === rOpts.vendor) { else if ('nvidia' === vendor) {
const {nvidiaOptions = {}} = rOpts; const {nvidiaOptions = {}} = rOpts;
opts = { opts = {
...opts, ...opts,
@@ -581,11 +576,29 @@ module.exports = (logger) => {
{NVIDIA_CUSTOM_CONFIGURATION: JSON.stringify(nvidiaOptions.customConfiguration)}), {NVIDIA_CUSTOM_CONFIGURATION: JSON.stringify(nvidiaOptions.customConfiguration)}),
}; };
} }
else if (vendor.startsWith('custom:')) {
let {options = {}} = rOpts;
const {auth_token, custom_stt_url} = sttCredentials;
options = {
...options,
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{hints: rOpts.hints}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{hints: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost})
};
stickyVars[rOpts.vendor].forEach((key) => { opts = {
...opts,
JAMBONZ_STT_API_KEY: auth_token,
JAMBONZ_STT_URL: custom_stt_url,
...(Object.keys(options).length > 0 && {JAMBONZ_STT_OPTIONS: JSON.stringify(options)}),
};
}
(stickyVars[vendor] || []).forEach((key) => {
if (!opts[key]) opts[key] = ''; if (!opts[key]) opts[key] = '';
}); });
//logger.debug({opts}, 'recognizer channel vars');
return opts; return opts;
}; };
@@ -604,7 +617,6 @@ module.exports = (logger) => {
ep.removeCustomEventListener(NuanceTranscriptionEvents.Transcription); ep.removeCustomEventListener(NuanceTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete); ep.removeCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete);
ep.removeCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech); ep.removeCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech);
ep.removeCustomEventListener(NuanceTranscriptionEvents.Error);
ep.removeCustomEventListener(NuanceTranscriptionEvents.VadDetected); ep.removeCustomEventListener(NuanceTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.Transcription); ep.removeCustomEventListener(DeepgramTranscriptionEvents.Transcription);
@@ -612,13 +624,17 @@ module.exports = (logger) => {
ep.removeCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure); ep.removeCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure);
ep.removeCustomEventListener(SonioxTranscriptionEvents.Transcription); ep.removeCustomEventListener(SonioxTranscriptionEvents.Transcription);
ep.removeCustomEventListener(SonioxTranscriptionEvents.Error);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.Transcription); ep.removeCustomEventListener(NvidiaTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete); ep.removeCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech); ep.removeCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.Error);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.VadDetected); ep.removeCustomEventListener(NvidiaTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Transcription);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Connect);
ep.removeCustomEventListener(JambonzTranscriptionEvents.ConnectFailure);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Error);
}; };
const setSpeechCredentialsAtRuntime = (recognizer) => { const setSpeechCredentialsAtRuntime = (recognizer) => {
@@ -626,7 +642,7 @@ module.exports = (logger) => {
if (recognizer.vendor === 'nuance') { if (recognizer.vendor === 'nuance') {
const {clientId, secret, kryptonEndpoint} = recognizer.nuanceOptions || {}; const {clientId, secret, kryptonEndpoint} = recognizer.nuanceOptions || {};
if (clientId && secret) return {client_id: clientId, secret}; if (clientId && secret) return {client_id: clientId, secret};
if (kryptonEndpoint) return {krypton_endpoint: kryptonEndpoint}; if (kryptonEndpoint) return {nuance_stt_uri: kryptonEndpoint};
} }
else if (recognizer.vendor === 'nvidia') { else if (recognizer.vendor === 'nvidia') {
const {rivaUri} = recognizer.nvidiaOptions || {}; const {rivaUri} = recognizer.nvidiaOptions || {};

View File

@@ -219,7 +219,6 @@ class WsRequestor extends BaseRequestor {
} }
_setHandlers(ws) { _setHandlers(ws) {
this.logger.debug('WsRequestor:_setHandlers');
ws ws
.once('open', this._onOpen.bind(this, ws)) .once('open', this._onOpen.bind(this, ws))
.once('close', this._onClose.bind(this)) .once('close', this._onClose.bind(this))
@@ -274,6 +273,7 @@ class WsRequestor extends BaseRequestor {
}, 'WsRequestor - unexpected response'); }, 'WsRequestor - unexpected response');
this.emit('connection-failure'); this.emit('connection-failure');
this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`)); this.emit('not-ready', new Error(`${res.statusCode} ${res.statusMessage}`));
this.connections++;
} }
_onSocketClosed() { _onSocketClosed() {
@@ -338,7 +338,7 @@ class WsRequestor extends BaseRequestor {
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`); this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
return; return;
} }
this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`); //this.logger.debug({url: this.url}, `WsRequestor:_recvAck - received response to ${msgid}`);
this.messagesInFlight.delete(msgid); this.messagesInFlight.delete(msgid);
const {success} = obj; const {success} = obj;
success && success(data); success && success(data);

2442
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "jambonz-feature-server", "name": "jambonz-feature-server",
"version": "v0.8.1", "version": "v0.8.2",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 10.16.0" "node": ">= 10.16.0"
@@ -19,15 +19,16 @@
"bugs": {}, "bugs": {},
"scripts": { "scripts": {
"start": "node app", "start": "node app",
"test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=error ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:JambonzR0ck$:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ", "test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=1 ENCRYPTION_SECRET=foobar DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=error ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:JambonzR0ck$:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
"jslint": "eslint app.js lib" "jslint": "eslint app.js lib"
}, },
"dependencies": { "dependencies": {
"@jambonz/db-helpers": "^0.7.4", "@jambonz/db-helpers": "^0.7.4",
"@jambonz/http-health-check": "^0.0.1", "@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.6.5", "@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/stats-collector": "^0.1.6", "@jambonz/speech-utils": "^0.0.12",
"@jambonz/stats-collector": "^0.1.8",
"@jambonz/time-series": "^0.2.5", "@jambonz/time-series": "^0.2.5",
"@jambonz/verb-specifications": "^0.0.11", "@jambonz/verb-specifications": "^0.0.11",
"@opentelemetry/api": "^1.4.0", "@opentelemetry/api": "^1.4.0",
@@ -43,7 +44,7 @@
"bent": "^7.3.12", "bent": "^7.3.12",
"debug": "^4.3.4", "debug": "^4.3.4",
"deepcopy": "^2.1.0", "deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.19", "drachtio-fsmrf": "^3.0.20",
"drachtio-srf": "^4.5.23", "drachtio-srf": "^4.5.23",
"express": "^4.18.2", "express": "^4.18.2",
"ip": "^1.1.8", "ip": "^1.1.8",
@@ -60,7 +61,7 @@
"uuid-random": "^1.3.2", "uuid-random": "^1.3.2",
"verify-aws-sns-signature": "^0.1.0", "verify-aws-sns-signature": "^0.1.0",
"ws": "^8.9.0", "ws": "^8.9.0",
"xml2js": "^0.4.23" "xml2js": "^0.5.0"
}, },
"devDependencies": { "devDependencies": {
"clear-module": "^4.1.2", "clear-module": "^4.1.2",

View File

@@ -18,30 +18,38 @@ test('creating schema', (t) => {
if (err) return t.end(err); if (err) return t.end(err);
t.pass('schema and test data successfully created'); t.pass('schema and test data successfully created');
if (process.env.GCP_JSON_KEY && process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) { const sql = [];
if (process.env.GCP_JSON_KEY) {
const google_credential = encrypt(process.env.GCP_JSON_KEY); const google_credential = encrypt(process.env.GCP_JSON_KEY);
t.pass('adding google credentials');
sql.push(`UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';`);
}
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
const aws_credential = encrypt(JSON.stringify({ const aws_credential = encrypt(JSON.stringify({
access_key_id: process.env.AWS_ACCESS_KEY_ID, access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY, secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
aws_region: process.env.AWS_REGION aws_region: process.env.AWS_REGION
})); }));
t.pass('adding aws credentials');
sql.push(`UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';`);
}
if (process.env.MICROSOFT_REGION && process.env.MICROSOFT_API_KEY) {
const microsoft_credential = encrypt(JSON.stringify({ const microsoft_credential = encrypt(JSON.stringify({
region: process.env.MICROSOFT_REGION || 'useast', region: process.env.MICROSOFT_REGION,
api_key: process.env.MICROSOFT_API_KEY || '1234567890' api_key: process.env.MICROSOFT_API_KEY
})); }));
const cmd = ` t.pass('adding microsoft credentials');
UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google'; sql.push(`UPDATE speech_credentials SET credential='${microsoft_credential}' WHERE vendor='microsoft';`);
UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws'; }
UPDATE speech_credentials SET credential='${microsoft_credential}' WHERE vendor='microsoft'; if (sql.length > 0) {
`;
const path = `${__dirname}/.creds.sql`; const path = `${__dirname}/.creds.sql`;
fs.writeFileSync(path, cmd); const cmd = sql.join('\n');
fs.writeFileSync(path, sql.join('\n'));
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${path}`, (err, stdout, stderr) => { exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${path}`, (err, stdout, stderr) => {
console.log(stdout); console.log(stdout);
console.log(stderr); console.log(stderr);
if (err) return t.end(err); if (err) return t.end(err);
fs.unlinkSync(path) fs.unlinkSync(path)
fs.writeFileSync(`${__dirname}/credentials/gcp.json`, process.env.GCP_JSON_KEY);
t.pass('set account-level speech credentials'); t.pass('set account-level speech credentials');
t.end(); t.end();
}); });

View File

@@ -5,7 +5,6 @@ test('dropping jambones_test database', (t) => {
exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/db/remove_test_db.sql`, (err, stdout, stderr) => { exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 < ${__dirname}/db/remove_test_db.sql`, (err, stdout, stderr) => {
if (err) return t.end(err); if (err) return t.end(err);
t.pass('database successfully dropped'); t.pass('database successfully dropped');
fs.unlinkSync(`${__dirname}/credentials/gcp.json`);
t.end(); t.end();
}); });
}); });