Compare commits

...

45 Commits

Author SHA1 Message Date
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
22 changed files with 1544 additions and 1388 deletions

View File

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

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_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.ENCRYPTION_SECRET || process.env.JWT_SECRET, 'missing ENCRYPTION_SECRET env var');
const Srf = require('drachtio-srf');
const srf = new Srf();

View File

@@ -104,7 +104,7 @@ router.post('/', async(req, res) => {
proxy: `sip:${sbcAddress}`,
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) {
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')) {
logger.info('getAccountDetails - rejecting call due to missing X-Account-Sid header');
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-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();
}
@@ -90,8 +103,10 @@ module.exports = function(srf, logger) {
.find((p) => p.type === 'application/sdp')
.content;
const {sdp1, sdp2, ...metadata} = await parseSiprecPayload(req, logger);
req.locals.calledNumber = metadata.caller.number;
req.locals.callingNumber = metadata.callee.number;
if (!req.locals.calledNumber && !req.locals.calledNumber) {
req.locals.calledNumber = metadata.caller.number;
req.locals.callingNumber = metadata.callee.number;
}
req.locals = {
...req.locals,
siprec: {

View File

@@ -33,6 +33,23 @@ class CallInfo {
this.callStatus = CallStatus.Trying;
this.originatingSipIp = req.get('X-Forwarded-For');
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) {
// outbound call that is a child of an existing call

View File

@@ -511,12 +511,24 @@ class CallSession extends Emitter {
async enableBotMode(gather, autoEnable) {
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]);
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.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}`);
this.backgroundGatherTask.span = span;
this.backgroundGatherTask.ctx = ctx;
this.backgroundGatherTask.sticky = autoEnable;
this.backgroundGatherTask.exec(this, resources)
.then(() => {
this.logger.info('CallSession:enableBotMode: gather completed');
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
this.backgroundGatherTask && this.backgroundGatherTask.span.end();
const sticky = this.backgroundGatherTask?.sticky;
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');
setImmediate(() => this.enableBotMode(gather, true));
}
@@ -636,7 +650,9 @@ class CallSession extends Emitter {
return {
speech_credential_sid: credential.speech_credential_sid,
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) {
@@ -698,7 +714,7 @@ class CallSession extends Emitter {
let skip = false;
this.currentTask = task;
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}`);
skip = true;
}
@@ -762,7 +778,6 @@ class CallSession extends Emitter {
trackTmpFile(path) {
// 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);
}
@@ -1137,14 +1152,14 @@ class CallSession extends Emitter {
_injectTasks(newTasks) {
const gatherPos = this.tasks.map((t) => t.name).indexOf(TaskName.Gather);
const currentlyExecutingGather = this.currentTask?.name === TaskName.Gather;
/*
this.logger.debug({
currentTaskList: listTaskNames(this.tasks),
newContent: listTaskNames(newTasks),
currentlyExecutingGather,
gatherPos
}, 'CallSession:_injectTasks - starting');
*/
const killGather = () => {
this.logger.debug('CallSession:_injectTasks - killing current gather because we have new content');
this.currentTask.kill(this);
@@ -1153,10 +1168,11 @@ class CallSession extends Emitter {
if (-1 === gatherPos) {
/* no gather in the stack simply append tasks */
this.tasks.push(...newTasks);
/*
this.logger.debug({
updatedTaskList: listTaskNames(this.tasks)
}, 'CallSession:_injectTasks - completed (simple append)');
*/
/* we do need to kill the current gather if we are executing one */
if (currentlyExecutingGather) killGather();
return;
@@ -1184,12 +1200,10 @@ class CallSession extends Emitter {
this.replaceApplication(t);
}
else if (process.env.JAMBONES_INJECT_CONTENT) {
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks (injecting content)');
this._injectTasks(t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
}
else {
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand - queueing tasks');
this.tasks.push(...t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
}
@@ -1233,7 +1247,7 @@ class CallSession extends Emitter {
this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
}
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 = null;
}

View File

@@ -1,6 +1,7 @@
const InboundCallSession = require('./inbound-call-session');
const {createSipRecPayload} = require('../utils/siprec-utils');
const {CallStatus} = require('../utils/constants');
const {parseSiprecPayload} = require('../utils/siprec-utils');
/**
* @classdesc Subclass of InboundCallSession. This represents a CallSession that is
* established for an inbound SIPREC call.
@@ -16,6 +17,32 @@ class SipRecCallSession extends InboundCallSession {
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() {
try {
this.ms = this.getMS();

View File

@@ -54,7 +54,7 @@ class TaskGather extends Task {
/* timeout of zero means no timeout */
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.minBargeinWordCount = this.data.minBargeinWordCount || 1;
if (this.data.recognizer) {
@@ -70,6 +70,11 @@ class TaskGather extends Task {
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit;
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.altLanguages = this.data.recognizer.altLanguages || [];
}
@@ -93,12 +98,17 @@ class TaskGather extends Task {
this._sonioxTranscripts = [];
this.parentTask = parentTask;
this.partialTranscriptsCount = 0;
}
get name() { return TaskName.Gather; }
get needsStt() { return this.input.includes('speech'); }
get wantsSingleUtterance() {
return this.data.recognizer?.singleUtterance === true;
}
get earlyMedia() {
return (this.sayTask && this.sayTask.earlyMedia) ||
(this.playTask && this.playTask.earlyMedia);
@@ -120,14 +130,17 @@ class TaskGather extends Task {
}
async exec(cs, {ep}) {
this.logger.debug('Gather:exec');
this.logger.debug({options: this.data}, 'Gather:exec');
await super.exec(cs);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
if (cs.hasGlobalSttHints) {
if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) {
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;
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
'Gather:exec - applying global sttHints');
@@ -188,7 +201,6 @@ class TaskGather extends Task {
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) {
/* get nuance access token */
const {client_id, secret} = this.sttCredentials;
@@ -207,7 +219,6 @@ class TaskGather extends Task {
this._startTimer();
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
if (this.input.includes('speech') && !this.listenDuringPrompt) {
this.logger.debug('Gather:exec - calling _initSpeech');
this._initSpeech(cs, ep)
.then(() => {
if (this.killed) {
@@ -309,6 +320,7 @@ class TaskGather extends Task {
const {timeout} = opts;
this.timeout = timeout;
this._startTimer();
return true;
}
_onDtmf(cs, ep, evt) {
@@ -348,7 +360,6 @@ class TaskGather extends Task {
async _initSpeech(cs, ep) {
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
this.logger.debug(opts, 'TaskGather:_initSpeech - channel vars');
switch (this.vendor) {
case 'google':
this.bugname = 'google_transcribe';
@@ -550,8 +561,13 @@ class TaskGather extends Task {
}
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 hints = this.data.recognizer?.hints || [];
if (hints.find((h) => h.toLowerCase() === transcript)) {
@@ -625,6 +641,8 @@ class TaskGather extends Task {
others do not.
*/
//const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
this._clearTimer();
this._startTimer();
if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) {
if (!this.playComplete) {
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
@@ -653,7 +671,16 @@ class TaskGather extends Task {
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);
}
}
@@ -767,6 +794,10 @@ class TaskGather extends Task {
if (this.resolved) return;
this.resolved = true;
// Clear dtmf event
if (this.dtmfBargein) {
this.ep.removeAllListeners('dtmf');
}
clearTimeout(this.interDigitTimer);
this._clearTimer();

View File

@@ -2,6 +2,7 @@ const Task = require('./task');
const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants');
const makeTask = require('./make_task');
const moment = require('moment');
const MAX_PLAY_AUDIO_QUEUE_SIZE = 10;
class TaskListen extends Task {
constructor(logger, opts, parentTask) {
@@ -20,6 +21,8 @@ class TaskListen extends Task {
this.nested = parentTask instanceof Task;
this.results = {};
this.playAudioQueue = [];
this.isPlayingAudioFromQueue = false;
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
}
@@ -58,6 +61,7 @@ class TaskListen extends Task {
super.kill(cs);
this.logger.debug(`TaskListen:kill endpoint connected? ${this.ep && this.ep.connected}`);
this._clearTimer();
this.playAudioQueue = [];
if (this.ep && this.ep.connected) {
this.logger.debug('TaskListen:kill closing websocket');
try {
@@ -184,16 +188,36 @@ class TaskListen extends Task {
this.notifyTaskDone();
}
async _onPlayAudio(ep, evt) {
this.logger.info(`received play_audio event: ${JSON.stringify(evt)}`);
async _playAudio(ep, evt, logger) {
try {
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)});
} 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) {

View File

@@ -154,7 +154,6 @@ class TaskSay extends Task {
const arr = this.text.map((t) => generateAudio(t));
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
this.logger.debug({filepath}, 'synthesized files for tts');
this.notifyStatus({event: 'start-playback'});
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {

View File

@@ -259,8 +259,11 @@ class TaskTranscribe extends Task {
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization');
evt = this.normalizeTranscription(evt, this.vendor, channel, this.language);
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 (['microsoft', 'deepgram'].includes(this.vendor)) {

View File

@@ -267,7 +267,6 @@ module.exports = (logger) => {
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.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'));
amd

View File

@@ -50,6 +50,8 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
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) {
const o = JSON.parse(decrypt(credential));

View File

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

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'));
}
@@ -242,7 +250,8 @@ const createSipRecPayload = (sdp1, sdp2, logger) => {
.replace(/a=sendonly\r\n/g, '')
.replace(/a=direction:both\r\n/g, '');
*/
return combinedSdp;
return combinedSdp.replace(/sendrecv/g, 'recvonly');
};
module.exports = { parseSiprecPayload, createSipRecPayload } ;

View File

@@ -29,6 +29,7 @@ const stickyVars = {
'AZURE_SERVICE_ENDPOINT_ID',
'AZURE_REQUEST_SNR',
'AZURE_PROFANITY_OPTION',
'AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES',
'AZURE_SERVICE_ENDPOINT',
'AZURE_INITIAL_SPEECH_TIMEOUT_MS',
'AZURE_USE_OUTPUT_FORMAT_DETAILED',
@@ -335,55 +336,38 @@ module.exports = (logger) => {
};
if ('google' === vendor) {
const model = task.name === TaskName.Gather ? 'command_and_search' : 'latest_long';
opts = {
...opts,
...(sttCredentials &&
{GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),
...(rOpts.enhancedModel &&
{GOOGLE_SPEECH_USE_ENHANCED: 1}),
...(rOpts.separateRecognitionPerChannel &&
{GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
...(rOpts.profanityFilter &&
{GOOGLE_SPEECH_PROFANITY_FILTER: 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}),
...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),
...(rOpts.separateRecognitionPerChannel && {GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
...(rOpts.separateRecognitionPerChanne === false && {GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 0}),
...(rOpts.profanityFilter && {GOOGLE_SPEECH_PROFANITY_FILTER: 1}),
...(rOpts.punctuation && {GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1}),
...(rOpts.words && {GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 1}),
...(rOpts.singleUtterance && {GOOGLE_SPEECH_SINGLE_UTTERANCE: 1}),
...(rOpts.diarization && {GOOGLE_SPEECH_SPEAKER_DIARIZATION: 1}),
...(rOpts.diarization && rOpts.diarizationMinSpeakers > 0 &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT: rOpts.diarizationMinSpeakers}),
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
{GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}),
...(rOpts.enhancedModel === false &&
{GOOGLE_SPEECH_USE_ENHANCED: 0}),
...(rOpts.separateRecognitionPerChannel === false &&
{GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL: 0}),
...(rOpts.profanityFilter === false &&
{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.enhancedModel !== false && {GOOGLE_SPEECH_USE_ENHANCED: 1}),
...(rOpts.profanityFilter === false && {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.diarization === false && {GOOGLE_SPEECH_SPEAKER_DIARIZATION: 0}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{GOOGLE_SPEECH_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{GOOGLE_SPEECH_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' &&
{GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}),
...(typeof rOpts.hintsBoost === 'number' && {GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}),
...(rOpts.altLanguages.length > 0 &&
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: rOpts.altLanguages.join(',')}),
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
...(rOpts.interactionType &&
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
...{GOOGLE_SPEECH_MODEL: rOpts.model || (task.name === TaskName.Gather ? 'latest_short' : 'phone_call')},
...(rOpts.naicsCode > 0 &&
{GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
...{GOOGLE_SPEECH_MODEL: rOpts.model || model},
...(rOpts.naicsCode > 0 && {GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
GOOGLE_SPEECH_METADATA_RECORDING_DEVICE_TYPE: 'phone_line',
};
}
else if (['aws', 'polly'].includes(vendor)) {
@@ -407,7 +391,7 @@ module.exports = (logger) => {
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.phrase).join(',')}),
...(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.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}),
...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}),
@@ -432,12 +416,9 @@ module.exports = (logger) => {
const {nuanceOptions = {}} = rOpts;
opts = {
...opts,
...(sttCredentials.access_token) &&
{NUANCE_ACCESS_TOKEN: sttCredentials.access_token},
...(sttCredentials.krypton_endpoint) &&
{NUANCE_KRYPTON_ENDPOINT: sttCredentials.krypton_endpoint},
...(nuanceOptions.topic) &&
{NUANCE_TOPIC: nuanceOptions.topic},
...(sttCredentials.access_token) && {NUANCE_ACCESS_TOKEN: sttCredentials.access_token},
...(sttCredentials.nuance_stt_uri) && {NUANCE_KRYPTON_ENDPOINT: sttCredentials.nuance_stt_uri},
...(nuanceOptions.topic) && {NUANCE_TOPIC: nuanceOptions.topic},
...(nuanceOptions.utteranceDetectionMode) &&
{NUANCE_UTTERANCE_DETECTION_MODE: nuanceOptions.utteranceDetectionMode},
...(nuanceOptions.punctuation || rOpts.punctuation) && {NUANCE_PUNCTUATION: nuanceOptions.punctuation},
@@ -661,7 +642,7 @@ module.exports = (logger) => {
if (recognizer.vendor === 'nuance') {
const {clientId, secret, kryptonEndpoint} = recognizer.nuanceOptions || {};
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') {
const {rivaUri} = recognizer.nvidiaOptions || {};

View File

@@ -219,7 +219,6 @@ class WsRequestor extends BaseRequestor {
}
_setHandlers(ws) {
this.logger.debug('WsRequestor:_setHandlers');
ws
.once('open', this._onOpen.bind(this, ws))
.once('close', this._onClose.bind(this))
@@ -339,7 +338,7 @@ class WsRequestor extends BaseRequestor {
this.logger.info({url: this.url}, `WsRequestor:_recvAck - ack to unknown msgid ${msgid}, discarding`);
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);
const {success} = obj;
success && success(data);

2533
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-feature-server",
"version": "v0.8.1",
"version": "v0.8.2",
"main": "app.js",
"engines": {
"node": ">= 10.16.0"
@@ -19,7 +19,7 @@
"bugs": {},
"scripts": {
"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",
"jslint": "eslint app.js lib"
},
@@ -27,8 +27,8 @@
"@jambonz/db-helpers": "^0.7.4",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/speech-utils": "^0.0.2",
"@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/verb-specifications": "^0.0.11",
"@opentelemetry/api": "^1.4.0",
@@ -61,7 +61,7 @@
"uuid-random": "^1.3.2",
"verify-aws-sns-signature": "^0.1.0",
"ws": "^8.9.0",
"xml2js": "^0.4.23"
"xml2js": "^0.5.0"
},
"devDependencies": {
"clear-module": "^4.1.2",

View File

@@ -18,30 +18,38 @@ test('creating schema', (t) => {
if (err) return t.end(err);
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);
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({
access_key_id: process.env.AWS_ACCESS_KEY_ID,
secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
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({
region: process.env.MICROSOFT_REGION || 'useast',
api_key: process.env.MICROSOFT_API_KEY || '1234567890'
region: process.env.MICROSOFT_REGION,
api_key: process.env.MICROSOFT_API_KEY
}));
const cmd = `
UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';
UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';
UPDATE speech_credentials SET credential='${microsoft_credential}' WHERE vendor='microsoft';
`;
t.pass('adding microsoft credentials');
sql.push(`UPDATE speech_credentials SET credential='${microsoft_credential}' WHERE vendor='microsoft';`);
}
if (sql.length > 0) {
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) => {
console.log(stdout);
console.log(stderr);
if (err) return t.end(err);
fs.unlinkSync(path)
fs.writeFileSync(`${__dirname}/credentials/gcp.json`, process.env.GCP_JSON_KEY);
t.pass('set account-level speech credentials');
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) => {
if (err) return t.end(err);
t.pass('database successfully dropped');
fs.unlinkSync(`${__dirname}/credentials/gcp.json`);
t.end();
});
});