Compare commits

...

22 Commits

Author SHA1 Message Date
Dave Horton
7393e3bcb7 standardize on passing .query args as array (#356) 2023-05-22 09:56:05 -04:00
Hoan Luu Huu
32a84b7b19 feat: rest:dial amd (#339)
Add support for sending 'amd' property in createCall REST API and also added support for using any of the speech vendors for STT
---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
2023-05-16 16:20:08 -04:00
Dave Horton
6933e82d46 fix docker build 2023-05-15 14:04:29 -04:00
Dave Horton
fb1801ce11 0.8.3 2023-05-11 10:51:08 -04:00
Dave Horton
09abb23968 minor test change 2023-05-11 10:50:56 -04:00
two56
eb1e0d3bf5 Fix: REST dial timeout (#351)
* Fix #343 by cancelling the request if the session isn't available

* Commit that works for 302's calls

---------

Co-authored-by: Matt Preskett <matt.preskett@netcall.com>
2023-05-11 10:17:39 -04:00
Hoan Luu Huu
3b6c103618 feat: update db-helper and speech-utils (#347) 2023-05-10 08:04:56 -04:00
Dave Horton
feccc0fca7 add support for azure custom voices on a per-say basis (#346) 2023-05-09 13:25:43 -04:00
Hoan Luu Huu
51bcb5a2d2 fix: rivauri (#345)
* fix: rivauri

* fix: rivauri
2023-05-09 10:27:15 -04:00
Dave Horton
7a184a8bbc Fix/tracing cleanup (#342)
* tracing usability

* fix bug in prev commit

* more cleanup

* further tracing UI cleanup
2023-05-08 14:35:07 -04:00
Dave Horton
5043edfd4e addresses #340 and #331 (#341) 2023-05-08 12:23:32 -04:00
Hoan Luu Huu
9948592080 feat: update verb specification version (#330) 2023-04-28 08:44:10 -04:00
Dave Horton
6dc019e836 fix: amd support for language other than en-US (#322) 2023-04-19 15:12:51 -04:00
Dave Horton
a22bc8ea42 fix issue where multiple gathers running simultaneously (#321) 2023-04-18 21:58:30 -04:00
Hoan Luu Huu
0356b996ba fix: wss requestor incase mysql cache is used (#319) 2023-04-18 06:34:39 -04:00
Dave Horton
271587617e update verb specifications 2023-04-13 13:28:21 -04:00
Dave Horton
0b29e67a0c better logging of ws commands 2023-04-13 13:26:52 -04:00
EgleH
e656d275fe update mase image to node:18.15-alpine3.16 (#316) 2023-04-12 16:18:54 -04:00
Hoan Luu Huu
fabf01f8b5 feat: callerName to rest_dial and dial verb (#312)
* feat: callerName to rest_dial and dial verb

* update verb specification
2023-04-12 10:04:55 -04:00
Dave Horton
85ab75d8e3 cherry-pick commit from pr 313 2023-04-12 08:15:32 -04:00
Dave Horton
5c2630fe1f webapp-scaffold can not reference code in higher level app (#314) 2023-04-12 07:54:21 -04:00
Dave Horton
9942313ea1 update drachtio-fsmrf to fix bug in prev commit 2023-04-11 22:55:19 -04:00
24 changed files with 2797 additions and 2264 deletions

View File

@@ -18,7 +18,7 @@ jobs:
- name: prepare tag
id: prepare_tag
run: |
IMAGE_ID=$GITHUB_REPOSITORY
IMAGE_ID=feature-server
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')

View File

@@ -1,4 +1,4 @@
FROM --platform=linux/amd64 node:18.14.1-alpine3.16 as base
FROM --platform=linux/amd64 node:18.15-alpine3.16 as base
RUN apk --update --no-cache add --virtual .builds-deps build-base python3

View File

@@ -5,14 +5,11 @@
"at the tone",
"leave a message",
"leave me a message",
"not available right now",
"not available to take your call",
"not available",
"can't take your call",
"I will get back to you",
"will get back to you",
"I'll get back to you",
"we will get back to you",
"we are unable",
"we are not available"
"we are unable"
],
"es-ES": [
"le pasamos la llamada",
@@ -48,5 +45,18 @@
"ens posarem en contacto",
"ara no estem disponibles",
"no hi som"
],
"de-DE": [
"nicht erreichbar",
"nnruf wurde weitergeleitet",
"beim piepsen",
"am ton",
"eine nachricht hinterlassen",
"hinterlasse mir eine Nachricht",
"nicht verfügbar",
"kann ihren anruf nicht entgegennehmen",
"wird sich bei Ihnen melden",
"ich melde mich bei dir",
"wir können nicht"
]
}

View File

@@ -27,6 +27,7 @@ router.post('/', async(req, res) => {
const target = restDial.to;
const opts = {
callingNumber: restDial.from,
...(restDial.callerName && {callingName: restDial.callerName}),
headers: req.body.headers || {}
};

View File

@@ -248,8 +248,9 @@ module.exports = function(srf, logger) {
const app2 = JAMBONES_MYSQL_REFRESH_TTL ? JSON.parse(JSON.stringify(app)) : app;
if ('WS' === app.call_hook?.method ||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
app2.requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
app2.notifier = app.requestor;
const requestor = new WsRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret) ;
app2.requestor = requestor;
app2.notifier = requestor;
app2.call_hook.method = 'WS';
}
else {

View File

@@ -64,6 +64,16 @@ class CallSession extends Emitter {
this.notifiedComplete = false;
this.rootSpan = rootSpan;
this._origRecognizerSettings = {
vendor: this.application?.speech_recognizer_vendor,
language: this.application?.speech_recognizer_language,
};
this._origSynthesizerSettings = {
vendor: this.application?.speech_synthesis_vendor,
language: this.application?.speech_synthesis_language,
voice: this.application?.speech_synthesis_voice,
};
assert(rootSpan);
this._recordState = RecordState.RecordingOff;
@@ -324,6 +334,22 @@ class CallSession extends Emitter {
return this._globalSttPunctuation !== undefined;
}
resetRecognizer() {
this._globalSttHints = undefined;
this._globalSttPunctuation = undefined;
this._globalAltLanguages = undefined;
this.isContinuousAsr = false;
this.asrDtmfTerminationDigits = undefined;
this.speechRecognizerLanguage = this._origRecognizerSettings.language;
this.speechRecognizerVendor = this._origRecognizerSettings.vendor;
}
resetSynthesizer() {
this.speechSynthesisLanguage = this._origSynthesizerSettings.language;
this.speechSynthesisVendor = this._origSynthesizerSettings.vendor;
this.speechSynthesisVoice = this._origSynthesizerSettings.voice;
}
async notifyRecordOptions(opts) {
const {action} = opts;
this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
@@ -529,7 +555,7 @@ class CallSession extends Emitter {
this.logger.info({currInput, newInput},
'CallSession:enableBotMode - restarting background gather to apply new input type');
this.backgroundGatherTask.sticky = false;
this.disableBotMode();
await this.disableBotMode();
}
}
this.backgroundGatherTask = task;
@@ -568,12 +594,12 @@ class CallSession extends Emitter {
this.logger.info({err, gather}, 'CallSession:enableBotMode - Error creating gather task');
}
}
disableBotMode() {
async disableBotMode() {
this._bargeInEnabled = false;
if (this.backgroundGatherTask) {
try {
this.backgroundGatherTask.removeAllListeners();
this.backgroundGatherTask.kill().catch((err) => {});
await this.backgroundGatherTask.kill();
} catch (err) {}
this.backgroundGatherTask = null;
}
@@ -680,6 +706,12 @@ class CallSession extends Emitter {
stt_region: credential.stt_region
};
}
else if ('nvidia' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
riva_server_uri: credential.riva_server_uri
};
}
else if (vendor.startsWith('custom:')) {
return {
speech_credential_sid: credential.speech_credential_sid,
@@ -724,11 +756,12 @@ class CallSession extends Emitter {
}
else {
this.logger.info('CallSession:exec disabling bot mode to start gather with new options');
this.disableBotMode();
await this.disableBotMode();
}
}
if (!skip) {
const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`);
span.setAttributes({'verb.summary': task.summary});
task.span = span;
task.ctx = ctx;
await task.exec(this, resources);
@@ -750,19 +783,22 @@ class CallSession extends Emitter {
}
if (0 === this.tasks.length && this.requestor instanceof WsRequestor && !this.callGone) {
let span;
//let span;
try {
const {span} = this.rootSpan.startChildSpan('waiting for commands');
const {reason, queue, command} = await this._awaitCommandsOrHangup();
//const {span} = this.rootSpan.startChildSpan('waiting for commands');
//const {reason, queue, command} = await this._awaitCommandsOrHangup();
/*
span.setAttributes({
'completion.reason': reason,
'async.request.queue': queue,
'async.request.command': command
});
span.end();
*/
await this._awaitCommandsOrHangup();
if (this.callGone) break;
} catch (err) {
span.end();
//span.end();
this.logger.info(err, 'CallSession:exec - error waiting for new commands');
break;
}
@@ -1192,7 +1228,7 @@ class CallSession extends Emitter {
}
_onCommand({msgid, command, call_sid, queueCommand, data}) {
this.logger.info({msgid, command, queueCommand}, 'CallSession:_onCommand - received command');
this.logger.info({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command');
const resolution = {reason: 'received command', queue: queueCommand, command};
switch (command) {
case 'redirect':
@@ -1255,15 +1291,17 @@ class CallSession extends Emitter {
this.wakeupResolver(resolution);
this.wakeupResolver = null;
}
/*
else {
const {span} = this.rootSpan.startChildSpan('async command');
const {queue, command} = resolution;
const {span} = this.rootSpan.startChildSpan(`recv cmd: ${command}`);
span.setAttributes({
'async.request.queue': queue,
'async.request.command': command
});
span.end();
}
*/
}
_onWsConnectionDropped() {
@@ -1549,7 +1587,7 @@ class CallSession extends Emitter {
const pp = this._pool.promise();
try {
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: looking up account');
const [r] = await pp.query(sqlRetrieveQueueEventHook, this.accountSid);
const [r] = await pp.query(sqlRetrieveQueueEventHook, [this.accountSid]);
if (0 === r.length) {
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: no webhook provisioned');
this.queueEventHookRequestor = null;

View File

@@ -21,6 +21,10 @@ class RestCallSession extends CallSession {
});
this.req = req;
this.ep = ep;
// keep restDialTask reference for closing AMD
if (tasks.length) {
this.restDialTask = tasks[0];
}
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
this._notifyCallStatusChange({
@@ -44,6 +48,10 @@ class RestCallSession extends CallSession {
* This is invoked when the called party hangs up, in order to calculate the call duration.
*/
_callerHungup() {
if (this.restDialTask) {
this.logger.info('RestCallSession: releasing AMD');
this.restDialTask.turnOffAmd();
}
this.callInfo.callTerminationBy = 'caller';
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});

View File

@@ -30,6 +30,12 @@ class TaskConfig extends Task {
if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
});
}
if (this.data.reset) {
if (typeof this.data.reset === 'string') this.data.reset = [this.data.reset];
}
else this.data.reset = [];
if (this.bargeIn.sticky) this.autoEnable = true;
this.preconditions = (this.bargeIn.enable || this.record?.action || this.listen?.url || this.data.amd) ?
TaskPreconditions.Endpoint :
@@ -45,6 +51,10 @@ class TaskConfig extends Task {
get summary() {
const phrase = [];
/* reset recognizer and/or synthesizer to default values? */
if (this.data.reset.length) phrase.push(`reset ${this.data.reset.join(',')}`);
if (this.bargeIn.enable) phrase.push('enable barge-in');
if (this.hasSynthesizer) {
const {vendor:v, language:l, voice} = this.synthesizer;
@@ -62,7 +72,7 @@ class TaskConfig extends Task {
}
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} = {}) {
@@ -86,6 +96,11 @@ class TaskConfig extends Task {
}
}
this.data.reset.forEach((k) => {
if (k === 'synthesizer') cs.resetSynthesizer();
else if (k === 'recognizer') cs.resetRecognizer();
});
if (this.hasSynthesizer) {
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
? this.synthesizer.vendor

View File

@@ -85,6 +85,7 @@ class TaskDial extends Task {
this.earlyMedia = this.data.answerOnBridge === true;
this.callerId = this.data.callerId;
this.callerName = this.data.callerName;
this.dialMusic = this.data.dialMusic;
this.headers = this.data.headers || {};
this.method = this.data.method || 'POST';
@@ -415,7 +416,8 @@ class TaskDial extends Task {
const opts = {
headers: this.headers,
proxy: `sip:${sbcAddress}`,
callingNumber: this.callerId || req.callingNumber
callingNumber: this.callerId || req.callingNumber,
...(this.callerName && {callingName: this.callerName})
};
const t = this.target.find((t) => t.type === 'teams');

View File

@@ -11,6 +11,7 @@ class TaskRestDial extends Task {
super(logger, opts);
this.from = this.data.from;
this.callerName = this.data.callerName;
this.fromHost = this.data.fromHost;
this.to = this.data.to;
this.call_hook = this.data.call_hook;
@@ -27,18 +28,29 @@ class TaskRestDial extends Task {
*/
async exec(cs) {
await super.exec(cs);
this.cs = cs;
this.canCancel = true;
if (this.data.amd) {
this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd;
this.on('amd', this._onAmdEvent.bind(this, cs));
}
this._setCallTimer();
await this.awaitTaskDone();
}
turnOffAmd() {
if (this.callSession.ep && this.callSession.ep.amd) this.stopAmd(this.callSession.ep, this);
}
kill(cs) {
super.kill(cs);
this._clearCallTimer();
if (this.canCancel && cs?.req) {
if (this.canCancel) {
this.canCancel = false;
cs.req.cancel();
cs?.req?.cancel();
}
this.notifyTaskDone();
}
@@ -65,6 +77,13 @@ class TaskRestDial extends Task {
}
}
};
if (this.startAmd) {
try {
this.startAmd(this.callSession, this.callSession.ep, this, this.data.amd);
} catch (err) {
this.logger.info({err}, 'Rest:dial:Call established - Error calling startAmd');
}
}
const tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
if (tasks && Array.isArray(tasks)) {
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
@@ -97,7 +116,16 @@ class TaskRestDial extends Task {
_onCallTimeout() {
this.logger.debug('TaskRestDial: timeout expired without answer, killing task');
this.timer = null;
this.kill();
this.kill(this.cs);
}
_onAmdEvent(cs, evt) {
this.logger.info({evt}, 'Rest:dial:_onAmdEvent');
const {actionHook} = this.data.amd;
this.performHook(cs, actionHook, evt)
.catch((err) => {
this.logger.error({err}, 'Rest:dial:_onAmdEvent - error calling actionHook');
});
}
}

View File

@@ -36,6 +36,7 @@ class TaskSay extends Task {
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
this.synthesizer = this.data.synthesizer || {};
this.disableTtsCache = this.data.disableTtsCache;
this.options = this.synthesizer.options || {};
}
get name() { return TaskName.Say; }
@@ -66,7 +67,7 @@ class TaskSay extends Task {
cs.speechSynthesisVoice;
const engine = this.synthesizer.engine || 'standard';
const salt = cs.callSid;
const credentials = cs.getSpeechCredentials(vendor, 'tts');
let credentials = cs.getSpeechCredentials(vendor, 'tts');
/* parse Nuance voices into name and model */
let model;
@@ -78,6 +79,16 @@ class TaskSay extends Task {
}
}
/* allow for microsoft custom region voice and api_key to be specified as an override */
if (vendor === 'microsoft' && this.options.deploymentId) {
credentials = credentials || {};
credentials.use_custom_tts = true;
credentials.custom_tts_endpoint = this.options.deploymentId;
credentials.api_key = this.options.apiKey || credentials.apiKey;
credentials.region = this.options.region || credentials.region;
voice = this.options.voice || voice;
}
this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
this.ep = ep;
try {

View File

@@ -155,7 +155,7 @@ class Task extends Emitter {
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(`${type} (${this.actionHook})`);
const b3 = this.getTracingPropagation('b3', span);
const httpHeaders = b3 && {b3};
span.setAttributes({'http.body': JSON.stringify(params)});

View File

@@ -1,9 +1,16 @@
const Emitter = require('events');
const {readFile} = require('fs');
const {
TaskName,
GoogleTranscriptionEvents,
AwsTranscriptionEvents,
AzureTranscriptionEvents,
NuanceTranscriptionEvents,
NvidiaTranscriptionEvents,
IbmTranscriptionEvents,
SonioxTranscriptionEvents,
DeepgramTranscriptionEvents,
JambonzTranscriptionEvents,
AmdEvents,
AvmdEvents
} = require('./constants');
@@ -54,6 +61,11 @@ class Amd extends Emitter {
this.thresholdWordCount = opts.thresholdWordCount || 9;
const {normalizeTranscription} = require('./transcription-utils')(logger);
this.normalizeTranscription = normalizeTranscription;
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
this.getNuanceAccessToken = getNuanceAccessToken;
this.getIbmAccessToken = getIbmAccessToken;
const {setChannelVarsForStt} = require('./transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt;
const {
noSpeechTimeoutMs = 5000,
@@ -184,7 +196,7 @@ module.exports = (logger) => {
const {vendor, language} = ep.amd;
ep.startTranscription({
vendor,
language,
locale: language,
interim: true,
bugname
}).catch((err) => {
@@ -229,51 +241,92 @@ module.exports = (logger) => {
const startAmd = async(cs, ep, task, opts) => {
const amd = ep.amd = new Amd(logger, cs, opts);
const {vendor, language, sttCredentials} = amd;
const sttOpts = {};
const {vendor, language} = amd;
let sttCredentials = amd.sttCredentials;
const hints = voicemailHints[language] || [];
if (vendor === 'nuance' && sttCredentials.client_id) {
/* get nuance access token */
const {getNuanceAccessToken} = amd;
const {client_id, secret} = sttCredentials;
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
logger.debug({client_id}, `Gather:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
sttCredentials = {...sttCredentials, access_token};
}
else if (vendor == 'ibm' && sttCredentials.stt_api_key) {
/* get ibm access token */
const {getIbmAccessToken} = amd;
const {stt_api_key, stt_region} = sttCredentials;
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
sttCredentials = {...sttCredentials, access_token, stt_region};
}
/* set stt options */
logger.info(`starting amd for vendor ${vendor} and language ${language}`);
if ('google' === vendor) {
sttOpts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(sttCredentials.credentials);
sttOpts.GOOGLE_SPEECH_USE_ENHANCED = true;
sttOpts.GOOGLE_SPEECH_HINTS = hints.join(',');
if (opts.recognizer?.altLanguages) {
sttOpts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
}
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, onEndOfUtterance.bind(null, cs, ep, task));
}
else if (['aws', 'polly'].includes(vendor)) {
Object.assign(sttOpts, {
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
AWS_REGION: sttCredentials.region
});
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
}
else if ('microsoft' === vendor) {
Object.assign(sttOpts, {
'AZURE_SUBSCRIPTION_KEY': sttCredentials.api_key,
'AZURE_REGION': sttCredentials.region
});
sttOpts.AZURE_SPEECH_HINTS = hints.join(',');
if (opts.recognizer?.altLanguages) {
sttOpts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
}
sttOpts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = opts.resolveTimeoutMs || 20000;
const sttOpts = amd.setChannelVarsForStt({name: TaskName.Gather}, sttCredentials, {
vendor,
hints,
enhancedModel: true,
altLanguages: opts.recognizer?.altLanguages || [],
initialSpeechTimeoutMs: opts.resolveTimeoutMs,
});
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, onNoSpeechDetected.bind(null, cs, ep, task));
}
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
amd.transcriptionHandler = onTranscription.bind(null, cs, ep, task);
amd.EndOfUtteranceHandler = onEndOfUtterance.bind(null, cs, ep, task);
amd.noSpeechHandler = onNoSpeechDetected.bind(null, cs, ep, task);
switch (vendor) {
case 'google':
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, amd.transcriptionHandler);
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, amd.EndOfUtteranceHandler);
break;
case 'aws':
case 'polly':
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'microsoft':
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, amd.transcriptionHandler);
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, amd.noSpeechHandler);
break;
case 'nuance':
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'deepgram':
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'soniox':
amd.bugname = 'soniox_amd_transcribe';
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'ibm':
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
case 'nvidia':
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
default:
if (vendor.startsWith('custom:')) {
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
}
else {
throw new Error(`Invalid vendor ${this.vendor}`);
}
}
amd
.on(AmdEvents.NoSpeechDetected, (evt) => {
task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
stopAmd(ep, task);
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
@@ -281,7 +334,7 @@ module.exports = (logger) => {
.on(AmdEvents.HumanDetected, (evt) => {
task.emit('amd', {type: AmdEvents.HumanDetected, ...evt});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
stopAmd(ep, task);
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
@@ -292,7 +345,7 @@ module.exports = (logger) => {
.on(AmdEvents.DecisionTimeout, (evt) => {
task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
stopAmd(ep, task);
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
@@ -300,7 +353,7 @@ module.exports = (logger) => {
.on(AmdEvents.ToneTimeout, (evt) => {
//task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt});
try {
ep.connected && ep.execute('avmd_stop').catch((err) => logger.info(err, 'Error stopping avmd'));
stopAmd(ep, task);
} catch (err) {
logger.info({err}, 'Error stopping avmd');
}
@@ -308,7 +361,7 @@ module.exports = (logger) => {
.on(AmdEvents.MachineStoppedSpeaking, () => {
task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking});
try {
ep.connected && ep.stopTranscription({vendor, bugname});
stopAmd(ep, task);
} catch (err) {
logger.info({err}, 'Error stopping transcription');
}
@@ -327,6 +380,19 @@ module.exports = (logger) => {
if (ep.amd) {
vendor = ep.amd.vendor;
ep.amd.stopAllTimers();
ep.removeListener(GoogleTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(GoogleTranscriptionEvents.EndOfUtterance, ep.amd.EndOfUtteranceHandler);
ep.removeListener(AwsTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(AzureTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(AzureTranscriptionEvents.NoSpeechDetected, ep.amd.noSpeechHandler);
ep.removeListener(NuanceTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(DeepgramTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(SonioxTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(IbmTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(NvidiaTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.removeListener(JambonzTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
ep.amd = null;
}

View File

@@ -78,6 +78,10 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
else if ('nvidia' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri;
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.auth_token = o.auth_token;
@@ -96,13 +100,13 @@ module.exports = (logger, srf) => {
const lookupAccountDetails = async(account_sid) => {
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, account_sid);
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, [account_sid]);
if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`);
const [r2] = await pp.query(sqlSpeechCredentials, account_sid);
const [r2] = await pp.query(sqlSpeechCredentials, [account_sid]);
const speech = r2.map(speechMapper);
/* add service provider creds unless we have that vendor at the account level */
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
const [r3] = await pp.query(sqlSpeechCredentialsForSP, [account_sid]);
r3.forEach((s) => {
if (!speech.find((s2) => s2.vendor === s.vendor)) {
speech.push(speechMapper(s));

View File

@@ -549,6 +549,7 @@ module.exports = (logger) => {
}
else if ('nvidia' === vendor) {
const {nvidiaOptions = {}} = rOpts;
const rivaUri = nvidiaOptions.rivaUri || sttCredentials.riva_server_uri;
opts = {
...opts,
...((nvidiaOptions.profanityFilter || rOpts.profanityFilter) && {NVIDIA_PROFANITY_FILTER: 1}),
@@ -560,7 +561,7 @@ module.exports = (logger) => {
...(nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: nvidiaOptions.maxAlternatives}),
...(!nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: 1}),
...(rOpts.model && {NVIDIA_MODEL: rOpts.model}),
...(nvidiaOptions.rivaUri && {NVIDIA_RIVA_URI: nvidiaOptions.rivaUri}),
...(rivaUri && {NVIDIA_RIVA_URI: rivaUri}),
...(nvidiaOptions.verbatimTranscripts && {NVIDIA_VERBATIM_TRANSCRIPTS: 1}),
...(rOpts.diarization && {NVIDIA_SPEAKER_DIARIZATION: 1}),
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&

4522
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "jambonz-feature-server",
"version": "v0.8.2",
"version": "0.8.3",
"main": "app.js",
"engines": {
"node": ">= 10.16.0"
@@ -25,13 +25,13 @@
"jslint:fix": "eslint app.js tracer.js lib --fix"
},
"dependencies": {
"@jambonz/db-helpers": "^0.7.4",
"@jambonz/db-helpers": "^0.8.1",
"@jambonz/http-health-check": "^0.0.1",
"@jambonz/realtimedb-helpers": "^0.7.0",
"@jambonz/speech-utils": "^0.0.12",
"@jambonz/realtimedb-helpers": "^0.7.2",
"@jambonz/speech-utils": "^0.0.13",
"@jambonz/stats-collector": "^0.1.8",
"@jambonz/time-series": "^0.2.5",
"@jambonz/verb-specifications": "^0.0.12",
"@jambonz/verb-specifications": "^0.0.22",
"@opentelemetry/api": "^1.4.0",
"@opentelemetry/exporter-jaeger": "^1.9.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.35.0",
@@ -45,7 +45,7 @@
"bent": "^7.3.12",
"debug": "^4.3.4",
"deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.20",
"drachtio-fsmrf": "^3.0.21",
"drachtio-srf": "^4.5.23",
"express": "^4.18.2",
"ip": "^1.1.8",

View File

@@ -106,3 +106,61 @@ test('test create-call call-hook basic authentication', async(t) => {
t.error(err);
}
});
test('test create-call amd', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let from = 'create-call-amd';
let account_sid = 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f';
// Give UAS app time to come up
const p = sippUac('uas.xml', '172.38.0.10', from);
await waitFor(1000);
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
post('v1/createCall', {
'account_sid':account_sid,
"call_hook": {
"url": "http://127.0.0.1:3100/",
"method": "POST",
"username": "username",
"password": "password"
},
"from": from,
"to": {
"type": "phone",
"number": "15583084809"
},
"amd": {
"actionHook": "/actionHook"
},
"speech_recognizer_vendor": "google",
"speech_recognizer_language": "en"
});
let verbs = [
{
"verb": "pause",
"length": 7
}
];
provisionCallHook(from, verbs);
//THEN
await p;
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`)
t.ok(obj.body.type = 'amd_no_speech_detected',
'create-call: AMD detected');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -31,6 +31,7 @@ test('\'dial-phone\'', async(t) => {
{
"verb": "dial",
"callerId": from,
"callerName": "test_callerName",
"actionHook": "/actionHook",
"timeLimit": 5,
"target": [
@@ -56,6 +57,7 @@ test('\'dial-phone\'', async(t) => {
"method": "POST",
},
"from": from,
"callerName": "Tom",
"to": {
"type": "phone",
"number": "15583084808"

View File

@@ -102,6 +102,53 @@ test('\'gather\' test - default (google)', async(t) => {
}
});
test('\'config\' test - reset to app defaults', async(t) => {
if (!GCP_JSON_KEY) {
t.pass('skipping config tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "config",
"recognizer": {
"vendor": "google",
"language": "fr-FR"
},
},
{
"verb": "config",
"reset": ['recognizer'],
},
{
"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',
'config: resets recognizer to app defaults');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'gather\' test - microsoft', async(t) => {
if (!MICROSOFT_REGION || !MICROSOFT_API_KEY) {
t.pass('skipping microsoft tests');
@@ -214,7 +261,7 @@ test('\'gather\' test - deepgram', async(t) => {
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'),
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().includes('like to speak to customer support'),
'gather: succeeds when using deepgram credentials');
disconnect();
} catch (err) {

View File

@@ -43,3 +43,84 @@ test('\'say\' tests', async(t) => {
t.error(err);
}
});
test('\'config\' reset synthesizer tests', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
"verb": "config",
"synthesizer": {
"vendor": "microsft",
"voice": "foobar"
},
},
{
"verb": "config",
"reset": 'synthesizer',
},
{
verb: 'say',
text: 'hello'
}
];
const from = 'say_test_success';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('say: succeeds when using using account credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
const {MICROSOFT_CUSTOM_API_KEY, MICROSOFT_DEPLOYMENT_ID, MICROSOFT_CUSTOM_REGION, MICROSOFT_CUSTOM_VOICE} = process.env;
if (MICROSOFT_CUSTOM_API_KEY && MICROSOFT_DEPLOYMENT_ID && MICROSOFT_CUSTOM_REGION && MICROSOFT_CUSTOM_VOICE) {
test('\'say\' tests - microsoft custom voice', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'say',
text: 'hello',
synthesizer: {
vendor: 'microsoft',
voice: MICROSOFT_CUSTOM_VOICE,
options: {
deploymentId: MICROSOFT_DEPLOYMENT_ID,
apiKey: MICROSOFT_CUSTOM_API_KEY,
region: MICROSOFT_CUSTOM_REGION,
}
}
}
];
const from = 'say_test_success';
provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('say: succeeds when using microsoft custom voice');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
}

View File

@@ -166,7 +166,7 @@ test('\'transcribe\' test - deepgram', 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`);
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().includes('like to speak to customer support'),
'transcribe: succeeds when using deepgram credentials');
disconnect();
@@ -206,7 +206,7 @@ test('\'transcribe\' test - soniox', 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));
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using soniox credentials');

View File

@@ -1,8 +1,7 @@
const express = require('express');
const app = express();
const Websocket = require('ws');
const {PORT} = require('../../lib/config');
const listenPort = PORT;
const listenPort = process.env.HTTP_PORT || 3000;
let json_mapping = new Map();
let hook_mapping = new Map();
let ws_packet_count = new Map();

View File

@@ -12,6 +12,7 @@ const {
OTEL_EXPORTER_JAEGER_AGENT_HOST,
OTEL_EXPORTER_JAEGER_ENDPOINT,
OTEL_EXPORTER_ZIPKIN_URL,
OTEL_EXPORTER_COLLECTOR_URL
} = require('./lib/config');
module.exports = (serviceName) => {
@@ -33,7 +34,7 @@ module.exports = (serviceName) => {
}
else {
exporter = new OTLPTraceExporter({
url: process.OTEL_EXPORTER_COLLECTOR_URL
url: OTEL_EXPORTER_COLLECTOR_URL
});
}