Skip to content

Commit

Permalink
Merge branch 'main' into fix-redirect-audio
Browse files Browse the repository at this point in the history
  • Loading branch information
xquanluu authored Dec 24, 2023
2 parents ed16f31 + 2ec1460 commit f0f4294
Show file tree
Hide file tree
Showing 24 changed files with 8,699 additions and 6,268 deletions.
Empty file modified bin/k8s-pre-stop-hook.js
100755 → 100644
Empty file.
2 changes: 1 addition & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The GCP credential is the JSON service key in stringified format.

#### Install Docker

The test suite ralso equires [Docker](https://www.docker.com/) and docker-compose to be installed on your laptop. Docker is used to set up a network with all of the elements required to test the jambonz-feature-server in a black-box type of fashion.
The test suite also requires [Docker](https://www.docker.com/) and docker-compose to be installed on your laptop. Docker is used to set up a network with all of the elements required to test the jambonz-feature-server in a black-box type of fashion.

Once you have docker installed, you can optionally make sure everything Docker-wise is working properly by running this command from the project folder:

Expand Down
5 changes: 1 addition & 4 deletions lib/http-routes/schemas/create-call.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,9 @@ const customSanitizeFunction = (value) => {
/* trims characters at the beginning and at the end of a string */
value = value.trim();

/* We don't escape URLs but verify them via new URL */
/* Verify strings including 'http' via new URL */
if (value.includes('http')) {
value = new URL(value).toString();
} else {
/* replaces <, >, &, ', " and / with their corresponding HTML entities */
value = escape(value);
}
}
} catch (error) {
Expand Down
10 changes: 8 additions & 2 deletions lib/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,16 @@ module.exports = function(srf, logger) {
let clientDb = null;
if (req.has('X-Authenticated-User')) {
req.locals.originatingUser = req.get('X-Authenticated-User');
let clientSettings;
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
if (arr) {
[clientDb] = await lookupClientByAccountAndUsername(account_sid, arr[1]);
[clientSettings] = await lookupClientByAccountAndUsername(account_sid, arr[1]);
}
clientDb = await registrar.query(req.locals.originatingUser);
clientDb = {
...clientDb,
...clientSettings,
};
}

// check for call to application
Expand Down Expand Up @@ -160,7 +166,7 @@ module.exports = function(srf, logger) {
};
logger.info({callId, metadata, sdp}, 'successfully parsed SIPREC payload');
} catch (err) {
logger.info({callId}, 'Error parsing multipart payload');
logger.info({err, callId}, 'Error parsing multipart payload');
return res.send(503);
}
}
Expand Down
15 changes: 15 additions & 0 deletions lib/session/adulting-call-session.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const CallSession = require('./call-session');
const {CallStatus} = require('../utils/constants');
const moment = require('moment');

/**
* @classdesc Subclass of CallSession. Represents a CallSession
Expand Down Expand Up @@ -26,6 +28,7 @@ class AdultingCallSession extends CallSession {
this._callReleased();
});
this.sd.emit('adulting');
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
}

get dlg() {
Expand All @@ -50,6 +53,18 @@ class AdultingCallSession extends CallSession {
}

_callerHungup() {
if (this.dlg.connectTime) {
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.rootSpan.setAttributes({'call.termination': 'hangup by caller'});
this.callInfo.callTerminationBy = 'caller';
this.emit('callStatusChange', {
callStatus: CallStatus.Completed,
duration
});
}
this.logger.info('InboundCallSession: caller hung up');
this._callReleased();
this.req.removeAllListeners('cancel');
}
}

Expand Down
69 changes: 67 additions & 2 deletions lib/session/call-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,18 @@ class CallSession extends Emitter {
return this.application.notifier;
}

/**
* syntheizer
*/

get synthesizer() {
return this._synthesizer;
}

set synthesizer(synth) {
this._synthesizer = synth;
}

/**
* default vendor to use for speech synthesis if not provided in the app
*/
Expand Down Expand Up @@ -253,6 +265,16 @@ class CallSession extends Emitter {
set fallbackSpeechRecognizerVendor(vendor) {
this.application.fallback_speech_recognizer_vendor = vendor;
}
/**
* recognizer
*/
get recognizer() {
return this._recognizer;
}

set recognizer(rec) {
this._recognizer = rec;
}
/**
* default vendor to use for speech recognition if not provided in the app
*/
Expand Down Expand Up @@ -834,7 +856,8 @@ class CallSession extends Emitter {
} else if ('elevenlabs' === vendor) {
return {
api_key: credential.api_key,
model_id: credential.model_id
model_id: credential.model_id,
options: credential.options
};
} else if ('assemblyai' === vendor) {
return {
Expand Down Expand Up @@ -919,7 +942,8 @@ class CallSession extends Emitter {
if (0 === this.tasks.length &&
this.requestor instanceof WsRequestor &&
!this.requestor.closedGracefully &&
!this.callGone
!this.callGone &&
!this.disableWaitCommand
) {
try {
await this._awaitCommandsOrHangup();
Expand Down Expand Up @@ -1187,6 +1211,38 @@ class CallSession extends Emitter {
}
}

/**
* perform live call control - send RFC 2833 DTMF
* @param {obj} opts
* @param {string} opts.dtmf.digit - DTMF digit
* @param {string} opts.dtmf.duration - Optional, Duration
*/
async _lccDtmf(opts, callSid) {
const {dtmf} = opts;
const {digit, duration = 250} = dtmf;
if (!this.hasStableDialog) {
this.logger.info('CallSession:_lccDtmf - invalid command as we do not have a stable call');
return;
}
try {
const dlg = callSid === this.callSid ? this.dlg : this.currentTask.dlg;
const res = await dlg.request({
method: 'INFO',
headers: {
'Content-Type': 'application/dtmf',
'X-Reason': 'Dtmf'
},
body: `Signal=${digit}
Duration=${duration} `
});
this.logger.debug({res}, `CallSession:_lccDtmf
got response to INFO DTMF digit=${digit} and duration=${duration}`);
return res;
} catch (err) {
this.logger.error({err}, 'CallSession:_lccDtmf - error sending INFO RFC 2833 DTMF');
}
}

/**
* perform live call control -- whisper to one party or the other on a call
* @param {array} opts - array of play or say tasks
Expand Down Expand Up @@ -1270,6 +1326,8 @@ class CallSession extends Emitter {
else if (opts.sip_request) {
const res = await this._lccSipRequest(opts, callSid);
return {status: res.status, reason: res.reason};
} else if (opts.dtmf) {
await this._lccDtmf(opts, callSid);
}
else if (opts.record) {
await this.notifyRecordOptions(opts.record);
Expand Down Expand Up @@ -1461,6 +1519,13 @@ class CallSession extends Emitter {
});
break;

case 'dtmf':
this._lccDtmf(data, call_sid)
.catch((err) => {
this.logger.info({err, data}, `CallSession:_onCommand - error sending RFC 2833 DTMF ${data}`);
});
break;

default:
this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
}
Expand Down
1 change: 1 addition & 0 deletions lib/session/confirm-call-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class ConfirmCallSession extends CallSession {
});
this.dlg = dlg;
this.ep = ep;
this.disableWaitCommand = true;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions lib/tasks/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class TaskConfig extends Task {
});

if (this.hasSynthesizer) {
cs.synthesizer = this.synthesizer;
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
? this.synthesizer.vendor
: cs.speechSynthesisVendor;
Expand Down Expand Up @@ -138,6 +139,7 @@ class TaskConfig extends Task {
this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer');
}
if (this.hasRecognizer) {
cs.recognizer = this.recognizer;
cs.speechRecognizerVendor = this.recognizer.vendor !== 'default'
? this.recognizer.vendor
: cs.speechRecognizerVendor;
Expand Down
9 changes: 6 additions & 3 deletions lib/tasks/dial.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const dbUtils = require('../utils/db-utils');
const debug = require('debug')('jambonz:feature-server');
const {parseUri} = require('drachtio-srf');
const {ANCHOR_MEDIA_ALWAYS, JAMBONZ_DISABLE_DIAL_PAI_HEADER} = require('../config');
const { isOnhold } = require('../utils/sdp-utils');
const { isOnhold, isOpusFirst } = require('../utils/sdp-utils');
const { normalizeJambones } = require('@jambonz/verb-specifications');

function parseDtmfOptions(logger, dtmfCapture) {
Expand Down Expand Up @@ -488,7 +488,8 @@ class TaskDial extends Task {
headers: this.headers,
proxy: `sip:${sbcAddress}`,
callingNumber: this.callerId || req.callingNumber,
...(this.callerName && {callingName: this.callerName})
...(this.callerName && {callingName: this.callerName}),
opusFirst: isOpusFirst(this.cs.ep.remote.sdp)
};

const t = this.target.find((t) => t.type === 'teams');
Expand Down Expand Up @@ -790,9 +791,11 @@ class TaskDial extends Task {
assert(cs.ep && sd.ep);

try {
// Wait until we got new SDP from B leg to ofter to A Leg
const aLegSdp = cs.ep.remote.sdp;
await sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp);
const bLegSdp = sd.dlg.remote.sdp;
await Promise.all[sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp), cs.releaseMediaToSBC(bLegSdp)];
await cs.releaseMediaToSBC(bLegSdp);
this.epOther = null;
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
} catch (err) {
Expand Down
69 changes: 13 additions & 56 deletions lib/tasks/gather.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class TaskGather extends SttTask {

async exec(cs, {ep}) {
this.logger.debug({options: this.data}, 'Gather:exec');
await super.exec(cs);
await super.exec(cs, {ep});
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);

if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) {
Expand Down Expand Up @@ -141,59 +141,6 @@ class TaskGather extends SttTask {
this.interim = true;
this.logger.debug('Gather:exec - early hints match enabled');
}

this.ep = ep;
if ('default' === this.vendor || !this.vendor) {
this.vendor = cs.speechRecognizerVendor;
if (this.data.recognizer) this.data.recognizer.vendor = this.vendor;
}
if ('default' === this.language || !this.language) {
this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language;
}
if ('default' === this.label || !this.label) {
this.label = cs.speechRecognizerLabel;
if (this.data.recognizer) this.data.recognizer.label = this.label;
}
// Fallback options
if ('default' === this.fallbackVendor || !this.fallbackVendor) {
this.fallbackVendor = cs.fallbackSpeechRecognizerVendor;
if (this.data.recognizer) this.data.recognizer.fallbackVendor = this.fallbackVendor;
}
if ('default' === this.fallbackLanguage || !this.fallbackLanguage) {
this.fallbackLanguage = cs.fallbackSpeechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.fallbackLanguage = this.fallbackLanguage;
}
if ('default' === this.fallbackLabel || !this.fallbackLabel) {
this.fallbackLabel = cs.fallbackSpeechRecognizerLabel;
if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel;
}
if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor;
}
if (this.vendor === 'cobalt' && !this.data.recognizer.model) {
// By default, application saves cobalt model in language
this.data.recognizer.model = cs.speechRecognizerLanguage;
}

if (this.needsStt && !this.sttCredentials) {
try {
this.sttCredentials = await this._initSpeechCredentials(cs, this.vendor, this.label);
} catch (error) {
if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
await this._fallback();
} else {
throw error;
}
}
}

/* when using cobalt model is required */
if (this.vendor === 'cobalt' && !this.data.recognizer.model) {
this.notifyError({ msg: 'ASR error', details:'Cobalt requires a model to be specified'});
throw new Error('Cobalt requires a model to be specified');
}

const startListening = async(cs, ep) => {
this._startTimer();
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
Expand Down Expand Up @@ -319,6 +266,13 @@ class TaskGather extends SttTask {
this._resolve('dtmf-terminator-key');
}
else if (this.input.includes('digits')) {
if (this.digitBuffer.length === 0 && this.needsStt) {
// DTMF is higher priority than STT.
this.removeSpeechListeners(ep);
ep.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.error({err},
` Received DTMF, Error stopping transcription for vendor ${this.vendor}`));
}
this.digitBuffer += evt.dtmf;
const len = this.digitBuffer.length;
if (len === this.numDigits || len === this.maxDigits) {
Expand Down Expand Up @@ -528,7 +482,9 @@ class TaskGather extends SttTask {
this._clearTimer();
this._timeoutTimer = setTimeout(() => {
if (this.isContinuousAsr) this._startAsrTimer();
else if (this.interDigitTimeout <= 0 || this.digitBuffer.length < this.minDigits || this.needsStt) {
else if (this.interDigitTimeout <= 0 ||
this.digitBuffer.length < this.minDigits ||
this.needsStt && this.digitBuffer.length === 0) {
this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
}
}, this.timeout);
Expand Down Expand Up @@ -634,7 +590,8 @@ class TaskGather extends SttTask {
return;
}

evt = this.normalizeTranscription(evt, this.vendor, 1, this.language, this.shortUtterance);
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language,
this.shortUtterance, this.data.recognizer.punctuation);
if (evt.alternatives.length === 0) {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
return;
Expand Down
8 changes: 8 additions & 0 deletions lib/tasks/say.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ class TaskSay extends Task {
credentials.api_key = this.options.apiKey || credentials.apiKey;
credentials.region = this.options.region || credentials.region;
voice = this.options.voice || voice;
} else if (vendor === 'elevenlabs') {
credentials = credentials || {};
credentials.model_id = this.options.model_id || credentials.model_id;
credentials.voice_settings = this.options.voice_settings || {};
credentials.optimize_streaming_latency = this.options.optimize_streaming_latency
|| credentials.optimize_streaming_latency;
voice = this.options.voice_id || voice;
}

this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
Expand Down Expand Up @@ -127,6 +134,7 @@ class TaskSay extends Task {
model,
salt,
credentials,
options: this.options,
disableTtsCache : this.disableTtsCache
});
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
Expand Down
Loading

0 comments on commit f0f4294

Please sign in to comment.