Compare commits

..

8 Commits

Author SHA1 Message Date
Dave Horton
c4a6057fc6 bump version 2024-09-04 13:31:05 +01:00
rammohan-y
174438bb01 Feat/882: default model setting for en-IN language (#888)
* feat/882: default model setting for en-IN language

* feat/882: refactored if into ||
2024-09-03 13:22:38 +01:00
Antony Jukes
4348615b75 Create Call Rest is missing target headers on outdial (#874)
* add target headers for rest create-call

* rebased

---------

Co-authored-by: ajukes <ajukes@callable.io>
2024-09-02 21:48:09 +01:00
Hoan Luu Huu
d365883bfe fix #883 that after kicked from conference, no long receive freeswitch CUSTOM event (#886)
* fix #883 that after kicked from conference, no long receive freeswitch CUSTOM event

* fix #883 that after kicked from conference, no long receive freeswitch CUSTOM event

* reset Esl Custom event after conference.

* update drachtio fsmrf version
2024-08-31 14:47:39 +01:00
Dave Horton
c0ab936b76 wip (#830)
* wip

* wip

* wip

* wip

* update deps

* update test to use latest freeswitch image
2024-08-29 15:23:49 -04:00
Dave Horton
600ff763fa fix #840 (#880) 2024-08-26 10:14:59 -04:00
Hoan Luu Huu
4d077e990f Fix/audio issue kick conference (#878)
* rest call session does not handle for RE-INVITE

* fixed audio is bad after kicked from conference

* fix review comment
2024-08-23 09:28:39 -04:00
RJ Burnham
eccef54b04 Add support for configuring the IP address that is advertised to the API server. (#875)
This is needed when running in fargate as ip.address() will return the wrong ip address.
2024-08-23 08:33:16 -04:00
14 changed files with 174 additions and 238 deletions

View File

@@ -21,6 +21,7 @@ Configuration is provided via environment variables:
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes| |ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes|
|GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes| |GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes|
|HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes| |HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes|
|HTTP_IP| IP Address for API requests from jambonz-api-server |no|
|JAMBONES_GATHER_EARLY_HINTS_MATCH| if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency |no| |JAMBONES_GATHER_EARLY_HINTS_MATCH| if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency |no|
|JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|yes| |JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|yes|
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no| |JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no|

View File

@@ -73,6 +73,7 @@ const JAMBONES_LOGLEVEL = process.env.JAMBONES_LOGLEVEL || 'info';
const JAMBONES_INJECT_CONTENT = process.env.JAMBONES_INJECT_CONTENT; const JAMBONES_INJECT_CONTENT = process.env.JAMBONES_INJECT_CONTENT;
const PORT = parseInt(process.env.HTTP_PORT, 10) || 3000; const PORT = parseInt(process.env.HTTP_PORT, 10) || 3000;
const HTTP_IP = process.env.HTTP_IP;
const HTTP_PORT_MAX = parseInt(process.env.HTTP_PORT_MAX, 10); const HTTP_PORT_MAX = parseInt(process.env.HTTP_PORT_MAX, 10);
const K8S = process.env.K8S; const K8S = process.env.K8S;
@@ -170,6 +171,7 @@ module.exports = {
JAMBONES_CLUSTER_ID, JAMBONES_CLUSTER_ID,
PORT, PORT,
HTTP_PORT_MAX, HTTP_PORT_MAX,
HTTP_IP,
K8S, K8S,
K8S_SBC_SIP_SERVICE_NAME, K8S_SBC_SIP_SERVICE_NAME,
JAMBONES_SUBNET, JAMBONES_SUBNET,

View File

@@ -97,7 +97,8 @@ router.post('/',
'X-Trace-ID': rootSpan.traceId, 'X-Trace-ID': rootSpan.traceId,
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}), ...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}), ...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat}) ...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat}),
...target.headers
}; };
switch (target.type) { switch (target.type) {

View File

@@ -566,9 +566,7 @@ class CallSession extends Emitter {
//this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - ahd settings'); //this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - ahd settings');
//await this.clearActionHookDelayProcessor(); //await this.clearActionHookDelayProcessor();
} }
else { this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - say or play action completed');
this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - restore ahd after gather override');
}
} }
} }
@@ -1069,7 +1067,7 @@ class CallSession extends Emitter {
try { try {
await this._awaitCommandsOrHangup(); await this._awaitCommandsOrHangup();
await this.clearOrRestoreActionHookDelayProcessor(); //await this.clearOrRestoreActionHookDelayProcessor();
//TODO: remove filler noise code and simply create as action hook delay //TODO: remove filler noise code and simply create as action hook delay
if (this._isPlayingFillerNoise) { if (this._isPlayingFillerNoise) {
@@ -2074,6 +2072,9 @@ Duration=${duration} `
// When this call kicked out from conference, session need to replace endpoint // When this call kicked out from conference, session need to replace endpoint
// but this.ms might be undefined/null at this case. // but this.ms might be undefined/null at this case.
this.ms = this.ms || this.getMS(); this.ms = this.ms || this.getMS();
// Destroy previous ep if it's still running.
if (this.ep?.connected) this.ep.destroy();
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp}); this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
this._configMsEndpoint(); this._configMsEndpoint();

View File

@@ -118,7 +118,9 @@ class Conference extends Task {
this.emitter.emit('kill'); this.emitter.emit('kill');
await this._doFinalMemberCheck(cs); await this._doFinalMemberCheck(cs);
if (this.ep && this.ep.connected) { if (this.ep && this.ep.connected) {
this.ep.conn.removeAllListeners('esl::event::CUSTOM::*'); // drachtio-fsmrf override esl::event::CUSTOM to conference join listerner, After finish the conference
// the application need to reset the esl::event::CUSTOM for another use on the same endpoint
this.ep.resetEslCustomEvent();
this.ep.api(`conference ${this.confName} kick ${this.memberId}`) this.ep.api(`conference ${this.confName} kick ${this.memberId}`)
.catch((err) => this.logger.info({err}, 'Error kicking participant')); .catch((err) => this.logger.info({err}, 'Error kicking participant'));
} }

View File

@@ -1,4 +1,3 @@
const assert = require('assert');
const Task = require('../task'); const Task = require('../task');
const {TaskName, TaskPreconditions} = require('../../utils/constants'); const {TaskName, TaskPreconditions} = require('../../utils/constants');
const Intent = require('./intent'); const Intent = require('./intent');
@@ -11,29 +10,19 @@ class Dialogflow extends Task {
super(logger, opts); super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
this.credentials = this.data.credentials; this.credentials = this.data.credentials;
this.project = this.data.project;
this.agent = this.data.agent;
this.region = this.data.region || 'default';
this.model = this.data.model || 'es';
this.queryInput = this.data.queryInput || {};
assert(this.agent || !this.isCX, 'agent is required for dialogflow cx'); /* set project id with environment and region (optionally) */
assert(this.credentials, 'dialogflow credentials are required'); if (this.data.environment && this.data.region) {
this.project = `${this.data.project}:${this.data.environment}:${this.data.region}`;
if (this.isCX) { }
this.environment = this.data.environment || 'default'; else if (this.data.environment) {
this.project = `${this.data.project}:${this.data.environment}`;
}
else if (this.data.region) {
this.project = `${this.data.project}::${this.data.region}`;
} }
else { else {
/* ES: set project id with environment and region (optionally) */ this.project = this.data.project;
if (this.data.environment && this.data.region) {
this.project = `${this.data.project}:${this.data.environment}:${this.data.region}`;
}
else if (this.data.environment) {
this.project = `${this.data.project}:${this.data.environment}`;
}
else if (this.data.region) {
this.project = `${this.data.project}::${this.data.region}`;
}
} }
this.lang = this.data.lang || 'en-US'; this.lang = this.data.lang || 'en-US';
@@ -78,23 +67,31 @@ class Dialogflow extends Task {
this.fallbackLabel = this.data.tts.fallbackLabel; this.fallbackLabel = this.data.tts.fallbackLabel;
} }
this.bargein = this.data.bargein; this.bargein = this.data.bargein;
this.cmd = this.model === 'cx' ? 'dialogflow_cx_start' : 'dialogflow_start';
this.cmdStop = this.model === 'cx' ? 'dialogflow_cx_stop' : 'dialogflow_stop';
} }
get name() { return TaskName.Dialogflow; } get name() { return TaskName.Dialogflow; }
get isCX() { return this.model === 'cx'; }
get isES() { return !this.isCX; }
async exec(cs, {ep}) { async exec(cs, {ep}) {
await super.exec(cs); await super.exec(cs);
try { try {
await this.init(cs, ep); await this.init(cs, ep);
await this.startBot('default');
this.logger.debug(`starting dialogflow bot ${this.project}`);
// kick it off
const baseArgs = `${this.ep.uuid} ${this.project} ${this.lang} ${this.welcomeEvent}`;
if (this.welcomeEventParams) {
this.ep.api('dialogflow_start', `${baseArgs} '${JSON.stringify(this.welcomeEventParams)}'`);
}
else if (this.welcomeEvent.length) {
this.ep.api('dialogflow_start', baseArgs);
}
else {
this.ep.api('dialogflow_start', `${this.ep.uuid} ${this.project} ${this.lang}`);
}
this.logger.debug(`started dialogflow bot ${this.project}`);
await this.awaitTaskDone(); await this.awaitTaskDone();
} catch (err) { } catch (err) {
this.logger.error({err}, 'Dialogflow:exec error'); this.logger.error({err}, 'Dialogflow:exec error');
@@ -111,12 +108,6 @@ class Dialogflow extends Task {
this.ep.removeCustomEventListener('dialogflow::end_of_utterance'); this.ep.removeCustomEventListener('dialogflow::end_of_utterance');
this.ep.removeCustomEventListener('dialogflow::error'); this.ep.removeCustomEventListener('dialogflow::error');
this.ep.removeCustomEventListener('dialogflow_cx::intent');
this.ep.removeCustomEventListener('dialogflow_cx::transcription');
this.ep.removeCustomEventListener('dialogflow_cx::audio_provided');
this.ep.removeCustomEventListener('dialogflow_cx::end_of_utterance');
this.ep.removeCustomEventListener('dialogflow_cx::error');
this._clearNoinputTimer(); this._clearNoinputTimer();
if (!this.reportedFinalAction) this.performAction({dialogflowResult: 'caller hungup'}) if (!this.reportedFinalAction) this.performAction({dialogflowResult: 'caller hungup'})
@@ -150,12 +141,6 @@ class Dialogflow extends Task {
this.ep.addCustomEventListener('dialogflow::end_of_utterance', this._onEndOfUtterance.bind(this, ep, cs)); this.ep.addCustomEventListener('dialogflow::end_of_utterance', this._onEndOfUtterance.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow::error', this._onError.bind(this, ep, cs)); this.ep.addCustomEventListener('dialogflow::error', this._onError.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow_cx::intent', this._onIntent.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow_cx::transcription', this._onTranscription.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow_cx::audio_provided', this._onAudioProvided.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow_cx::end_of_utterance', this._onEndOfUtterance.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow_cx::error', this._onError.bind(this, ep, cs));
const obj = typeof this.credentials === 'string' ? JSON.parse(this.credentials) : this.credentials; const obj = typeof this.credentials === 'string' ? JSON.parse(this.credentials) : this.credentials;
const creds = JSON.stringify(obj); const creds = JSON.stringify(obj);
await this.ep.set('GOOGLE_APPLICATION_CREDENTIALS', creds); await this.ep.set('GOOGLE_APPLICATION_CREDENTIALS', creds);
@@ -166,51 +151,6 @@ class Dialogflow extends Task {
} }
} }
async startBot(intent) {
if (this.isCX) {
await this.startBotCX(intent);
}
else {
await this.startBotES(intent);
}
}
async startBotES() {
this.logger.info('starting dialogflow ES bot');
const baseArgs = `${this.ep.uuid} ${this.project} ${this.lang} ${this.welcomeEvent}`;
if (this.welcomeEventParams) {
await this.ep.api(this.cmd, `${baseArgs} '${JSON.stringify(this.welcomeEventParams)}'`);
}
else if (this.welcomeEvent.length) {
await this.ep.api(this.cmd, baseArgs);
}
else {
await this.ep.api(this.cmd, `${this.ep.uuid} ${this.project} ${this.lang}`);
}
}
async startBotCX(intent) {
const baseArgs = [
this.ep.uuid,
this.region,
this.project,
this.agent,
this.environment,
this.lang,
];
if (intent) {
baseArgs.push(intent);
}
/*
if (Object.keys(this.queryInput).length > 0) {
baseArgs.push(`'${JSON.stringify(this.queryInput)}'`);
}
*/
this.logger.info({args: baseArgs}, 'starting dialogflow CX bot');
await this.ep.api(this.cmd, `${baseArgs.join(' ')}`);
}
/** /**
* An intent has been returned. Since we are using SINGLE_UTTERANCE on the dialogflow side, * An intent has been returned. Since we are using SINGLE_UTTERANCE on the dialogflow side,
* we may get an empty intent, signified by the lack of a 'response_id' attribute. * we may get an empty intent, signified by the lack of a 'response_id' attribute.
@@ -231,20 +171,20 @@ class Dialogflow extends Task {
if (this.noinput && this.greetingPlayed) { if (this.noinput && this.greetingPlayed) {
this.logger.info('no input timer fired, reprompting..'); this.logger.info('no input timer fired, reprompting..');
this.noinput = false; this.noinput = false;
ep.api(this.cmd, `${ep.uuid} ${this.project} ${this.lang} ${this.noInputEvent}`); ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang} ${this.noInputEvent}`);
} }
else if (this.dtmfEntry && this.greetingPlayed) { else if (this.dtmfEntry && this.greetingPlayed) {
this.logger.info('dtmf detected, reprompting..'); this.logger.info('dtmf detected, reprompting..');
ep.api(this.cmd, `${ep.uuid} ${this.project} ${this.lang} none \'${this.dtmfEntry}\'`); ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang} none \'${this.dtmfEntry}\'`);
this.dtmfEntry = null; this.dtmfEntry = null;
} }
else if (this.greetingPlayed) { else if (this.greetingPlayed) {
this.logger.info('starting another intent'); this.logger.info('starting another intent');
ep.api(this.cmd, `${ep.uuid} ${this.project} ${this.lang}`); ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
} }
else { else {
this.logger.info('got empty intent'); this.logger.info('got empty intent');
ep.api(this.cmd, `${ep.uuid} ${this.project} ${this.lang}`); ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
} }
return; return;
} }
@@ -291,7 +231,7 @@ class Dialogflow extends Task {
// start a new intent, (we want to continue to listen during the audio playback) // start a new intent, (we want to continue to listen during the audio playback)
// _unless_ we are transferring or ending the session // _unless_ we are transferring or ending the session
if (!this.hangupAfterPlayDone) { if (!this.hangupAfterPlayDone) {
ep.api(this.cmd, `${ep.uuid} ${this.project} ${this.lang}`); ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
} }
try { try {
@@ -446,12 +386,10 @@ class Dialogflow extends Task {
// kill filler audio // kill filler audio
await ep.api('uuid_break', ep.uuid); await ep.api('uuid_break', ep.uuid);
// if ES start a new intent (for CX we do not set single_utterance on), // start a new intent, (we want to continue to listen during the audio playback)
// (we want to continue to listen during the audio playback)
// _unless_ we are transferring or ending the session // _unless_ we are transferring or ending the session
if (!this.hangupAfterPlayDone) { if (/*this.greetingPlayed &&*/ !this.hangupAfterPlayDone) {
this.startBot(); ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
//ep.api(this.cmd, `${ep.uuid} ${this.project} ${this.lang}`);
} }
this.playInProgress = true; this.playInProgress = true;
@@ -476,7 +414,12 @@ class Dialogflow extends Task {
return; return;
} }
} }
/*
if (!this.inbound && !this.greetingPlayed) {
this.logger.info('finished greeting on outbound call, starting new intent');
this.ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
}
*/
this.greetingPlayed = true; this.greetingPlayed = true;
if (this.hangupAfterPlayDone) { if (this.hangupAfterPlayDone) {
@@ -511,7 +454,7 @@ class Dialogflow extends Task {
} }
// kill the current dialogflow, which will result in us getting an immediate intent // kill the current dialogflow, which will result in us getting an immediate intent
ep.api(this.cmdStop, `${ep.uuid}`) ep.api('dialogflow_stop', `${ep.uuid}`)
.catch((err) => this.logger.info(`dialogflow_stop failed: ${err.message}`)); .catch((err) => this.logger.info(`dialogflow_stop failed: ${err.message}`));
} }
@@ -529,7 +472,7 @@ class Dialogflow extends Task {
} }
// kill the current dialogflow, which will result in us getting an immediate intent // kill the current dialogflow, which will result in us getting an immediate intent
ep.api(this.cmdStop, `${ep.uuid}`) ep.api('dialogflow_stop', `${ep.uuid}`)
.catch((err) => this.logger.info(`dialogflow_stop failed: ${err.message}`)); .catch((err) => this.logger.info(`dialogflow_stop failed: ${err.message}`));
} }

View File

@@ -4,31 +4,19 @@ class Intent {
this.evt = evt; this.evt = evt;
this.logger.debug({evt}, 'intent'); this.logger.debug({evt}, 'intent');
this.qr = this.isCX ? evt.detect_intent_response.query_result : evt.query_result; this.dtmfRequest = checkIntentForDtmfEntry(logger, evt);
this.dtmfRequest = this._checkIntentForDtmfEntry(logger, evt);
}
get response_id() {
return this.isCX ? this.evt.detect_intent_response.response_id : this.evt.response_id;
} }
get isEmpty() { get isEmpty() {
return !(this.response_id?.length > 0); return this.evt.response_id.length === 0;
} }
get fulfillmentText() { get fulfillmentText() {
return this.qr.fulfillment_text; return this.evt.query_result.fulfillment_text;
} }
get saysEndInteraction() { get saysEndInteraction() {
if (this.isCX) { return this.evt.query_result.intent.end_interaction ;
const end_interaction = this.qr.response_messages
.find((m) => typeof m === 'object' && 'end_interaction' in m)?.end_interaction;
//TODO: need to do more checking on the actual contents
return end_interaction && Object.keys(end_interaction).length > 0;
}
else return this.qr.intent.end_interaction ;
} }
get saysCollectDtmf() { get saysCollectDtmf() {
@@ -40,22 +28,7 @@ class Intent {
} }
get name() { get name() {
if (!this.isEmpty) { if (!this.isEmpty) return this.evt.query_result.intent.display_name;
if (this.isCX) {
return this.qr.match?.intent?.display_name;
}
else {
return this.qr.intent.display_name;
}
}
}
get isCX() {
return typeof this.evt.detect_intent_response === 'object';
}
get isES() {
return !this.isCX;
} }
toJSON() { toJSON() {
@@ -65,7 +38,11 @@ class Intent {
}; };
} }
/** }
module.exports = Intent;
/**
* Parse a returned intent for DTMF entry information * Parse a returned intent for DTMF entry information
* i.e. * i.e.
* allow-dtmf-x-y-z * allow-dtmf-x-y-z
@@ -78,39 +55,35 @@ class Intent {
* allow-dtmf-1-4-# : collect 1-4 digits, terminating if '#' is entered * allow-dtmf-1-4-# : collect 1-4 digits, terminating if '#' is entered
* @param {*} intent - dialogflow intent * @param {*} intent - dialogflow intent
*/ */
_checkIntentForDtmfEntry(logger, intent) { const checkIntentForDtmfEntry = (logger, intent) => {
const qr = this.isCX ? intent.detect_intent_response.query_result : intent.query_result; const qr = intent.query_result;
if (!qr || !qr.fulfillment_messages || !qr.output_contexts) {
logger.info({f: qr.fulfillment_messages, o: qr.output_contexts}, 'no dtmfs');
return;
}
if (!qr || !qr.fulfillment_messages || !qr.output_contexts) { // check for custom payloads with a gather verb
logger.info({f: qr.fulfillment_messages, o: qr.output_contexts}, 'no dtmfs'); const custom = qr.fulfillment_messages.find((f) => f.payload && f.payload.verb === 'gather');
return; if (custom && custom.payload && custom.payload.verb === 'gather') {
} logger.info({custom}, 'found dtmf custom payload');
return {
max: custom.payload.numDigits,
term: custom.payload.finishOnKey,
template: custom.payload.responseTemplate
};
}
// check for custom payloads with a gather verb // check for an output context with a specific naming convention
const custom = qr.fulfillment_messages.find((f) => f.payload && f.payload.verb === 'gather'); const context = qr.output_contexts.find((oc) => oc.name.includes('/contexts/allow-dtmf-'));
if (custom && custom.payload && custom.payload.verb === 'gather') { if (context) {
logger.info({custom}, 'found dtmf custom payload'); const arr = /allow-dtmf-(\d+)(?:-(\d+))?(?:-(.*))?/.exec(context.name);
if (arr) {
logger.info({custom}, 'found dtmf output context');
return { return {
max: custom.payload.numDigits, min: parseInt(arr[1]),
term: custom.payload.finishOnKey, max: arr.length > 2 ? parseInt(arr[2]) : null,
template: custom.payload.responseTemplate term: arr.length > 3 ? arr[3] : null
}; };
} }
// check for an output context with a specific naming convention
const context = qr.output_contexts.find((oc) => oc.name.includes('/contexts/allow-dtmf-'));
if (context) {
const arr = /allow-dtmf-(\d+)(?:-(\d+))?(?:-(.*))?/.exec(context.name);
if (arr) {
logger.info({custom}, 'found dtmf output context');
return {
min: parseInt(arr[1]),
max: arr.length > 2 ? parseInt(arr[2]) : null,
term: arr.length > 3 ? arr[3] : null
};
}
}
} }
} };
module.exports = Intent;

View File

@@ -39,9 +39,9 @@ class TaskRestDial extends Task {
if (this.data.amd) { if (this.data.amd) {
this.startAmd = cs.startAmd; this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd;
this.on('amd', this._onAmdEvent.bind(this, cs)); this.on('amd', this._onAmdEvent.bind(this, cs));
} }
this.stopAmd = cs.stopAmd;
this._setCallTimer(); this._setCallTimer();
await this.awaitTaskDone(); await this.awaitTaskDone();

View File

@@ -141,12 +141,12 @@ class TaskSay extends TtsTask {
await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]); await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]);
} }
else { else {
let tts_cache_filename; const isStreaming = filepath[segment].startsWith('say:{');
if (filepath[segment].startsWith('say:{')) { if (isStreaming) {
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]); const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
if (arr) this.logger.debug(`Say:exec sending streaming tts request: ${arr[1].substring(0, 64)}..`); if (arr) this.logger.debug(`Say:exec sending streaming tts request: ${arr[1].substring(0, 64)}..`);
} }
else this.logger.debug(`Say:exec sending ${filepath[segment].substring(0, 64)}`); else this.logger.debug(`Say:exec sending ${filepath[segment].substring(0, 64)}`);
ep.once('playback-start', (evt) => { ep.once('playback-start', (evt) => {
this.logger.debug({evt}, 'Say got playback-start'); this.logger.debug({evt}, 'Say got playback-start');
if (this.otelSpan) { if (this.otelSpan) {
@@ -154,17 +154,19 @@ class TaskSay extends TtsTask {
this.otelSpan.end(); this.otelSpan.end();
this.otelSpan = null; this.otelSpan = null;
if (evt.variable_tts_cache_filename) { if (evt.variable_tts_cache_filename) {
tts_cache_filename = evt.variable_tts_cache_filename;
cs.trackTmpFile(evt.variable_tts_cache_filename); cs.trackTmpFile(evt.variable_tts_cache_filename);
} }
else {
this.logger.info('No tts_cache_filename in playback-start event');
}
} }
}); });
ep.once('playback-stop', (evt) => { ep.once('playback-stop', (evt) => {
if (!tts_cache_filename || evt.variable_tts_cache_filename !== tts_cache_filename) { this.logger.debug({evt}, 'Say got playback-stop');
this.logger.info({evt}, 'Say: discarding playback-stop from other say verb'); if (evt.variable_tts_error) {
writeAlerts({
account_sid,
alert_type: AlertType.TTS_FAILURE,
vendor,
detail: evt.variable_tts_error
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
} }
else { else {
this.logger.debug({evt}, 'Say got playback-stop'); this.logger.debug({evt}, 'Say got playback-stop');

View File

@@ -79,7 +79,6 @@ class ActionHookDelayProcessor extends Emitter {
} }
async stop() { async stop() {
this.logger.debug('ActionHookDelayProcessor#stop');
this._active = false; this._active = false;
if (this._noResponseTimer) { if (this._noResponseTimer) {
@@ -91,25 +90,19 @@ class ActionHookDelayProcessor extends Emitter {
this._noResponseGiveUpTimer = null; this._noResponseGiveUpTimer = null;
} }
if (this._taskInProgress) { if (this._taskInProgress) {
this.logger.debug(`ActionHookDelayProcessor#stop: killing task in progress: ${this._taskInProgress.name}`); this.logger.debug(`ActionHookDelayProcessor#stop: stopping ${this._taskInProgress.name}`);
/** if we are doing a play, kill it immediately this._sayResolver = () => {
* if we are doing a say, wait for it to finish this.logger.debug('ActionHookDelayProcessor#stop: play/say is done, continue on..');
*/ //this._taskInProgress.kill(this.cs);
if (TaskName.Say === this._taskInProgress.name) {
this._sayResolver = () => {
this.logger.debug('ActionHookDelayProcessor#stop: say is done, continue on..');
this._taskInProgress.kill(this.cs);
this._taskInProgress = null;
};
this.logger.debug('ActionHookDelayProcessor#stop returning promise');
return new Promise((resolve) => this._sayResolver = resolve);
}
else {
/* play */
this._taskInProgress.kill(this.cs);
this._taskInProgress = null; this._taskInProgress = null;
};
/* we let Say finish, but interrupt Play */
if (TaskName.Play === this._taskInProgress.name) {
await this._taskInProgress.kill(this.cs);
} }
return new Promise((resolve) => this._sayResolver = resolve);
} }
this.logger.debug('ActionHookDelayProcessor#stop returning'); this.logger.debug('ActionHookDelayProcessor#stop returning');
} }
@@ -147,7 +140,7 @@ class ActionHookDelayProcessor extends Emitter {
this._taskInProgress = null; this._taskInProgress = null;
if (this._sayResolver) { if (this._sayResolver) {
/* we were waiting for the play to finish before continuing to next task */ /* we were waiting for the play to finish before continuing to next task */
this.logger.debug({evt}, 'got playback-stop'); this.logger.debug({evt}, 'ActionHookDelayProcessor#_onNoResponseTimer got playback-stop');
this._sayResolver(); this._sayResolver();
this._sayResolver = null; this._sayResolver = null;
} }

View File

@@ -12,6 +12,7 @@ const {
JAMBONES_TIME_SERIES_HOST, JAMBONES_TIME_SERIES_HOST,
JAMBONES_ESL_LISTEN_ADDRESS, JAMBONES_ESL_LISTEN_ADDRESS,
PORT, PORT,
HTTP_IP,
NODE_ENV, NODE_ENV,
} = require('../config'); } = require('../config');
const Registrar = require('@jambonz/mw-registrar'); const Registrar = require('@jambonz/mw-registrar');
@@ -193,7 +194,8 @@ function installSrfLocals(srf, logger) {
let localIp; let localIp;
try { try {
localIp = ip.address(); // Either use the configured IP address or call ip.address() to find it
localIp = HTTP_IP || ip.address();
} catch (err) { } catch (err) {
logger.error({err}, 'installSrfLocals - error detecting local ipv4 address'); logger.error({err}, 'installSrfLocals - error detecting local ipv4 address');
} }

View File

@@ -142,7 +142,6 @@ const optimalDeepramModels = {
tr: ['nova-2', 'nova-2'], tr: ['nova-2', 'nova-2'],
uk: ['nova-2', 'nova-2'] uk: ['nova-2', 'nova-2']
}; };
const selectDefaultDeepgramModel = (task, language) => { const selectDefaultDeepgramModel = (task, language) => {
if (language in optimalDeepramModels) { if (language in optimalDeepramModels) {
const [gather, transcribe] = optimalDeepramModels[language]; const [gather, transcribe] = optimalDeepramModels[language];
@@ -151,6 +150,24 @@ const selectDefaultDeepgramModel = (task, language) => {
return 'base'; return 'base';
}; };
const optimalGoogleModels = {
'v1' : {
'en-IN':['telephony', 'latest_long']
},
'v2' : {
'en-IN':['telephony', 'long']
}
};
const selectDefaultGoogleModel = (task, language, version) => {
const useV2 = version === 'v2';
if (language in optimalGoogleModels[version]) {
const [gather, transcribe] = optimalGoogleModels[version][language];
return task.name === TaskName.Gather ? gather : transcribe;
}
return task.name === TaskName.Gather ?
(useV2 ? 'telephony_short' : 'command_and_search') :
(useV2 ? 'long' : 'latest_long');
};
const consolidateTranscripts = (bufferedTranscripts, channel, language, vendor) => { const consolidateTranscripts = (bufferedTranscripts, channel, language, vendor) => {
if (bufferedTranscripts.length === 1) return bufferedTranscripts[0]; if (bufferedTranscripts.length === 1) return bufferedTranscripts[0];
let totalConfidence = 0; let totalConfidence = 0;
@@ -497,9 +514,9 @@ module.exports = (logger) => {
if ('google' === vendor) { if ('google' === vendor) {
const useV2 = rOpts.googleOptions?.serviceVersion === 'v2'; const useV2 = rOpts.googleOptions?.serviceVersion === 'v2';
const model = task.name === TaskName.Gather ? const version = useV2 ? 'v2' : 'v1';
(useV2 ? 'telephony_short' : 'command_and_search') : let {model} = rOpts;
(useV2 ? 'long' : 'latest_long'); model = model || selectDefaultGoogleModel(task, language, version);
opts = { opts = {
...opts, ...opts,
...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}), ...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),

63
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "jambonz-feature-server", "name": "jambonz-feature-server",
"version": "0.9.0", "version": "0.9.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jambonz-feature-server", "name": "jambonz-feature-server",
"version": "0.9.0", "version": "0.9.2",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@aws-sdk/client-auto-scaling": "^3.549.0", "@aws-sdk/client-auto-scaling": "^3.549.0",
@@ -18,7 +18,7 @@
"@jambonz/speech-utils": "^0.1.15", "@jambonz/speech-utils": "^0.1.15",
"@jambonz/stats-collector": "^0.1.10", "@jambonz/stats-collector": "^0.1.10",
"@jambonz/time-series": "^0.2.9", "@jambonz/time-series": "^0.2.9",
"@jambonz/verb-specifications": "^0.0.79", "@jambonz/verb-specifications": "^0.0.76",
"@opentelemetry/api": "^1.8.0", "@opentelemetry/api": "^1.8.0",
"@opentelemetry/exporter-jaeger": "^1.23.0", "@opentelemetry/exporter-jaeger": "^1.23.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0", "@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
@@ -31,7 +31,7 @@
"bent": "^7.3.12", "bent": "^7.3.12",
"debug": "^4.3.4", "debug": "^4.3.4",
"deepcopy": "^2.1.0", "deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.43", "drachtio-fsmrf": "^3.0.45",
"drachtio-srf": "^4.5.35", "drachtio-srf": "^4.5.35",
"express": "^4.19.2", "express": "^4.19.2",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
@@ -45,10 +45,10 @@
"short-uuid": "^5.1.0", "short-uuid": "^5.1.0",
"sinon": "^17.0.1", "sinon": "^17.0.1",
"to-snake-case": "^1.0.0", "to-snake-case": "^1.0.0",
"undici": "^6.19.2", "undici": "^6.19.4",
"uuid-random": "^1.3.2", "uuid-random": "^1.3.2",
"verify-aws-sns-signature": "^0.1.0", "verify-aws-sns-signature": "^0.1.0",
"ws": "^8.17.1", "ws": "^8.18.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
"devDependencies": { "devDependencies": {
@@ -1575,9 +1575,9 @@
} }
}, },
"node_modules/@jambonz/verb-specifications": { "node_modules/@jambonz/verb-specifications": {
"version": "0.0.79", "version": "0.0.76",
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.79.tgz", "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.76.tgz",
"integrity": "sha512-SJpUfRivPaBBF16sUhkKPuXC4KFf2vE03LuSNYGhtjzZ03PnIGXbsuz16cK+XeQow5tkof+ptmxwFgfv6TM5RQ==", "integrity": "sha512-7s61qAsG07xLLaEAHW236rSYzEoh9Qg0aRWHPbTfxCsuTKDNeq+5EwGAShDU5R5ZpjgweZJLhArQm8Ym+4xJ2A==",
"dependencies": { "dependencies": {
"debug": "^4.3.4", "debug": "^4.3.4",
"pino": "^8.8.0" "pino": "^8.8.0"
@@ -3831,9 +3831,10 @@
} }
}, },
"node_modules/drachtio-fsmrf": { "node_modules/drachtio-fsmrf": {
"version": "3.0.43", "version": "3.0.45",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.43.tgz", "resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.45.tgz",
"integrity": "sha512-ZDgO4WCzZssmOpf+ng20P8r5pu7demWFiECqwRivbWadXqAm3LNabZwWOred2MdCRDvzgHWUwBZoJPzCqKBL5g==", "integrity": "sha512-YsrevTwGvg9v0OQXB4gZedlmzegB83fyd3NX3X2x7NXsKa5jO6TZCvZVHtBf/jR/ELE3B0aVLpxHw2YviRMIuQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"camel-case": "^4.1.2", "camel-case": "^4.1.2",
"debug": "^2.6.9", "debug": "^2.6.9",
@@ -8831,10 +8832,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.19.2", "version": "6.19.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.2.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.4.tgz",
"integrity": "sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==", "integrity": "sha512-i3uaEUwNdkRq2qtTRRJb13moW5HWqviu7Vl7oYRYz++uPtGHJj+x7TGjcEuwS5Mt2P4nA0U9dhIX3DdB6JGY0g==",
"license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"
} }
@@ -9193,10 +9193,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.17.1", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@@ -10539,9 +10538,9 @@
} }
}, },
"@jambonz/verb-specifications": { "@jambonz/verb-specifications": {
"version": "0.0.79", "version": "0.0.76",
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.79.tgz", "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.76.tgz",
"integrity": "sha512-SJpUfRivPaBBF16sUhkKPuXC4KFf2vE03LuSNYGhtjzZ03PnIGXbsuz16cK+XeQow5tkof+ptmxwFgfv6TM5RQ==", "integrity": "sha512-7s61qAsG07xLLaEAHW236rSYzEoh9Qg0aRWHPbTfxCsuTKDNeq+5EwGAShDU5R5ZpjgweZJLhArQm8Ym+4xJ2A==",
"requires": { "requires": {
"debug": "^4.3.4", "debug": "^4.3.4",
"pino": "^8.8.0" "pino": "^8.8.0"
@@ -12255,9 +12254,9 @@
} }
}, },
"drachtio-fsmrf": { "drachtio-fsmrf": {
"version": "3.0.43", "version": "3.0.45",
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.43.tgz", "resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.45.tgz",
"integrity": "sha512-ZDgO4WCzZssmOpf+ng20P8r5pu7demWFiECqwRivbWadXqAm3LNabZwWOred2MdCRDvzgHWUwBZoJPzCqKBL5g==", "integrity": "sha512-YsrevTwGvg9v0OQXB4gZedlmzegB83fyd3NX3X2x7NXsKa5jO6TZCvZVHtBf/jR/ELE3B0aVLpxHw2YviRMIuQ==",
"requires": { "requires": {
"camel-case": "^4.1.2", "camel-case": "^4.1.2",
"debug": "^2.6.9", "debug": "^2.6.9",
@@ -16056,9 +16055,9 @@
} }
}, },
"undici": { "undici": {
"version": "6.19.2", "version": "6.19.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.2.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.4.tgz",
"integrity": "sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==" "integrity": "sha512-i3uaEUwNdkRq2qtTRRJb13moW5HWqviu7Vl7oYRYz++uPtGHJj+x7TGjcEuwS5Mt2P4nA0U9dhIX3DdB6JGY0g=="
}, },
"undici-types": { "undici-types": {
"version": "5.26.5", "version": "5.26.5",
@@ -16329,9 +16328,9 @@
} }
}, },
"ws": { "ws": {
"version": "8.17.1", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"requires": {} "requires": {}
}, },
"xml2js": { "xml2js": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "jambonz-feature-server", "name": "jambonz-feature-server",
"version": "0.9.0", "version": "0.9.2",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 18.x" "node": ">= 18.x"
@@ -34,7 +34,7 @@
"@jambonz/speech-utils": "^0.1.15", "@jambonz/speech-utils": "^0.1.15",
"@jambonz/stats-collector": "^0.1.10", "@jambonz/stats-collector": "^0.1.10",
"@jambonz/time-series": "^0.2.9", "@jambonz/time-series": "^0.2.9",
"@jambonz/verb-specifications": "^0.0.79", "@jambonz/verb-specifications": "^0.0.76",
"@opentelemetry/api": "^1.8.0", "@opentelemetry/api": "^1.8.0",
"@opentelemetry/exporter-jaeger": "^1.23.0", "@opentelemetry/exporter-jaeger": "^1.23.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0", "@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
@@ -47,7 +47,7 @@
"bent": "^7.3.12", "bent": "^7.3.12",
"debug": "^4.3.4", "debug": "^4.3.4",
"deepcopy": "^2.1.0", "deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.43", "drachtio-fsmrf": "^3.0.45",
"drachtio-srf": "^4.5.35", "drachtio-srf": "^4.5.35",
"express": "^4.19.2", "express": "^4.19.2",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
@@ -61,10 +61,10 @@
"short-uuid": "^5.1.0", "short-uuid": "^5.1.0",
"sinon": "^17.0.1", "sinon": "^17.0.1",
"to-snake-case": "^1.0.0", "to-snake-case": "^1.0.0",
"undici": "^6.19.2", "undici": "^6.19.4",
"uuid-random": "^1.3.2", "uuid-random": "^1.3.2",
"verify-aws-sns-signature": "^0.1.0", "verify-aws-sns-signature": "^0.1.0",
"ws": "^8.17.1", "ws": "^8.18.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
"devDependencies": { "devDependencies": {