mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-10 08:21:33 +00:00
Compare commits
32 Commits
v0.8.0-rc4
...
v0.7.8-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e7c8f207a | ||
|
|
3397e1cab5 | ||
|
|
e7dbfe755d | ||
|
|
e2ad0dca0e | ||
|
|
e2c99609bf | ||
|
|
4d54aa2666 | ||
|
|
a076fc43b5 | ||
|
|
8592a71978 | ||
|
|
00462b2fd9 | ||
|
|
7c85d6aeca | ||
|
|
cc87b205a2 | ||
|
|
fff556a6c8 | ||
|
|
bb4ca8e467 | ||
|
|
46302703da | ||
|
|
c728417581 | ||
|
|
8853f84f01 | ||
|
|
665d26b6fb | ||
|
|
d69c773de0 | ||
|
|
21eaa442b2 | ||
|
|
6484086222 | ||
|
|
01645df920 | ||
|
|
b2363b09c1 | ||
|
|
c11d892f0a | ||
|
|
9fd116b05f | ||
|
|
19098aee98 | ||
|
|
d15dbf7f5a | ||
|
|
824f983955 | ||
|
|
7c76bc52f6 | ||
|
|
bfc8a99950 | ||
|
|
9097c6d6ac | ||
|
|
15b2fdd5a8 | ||
|
|
979e17c814 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: lts/*
|
||||
- run: npm ci
|
||||
- run: npm run jslint
|
||||
- run: docker pull drachtio/sipp
|
||||
@@ -20,5 +20,3 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
MICROSOFT_REGION: ${{ secrets.MICROSOFT_REGION }}
|
||||
MICROSOFT_API_KEY: ${{ secrets.MICROSOFT_API_KEY }}
|
||||
@@ -41,7 +41,7 @@ function retrieveCallSession(callSid, opts) {
|
||||
router.post('/:callSid', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const callSid = req.params.callSid;
|
||||
logger.debug({body: req.body}, 'got upateCall request');
|
||||
logger.debug({body: req.body}, 'got updateCall request');
|
||||
try {
|
||||
const cs = retrieveCallSession(callSid, req.body);
|
||||
if (!cs) {
|
||||
|
||||
@@ -63,7 +63,6 @@ class CallSession extends Emitter {
|
||||
assert(rootSpan);
|
||||
|
||||
this._recordState = RecordState.RecordingOff;
|
||||
this._notifyEvents = false;
|
||||
|
||||
this.tmpFiles = new Set();
|
||||
|
||||
@@ -82,18 +81,12 @@ class CallSession extends Emitter {
|
||||
|
||||
this._pool = srf.locals.dbHelpers.pool;
|
||||
|
||||
const handover = (newRequestor) => {
|
||||
this.logger.info(`handover to new base url ${newRequestor.url}`);
|
||||
this.requestor.removeAllListeners();
|
||||
this.application.requestor = newRequestor;
|
||||
this.requestor.on('command', this._onCommand.bind(this));
|
||||
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
|
||||
this.requestor.on('handover', handover.bind(this));
|
||||
};
|
||||
|
||||
this.requestor.on('command', this._onCommand.bind(this));
|
||||
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
|
||||
this.requestor.on('handover', handover.bind(this));
|
||||
this.requestor.on('handover', (newRequestor) => {
|
||||
this.logger.info(`handover to new base url ${newRequestor.url}`);
|
||||
this.application.requestor = newRequestor;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,10 +107,6 @@ class CallSession extends Emitter {
|
||||
return this.callInfo.applicationSid;
|
||||
}
|
||||
|
||||
get callStatus() {
|
||||
return this.callInfo.callStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* SIP call-id for the call
|
||||
*/
|
||||
@@ -276,9 +265,6 @@ class CallSession extends Emitter {
|
||||
|
||||
get recordState() { return this._recordState; }
|
||||
|
||||
get notifyEvents() { return this._notifyEvents; }
|
||||
set notifyEvents(notify) { this._notifyEvents = !!notify; }
|
||||
|
||||
set globalSttHints({hints, hintsBoost}) {
|
||||
this._globalSttHints = {hints, hintsBoost};
|
||||
}
|
||||
@@ -463,10 +449,6 @@ 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]);
|
||||
this._bargeInEnabled = true;
|
||||
@@ -584,28 +566,6 @@ class CallSession extends Emitter {
|
||||
api_key: credential.api_key
|
||||
};
|
||||
}
|
||||
else if ('nuance' === vendor) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
client_id: credential.client_id,
|
||||
secret: credential.secret
|
||||
};
|
||||
}
|
||||
else if ('deepgram' === vendor) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
api_key: credential.api_key
|
||||
};
|
||||
}
|
||||
else if ('ibm' === vendor) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
tts_api_key: credential.tts_api_key,
|
||||
tts_region: credential.tts_region,
|
||||
stt_api_key: credential.stt_api_key,
|
||||
stt_region: credential.stt_region
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
writeAlerts({
|
||||
@@ -630,7 +590,6 @@ class CallSession extends Emitter {
|
||||
const stackNum = this.stackIdx;
|
||||
const task = this.tasks.shift();
|
||||
this.logger.info(`CallSession:exec starting task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
this._notifyTaskStatus(task, {event: 'starting'});
|
||||
try {
|
||||
const resources = await this._evaluatePreconditions(task);
|
||||
let skip = false;
|
||||
@@ -654,7 +613,6 @@ class CallSession extends Emitter {
|
||||
}
|
||||
this.currentTask = null;
|
||||
this.logger.info(`CallSession:exec completed task #${stackNum}:${taskNum}: ${task.name}`);
|
||||
this._notifyTaskStatus(task, {event: 'finished'});
|
||||
} catch (err) {
|
||||
task.span?.end();
|
||||
this.currentTask = null;
|
||||
@@ -667,7 +625,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === this.tasks.length && this.requestor instanceof WsRequestor && !this.callGone) {
|
||||
if (0 === this.tasks.length && this.hasStableDialog && this.requestor instanceof WsRequestor) {
|
||||
let span;
|
||||
try {
|
||||
const {span} = this.rootSpan.startChildSpan('waiting for commands');
|
||||
@@ -678,7 +636,7 @@ class CallSession extends Emitter {
|
||||
'async.request.command': command
|
||||
});
|
||||
span.end();
|
||||
if (this.callGone) break;
|
||||
if (!this.hasStableDialog || this.callGone) break;
|
||||
} catch (err) {
|
||||
span.end();
|
||||
this.logger.info(err, 'CallSession:exec - error waiting for new commands');
|
||||
@@ -1223,6 +1181,11 @@ class CallSession extends Emitter {
|
||||
});
|
||||
//ep.cs = this;
|
||||
this.ep = ep;
|
||||
ep.set({
|
||||
hangup_after_bridge: false,
|
||||
park_after_bridge: true
|
||||
}).catch((err) => this.logger.error({err}, 'Error setting park_after_bridge'));
|
||||
|
||||
this.logger.debug(`allocated endpoint ${ep.uuid}`);
|
||||
|
||||
this.ep.on('destroy', () => {
|
||||
@@ -1295,6 +1258,7 @@ class CallSession extends Emitter {
|
||||
return;
|
||||
}
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||
await this.ep.set('hangup_after_bridge', false);
|
||||
|
||||
await this.dlg.modify(this.ep.local.sdp);
|
||||
this.logger.debug('CallSession:replaceEndpoint completed');
|
||||
@@ -1432,6 +1396,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
if (!this.ep) {
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
|
||||
await this.ep.set('hangup_after_bridge', false);
|
||||
}
|
||||
return {ms: this.ms, ep: this.ep};
|
||||
}
|
||||
@@ -1637,25 +1602,6 @@ class CallSession extends Emitter {
|
||||
.catch((err) => this.logger.error(err, 'redis error'));
|
||||
}
|
||||
|
||||
/**
|
||||
* notifyTaskError - only used when websocket connection is used instead of webhooks
|
||||
*/
|
||||
|
||||
_notifyTaskError(obj) {
|
||||
if (this.requestor instanceof WsRequestor) {
|
||||
this.requestor.request('jambonz:error', '/error', obj)
|
||||
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskError - Error sending'));
|
||||
}
|
||||
}
|
||||
|
||||
_notifyTaskStatus(task, evt) {
|
||||
if (this.notifyEvents && this.requestor instanceof WsRequestor) {
|
||||
const obj = {...evt, id: task.id, name: task.name};
|
||||
this.requestor.request('verb:status', '/status', obj)
|
||||
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskStatus - Error sending'));
|
||||
}
|
||||
}
|
||||
|
||||
_awaitCommandsOrHangup() {
|
||||
assert(!this.wakeupResolver);
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -108,10 +108,6 @@ class Conference extends Task {
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
this.logger.info(`Conference:kill ${this.confName}`);
|
||||
if (this._playSession) {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
this.emitter.emit('kill');
|
||||
await this._doFinalMemberCheck(cs);
|
||||
if (this.ep && this.ep.connected) this.ep.conn.removeAllListeners('esl::event::CUSTOM::*') ;
|
||||
@@ -431,19 +427,13 @@ class Conference extends Task {
|
||||
.catch((err) => this.logger.info({err}, 'Error deafing or undeafing participant'));
|
||||
}
|
||||
|
||||
if (wait_hook) {
|
||||
if (this.wait_hook)
|
||||
delete this.wait_hook.url;
|
||||
this.wait_hook = {url: wait_hook};
|
||||
}
|
||||
|
||||
if (hookOnly && this._playSession) {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
if (this.wait_hook?.url && this.conf_hold_status === 'hold') {
|
||||
if (wait_hook && this.conf_hold_status === 'hold') {
|
||||
const {dlg} = cs;
|
||||
this._doWaitHookWhileOnHold(cs, dlg, this.wait_hook);
|
||||
this._doWaitHookWhileOnHold(cs, dlg, wait_hook);
|
||||
}
|
||||
else if (this.conf_hold_status !== 'hold' && this._playSession) {
|
||||
this._playSession.kill();
|
||||
@@ -454,9 +444,7 @@ class Conference extends Task {
|
||||
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
|
||||
do {
|
||||
try {
|
||||
let tasks = [];
|
||||
if (wait_hook.url)
|
||||
tasks = await this._playHook(cs, dlg, wait_hook.url);
|
||||
const tasks = await this._playHook(cs, dlg, wait_hook);
|
||||
if (0 === tasks.length) break;
|
||||
} catch (err) {
|
||||
if (!this.killed) {
|
||||
@@ -583,10 +571,6 @@ class Conference extends Task {
|
||||
*/
|
||||
_kicked(cs, dlg) {
|
||||
this.logger.info(`Conference:kicked - I was dropped from conference ${this.confName}, task is complete`);
|
||||
if (this._playSession) {
|
||||
this._playSession.kill();
|
||||
this._playSession = null;
|
||||
}
|
||||
this.replaceEndpointAndEnd(cs);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@ class TaskConfig extends Task {
|
||||
'record'
|
||||
].forEach((k) => this[k] = this.data[k] || {});
|
||||
|
||||
if ('notifyEvents' in this.data) {
|
||||
this.notifyEvents = !!this.data.notifyEvents;
|
||||
}
|
||||
|
||||
if (this.bargeIn.enable) {
|
||||
this.gatherOpts = {
|
||||
verb: 'gather',
|
||||
@@ -55,19 +51,12 @@ class TaskConfig extends Task {
|
||||
phrase.push(`set recognizer${s}`);
|
||||
}
|
||||
if (this.data.amd) phrase.push('enable amd');
|
||||
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||
return `${this.name}{${phrase.join(',')}`;
|
||||
return `${this.name}{${phrase.join(',')}`;
|
||||
}
|
||||
|
||||
async exec(cs, {ep} = {}) {
|
||||
await super.exec(cs);
|
||||
|
||||
if (this.notifyEvents) {
|
||||
this.logger.debug(`turning event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||
cs.notifyEvents = !!this.data.notifEvents;
|
||||
|
||||
}
|
||||
|
||||
if (this.data.amd) {
|
||||
this.startAmd = cs.startAmd;
|
||||
this.stopAmd = cs.stopAmd;
|
||||
|
||||
@@ -3,24 +3,25 @@ const {
|
||||
TaskName,
|
||||
TaskPreconditions,
|
||||
GoogleTranscriptionEvents,
|
||||
NuanceTranscriptionEvents,
|
||||
AwsTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
DeepgramTranscriptionEvents,
|
||||
IbmTranscriptionEvents
|
||||
AzureTranscriptionEvents
|
||||
} = require('../utils/constants');
|
||||
|
||||
const makeTask = require('./make_task');
|
||||
const assert = require('assert');
|
||||
//const GATHER_STABILITY_THRESHOLD = Number(process.env.JAMBONZ_GATHER_STABILITY_THRESHOLD || 0.7);
|
||||
|
||||
const compileTranscripts = (logger, evt, arr) => {
|
||||
//logger.debug({arr, evt}, 'compile transcripts');
|
||||
if (!Array.isArray(arr) || arr.length === 0) return;
|
||||
let t = '';
|
||||
for (const a of arr) {
|
||||
//logger.debug(`adding ${a.alternatives[0].transcript}`);
|
||||
t += ` ${a.alternatives[0].transcript}`;
|
||||
}
|
||||
t += ` ${evt.alternatives[0].transcript}`;
|
||||
evt.alternatives[0].transcript = t.trim();
|
||||
//logger.debug(`compiled transcript: ${evt.alternatives[0].transcript}`);
|
||||
};
|
||||
|
||||
class TaskGather extends Task {
|
||||
@@ -28,16 +29,6 @@ class TaskGather extends Task {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
const {
|
||||
setChannelVarsForStt,
|
||||
normalizeTranscription,
|
||||
removeSpeechListeners,
|
||||
setSpeechCredentialsAtRuntime
|
||||
} = require('../utils/transcription-utils')(logger);
|
||||
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||
this.normalizeTranscription = normalizeTranscription;
|
||||
this.removeSpeechListeners = removeSpeechListeners;
|
||||
|
||||
[
|
||||
'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
|
||||
@@ -56,19 +47,48 @@ class TaskGather extends Task {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
this.language = recognizer.language;
|
||||
|
||||
/* let credentials be supplied in the recognizer object at runtime */
|
||||
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
||||
this.hints = recognizer.hints || [];
|
||||
this.hintsBoost = recognizer.hintsBoost;
|
||||
this.profanityFilter = recognizer.profanityFilter;
|
||||
this.punctuation = !!recognizer.punctuation;
|
||||
this.enhancedModel = !!recognizer.enhancedModel;
|
||||
this.model = recognizer.model || 'command_and_search';
|
||||
this.words = !!recognizer.words;
|
||||
this.singleUtterance = recognizer.singleUtterance || true;
|
||||
this.diarization = !!recognizer.diarization;
|
||||
this.diarizationMinSpeakers = recognizer.diarizationMinSpeakers || 0;
|
||||
this.diarizationMaxSpeakers = recognizer.diarizationMaxSpeakers || 0;
|
||||
this.interactionType = recognizer.interactionType || 'unspecified';
|
||||
this.naicsCode = recognizer.naicsCode || 0;
|
||||
this.altLanguages = recognizer.altLanguages || [];
|
||||
|
||||
/* continuous ASR (i.e. compile transcripts until a special timeout or dtmf key) */
|
||||
this.asrTimeout = typeof recognizer.asrTimeout === 'number' ? recognizer.asrTimeout * 1000 : 0;
|
||||
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit;
|
||||
this.isContinuousAsr = this.asrTimeout > 0;
|
||||
|
||||
this.data.recognizer.hints = this.data.recognizer.hints || [];
|
||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || [];
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
||||
this.vad = {enable, voiceMs, mode};
|
||||
|
||||
/* aws options */
|
||||
this.vocabularyName = recognizer.vocabularyName;
|
||||
this.vocabularyFilterName = recognizer.vocabularyFilterName;
|
||||
this.filterMethod = recognizer.filterMethod;
|
||||
|
||||
/* microsoft options */
|
||||
this.outputFormat = recognizer.outputFormat || 'simple';
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
|
||||
this.azureSttEndpointId = recognizer.azureSttEndpointId;
|
||||
this.azureAudioLogging = recognizer.audioLogging;
|
||||
}
|
||||
else {
|
||||
this.hints = [];
|
||||
this.altLanguages = [];
|
||||
}
|
||||
else this.data.recognizer = {hints: [], altLanguages: []};
|
||||
|
||||
this.digitBuffer = '';
|
||||
this._earlyMedia = this.data.earlyMedia === true;
|
||||
@@ -115,22 +135,29 @@ class TaskGather extends Task {
|
||||
this.logger.debug('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) {
|
||||
const {hints, hintsBoost} = cs.globalSttHints;
|
||||
this.data.recognizer.hints = this.data.recognizer.hints.concat(hints);
|
||||
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.hints = this.hints.concat(hints);
|
||||
if (!this.hintsBoost && hintsBoost) this.hintsBoost = hintsBoost;
|
||||
this.logger.debug({hints: this.hints, hintsBoost: this.hintsBoost},
|
||||
'Gather:exec - applying global sttHints');
|
||||
}
|
||||
if (process.env.JAMBONZ_GATHER_EARLY_HINTS_MATCH &&
|
||||
!this.isContinuousAsr &&
|
||||
this.hints.length > 0 && this.hints.length <= 10) {
|
||||
this.earlyHintsMatch = true;
|
||||
this.interim = true;
|
||||
this.logger.debug('Gather:exec - early hints match enabled');
|
||||
}
|
||||
|
||||
if (cs.hasAltLanguages) {
|
||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
|
||||
this.altLanguages = this.altLanguages.concat(cs.altLanguages);
|
||||
this.logger.debug({altLanguages: this.altLanguages},
|
||||
'Gather:exec - applying altLanguages');
|
||||
}
|
||||
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
||||
this.data.recognizer.punctuation = cs.globalSttPunctuation;
|
||||
if (cs.hasGlobalSttPunctuation) {
|
||||
this.punctuation = cs.globalSttPunctuation;
|
||||
}
|
||||
if (!this.isContinuousAsr && cs.isContinuousAsr) {
|
||||
this.isContinuousAsr = true;
|
||||
@@ -144,10 +171,7 @@ class TaskGather extends Task {
|
||||
this.ep = ep;
|
||||
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
|
||||
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
|
||||
if (!this.data.recognizer.vendor) {
|
||||
this.data.recognizer.vendor = this.vendor;
|
||||
}
|
||||
if (this.needsStt && !this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
if (this.needsStt && !this.sttCredentials) {
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
|
||||
@@ -160,38 +184,17 @@ 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;
|
||||
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
|
||||
this.logger.debug({client_id}, `Gather:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
this.sttCredentials = {...this.sttCredentials, access_token};
|
||||
}
|
||||
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
|
||||
/* get ibm access token */
|
||||
const {stt_api_key, stt_region} = this.sttCredentials;
|
||||
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
|
||||
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
|
||||
}
|
||||
const startListening = (cs, ep) => {
|
||||
this._startTimer();
|
||||
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
|
||||
// dont start asr timer until we have a transcription
|
||||
//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) {
|
||||
this.logger.info('Gather:exec - task was quickly killed so do not transcribe');
|
||||
return;
|
||||
}
|
||||
this._startTranscribing(ep);
|
||||
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error({err}, 'error in initSpeech');
|
||||
});
|
||||
.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -205,15 +208,7 @@ class TaskGather extends Task {
|
||||
span.end();
|
||||
if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
|
||||
this.logger.debug('Gather: nested say task completed');
|
||||
if (!this.killed) {
|
||||
startListening(cs, ep);
|
||||
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
|
||||
this.logger.debug('Gather:exec - starting transcription timers after say completes');
|
||||
ep.startTranscriptionTimers((err) => {
|
||||
if (err) this.logger.error({err}, 'Gather:exec - error starting transcription timers');
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!this.killed) startListening(cs, ep);
|
||||
});
|
||||
}
|
||||
else if (this.playTask) {
|
||||
@@ -225,24 +220,10 @@ class TaskGather extends Task {
|
||||
span.end();
|
||||
if (err) this.logger.error({err}, 'Gather:exec Error playing url');
|
||||
this.logger.debug('Gather: nested play task completed');
|
||||
if (!this.killed) {
|
||||
startListening(cs, ep);
|
||||
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
|
||||
this.logger.debug('Gather:exec - starting transcription timers after play completes');
|
||||
ep.startTranscriptionTimers((err) => {
|
||||
if (err) this.logger.error({err}, 'Gather:exec - error starting transcription timers');
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!this.killed) startListening(cs, ep);
|
||||
});
|
||||
}
|
||||
else {
|
||||
if (this.killed) {
|
||||
this.logger.info('Gather:exec - task was immediately killed so do not transcribe');
|
||||
return;
|
||||
}
|
||||
startListening(cs, ep);
|
||||
}
|
||||
else startListening(cs, ep);
|
||||
|
||||
if (this.input.includes('speech') && this.listenDuringPrompt) {
|
||||
await this._initSpeech(cs, ep);
|
||||
@@ -259,7 +240,14 @@ class TaskGather extends Task {
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'TaskGather:exec error');
|
||||
}
|
||||
this.removeSpeechListeners(ep);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.VadDetected);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.VadDetected);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.VadDetected);
|
||||
}
|
||||
|
||||
kill(cs) {
|
||||
@@ -280,6 +268,7 @@ class TaskGather extends Task {
|
||||
const {timeout} = opts;
|
||||
this.timeout = timeout;
|
||||
this._startTimer();
|
||||
return true;
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
@@ -318,70 +307,107 @@ 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';
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
break;
|
||||
const opts = {};
|
||||
|
||||
case 'aws':
|
||||
case 'polly':
|
||||
this.bugname = 'aws_transcribe';
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
break;
|
||||
case 'microsoft':
|
||||
this.bugname = 'azure_transcribe';
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected,
|
||||
this._onNoSpeechDetected.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
break;
|
||||
case 'nuance':
|
||||
this.bugname = 'nuance_transcribe';
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech,
|
||||
this._onStartOfSpeech.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
|
||||
this._onTranscriptionComplete.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.VadDetected,
|
||||
this._onVadDetected.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.Error,
|
||||
this._onNuanceError.bind(this, cs, ep));
|
||||
|
||||
/* stall timers until prompt finishes playing */
|
||||
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
|
||||
opts.NUANCE_STALL_TIMERS = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deepgram':
|
||||
this.bugname = 'deepgram_transcribe';
|
||||
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(DeepgramTranscriptionEvents.Connect, this._onDeepgramConnect.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure,
|
||||
this._onDeepGramConnectFailure.bind(this, cs, ep));
|
||||
break;
|
||||
|
||||
case 'ibm':
|
||||
this.bugname = 'ibm_transcribe';
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.Connect, this._onIbmConnect.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
|
||||
this._onIbmConnectFailure.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.Error,
|
||||
this._onIbmError.bind(this, cs, ep));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid vendor ${this.vendor}`);
|
||||
if (this.vad?.enable) {
|
||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
||||
else opts.RECOGNIZER_VAD_VOICE_MS = 125;
|
||||
if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
|
||||
}
|
||||
|
||||
if ('google' === this.vendor) {
|
||||
this.bugname = 'google_transcribe';
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
[
|
||||
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
|
||||
['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
|
||||
['profanityFilter', 'GOOGLE_SPEECH_PROFANITY_FILTER'],
|
||||
['punctuation', 'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION'],
|
||||
['words', 'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS'],
|
||||
['singleUtterance', 'GOOGLE_SPEECH_SINGLE_UTTERANCE'],
|
||||
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
|
||||
].forEach((arr) => {
|
||||
if (this[arr[0]]) opts[arr[1]] = true;
|
||||
else if (this[arr[0]] === false) opts[arr[1]] = false;
|
||||
});
|
||||
if (this.hints.length > 0) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
||||
if (typeof this.hintsBoost === 'number') {
|
||||
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
|
||||
}
|
||||
}
|
||||
if (this.altLanguages.length > 0) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
else opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
|
||||
if ('unspecified' !== this.interactionType) {
|
||||
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
|
||||
}
|
||||
opts.GOOGLE_SPEECH_MODEL = this.model;
|
||||
if (this.diarization && this.diarizationMinSpeakers > 0) {
|
||||
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT = this.diarizationMinSpeakers;
|
||||
}
|
||||
if (this.diarization && this.diarizationMaxSpeakers > 0) {
|
||||
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT = this.diarizationMaxSpeakers;
|
||||
}
|
||||
if (this.naicsCode > 0) opts.GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE = this.naicsCode;
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
}
|
||||
else if (['aws', 'polly'].includes(this.vendor)) {
|
||||
this.bugname = 'aws_transcribe';
|
||||
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
|
||||
if (this.vocabularyFilterName) {
|
||||
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
|
||||
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
|
||||
}
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
|
||||
AWS_REGION: this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
}
|
||||
else if ('microsoft' === this.vendor) {
|
||||
this.bugname = 'azure_transcribe';
|
||||
if (this.sttCredentials) {
|
||||
const {api_key, region, use_custom_stt, custom_stt_endpoint} = this.sttCredentials;
|
||||
|
||||
Object.assign(opts, {
|
||||
'AZURE_SUBSCRIPTION_KEY': api_key,
|
||||
'AZURE_REGION': region
|
||||
});
|
||||
if (this.azureSttEndpointId) {
|
||||
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': this.azureSttEndpointId});
|
||||
}
|
||||
else if (use_custom_stt && custom_stt_endpoint) {
|
||||
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': custom_stt_endpoint});
|
||||
}
|
||||
}
|
||||
if (this.hints && this.hints.length > 0) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.altLanguages && this.altLanguages.length > 0) {
|
||||
opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
}
|
||||
else {
|
||||
opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
|
||||
}
|
||||
if (this.azureAudioLogging) opts.AZURE_AUDIO_LOGGING = 1;
|
||||
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
||||
if (this.profanityOption && this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
|
||||
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
|
||||
else if (this.timeout === 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = 120000; // lengthy
|
||||
opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoSpeechDetected.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
|
||||
}
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
}
|
||||
@@ -414,8 +440,7 @@ class TaskGather extends Task {
|
||||
if (0 === this.timeout) return;
|
||||
this._clearTimer();
|
||||
this._timeoutTimer = setTimeout(() => {
|
||||
if (this.isContinuousAsr) this._startAsrTimer();
|
||||
else this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
|
||||
this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
|
||||
}, this.timeout);
|
||||
}
|
||||
|
||||
@@ -481,29 +506,65 @@ class TaskGather extends Task {
|
||||
// make sure this is not a transcript from answering machine detection
|
||||
const bugname = fsEvent.getHeader('media-bugname');
|
||||
const finished = fsEvent.getHeader('transcription-session-finished');
|
||||
this.logger.debug({evt, bugname, finished}, 'Gather:_onTranscription');
|
||||
if (bugname && this.bugname !== bugname) return;
|
||||
|
||||
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language);
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === this.vendor) {
|
||||
const final = evt.RecognitionStatus === 'Success';
|
||||
if (final) {
|
||||
// don't sort based on confidence: https://github.com/Azure-Samples/cognitive-services-speech-sdk/issues/1463
|
||||
//const nbest = evt.NBest.sort((a, b) => b.Confidence - a.Confidence);
|
||||
const nbest = evt.NBest;
|
||||
const language_code = evt.PrimaryLanguage?.Language || this.language;
|
||||
evt = {
|
||||
is_final: true,
|
||||
language_code,
|
||||
alternatives: [
|
||||
{
|
||||
confidence: nbest[0].Confidence,
|
||||
transcript: nbest[0].Display
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
else {
|
||||
evt = {
|
||||
is_final: false,
|
||||
alternatives: [
|
||||
{
|
||||
transcript: evt.Text
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.earlyHintsMatch && evt.is_final === false) {
|
||||
const transcript = evt.alternatives[0].transcript?.toLowerCase();
|
||||
if (this.hints.find((h) => h.toLowerCase() === transcript)) {
|
||||
this.logger.debug({evt}, 'Gather:_onTranscription: early hint match');
|
||||
this._resolve('speech', evt);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* count words for bargein feature */
|
||||
const words = evt.alternatives[0]?.transcript.split(' ').length;
|
||||
const words = evt.alternatives[0].transcript.split(' ').length;
|
||||
const bufferedWords = this._bufferedTranscripts.reduce((count, e) => {
|
||||
return count + e.alternatives[0]?.transcript.split(' ').length;
|
||||
return count + e.alternatives[0].transcript.split(' ').length;
|
||||
}, 0);
|
||||
|
||||
if (evt.is_final) {
|
||||
if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) {
|
||||
if (finished === 'true' && ['microsoft', 'deepgram'].includes(this.vendor)) {
|
||||
if ('microsoft' === this.vendor && finished === 'true') {
|
||||
this.logger.debug({evt}, 'TaskGather:_onTranscription - got empty transcript from old gather, disregarding');
|
||||
}
|
||||
else {
|
||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
|
||||
//this._startTranscribing(ep);
|
||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
|
||||
this._startTranscribing(ep);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isContinuousAsr) {
|
||||
/* append the transcript and start listening again for asrTimeout */
|
||||
const t = evt.alternatives[0].transcript;
|
||||
@@ -570,62 +631,6 @@ class TaskGather extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
_onStartOfSpeech(cs, ep) {
|
||||
this.logger.debug('TaskGather:_onStartOfSpeech');
|
||||
}
|
||||
_onTranscriptionComplete(cs, ep) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
_onDeepgramConnect(_cs, _ep) {
|
||||
this.logger.debug('TaskGather:_onDeepgramConnect');
|
||||
}
|
||||
|
||||
_onDeepGramConnectFailure(cs, _ep, evt) {
|
||||
const {reason} = evt;
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info({evt}, 'TaskGather:_onDeepgramConnectFailure');
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Failed connecting to Deepgram speech recognizer: ${reason}`,
|
||||
vendor: 'deepgram',
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
|
||||
this.notifyError(`Failed connecting to speech vendor deepgram: ${reason}`);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_onIbmConnect(_cs, _ep) {
|
||||
this.logger.debug('TaskGather:_onIbmConnect');
|
||||
}
|
||||
|
||||
_onIbmConnectFailure(cs, _ep, evt) {
|
||||
const {reason} = evt;
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info({evt}, 'TaskGather:_onIbmConnectFailure');
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Failed connecting to IBM watson speech recognizer: ${reason}`,
|
||||
vendor: 'ibm',
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
|
||||
this.notifyError(`Failed connecting to speech vendor IBM: ${reason}`);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_onIbmError(cs, _ep, evt) {
|
||||
this.logger.info({evt}, 'TaskGather:_onIbmError'); }
|
||||
|
||||
_onVadDetected(cs, ep) {
|
||||
if (this.bargein && this.minBargeinWordCount === 0) {
|
||||
this.logger.debug('TaskGather:_onVadDetected');
|
||||
|
||||
@@ -22,20 +22,6 @@ class TaskPlay extends Task {
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
let timeout;
|
||||
let playbackSeconds = 0;
|
||||
let playbackMilliseconds = 0;
|
||||
let completed = !(this.timeoutSecs > 0 || this.loop);
|
||||
if (this.timeoutSecs > 0) {
|
||||
timeout = setTimeout(async() => {
|
||||
completed = true;
|
||||
try {
|
||||
await this.kill(cs);
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'Error killing audio on timeoutSecs');
|
||||
}
|
||||
}, this.timeoutSecs * 1000);
|
||||
}
|
||||
try {
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep.connected) {
|
||||
if (cs.isInConference) {
|
||||
@@ -48,24 +34,14 @@ class TaskPlay extends Task {
|
||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, this.url);
|
||||
}
|
||||
} else {
|
||||
let file = this.url;
|
||||
if (this.seekOffset >= 0) {
|
||||
file = {file: this.url, seekOffset: this.seekOffset};
|
||||
this.seekOffset = -1;
|
||||
}
|
||||
const file = (this.timeoutSecs >= 0 || this.seekOffset >= 0) ?
|
||||
{file: this.url, seekOffset: this.seekOffset, timeoutSecs: this.timeoutSecs} : this.url;
|
||||
const result = await ep.play(file);
|
||||
playbackSeconds += parseInt(result.playbackSeconds);
|
||||
playbackMilliseconds += parseInt(result.playbackMilliseconds);
|
||||
if (this.killed || !this.loop || completed) {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
await this.performAction(
|
||||
Object.assign(result, {reason: 'playCompleted', playbackSeconds, playbackMilliseconds}),
|
||||
!(this.parentTask || cs.isConfirmCallSession));
|
||||
}
|
||||
await this.performAction(Object.assign(result, {reason: 'playCompleted'}),
|
||||
!(this.parentTask || cs.isConfirmCallSession));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
|
||||
}
|
||||
this.emit('playDone');
|
||||
|
||||
@@ -105,7 +105,6 @@ class TaskSay extends Task {
|
||||
this.loop = this.data.loop || 1;
|
||||
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||
this.synthesizer = this.data.synthesizer || {};
|
||||
this.disableTtsCache = this.data.disableTtsCache;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Say; }
|
||||
@@ -131,24 +130,14 @@ class TaskSay extends Task {
|
||||
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
|
||||
this.synthesizer.language :
|
||||
cs.speechSynthesisLanguage ;
|
||||
let voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
|
||||
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
|
||||
this.synthesizer.voice :
|
||||
cs.speechSynthesisVoice;
|
||||
const engine = this.synthesizer.engine || 'standard';
|
||||
const salt = cs.callSid;
|
||||
const credentials = cs.getSpeechCredentials(vendor, 'tts');
|
||||
|
||||
/* parse Nuance voices into name and model */
|
||||
let model;
|
||||
if (vendor === 'nuance' && voice) {
|
||||
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
|
||||
if (arr) {
|
||||
voice = arr[1];
|
||||
model = arr[2];
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
|
||||
this.logger.info({vendor, language, voice}, 'TaskSay:exec');
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (!credentials) {
|
||||
@@ -157,10 +146,7 @@ class TaskSay extends Task {
|
||||
alert_type: AlertType.TTS_NOT_PROVISIONED,
|
||||
vendor
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
||||
this.notifyError({
|
||||
msg: 'TTS error',
|
||||
details:`No speech credentials provisioned for selected vendor ${vendor}`
|
||||
});
|
||||
this.notifyError(`No speech credentials have been provisioned for ${vendor}`);
|
||||
throw new Error('no provisioned speech credentials for TTS');
|
||||
}
|
||||
// synthesize all of the text elements
|
||||
@@ -178,16 +164,18 @@ class TaskSay extends Task {
|
||||
'tts.voice': voice
|
||||
});
|
||||
try {
|
||||
const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
|
||||
if (vendor === 'microsoft' && this.synthesizer.azureServiceEndpoint) {
|
||||
credentials.use_custom_tts = true;
|
||||
credentials.custom_tts_endpoint = this.synthesizer.azureServiceEndpoint;
|
||||
}
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
text,
|
||||
vendor,
|
||||
language,
|
||||
voice,
|
||||
engine,
|
||||
model,
|
||||
salt,
|
||||
credentials,
|
||||
disableTtsCache : this.disableTtsCache
|
||||
credentials
|
||||
});
|
||||
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
|
||||
if (filePath) cs.trackTmpFile(filePath);
|
||||
@@ -198,15 +186,6 @@ class TaskSay extends Task {
|
||||
}
|
||||
span.setAttributes({'tts.cached': servedFromCache});
|
||||
span.end();
|
||||
if (!servedFromCache && rtt) {
|
||||
this.notifyStatus({
|
||||
event: 'synthesized-audio',
|
||||
vendor,
|
||||
language,
|
||||
characters: text.length,
|
||||
elapsedTime: rtt
|
||||
});
|
||||
}
|
||||
return filePath;
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Error synthesizing tts');
|
||||
@@ -217,7 +196,7 @@ class TaskSay extends Task {
|
||||
vendor,
|
||||
detail: err.message
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||
this.notifyError({msg: 'TTS error', details: err.message || err});
|
||||
this.notifyError(err.message || err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -225,7 +204,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) {
|
||||
let segment = 0;
|
||||
@@ -257,7 +235,6 @@ class TaskSay extends Task {
|
||||
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||
}
|
||||
else {
|
||||
this.notifyStatus({event: 'kill-playback'});
|
||||
this.ep.api('uuid_break', this.ep.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"sip:decline": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"status": "number",
|
||||
"reason": "string",
|
||||
"headers": "object"
|
||||
@@ -12,7 +11,6 @@
|
||||
},
|
||||
"sip:request": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"method": "string",
|
||||
"body": "string",
|
||||
"headers": "object",
|
||||
@@ -24,7 +22,6 @@
|
||||
},
|
||||
"sip:refer": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"referTo": "string",
|
||||
"referredBy": "string",
|
||||
"headers": "object",
|
||||
@@ -37,13 +34,11 @@
|
||||
},
|
||||
"config": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"synthesizer": "#synthesizer",
|
||||
"recognizer": "#recognizer",
|
||||
"bargeIn": "#bargeIn",
|
||||
"record": "#recordOptions",
|
||||
"amd": "#amd",
|
||||
"notifyEvents": "boolean"
|
||||
"amd": "#amd"
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
@@ -67,7 +62,6 @@
|
||||
},
|
||||
"dequeue": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"actionHook": "object|string",
|
||||
"timeout": "number",
|
||||
@@ -79,7 +73,6 @@
|
||||
},
|
||||
"enqueue": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"actionHook": "object|string",
|
||||
"waitHook": "object|string",
|
||||
@@ -91,12 +84,11 @@
|
||||
},
|
||||
"leave": {
|
||||
"properties": {
|
||||
"id": "string"
|
||||
|
||||
}
|
||||
},
|
||||
"hangup": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"headers": "object"
|
||||
},
|
||||
"required": [
|
||||
@@ -104,7 +96,6 @@
|
||||
},
|
||||
"play": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"url": "string|array",
|
||||
"loop": "number|string",
|
||||
"earlyMedia": "boolean",
|
||||
@@ -118,12 +109,10 @@
|
||||
},
|
||||
"say": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"text": "string|array",
|
||||
"loop": "number|string",
|
||||
"synthesizer": "#synthesizer",
|
||||
"earlyMedia": "boolean",
|
||||
"disableTtsCache": "boolean"
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
@@ -131,7 +120,6 @@
|
||||
},
|
||||
"gather": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"actionHook": "object|string",
|
||||
"finishOnKey": "string",
|
||||
"input": "array",
|
||||
@@ -155,7 +143,6 @@
|
||||
},
|
||||
"conference": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"beep": "boolean",
|
||||
"startConferenceOnEnter": "boolean",
|
||||
@@ -175,7 +162,6 @@
|
||||
},
|
||||
"dial": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"actionHook": "object|string",
|
||||
"answerOnBridge": "boolean",
|
||||
"callerId": "string",
|
||||
@@ -199,7 +185,6 @@
|
||||
},
|
||||
"dialogflow": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"credentials": "object|string",
|
||||
"project": "string",
|
||||
"environment": "string",
|
||||
@@ -228,7 +213,6 @@
|
||||
},
|
||||
"dtmf": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"dtmf": "string",
|
||||
"duration": "number"
|
||||
},
|
||||
@@ -238,7 +222,6 @@
|
||||
},
|
||||
"lex": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"botId": "string",
|
||||
"botAlias": "string",
|
||||
"credentials": "object",
|
||||
@@ -263,7 +246,6 @@
|
||||
},
|
||||
"listen": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"actionHook": "object|string",
|
||||
"auth": "#auth",
|
||||
"finishOnKey": "string",
|
||||
@@ -288,7 +270,6 @@
|
||||
},
|
||||
"message": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"carrier": "string",
|
||||
"account_sid": "string",
|
||||
"message_sid": "string",
|
||||
@@ -305,7 +286,6 @@
|
||||
},
|
||||
"pause": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"length": "number"
|
||||
},
|
||||
"required": [
|
||||
@@ -314,7 +294,6 @@
|
||||
},
|
||||
"rasa": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"url": "string",
|
||||
"recognizer": "#recognizer",
|
||||
"tts": "#synthesizer",
|
||||
@@ -349,7 +328,6 @@
|
||||
},
|
||||
"redirect": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"actionHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
@@ -358,7 +336,6 @@
|
||||
},
|
||||
"rest:dial": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"account_sid": "string",
|
||||
"application_sid": "string",
|
||||
"call_hook": "object|string",
|
||||
@@ -383,7 +360,6 @@
|
||||
},
|
||||
"tag": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"data": "object"
|
||||
},
|
||||
"required": [
|
||||
@@ -392,7 +368,6 @@
|
||||
},
|
||||
"transcribe": {
|
||||
"properties": {
|
||||
"id": "string",
|
||||
"transcriptionHook": "string",
|
||||
"recognizer": "#recognizer",
|
||||
"earlyMedia": "boolean"
|
||||
@@ -449,7 +424,7 @@
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google", "aws", "polly", "microsoft", "nuance", "ibm", "default"]
|
||||
"enum": ["google", "aws", "polly", "microsoft", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"voice": "string",
|
||||
@@ -460,7 +435,8 @@
|
||||
"gender": {
|
||||
"type": "string",
|
||||
"enum": ["MALE", "FEMALE", "NEUTRAL"]
|
||||
}
|
||||
},
|
||||
"azureServiceEndpoint": "string"
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
@@ -470,7 +446,7 @@
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google", "aws", "microsoft", "nuance", "deepgram", "ibm", "default"]
|
||||
"enum": ["google", "aws", "microsoft", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"vad": "#vad",
|
||||
@@ -535,189 +511,12 @@
|
||||
"azureSttEndpointId": "string",
|
||||
"asrDtmfTerminationDigit": "string",
|
||||
"asrTimeout": "number",
|
||||
"nuanceOptions": "#nuanceOptions",
|
||||
"deepgramOptions": "#deepgramOptions",
|
||||
"ibmOptions": "#ibmOptions"
|
||||
"audioLogging": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
]
|
||||
},
|
||||
"ibmOptions": {
|
||||
"properties": {
|
||||
"sttApiKey": "string",
|
||||
"sttRegion": "string",
|
||||
"ttsApiKey": "string",
|
||||
"ttsRegion": "string",
|
||||
"instanceId": "string",
|
||||
"model": "string",
|
||||
"languageCustomizationId": "string",
|
||||
"acousticCustomizationId": "string",
|
||||
"baseModelVersion": "string",
|
||||
"watsonMetadata": "string",
|
||||
"watsonLearningOptOut": "boolean"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"deepgramOptions": {
|
||||
"properties": {
|
||||
"apiKey": "string",
|
||||
"tier": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enhanced",
|
||||
"base"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"general",
|
||||
"meeting",
|
||||
"phonecall",
|
||||
"voicemail",
|
||||
"finance",
|
||||
"conversationalai",
|
||||
"video",
|
||||
"custom"
|
||||
]
|
||||
},
|
||||
"customModel": "string",
|
||||
"version": "string",
|
||||
"punctuate": "boolean",
|
||||
"profanityFilter": "boolean",
|
||||
"redact": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pci",
|
||||
"numbers",
|
||||
"true",
|
||||
"ssn"
|
||||
]
|
||||
},
|
||||
"diarize": "boolean",
|
||||
"diarizeVersion": "string",
|
||||
"ner": "boolean",
|
||||
"multichannel": "boolean",
|
||||
"alternatives": "number",
|
||||
"numerals": "boolean",
|
||||
"search": "array",
|
||||
"replace": "array",
|
||||
"keywords": "array",
|
||||
"endpointing": "boolean",
|
||||
"vadTurnoff": "number",
|
||||
"tag": "string"
|
||||
}
|
||||
},
|
||||
"nuanceOptions": {
|
||||
"properties": {
|
||||
"clientId": "string",
|
||||
"secret": "string",
|
||||
"kryptonEndpoint": "string",
|
||||
"topic": "string",
|
||||
"utteranceDetectionMode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"single",
|
||||
"multiple",
|
||||
"disabled"
|
||||
]
|
||||
},
|
||||
"punctuation": "boolean",
|
||||
"profanityFilter": "boolean",
|
||||
"includeTokenization": "boolean",
|
||||
"discardSpeakerAdaptation": "boolean",
|
||||
"suppressCallRecording": "boolean",
|
||||
"maskLoadFailures": "boolean",
|
||||
"suppressInitialCapitalization": "boolean",
|
||||
"allowZeroBaseLmWeight": "boolean",
|
||||
"filterWakeupWord": "boolean",
|
||||
"resultType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"final",
|
||||
"partial",
|
||||
"immutable_partial"
|
||||
]
|
||||
},
|
||||
"noInputTimeoutMs": "number",
|
||||
"recognitionTimeoutMs": "number",
|
||||
"utteranceEndSilenceMs": "number",
|
||||
"maxHypotheses": "number",
|
||||
"speechDomain": "string",
|
||||
"formatting": "#formatting",
|
||||
"clientData": "object",
|
||||
"userId": "string",
|
||||
"speechDetectionSensitivity": "number",
|
||||
"resources": ["#resource"]
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"resource": {
|
||||
"properties": {
|
||||
"externalReference": "#resourceReference",
|
||||
"inlineWordset": "string",
|
||||
"builtin": "string",
|
||||
"inlineGrammar": "string",
|
||||
"wakeupWord": "[string]",
|
||||
"weightName": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"defaultWeight",
|
||||
"lowest",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"highest"
|
||||
]
|
||||
},
|
||||
"weightValue": "number",
|
||||
"reuse": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"undefined_reuse",
|
||||
"low_reuse",
|
||||
"high_reuse"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"resourceReference": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"undefined_resource_type",
|
||||
"wordset",
|
||||
"compiled_wordset",
|
||||
"domain_lm",
|
||||
"speaker_profile",
|
||||
"grammar",
|
||||
"settings"
|
||||
]
|
||||
},
|
||||
"uri": "string",
|
||||
"maxLoadFailures": "boolean",
|
||||
"requestTimeoutMs": "number",
|
||||
"headers": "object"
|
||||
},
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"formatting": {
|
||||
"properties": {
|
||||
"scheme": "string",
|
||||
"options": "object"
|
||||
},
|
||||
"required": [
|
||||
"scheme",
|
||||
"options"
|
||||
]
|
||||
},
|
||||
"lexIntent": {
|
||||
"properties": {
|
||||
"name": "string",
|
||||
|
||||
@@ -4,8 +4,6 @@ const debug = require('debug')('jambonz:feature-server');
|
||||
const assert = require('assert');
|
||||
const {TaskPreconditions} = require('../utils/constants');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
const WsRequestor = require('../utils/ws-requestor');
|
||||
const {TaskName} = require('../utils/constants');
|
||||
const {trace} = require('@opentelemetry/api');
|
||||
const specs = new Map();
|
||||
const _specData = require('./specs');
|
||||
@@ -23,7 +21,6 @@ class Task extends Emitter {
|
||||
this.logger = logger;
|
||||
this.data = data;
|
||||
this.actionHook = this.data.actionHook;
|
||||
this.id = data.id;
|
||||
|
||||
this._killInProgress = false;
|
||||
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
||||
@@ -140,32 +137,21 @@ class Task extends Emitter {
|
||||
return this.callSession.normalizeUrl(url, method, auth);
|
||||
}
|
||||
|
||||
notifyError(obj) {
|
||||
if (this.cs.requestor instanceof WsRequestor) {
|
||||
const params = {...obj, verb: this.name, id: this.id};
|
||||
this.cs.requestor.request('jambonz:error', '/error', params)
|
||||
.catch((err) => this.logger.info({err}, 'Task:notifyError error sending error'));
|
||||
}
|
||||
}
|
||||
|
||||
notifyStatus(obj) {
|
||||
if (this.cs.notifyEvents && this.cs.requestor instanceof WsRequestor) {
|
||||
const params = {...obj, verb: this.name, id: this.id};
|
||||
this.cs.requestor.request('verb:status', '/status', params)
|
||||
.catch((err) => this.logger.info({err}, 'Task:notifyStatus error sending error'));
|
||||
}
|
||||
notifyError(errMsg) {
|
||||
const params = {error: errMsg, verb: this.name};
|
||||
this.cs.requestor.request('jambonz:error', '/error', params)
|
||||
.catch((err) => this.logger.info({err}, 'Task:notifyError error sending error'));
|
||||
}
|
||||
|
||||
async performAction(results, expectResponse = true) {
|
||||
if (this.actionHook) {
|
||||
const type = this.name === TaskName.Redirect ? 'session:redirect' : 'verb:hook';
|
||||
const params = results ? Object.assign(this.cs.callInfo.toJSON(), results) : this.cs.callInfo.toJSON();
|
||||
const span = this.startSpan(type, {'hook.url': this.actionHook});
|
||||
const span = this.startSpan('verb:hook', {'hook.url': this.actionHook});
|
||||
const b3 = this.getTracingPropagation('b3', span);
|
||||
const httpHeaders = b3 && {b3};
|
||||
span.setAttributes({'http.body': JSON.stringify(params)});
|
||||
try {
|
||||
const json = await this.cs.requestor.request(type, this.actionHook, params, httpHeaders);
|
||||
const json = await this.cs.requestor.request('verb:hook', this.actionHook, params, httpHeaders);
|
||||
span.setAttributes({'http.statusCode': 200});
|
||||
span.end();
|
||||
if (expectResponse && json && Array.isArray(json)) {
|
||||
|
||||
@@ -4,12 +4,8 @@ const {
|
||||
TaskPreconditions,
|
||||
GoogleTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
AwsTranscriptionEvents,
|
||||
NuanceTranscriptionEvents,
|
||||
DeepgramTranscriptionEvents,
|
||||
IbmTranscriptionEvents
|
||||
AwsTranscriptionEvents
|
||||
} = require('../utils/constants');
|
||||
const normalizeJambones = require('../utils/normalize-jambones');
|
||||
|
||||
class TaskTranscribe extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
@@ -17,16 +13,6 @@ class TaskTranscribe extends Task {
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
this.parentTask = parentTask;
|
||||
|
||||
const {
|
||||
setChannelVarsForStt,
|
||||
normalizeTranscription,
|
||||
removeSpeechListeners,
|
||||
setSpeechCredentialsAtRuntime
|
||||
} = require('../utils/transcription-utils')(logger);
|
||||
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||
this.normalizeTranscription = normalizeTranscription;
|
||||
this.removeSpeechListeners = removeSpeechListeners;
|
||||
|
||||
this.transcriptionHook = this.data.transcriptionHook;
|
||||
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||
|
||||
@@ -36,11 +22,40 @@ class TaskTranscribe extends Task {
|
||||
this.interim = !!recognizer.interim;
|
||||
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
|
||||
|
||||
/* let credentials be supplied in the recognizer object at runtime */
|
||||
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
||||
this.vad = {enable, voiceMs, mode};
|
||||
|
||||
recognizer.hints = recognizer.hints || [];
|
||||
recognizer.altLanguages = recognizer.altLanguages || [];
|
||||
/* google-specific options */
|
||||
this.hints = recognizer.hints || [];
|
||||
this.hintsBoost = recognizer.hintsBoost;
|
||||
this.profanityFilter = recognizer.profanityFilter;
|
||||
this.punctuation = !!recognizer.punctuation;
|
||||
this.enhancedModel = !!recognizer.enhancedModel;
|
||||
this.model = recognizer.model || 'phone_call';
|
||||
this.words = !!recognizer.words;
|
||||
this.singleUtterance = recognizer.singleUtterance || false;
|
||||
this.diarization = !!recognizer.diarization;
|
||||
this.diarizationMinSpeakers = recognizer.diarizationMinSpeakers || 0;
|
||||
this.diarizationMaxSpeakers = recognizer.diarizationMaxSpeakers || 0;
|
||||
this.interactionType = recognizer.interactionType || 'unspecified';
|
||||
this.naicsCode = recognizer.naicsCode || 0;
|
||||
this.altLanguages = recognizer.altLanguages || [];
|
||||
|
||||
/* aws-specific options */
|
||||
this.identifyChannels = !!recognizer.identifyChannels;
|
||||
this.vocabularyName = recognizer.vocabularyName;
|
||||
this.vocabularyFilterName = recognizer.vocabularyFilterName;
|
||||
this.filterMethod = recognizer.filterMethod;
|
||||
|
||||
/* microsoft options */
|
||||
this.outputFormat = recognizer.outputFormat || 'simple';
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
this.azureServiceEndpoint = recognizer.azureServiceEndpoint;
|
||||
this.azureSttEndpointId = recognizer.azureSttEndpointId;
|
||||
this.azureAudioLogging = recognizer.audioLogging;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Transcribe; }
|
||||
@@ -48,32 +63,28 @@ class TaskTranscribe extends Task {
|
||||
async exec(cs, {ep, ep2}) {
|
||||
super.exec(cs);
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
|
||||
|
||||
if (cs.hasGlobalSttHints) {
|
||||
const {hints, hintsBoost} = cs.globalSttHints;
|
||||
this.data.recognizer.hints = this.data.recognizer.hints.concat(hints);
|
||||
if (!this.data.recognizer.hintsBoost && hintsBoost) this.data.recognizer.hintsBoost = hintsBoost;
|
||||
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
|
||||
'Transcribe:exec - applying global sttHints');
|
||||
this.hints = this.hints.concat(hints);
|
||||
if (!this.hintsBoost && hintsBoost) this.hintsBoost = hintsBoost;
|
||||
this.logger.debug({hints: this.hints, hintsBoost: this.hintsBoost},
|
||||
'Transcribe:exec - applying global `sttHints');
|
||||
}
|
||||
if (cs.hasAltLanguages) {
|
||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
|
||||
this.altLanguages = this.altLanguages.concat(cs.altLanguages);
|
||||
this.logger.debug({altLanguages: this.altLanguages},
|
||||
'Transcribe:exec - applying altLanguages');
|
||||
'Gather:exec - applying altLanguages');
|
||||
}
|
||||
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
||||
this.data.recognizer.punctuation = cs.globalSttPunctuation;
|
||||
if (cs.hasGlobalSttPunctuation) {
|
||||
this.punctuation = cs.globalSttPunctuation;
|
||||
}
|
||||
|
||||
this.ep = ep;
|
||||
this.ep2 = ep2;
|
||||
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
|
||||
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
|
||||
if (!this.data.recognizer.vendor) {
|
||||
this.data.recognizer.vendor = this.vendor;
|
||||
}
|
||||
if (!this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
|
||||
try {
|
||||
if (!this.sttCredentials) {
|
||||
@@ -86,22 +97,6 @@ class TaskTranscribe extends Task {
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||
throw new Error('no provisioned speech credentials for TTS');
|
||||
}
|
||||
|
||||
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
|
||||
/* get nuance access token */
|
||||
const {client_id, secret} = this.sttCredentials;
|
||||
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
|
||||
this.logger.debug({client_id},
|
||||
`Transcribe:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
this.sttCredentials = {...this.sttCredentials, access_token};
|
||||
}
|
||||
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
|
||||
/* get ibm access token */
|
||||
const {stt_api_key, stt_region} = this.sttCredentials;
|
||||
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
|
||||
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
|
||||
}
|
||||
await this._startTranscribing(cs, ep, 1);
|
||||
if (this.separateRecognitionPerChannel && ep2) {
|
||||
await this._startTranscribing(cs, ep2, 2);
|
||||
@@ -115,7 +110,14 @@ class TaskTranscribe extends Task {
|
||||
this.logger.info(err, 'TaskTranscribe:exec - error');
|
||||
this.parentTask && this.parentTask.emit('error', err);
|
||||
}
|
||||
this.removeSpeechListeners(ep);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.NoAudioDetected);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
@@ -139,75 +141,125 @@ class TaskTranscribe extends Task {
|
||||
}
|
||||
|
||||
async _startTranscribing(cs, ep, channel) {
|
||||
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
|
||||
switch (this.vendor) {
|
||||
case 'google':
|
||||
this.bugname = 'google_transcribe';
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected,
|
||||
this._onNoAudio.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
||||
break;
|
||||
const opts = {};
|
||||
|
||||
case 'aws':
|
||||
case 'polly':
|
||||
this.bugname = 'aws_transcribe';
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected,
|
||||
this._onNoAudio.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
||||
break;
|
||||
case 'microsoft':
|
||||
this.bugname = 'azure_transcribe';
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected,
|
||||
this._onNoAudio.bind(this, cs, ep, channel));
|
||||
break;
|
||||
case 'nuance':
|
||||
this.bugname = 'nuance_transcribe';
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech,
|
||||
this._onStartOfSpeech.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
|
||||
this._onTranscriptionComplete.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Error,
|
||||
this._onNuanceError.bind(this, cs, ep, channel));
|
||||
break;
|
||||
case 'deepgram':
|
||||
this.bugname = 'deepgram_transcribe';
|
||||
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(DeepgramTranscriptionEvents.Connect,
|
||||
this._onDeepgramConnect.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure,
|
||||
this._onDeepGramConnectFailure.bind(this, cs, ep, channel));
|
||||
break;
|
||||
|
||||
case 'ibm':
|
||||
this.bugname = 'ibm_transcribe';
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.Connect,
|
||||
this._onIbmConnect.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
|
||||
this._onIbmConnectFailure.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(IbmTranscriptionEvents.Error,
|
||||
this._onIbmError.bind(this, cs, ep, channel));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid vendor ${this.vendor}`);
|
||||
if (this.vad.enable) {
|
||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
||||
if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
|
||||
}
|
||||
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep, channel));
|
||||
|
||||
if (this.vendor === 'google') {
|
||||
this.bugname = 'google_transcribe';
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
[
|
||||
['enhancedModel', 'GOOGLE_SPEECH_USE_ENHANCED'],
|
||||
//['separateRecognitionPerChannel', 'GOOGLE_SPEECH_SEPARATE_RECOGNITION_PER_CHANNEL'],
|
||||
['profanityFilter', 'GOOGLE_SPEECH_PROFANITY_FILTER'],
|
||||
['punctuation', 'GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION'],
|
||||
['words', 'GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS'],
|
||||
['singleUtterance', 'GOOGLE_SPEECH_SINGLE_UTTERANCE'],
|
||||
['diarization', 'GOOGLE_SPEECH_PROFANITY_FILTER']
|
||||
].forEach((arr) => {
|
||||
if (this[arr[0]]) opts[arr[1]] = true;
|
||||
else if (this[arr[0]] === false) opts[arr[1]] = false;
|
||||
});
|
||||
if (this.hints.length > 0) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
||||
if (typeof this.hintsBoost === 'number') {
|
||||
opts.GOOGLE_SPEECH_HINTS_BOOST = this.hintsBoost;
|
||||
}
|
||||
}
|
||||
if (this.altLanguages.length > 0) opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
else opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
|
||||
if ('unspecified' !== this.interactionType) {
|
||||
opts.GOOGLE_SPEECH_METADATA_INTERACTION_TYPE = this.interactionType;
|
||||
}
|
||||
opts.GOOGLE_SPEECH_MODEL = this.model;
|
||||
if (this.diarization && this.diarizationMinSpeakers > 0) {
|
||||
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MIN_SPEAKER_COUNT = this.diarizationMinSpeakers;
|
||||
}
|
||||
if (this.diarization && this.diarizationMaxSpeakers > 0) {
|
||||
opts.GOOGLE_SPEECH_SPEAKER_DIARIZATION_MAX_SPEAKER_COUNT = this.diarizationMaxSpeakers;
|
||||
}
|
||||
if (this.naicsCode > 0) opts.GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE = this.naicsCode;
|
||||
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with google'));
|
||||
}
|
||||
else if (this.vendor === 'aws') {
|
||||
this.bugname = 'aws_transcribe';
|
||||
[
|
||||
['diarization', 'AWS_SHOW_SPEAKER_LABEL'],
|
||||
['identifyChannels', 'AWS_ENABLE_CHANNEL_IDENTIFICATION']
|
||||
].forEach((arr) => {
|
||||
if (this[arr[0]]) opts[arr[1]] = true;
|
||||
});
|
||||
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
|
||||
if (this.vocabularyFilterName) {
|
||||
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
|
||||
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
|
||||
}
|
||||
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
|
||||
AWS_REGION: this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
else {
|
||||
Object.assign(opts, {
|
||||
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
AWS_REGION: process.env.AWS_REGION
|
||||
});
|
||||
}
|
||||
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
|
||||
}
|
||||
else if (this.vendor === 'microsoft') {
|
||||
this.bugname = 'azure_transcribe';
|
||||
const {api_key, region, use_custom_stt, custom_stt_endpoint} = this.sttCredentials;
|
||||
Object.assign(opts, {
|
||||
'AZURE_SUBSCRIPTION_KEY': api_key,
|
||||
'AZURE_REGION': region
|
||||
});
|
||||
if (this.azureSttEndpointId) {
|
||||
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': this.azureSttEndpointId});
|
||||
}
|
||||
else if (use_custom_stt && custom_stt_endpoint) {
|
||||
Object.assign(opts, {'AZURE_SERVICE_ENDPOINT_ID': custom_stt_endpoint});
|
||||
}
|
||||
if (this.hints && this.hints.length > 0) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.altLanguages.length > 0) opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
else opts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = '';
|
||||
if (this.azureAudioLogging) opts.AZURE_AUDIO_LOGGING = 1;
|
||||
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
||||
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
|
||||
if (this.outputFormat !== 'simple') opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
if (this.azureServiceEndpoint) opts.AZURE_SERVICE_ENDPOINT = this.azureServiceEndpoint;
|
||||
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with azure'));
|
||||
}
|
||||
await this._transcribe(ep);
|
||||
}
|
||||
|
||||
@@ -221,49 +273,50 @@ class TaskTranscribe extends Task {
|
||||
});
|
||||
}
|
||||
|
||||
async _onTranscription(cs, ep, channel, evt, fsEvent) {
|
||||
_onTranscription(cs, ep, channel, evt, fsEvent) {
|
||||
// make sure this is not a transcript from answering machine detection
|
||||
const bugname = fsEvent.getHeader('media-bugname');
|
||||
if (bugname && this.bugname !== bugname) return;
|
||||
|
||||
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization');
|
||||
this.logger.debug({evt, channel}, 'TaskTranscribe:_onTranscription');
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === this.vendor) {
|
||||
const nbest = evt.NBest;
|
||||
const language_code = evt.PrimaryLanguage?.Language || this.language;
|
||||
const alternatives = nbest ? nbest.map((n) => {
|
||||
return {
|
||||
confidence: n.Confidence,
|
||||
transcript: n.Display
|
||||
};
|
||||
}) :
|
||||
[
|
||||
{
|
||||
transcript: evt.DisplayText || evt.Text
|
||||
}
|
||||
];
|
||||
|
||||
evt = this.normalizeTranscription(evt, this.vendor, channel, this.language);
|
||||
|
||||
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription');
|
||||
|
||||
if (evt.alternatives[0]?.transcript === '' && !cs.callGone && !this.killed) {
|
||||
if (['microsoft', 'deepgram'].includes(this.vendor)) {
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, continue listening');
|
||||
}
|
||||
else {
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, listen again');
|
||||
this._transcribe(ep);
|
||||
}
|
||||
return;
|
||||
const newEvent = {
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
channel,
|
||||
language_code,
|
||||
alternatives
|
||||
};
|
||||
evt = newEvent;
|
||||
}
|
||||
|
||||
if (evt.alternatives[0].transcript === '' && !cs.callGone && !this.killed) {
|
||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, listen again');
|
||||
return this._transcribe(ep);
|
||||
}
|
||||
|
||||
evt.channel_tag = channel;
|
||||
|
||||
if (this.transcriptionHook) {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
try {
|
||||
const json = await this.cs.requestor.request('verb:hook', this.transcriptionHook, {
|
||||
...this.cs.callInfo,
|
||||
...httpHeaders,
|
||||
speech: evt
|
||||
});
|
||||
this.logger.info({json}, 'sent transcriptionHook');
|
||||
if (json && Array.isArray(json) && !this.parentTask) {
|
||||
const makeTask = require('./make_task');
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
if (tasks && tasks.length > 0) {
|
||||
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
|
||||
this.cs.replaceApplication(tasks);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'TranscribeTask:_onTranscription error');
|
||||
}
|
||||
this.cs.requestor.request('verb:hook', this.transcriptionHook,
|
||||
Object.assign({speech: evt}, this.cs.callInfo), httpHeaders)
|
||||
.catch((err) => this.logger.info(err, 'TranscribeTask:_onTranscription error'));
|
||||
}
|
||||
if (this.parentTask) {
|
||||
this.parentTask.emit('transcription', evt);
|
||||
@@ -291,57 +344,6 @@ class TaskTranscribe extends Task {
|
||||
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');
|
||||
}
|
||||
}
|
||||
_onDeepgramConnect(_cs, _ep) {
|
||||
this.logger.debug('TaskTranscribe:_onDeepgramConnect');
|
||||
}
|
||||
|
||||
_onDeepGramConnectFailure(cs, _ep, _channel, evt) {
|
||||
const {reason} = evt;
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onDeepgramConnectFailure');
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Failed connecting to Deepgram speech recognizer: ${reason}`,
|
||||
vendor: 'deepgram',
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
|
||||
this.notifyError(`Failed connecting to speech vendor deepgram: ${reason}`);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_onIbmConnect(_cs, _ep) {
|
||||
this.logger.debug('TaskTranscribe:_onIbmConnect');
|
||||
}
|
||||
|
||||
_onIbmConnectFailure(cs, _ep, _channel, evt) {
|
||||
const {reason} = evt;
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onIbmConnectFailure');
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Failed connecting to IBM watson speech recognizer: ${reason}`,
|
||||
vendor: 'ibm',
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
|
||||
this.notifyError(`Failed connecting to speech vendor IBM: ${reason}`);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
_onIbmError(cs, _ep, _channel, evt) {
|
||||
this.logger.info({evt}, 'TaskGather:_onIbmError');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = TaskTranscribe;
|
||||
|
||||
@@ -67,24 +67,6 @@
|
||||
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded",
|
||||
"VadDetected": "google_transcribe::vad_detected"
|
||||
},
|
||||
"NuanceTranscriptionEvents": {
|
||||
"Transcription": "nuance_transcribe::transcription",
|
||||
"StartOfSpeech": "nuance_transcribe::start_of_speech",
|
||||
"TranscriptionComplete": "nuance_transcribe::end_of_transcription",
|
||||
"Error": "nuance_transcribe::error",
|
||||
"VadDetected": "nuance_transcribe::vad_detected"
|
||||
},
|
||||
"DeepgramTranscriptionEvents": {
|
||||
"Transcription": "deepgram_transcribe::transcription",
|
||||
"ConnectFailure": "deepgram_transcribe::connect_failed",
|
||||
"Connect": "deepgram_transcribe::connect"
|
||||
},
|
||||
"IbmTranscriptionEvents": {
|
||||
"Transcription": "ibm_transcribe::transcription",
|
||||
"ConnectFailure": "ibm_transcribe::connect_failed",
|
||||
"Connect": "ibm_transcribe::connect",
|
||||
"Error": "ibm_transcribe::error"
|
||||
},
|
||||
"AwsTranscriptionEvents": {
|
||||
"Transcription": "aws_transcribe::transcription",
|
||||
"EndOfTranscript": "aws_transcribe::end_of_transcript",
|
||||
|
||||
@@ -46,24 +46,7 @@ const speechMapper = (cred) => {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
}
|
||||
else if ('nuance' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.client_id = o.client_id;
|
||||
obj.secret = o.secret;
|
||||
}
|
||||
else if ('ibm' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.tts_api_key = o.tts_api_key;
|
||||
obj.tts_region = o.tts_region;
|
||||
obj.stt_api_key = o.stt_api_key;
|
||||
obj.stt_region = o.stt_region;
|
||||
}
|
||||
else if ('deepgram' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
@@ -84,10 +67,7 @@ module.exports = (logger, srf) => {
|
||||
const haveAws = speech.find((s) => s.vendor === 'aws');
|
||||
const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
|
||||
const haveWellsaid = speech.find((s) => s.vendor === 'wellsaid');
|
||||
const haveNuance = speech.find((s) => s.vendor === 'nuance');
|
||||
const haveDeepgram = speech.find((s) => s.vendor === 'deepgram');
|
||||
const haveIbm = speech.find((s) => s.vendor === 'ibm');
|
||||
if (!haveGoogle || !haveAws || !haveMicrosoft || !haveWellsaid || !haveNuance) {
|
||||
if (!haveGoogle || !haveAws || !haveMicrosoft) {
|
||||
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
|
||||
if (r3.length) {
|
||||
if (!haveGoogle) {
|
||||
@@ -106,18 +86,6 @@ module.exports = (logger, srf) => {
|
||||
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 (!haveIbm) {
|
||||
const ibm = r3.find((s) => s.vendor === 'ibm');
|
||||
if (ibm) speech.push(speechMapper(ibm));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +96,6 @@ module.exports = (logger, srf) => {
|
||||
};
|
||||
|
||||
const updateSpeechCredentialLastUsed = async(speech_credential_sid) => {
|
||||
if (!speech_credential_sid) return;
|
||||
const pp = pool.promise();
|
||||
const sql = 'UPDATE speech_credentials SET last_used = NOW() WHERE speech_credential_sid = ?';
|
||||
try {
|
||||
|
||||
@@ -2,7 +2,6 @@ const {Client, Pool} = require('undici');
|
||||
const parseUrl = require('parse-url');
|
||||
const assert = require('assert');
|
||||
const BaseRequestor = require('./base-requestor');
|
||||
const WsRequestor = require('./ws-requestor');
|
||||
const {HookMsgTypes} = require('./constants.json');
|
||||
const snakeCaseKeys = require('./snakecase-keys');
|
||||
const pools = new Map();
|
||||
@@ -92,18 +91,6 @@ class HttpRequestor extends BaseRequestor {
|
||||
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
|
||||
const startAt = process.hrtime();
|
||||
|
||||
/* if we have an absolute url, and it is ws then do a websocket connection */
|
||||
if (this._isAbsoluteUrl(url) && url.startsWith('ws')) {
|
||||
this.logger.debug({hook}, 'HttpRequestor: switching to websocket connection');
|
||||
const h = typeof hook === 'object' ? hook : {url: hook};
|
||||
const requestor = new WsRequestor(this.logger, this.account_sid, h, this.secret);
|
||||
if (type === 'session:redirect') {
|
||||
this.close();
|
||||
this.emit('handover', requestor);
|
||||
}
|
||||
return requestor.request('session:new', hook, params, httpHeaders);
|
||||
}
|
||||
|
||||
let newClient;
|
||||
try {
|
||||
let client, path, query;
|
||||
|
||||
@@ -152,9 +152,7 @@ function installSrfLocals(srf, logger) {
|
||||
popFront,
|
||||
removeFromList,
|
||||
lengthOfList,
|
||||
getListPosition,
|
||||
getNuanceAccessToken,
|
||||
getIbmAccessToken,
|
||||
getListPosition
|
||||
} = require('@jambonz/realtimedb-helpers')({
|
||||
host: process.env.JAMBONES_REDIS_HOST,
|
||||
port: process.env.JAMBONES_REDIS_PORT || 6379
|
||||
@@ -206,9 +204,7 @@ function installSrfLocals(srf, logger) {
|
||||
popFront,
|
||||
removeFromList,
|
||||
lengthOfList,
|
||||
getListPosition,
|
||||
getNuanceAccessToken,
|
||||
getIbmAccessToken
|
||||
getListPosition
|
||||
},
|
||||
parentLogger: logger,
|
||||
getSBC,
|
||||
|
||||
@@ -1,419 +1,33 @@
|
||||
const {
|
||||
TaskName,
|
||||
AzureTranscriptionEvents,
|
||||
GoogleTranscriptionEvents,
|
||||
AwsTranscriptionEvents,
|
||||
NuanceTranscriptionEvents,
|
||||
DeepgramTranscriptionEvents,
|
||||
} = require('./constants');
|
||||
|
||||
const normalizeDeepgram = (evt, channel, language) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
const alternatives = (evt.channel?.alternatives || [])
|
||||
.map((alt) => ({
|
||||
confidence: alt.confidence,
|
||||
transcript: alt.transcript,
|
||||
}));
|
||||
|
||||
return {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives,
|
||||
vendor: {
|
||||
name: 'deepgram',
|
||||
evt: copy
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeIbm = (evt, channel, language) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
//const idx = evt.result_index;
|
||||
const result = evt.results[0];
|
||||
|
||||
return {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: result.final,
|
||||
alternatives: result.alternatives,
|
||||
vendor: {
|
||||
name: 'ibm',
|
||||
evt: copy
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeGoogle = (evt, channel, language) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
return {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives: evt.alternatives,
|
||||
vendor: {
|
||||
name: 'google',
|
||||
evt: copy
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeNuance = (evt, channel, language) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
return {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives: evt.alternatives,
|
||||
vendor: {
|
||||
name: 'nuance',
|
||||
evt: copy
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeMicrosoft = (evt, channel, language) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
const nbest = evt.NBest;
|
||||
const language_code = evt.PrimaryLanguage?.Language || language;
|
||||
const alternatives = nbest ? nbest.map((n) => {
|
||||
return {
|
||||
confidence: n.Confidence,
|
||||
transcript: n.Display
|
||||
};
|
||||
}) :
|
||||
[
|
||||
{
|
||||
transcript: evt.DisplayText || evt.Text
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
language_code,
|
||||
channel_tag: channel,
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
alternatives,
|
||||
vendor: {
|
||||
name: 'microsoft',
|
||||
evt: copy
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeAws = (evt, channel, language) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
return {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt[0].is_final,
|
||||
alternatives: evt[0].alternatives,
|
||||
vendor: {
|
||||
name: 'aws',
|
||||
evt: copy
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
module.exports = (logger) => {
|
||||
const normalizeTranscription = (evt, vendor, channel, language) => {
|
||||
const normalizeTranscription = (evt, vendor, channel) => {
|
||||
if ('aws' === vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === vendor) {
|
||||
const nbest = evt.NBest;
|
||||
const language_code = evt.PrimaryLanguage?.Language || this.language;
|
||||
const alternatives = nbest ? nbest.map((n) => {
|
||||
return {
|
||||
confidence: n.Confidence,
|
||||
transcript: n.Display
|
||||
};
|
||||
}) :
|
||||
[
|
||||
{
|
||||
transcript: evt.DisplayText || evt.Text
|
||||
}
|
||||
];
|
||||
|
||||
logger.debug({ evt, vendor, channel, language }, 'normalizeTranscription');
|
||||
switch (vendor) {
|
||||
case 'deepgram':
|
||||
return normalizeDeepgram(evt, channel, language);
|
||||
case 'microsoft':
|
||||
return normalizeMicrosoft(evt, channel, language);
|
||||
case 'google':
|
||||
return normalizeGoogle(evt, channel, language);
|
||||
case 'aws':
|
||||
return normalizeAws(evt, channel, language);
|
||||
case 'nuance':
|
||||
return normalizeNuance(evt, channel, language);
|
||||
case 'ibm':
|
||||
return normalizeIbm(evt, channel, language);
|
||||
default:
|
||||
logger.error(`Unknown vendor ${vendor}`);
|
||||
return evt;
|
||||
const newEvent = {
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
channel,
|
||||
language_code,
|
||||
alternatives
|
||||
};
|
||||
evt = newEvent;
|
||||
}
|
||||
evt.channel_tag = channel;
|
||||
//logger.debug({evt}, 'normalized transcription');
|
||||
return evt;
|
||||
};
|
||||
|
||||
const setChannelVarsForStt = (task, sttCredentials, rOpts = {}) => {
|
||||
let opts = {};
|
||||
const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {};
|
||||
const vad = {enable, voiceMs, mode};
|
||||
|
||||
/* voice activity detection works across vendors */
|
||||
opts = {
|
||||
...opts,
|
||||
...(vad.enable && {START_RECOGNIZING_ON_VAD: 1}),
|
||||
...(vad.enable && vad.voiceMs && {RECOGNIZER_VAD_VOICE_MS: vad.voiceMs}),
|
||||
...(vad.enable && typeof vad.mode === 'number' && {RECOGNIZER_VAD_MODE: vad.mode}),
|
||||
};
|
||||
|
||||
if ('google' === rOpts.vendor) {
|
||||
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}),
|
||||
...(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.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}),
|
||||
...(rOpts.altLanguages.length > 0 &&
|
||||
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: rOpts.altLanguages.join(',')}),
|
||||
...(rOpts.interactionType &&
|
||||
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
|
||||
...{GOOGLE_SPEECH_MODEL: rOpts.model || (task.name === TaskName.Gather ? 'command_and_search' : 'phone_call')},
|
||||
...(rOpts.naicsCode > 0 &&
|
||||
{GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
|
||||
};
|
||||
}
|
||||
else if (['aws', 'polly'].includes(rOpts.vendor)) {
|
||||
opts = {
|
||||
...opts,
|
||||
...(rOpts.vocabularyName && {AWS_VOCABULARY_NAME: rOpts.vocabularyName}),
|
||||
...(rOpts.vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: rOpts.vocabularyFilterName}),
|
||||
...(rOpts.filterMethod && {AWS_VOCABULARY_FILTER_METHOD: rOpts.filterMethod}),
|
||||
...(sttCredentials && {
|
||||
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
|
||||
AWS_REGION: sttCredentials.region
|
||||
}),
|
||||
};
|
||||
}
|
||||
else if ('microsoft' === rOpts.vendor) {
|
||||
opts = {
|
||||
...opts,
|
||||
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.trim()).join(',')}),
|
||||
...(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}),
|
||||
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
|
||||
...(rOpts.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}),
|
||||
...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}),
|
||||
...(rOpts.initialSpeechTimeoutMs > 0 &&
|
||||
{AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}),
|
||||
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
|
||||
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
|
||||
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
|
||||
...(sttCredentials && {
|
||||
AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key,
|
||||
AZURE_REGION: sttCredentials.region,
|
||||
}),
|
||||
...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint &&
|
||||
{AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint})
|
||||
};
|
||||
}
|
||||
else if ('nuance' === rOpts.vendor) {
|
||||
/**
|
||||
* Note: all nuance options are in recognizer.nuanceOptions, should migrate
|
||||
* other vendor settings to similar nested structure
|
||||
*/
|
||||
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},
|
||||
...(nuanceOptions.utteranceDetectionMode) &&
|
||||
{NUANCE_UTTERANCE_DETECTION_MODE: nuanceOptions.utteranceDetectionMode},
|
||||
...(nuanceOptions.punctuation) && {NUANCE_PUNCTUATION: nuanceOptions.punctuation},
|
||||
...(nuanceOptions.profanityFilter) &&
|
||||
{NUANCE_FILTER_PROFANITY: nuanceOptions.profanityFilter},
|
||||
...(nuanceOptions.includeTokenization) &&
|
||||
{NUANCE_INCLUDE_TOKENIZATION: nuanceOptions.includeTokenization},
|
||||
...(nuanceOptions.discardSpeakerAdaptation) &&
|
||||
{NUANCE_DISCARD_SPEAKER_ADAPTATION: nuanceOptions.discardSpeakerAdaptation},
|
||||
...(nuanceOptions.suppressCallRecording) &&
|
||||
{NUANCE_SUPPRESS_CALL_RECORDING: nuanceOptions.suppressCallRecording},
|
||||
...(nuanceOptions.maskLoadFailures) &&
|
||||
{NUANCE_MASK_LOAD_FAILURES: nuanceOptions.maskLoadFailures},
|
||||
...(nuanceOptions.suppressInitialCapitalization) &&
|
||||
{NUANCE_SUPPRESS_INITIAL_CAPITALIZATION: nuanceOptions.suppressInitialCapitalization},
|
||||
...(nuanceOptions.allowZeroBaseLmWeight)
|
||||
&& {NUANCE_ALLOW_ZERO_BASE_LM_WEIGHT: nuanceOptions.allowZeroBaseLmWeight},
|
||||
...(nuanceOptions.filterWakeupWord) &&
|
||||
{NUANCE_FILTER_WAKEUP_WORD: nuanceOptions.filterWakeupWord},
|
||||
...(nuanceOptions.resultType) &&
|
||||
{NUANCE_RESULT_TYPE: nuanceOptions.resultType || rOpts.interim ? 'partial' : 'final'},
|
||||
...(nuanceOptions.noInputTimeoutMs) &&
|
||||
{NUANCE_NO_INPUT_TIMEOUT_MS: nuanceOptions.noInputTimeoutMs},
|
||||
...(nuanceOptions.recognitionTimeoutMs) &&
|
||||
{NUANCE_RECOGNITION_TIMEOUT_MS: nuanceOptions.recognitionTimeoutMs},
|
||||
...(nuanceOptions.utteranceEndSilenceMs) &&
|
||||
{NUANCE_UTTERANCE_END_SILENCE_MS: nuanceOptions.utteranceEndSilenceMs},
|
||||
...(nuanceOptions.maxHypotheses) &&
|
||||
{NUANCE_MAX_HYPOTHESES: nuanceOptions.maxHypotheses},
|
||||
...(nuanceOptions.speechDomain) &&
|
||||
{NUANCE_SPEECH_DOMAIN: nuanceOptions.speechDomain},
|
||||
...(nuanceOptions.formatting) &&
|
||||
{NUANCE_FORMATTING: nuanceOptions.formatting},
|
||||
...(nuanceOptions.resources) &&
|
||||
{NUANCE_RESOURCES: JSON.stringify(nuanceOptions.resources)},
|
||||
};
|
||||
}
|
||||
else if ('deepgram' === rOpts.vendor) {
|
||||
const {deepgramOptions = {}} = rOpts;
|
||||
opts = {
|
||||
...opts,
|
||||
...(sttCredentials.api_key) &&
|
||||
{DEEPGRAM_API_KEY: sttCredentials.api_key},
|
||||
...(deepgramOptions.tier) &&
|
||||
{DEEPGRAM_SPEECH_TIER: deepgramOptions.tier},
|
||||
...(deepgramOptions.model) &&
|
||||
{DEEPGRAM_SPEECH_MODEL: deepgramOptions.model},
|
||||
...(deepgramOptions.punctuate) &&
|
||||
{DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1},
|
||||
...(deepgramOptions.profanityFilter) &&
|
||||
{DEEPGRAM_SPEECH_PROFANITY_FILTER: 1},
|
||||
...(deepgramOptions.redact) &&
|
||||
{DEEPGRAM_SPEECH_REDACT: 1},
|
||||
...(deepgramOptions.diarize) &&
|
||||
{DEEPGRAM_SPEECH_DIARIZE: 1},
|
||||
...(deepgramOptions.diarizeVersion) &&
|
||||
{DEEPGRAM_SPEECH_DIARIZE_VERSION: deepgramOptions.diarizeVersion},
|
||||
...(deepgramOptions.ner) &&
|
||||
{DEEPGRAM_SPEECH_NER: 1},
|
||||
...(deepgramOptions.alternatives) &&
|
||||
{DEEPGRAM_SPEECH_ALTERNATIVES: deepgramOptions.alternatives},
|
||||
...(deepgramOptions.numerals) &&
|
||||
{DEEPGRAM_SPEECH_NUMERALS: deepgramOptions.numerals},
|
||||
...(deepgramOptions.search) &&
|
||||
{DEEPGRAM_SPEECH_SEARCH: deepgramOptions.search.join(',')},
|
||||
...(deepgramOptions.replace) &&
|
||||
{DEEPGRAM_SPEECH_REPLACE: deepgramOptions.replace.join(',')},
|
||||
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.trim()).join(',')}),
|
||||
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.phrase).join(',')}),
|
||||
...(deepgramOptions.keywords) &&
|
||||
{DEEPGRAM_SPEECH_KEYWORDS: deepgramOptions.keywords.join(',')},
|
||||
...('endpointing' in deepgramOptions) &&
|
||||
{DEEPGRAM_SPEECH_ENDPOINTING: deepgramOptions.endpointing},
|
||||
...(deepgramOptions.vadTurnoff) &&
|
||||
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff},
|
||||
...(deepgramOptions.tag) &&
|
||||
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.tag}
|
||||
};
|
||||
}
|
||||
else if ('ibm' === rOpts.vendor) {
|
||||
const {ibmOptions = {}} = rOpts;
|
||||
opts = {
|
||||
...opts,
|
||||
...(sttCredentials.access_token) &&
|
||||
{IBM_ACCESS_TOKEN: sttCredentials.access_token},
|
||||
...(sttCredentials.stt_region) &&
|
||||
{IBM_SPEECH_REGION: sttCredentials.stt_region},
|
||||
...(sttCredentials.instance_id) &&
|
||||
{IBM_SPEECH_INSTANCE_ID: sttCredentials.instance_id},
|
||||
...(ibmOptions.model) &&
|
||||
{IBM_SPEECH_MODEL: ibmOptions.model},
|
||||
...(ibmOptions.language_customization_id) &&
|
||||
{IBM_SPEECH_LANGUAGE_CUSTOMIZATION_ID: ibmOptions.language_customization_id},
|
||||
...(ibmOptions.acoustic_customization_id) &&
|
||||
{IBM_SPEECH_ACOUSTIC_CUSTOMIZATION_ID: ibmOptions.acoustic_customization_id},
|
||||
...(ibmOptions.baseModelVersion) &&
|
||||
{IBM_SPEECH_BASE_MODEL_VERSION: ibmOptions.baseModelVersion},
|
||||
...(ibmOptions.watsonMetadata) &&
|
||||
{IBM_SPEECH_WATSON_METADATA: ibmOptions.watsonMetadata},
|
||||
...(ibmOptions.watsonLearningOptOut) &&
|
||||
{IBM_SPEECH_WATSON_LEARNING_OPT_OUT: ibmOptions.watsonLearningOptOut}
|
||||
};
|
||||
}
|
||||
|
||||
logger.debug({opts}, 'recognizer channel vars');
|
||||
return opts;
|
||||
};
|
||||
|
||||
const removeSpeechListeners = (ep) => {
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.VadDetected);
|
||||
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.VadDetected);
|
||||
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.VadDetected);
|
||||
|
||||
ep.removeCustomEventListener(NuanceTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete);
|
||||
ep.removeCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech);
|
||||
ep.removeCustomEventListener(NuanceTranscriptionEvents.Error);
|
||||
ep.removeCustomEventListener(NuanceTranscriptionEvents.VadDetected);
|
||||
|
||||
ep.removeCustomEventListener(DeepgramTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(DeepgramTranscriptionEvents.Connect);
|
||||
ep.removeCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure);
|
||||
};
|
||||
|
||||
const setSpeechCredentialsAtRuntime = (recognizer) => {
|
||||
if (!recognizer) return;
|
||||
if (recognizer.vendor === 'nuance') {
|
||||
const {clientId, secret} = recognizer.nuanceOptions || {};
|
||||
if (clientId && secret) return {client_id: clientId, secret};
|
||||
}
|
||||
else if (recognizer.vendor === 'deepgram') {
|
||||
const {apiKey} = recognizer.deepgramOptions || {};
|
||||
if (apiKey) return {api_key: apiKey};
|
||||
}
|
||||
else if (recognizer.vendor === 'ibm') {
|
||||
const {ttsApiKey, ttsRegion, sttApiKey, sttRegion, instanceId} = recognizer.ibmOptions || {};
|
||||
if (ttsApiKey || sttApiKey) return {
|
||||
tts_api_key: ttsApiKey,
|
||||
tts_region: ttsRegion,
|
||||
stt_api_key: sttApiKey,
|
||||
stt_region: sttRegion,
|
||||
instance_id: instanceId
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
normalizeTranscription,
|
||||
setChannelVarsForStt,
|
||||
removeSpeechListeners,
|
||||
setSpeechCredentialsAtRuntime
|
||||
};
|
||||
return {normalizeTranscription};
|
||||
};
|
||||
|
||||
@@ -54,8 +54,7 @@ class WsRequestor extends BaseRequestor {
|
||||
/* if we have an absolute url, and it is http then do a standard webhook */
|
||||
if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
|
||||
this.logger.debug({hook}, 'WsRequestor: sending a webhook (HTTP)');
|
||||
const h = typeof hook === 'object' ? hook : {url: hook};
|
||||
const requestor = new HttpRequestor(this.logger, this.account_sid, h, this.secret);
|
||||
const requestor = new HttpRequestor(this.logger, this.account_sid, {url: hook}, this.secret);
|
||||
if (type === 'session:redirect') {
|
||||
this.close();
|
||||
this.emit('handover', requestor);
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -14,16 +14,16 @@
|
||||
"@jambonz/realtimedb-helpers": "^0.6.3",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"@opentelemetry/api": "^1.2.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.7.0",
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.3.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.7.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.3.1",
|
||||
"@opentelemetry/instrumentation": "^0.27.0",
|
||||
"@opentelemetry/resources": "^1.7.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.7.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.7.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.7.0",
|
||||
"aws-sdk": "^2.1233.0",
|
||||
"@opentelemetry/resources": "^1.3.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.3.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.3.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.3.1",
|
||||
"aws-sdk": "^2.1152.0",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
@@ -40,7 +40,7 @@
|
||||
"undici": "^5.11.0",
|
||||
"uuid-random": "^1.3.2",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
"ws": "^8.9.0",
|
||||
"ws": "^8.8.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,7 +48,7 @@
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^5.6.1"
|
||||
"tape": "^5.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
|
||||
22
package.json
22
package.json
@@ -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 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:ClueCon: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"
|
||||
},
|
||||
@@ -29,16 +29,16 @@
|
||||
"@jambonz/realtimedb-helpers": "^0.6.3",
|
||||
"@jambonz/stats-collector": "^0.1.6",
|
||||
"@jambonz/time-series": "^0.2.5",
|
||||
"@opentelemetry/api": "^1.2.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.7.0",
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.3.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.27.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.7.0",
|
||||
"@opentelemetry/exporter-zipkin": "^1.3.1",
|
||||
"@opentelemetry/instrumentation": "^0.27.0",
|
||||
"@opentelemetry/resources": "^1.7.0",
|
||||
"@opentelemetry/sdk-trace-base": "^1.7.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.7.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.7.0",
|
||||
"aws-sdk": "^2.1233.0",
|
||||
"@opentelemetry/resources": "^1.3.1",
|
||||
"@opentelemetry/sdk-trace-base": "^1.3.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.3.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.3.1",
|
||||
"aws-sdk": "^2.1152.0",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
"undici": "^5.11.0",
|
||||
"uuid-random": "^1.3.2",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
"ws": "^8.9.0",
|
||||
"ws": "^8.8.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -63,7 +63,7 @@
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"nyc": "^15.1.0",
|
||||
"tape": "^5.6.1"
|
||||
"tape": "^5.5.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.6",
|
||||
|
||||
@@ -22,17 +22,11 @@ test('creating schema', (t) => {
|
||||
const google_credential = encrypt(process.env.GCP_JSON_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
|
||||
}));
|
||||
const microsoft_credential = encrypt(JSON.stringify({
|
||||
region: process.env.MICROSOFT_REGION || 'useast',
|
||||
api_key: process.env.MICROSOFT_API_KEY || '1234567890'
|
||||
secret_access_key: process.env.AWS_SECRET_ACCESS_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';
|
||||
`;
|
||||
const path = `${__dirname}/.creds.sql`;
|
||||
fs.writeFileSync(path, cmd);
|
||||
|
||||
@@ -614,10 +614,7 @@ CREATE TABLE `speech_credentials` (
|
||||
|
||||
LOCK TABLES `speech_credentials` WRITE;
|
||||
/*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */;
|
||||
INSERT INTO `speech_credentials` VALUES
|
||||
('2add163c-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),
|
||||
('2add347f-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','microsoft','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),
|
||||
('84154212-5c99-4c94-8993-bc2a46288daa',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',1,1,NULL,NULL,NULL,NULL);
|
||||
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),('84154212-5c99-4c94-8993-bc2a46288daa',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',0,0,NULL,NULL,NULL,NULL);
|
||||
/*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
freeswitch:
|
||||
image: drachtio/drachtio-freeswitch-mrf:0.4.18
|
||||
image: drachtio/drachtio-freeswitch-mrf:v1.10.1-full
|
||||
restart: always
|
||||
command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100
|
||||
environment:
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
- /tmp:/tmp
|
||||
- ./credentials:/opt/credentials
|
||||
healthcheck:
|
||||
test: ['CMD', 'fs_cli' ,'-p', 'JambonzR0ck$$', '-x', '"sofia status"']
|
||||
test: ['CMD', 'fs_cli' ,'-x', '"sofia status"']
|
||||
timeout: 5s
|
||||
retries: 15
|
||||
networks:
|
||||
|
||||
@@ -17,11 +17,7 @@ function connect(connectable) {
|
||||
});
|
||||
}
|
||||
|
||||
test('\'gather\' test - google', async(t) => {
|
||||
if (!process.env.GCP_JSON_KEY) {
|
||||
t.pass('skipping google tests');
|
||||
return t.end();
|
||||
}
|
||||
test('\'gather\' and \'transcribe\' tests', async(t) => {
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
@@ -45,168 +41,8 @@ test('\'gather\' test - google', async(t) => {
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
//console.log(JSON.stringify(obj));
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'gather: succeeds when using google credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\'gather\' test - default (google)', async(t) => {
|
||||
if (!process.env.GCP_JSON_KEY) {
|
||||
t.pass('skipping google tests');
|
||||
return t.end();
|
||||
}
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "gather",
|
||||
"input": ["speech"],
|
||||
"timeout": 10,
|
||||
"actionHook": "/actionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
//console.log(JSON.stringify(obj));
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase() === 'i\'d like to speak to customer support',
|
||||
'gather: succeeds when using default (google) credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\'gather\' test - microsoft', async(t) => {
|
||||
if (!process.env.MICROSOFT_REGION || !process.env.MICROSOFT_API_KEY) {
|
||||
t.pass('skipping microsoft tests');
|
||||
return t.end();
|
||||
}
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "gather",
|
||||
"input": ["speech"],
|
||||
"recognizer": {
|
||||
"vendor": "microsoft",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"]
|
||||
},
|
||||
"timeout": 10,
|
||||
"actionHook": "/actionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
//console.log(JSON.stringify(obj));
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'gather: succeeds when using microsoft credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\'gather\' test - aws', async(t) => {
|
||||
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
t.pass('skipping aws tests');
|
||||
return t.end();
|
||||
}
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "gather",
|
||||
"input": ["speech"],
|
||||
"recognizer": {
|
||||
"vendor": "aws",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"]
|
||||
},
|
||||
"timeout": 10,
|
||||
"actionHook": "/actionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
//console.log(JSON.stringify(obj));
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'gather: succeeds when using aws credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\'gather\' test - deepgram', async(t) => {
|
||||
if (!process.env.DEEPGRAM_API_KEY ) {
|
||||
t.pass('skipping deepgram tests');
|
||||
return t.end();
|
||||
}
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "gather",
|
||||
"input": ["speech"],
|
||||
"recognizer": {
|
||||
"vendor": "deepgram",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"],
|
||||
"deepgramOptions": {
|
||||
"apiKey": process.env.DEEPGRAM_API_KEY
|
||||
}
|
||||
},
|
||||
"timeout": 10,
|
||||
"actionHook": "/actionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
//console.log(JSON.stringify(obj));
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'gather: succeeds when using deepgram credentials');
|
||||
t.ok(obj.body.speech.alternatives[0].transcript = 'I\'d like to speak to customer support',
|
||||
'gather: succeeds when using account credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
|
||||
@@ -5,7 +5,6 @@ require('./account-validation-tests');
|
||||
require('./webhooks-tests');
|
||||
require('./say-tests');
|
||||
require('./gather-tests');
|
||||
require('./transcribe-tests');
|
||||
require('./sip-request-tests');
|
||||
require('./create-call-test');
|
||||
require('./play-tests');
|
||||
|
||||
@@ -41,7 +41,7 @@ obj.sippUac = (file, bindAddress, from='sipp', to='16174000000') => {
|
||||
|
||||
if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
|
||||
|
||||
//console.log(args.join(' '));
|
||||
console.log(args.join(' '));
|
||||
clearOutput();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
const test = require('tape');
|
||||
const { sippUac } = require('./sipp')('test_fs');
|
||||
const bent = require('bent');
|
||||
const getJSON = bent('json')
|
||||
const clearModule = require('clear-module');
|
||||
const {provisionCallHook} = require('./utils')
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
function connect(connectable) {
|
||||
return new Promise((resolve, reject) => {
|
||||
connectable.on('connect', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('\'transcribe\' test - google', async(t) => {
|
||||
if (!process.env.GCP_JSON_KEY) {
|
||||
t.pass('skipping google tests');
|
||||
return t.end();
|
||||
}
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "transcribe",
|
||||
"recognizer": {
|
||||
"vendor": "google",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"]
|
||||
},
|
||||
"transcriptionHook": "/transcriptionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'transcribe: succeeds when using google credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\'transcribe\' test - microsoft', async(t) => {
|
||||
if (!process.env.MICROSOFT_REGION || !process.env.MICROSOFT_API_KEY) {
|
||||
t.pass('skipping microsoft tests');
|
||||
return t.end();
|
||||
}
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "transcribe",
|
||||
"recognizer": {
|
||||
"vendor": "microsoft",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"]
|
||||
},
|
||||
"transcriptionHook": "/transcriptionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'transcribe: succeeds when using microsoft credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\'transcribe\' test - aws', async(t) => {
|
||||
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
t.pass('skipping aws tests');
|
||||
return t.end();
|
||||
}
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "transcribe",
|
||||
"recognizer": {
|
||||
"vendor": "aws",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"]
|
||||
},
|
||||
"transcriptionHook": "/transcriptionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'transcribe: succeeds when using aws credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\'transcribe\' test - deepgram', async(t) => {
|
||||
if (!process.env.DEEPGRAM_API_KEY ) {
|
||||
t.pass('skipping deepgram tests');
|
||||
return t.end();
|
||||
}
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "transcribe",
|
||||
"recognizer": {
|
||||
"vendor": "aws",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"],
|
||||
"deepgramOptions": {
|
||||
"apiKey": process.env.DEEPGRAM_API_KEY
|
||||
}
|
||||
},
|
||||
"transcriptionHook": "/transcriptionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'transcribe: succeeds when using deepgram credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
@@ -39,16 +39,7 @@ app.post('/callStatus', (req, res) => {
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
/*
|
||||
* transcriptionHook
|
||||
*/
|
||||
app.post('/transcriptionHook', (req, res) => {
|
||||
console.log({payload: req.body}, 'POST /transcriptionHook');
|
||||
let key = req.body.from + "_actionHook"
|
||||
addRequestToMap(key, req, hook_mapping);
|
||||
return res.json([{"verb": "hangup"}]);
|
||||
});
|
||||
/*
|
||||
* actionHook
|
||||
* action Hook
|
||||
*/
|
||||
app.post('/actionHook', (req, res) => {
|
||||
console.log({payload: req.body}, 'POST /actionHook');
|
||||
|
||||
685
test/webhook/package-lock.json
generated
685
test/webhook/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,6 @@
|
||||
"author": "Dave Horton",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.17.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ module.exports = (serviceName) => {
|
||||
});
|
||||
|
||||
let exporter;
|
||||
if (process.env.OTEL_EXPORTER_JAEGER_AGENT_HOST || process.env.OTEL_EXPORTER_JAEGER_ENDPOINT) {
|
||||
if (process.env.OTEL_EXPORTER_JAEGER_AGENT_HOST) {
|
||||
exporter = new JaegerExporter();
|
||||
}
|
||||
else if (process.env.OTEL_EXPORTER_ZIPKIN_URL) {
|
||||
|
||||
Reference in New Issue
Block a user