mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-10 08:21:33 +00:00
Compare commits
8 Commits
feat/dialo
...
v0.9.2-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4a6057fc6 | ||
|
|
174438bb01 | ||
|
|
4348615b75 | ||
|
|
d365883bfe | ||
|
|
c0ab936b76 | ||
|
|
600ff763fa | ||
|
|
4d077e990f | ||
|
|
eccef54b04 |
@@ -21,6 +21,7 @@ Configuration is provided via environment variables:
|
||||
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes|
|
||||
|GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes|
|
||||
|HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes|
|
||||
|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_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|
|
||||
|
||||
@@ -73,6 +73,7 @@ const JAMBONES_LOGLEVEL = process.env.JAMBONES_LOGLEVEL || 'info';
|
||||
const JAMBONES_INJECT_CONTENT = process.env.JAMBONES_INJECT_CONTENT;
|
||||
|
||||
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 K8S = process.env.K8S;
|
||||
@@ -170,6 +171,7 @@ module.exports = {
|
||||
JAMBONES_CLUSTER_ID,
|
||||
PORT,
|
||||
HTTP_PORT_MAX,
|
||||
HTTP_IP,
|
||||
K8S,
|
||||
K8S_SBC_SIP_SERVICE_NAME,
|
||||
JAMBONES_SUBNET,
|
||||
|
||||
@@ -97,7 +97,8 @@ router.post('/',
|
||||
'X-Trace-ID': rootSpan.traceId,
|
||||
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
|
||||
...(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) {
|
||||
|
||||
@@ -566,9 +566,7 @@ class CallSession extends Emitter {
|
||||
//this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - ahd settings');
|
||||
//await this.clearActionHookDelayProcessor();
|
||||
}
|
||||
else {
|
||||
this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - restore ahd after gather override');
|
||||
}
|
||||
this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - say or play action completed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1069,7 +1067,7 @@ class CallSession extends Emitter {
|
||||
try {
|
||||
await this._awaitCommandsOrHangup();
|
||||
|
||||
await this.clearOrRestoreActionHookDelayProcessor();
|
||||
//await this.clearOrRestoreActionHookDelayProcessor();
|
||||
|
||||
//TODO: remove filler noise code and simply create as action hook delay
|
||||
if (this._isPlayingFillerNoise) {
|
||||
@@ -2074,6 +2072,9 @@ Duration=${duration} `
|
||||
// When this call kicked out from conference, session need to replace endpoint
|
||||
// but this.ms might be undefined/null at this case.
|
||||
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._configMsEndpoint();
|
||||
|
||||
|
||||
@@ -118,7 +118,9 @@ class Conference extends Task {
|
||||
this.emitter.emit('kill');
|
||||
await this._doFinalMemberCheck(cs);
|
||||
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}`)
|
||||
.catch((err) => this.logger.info({err}, 'Error kicking participant'));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const assert = require('assert');
|
||||
const Task = require('../task');
|
||||
const {TaskName, TaskPreconditions} = require('../../utils/constants');
|
||||
const Intent = require('./intent');
|
||||
@@ -11,29 +10,19 @@ class Dialogflow extends Task {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
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');
|
||||
assert(this.credentials, 'dialogflow credentials are required');
|
||||
|
||||
if (this.isCX) {
|
||||
this.environment = this.data.environment || 'default';
|
||||
/* set project id with environment and region (optionally) */
|
||||
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}`;
|
||||
}
|
||||
else {
|
||||
/* ES: set project id with environment and region (optionally) */
|
||||
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.project = this.data.project;
|
||||
}
|
||||
|
||||
this.lang = this.data.lang || 'en-US';
|
||||
@@ -78,23 +67,31 @@ class Dialogflow extends Task {
|
||||
this.fallbackLabel = this.data.tts.fallbackLabel;
|
||||
}
|
||||
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 isCX() { return this.model === 'cx'; }
|
||||
|
||||
get isES() { return !this.isCX; }
|
||||
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
|
||||
try {
|
||||
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();
|
||||
} catch (err) {
|
||||
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::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();
|
||||
|
||||
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::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 creds = JSON.stringify(obj);
|
||||
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,
|
||||
* 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) {
|
||||
this.logger.info('no input timer fired, reprompting..');
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
else if (this.greetingPlayed) {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
@@ -291,7 +231,7 @@ class Dialogflow extends Task {
|
||||
// start a new intent, (we want to continue to listen during the audio playback)
|
||||
// _unless_ we are transferring or ending the session
|
||||
if (!this.hangupAfterPlayDone) {
|
||||
ep.api(this.cmd, `${ep.uuid} ${this.project} ${this.lang}`);
|
||||
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -446,12 +386,10 @@ class Dialogflow extends Task {
|
||||
// kill filler audio
|
||||
await ep.api('uuid_break', ep.uuid);
|
||||
|
||||
// if ES start a new intent (for CX we do not set single_utterance on),
|
||||
// (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
|
||||
if (!this.hangupAfterPlayDone) {
|
||||
this.startBot();
|
||||
//ep.api(this.cmd, `${ep.uuid} ${this.project} ${this.lang}`);
|
||||
if (/*this.greetingPlayed &&*/ !this.hangupAfterPlayDone) {
|
||||
ep.api('dialogflow_start', `${ep.uuid} ${this.project} ${this.lang}`);
|
||||
}
|
||||
|
||||
this.playInProgress = true;
|
||||
@@ -476,7 +414,12 @@ class Dialogflow extends Task {
|
||||
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;
|
||||
|
||||
if (this.hangupAfterPlayDone) {
|
||||
@@ -511,7 +454,7 @@ class Dialogflow extends Task {
|
||||
}
|
||||
|
||||
// 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}`));
|
||||
}
|
||||
|
||||
@@ -529,7 +472,7 @@ class Dialogflow extends Task {
|
||||
}
|
||||
|
||||
// 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}`));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,31 +4,19 @@ class Intent {
|
||||
this.evt = evt;
|
||||
|
||||
this.logger.debug({evt}, 'intent');
|
||||
this.qr = this.isCX ? evt.detect_intent_response.query_result : evt.query_result;
|
||||
this.dtmfRequest = this._checkIntentForDtmfEntry(logger, evt);
|
||||
}
|
||||
|
||||
get response_id() {
|
||||
return this.isCX ? this.evt.detect_intent_response.response_id : this.evt.response_id;
|
||||
this.dtmfRequest = checkIntentForDtmfEntry(logger, evt);
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return !(this.response_id?.length > 0);
|
||||
return this.evt.response_id.length === 0;
|
||||
}
|
||||
|
||||
get fulfillmentText() {
|
||||
return this.qr.fulfillment_text;
|
||||
return this.evt.query_result.fulfillment_text;
|
||||
}
|
||||
|
||||
get saysEndInteraction() {
|
||||
if (this.isCX) {
|
||||
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 ;
|
||||
return this.evt.query_result.intent.end_interaction ;
|
||||
}
|
||||
|
||||
get saysCollectDtmf() {
|
||||
@@ -40,22 +28,7 @@ class Intent {
|
||||
}
|
||||
|
||||
get name() {
|
||||
if (!this.isEmpty) {
|
||||
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;
|
||||
if (!this.isEmpty) return this.evt.query_result.intent.display_name;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@@ -65,7 +38,11 @@ class Intent {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
}
|
||||
|
||||
module.exports = Intent;
|
||||
|
||||
/**
|
||||
* Parse a returned intent for DTMF entry information
|
||||
* i.e.
|
||||
* allow-dtmf-x-y-z
|
||||
@@ -78,39 +55,35 @@ class Intent {
|
||||
* allow-dtmf-1-4-# : collect 1-4 digits, terminating if '#' is entered
|
||||
* @param {*} intent - dialogflow intent
|
||||
*/
|
||||
_checkIntentForDtmfEntry(logger, intent) {
|
||||
const qr = this.isCX ? intent.detect_intent_response.query_result : intent.query_result;
|
||||
const checkIntentForDtmfEntry = (logger, intent) => {
|
||||
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) {
|
||||
logger.info({f: qr.fulfillment_messages, o: qr.output_contexts}, 'no dtmfs');
|
||||
return;
|
||||
}
|
||||
// check for custom payloads with a gather verb
|
||||
const custom = qr.fulfillment_messages.find((f) => f.payload && f.payload.verb === 'gather');
|
||||
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
|
||||
const custom = qr.fulfillment_messages.find((f) => f.payload && f.payload.verb === 'gather');
|
||||
if (custom && custom.payload && custom.payload.verb === 'gather') {
|
||||
logger.info({custom}, 'found dtmf custom payload');
|
||||
// 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 {
|
||||
max: custom.payload.numDigits,
|
||||
term: custom.payload.finishOnKey,
|
||||
template: custom.payload.responseTemplate
|
||||
min: parseInt(arr[1]),
|
||||
max: arr.length > 2 ? parseInt(arr[2]) : null,
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -39,9 +39,9 @@ class TaskRestDial extends Task {
|
||||
|
||||
if (this.data.amd) {
|
||||
this.startAmd = cs.startAmd;
|
||||
this.stopAmd = cs.stopAmd;
|
||||
this.on('amd', this._onAmdEvent.bind(this, cs));
|
||||
}
|
||||
this.stopAmd = cs.stopAmd;
|
||||
|
||||
this._setCallTimer();
|
||||
await this.awaitTaskDone();
|
||||
|
||||
@@ -141,12 +141,12 @@ class TaskSay extends TtsTask {
|
||||
await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]);
|
||||
}
|
||||
else {
|
||||
let tts_cache_filename;
|
||||
if (filepath[segment].startsWith('say:{')) {
|
||||
const isStreaming = filepath[segment].startsWith('say:{');
|
||||
if (isStreaming) {
|
||||
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
|
||||
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) => {
|
||||
this.logger.debug({evt}, 'Say got playback-start');
|
||||
if (this.otelSpan) {
|
||||
@@ -154,17 +154,19 @@ class TaskSay extends TtsTask {
|
||||
this.otelSpan.end();
|
||||
this.otelSpan = null;
|
||||
if (evt.variable_tts_cache_filename) {
|
||||
tts_cache_filename = 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) => {
|
||||
if (!tts_cache_filename || evt.variable_tts_cache_filename !== tts_cache_filename) {
|
||||
this.logger.info({evt}, 'Say: discarding playback-stop from other say verb');
|
||||
this.logger.debug({evt}, 'Say got playback-stop');
|
||||
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 {
|
||||
this.logger.debug({evt}, 'Say got playback-stop');
|
||||
|
||||
@@ -79,7 +79,6 @@ class ActionHookDelayProcessor extends Emitter {
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.logger.debug('ActionHookDelayProcessor#stop');
|
||||
this._active = false;
|
||||
|
||||
if (this._noResponseTimer) {
|
||||
@@ -91,25 +90,19 @@ class ActionHookDelayProcessor extends Emitter {
|
||||
this._noResponseGiveUpTimer = null;
|
||||
}
|
||||
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
|
||||
* if we are doing a say, wait for it to finish
|
||||
*/
|
||||
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._sayResolver = () => {
|
||||
this.logger.debug('ActionHookDelayProcessor#stop: play/say is done, continue on..');
|
||||
//this._taskInProgress.kill(this.cs);
|
||||
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');
|
||||
}
|
||||
@@ -147,7 +140,7 @@ class ActionHookDelayProcessor extends Emitter {
|
||||
this._taskInProgress = null;
|
||||
if (this._sayResolver) {
|
||||
/* 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 = null;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ const {
|
||||
JAMBONES_TIME_SERIES_HOST,
|
||||
JAMBONES_ESL_LISTEN_ADDRESS,
|
||||
PORT,
|
||||
HTTP_IP,
|
||||
NODE_ENV,
|
||||
} = require('../config');
|
||||
const Registrar = require('@jambonz/mw-registrar');
|
||||
@@ -193,7 +194,8 @@ function installSrfLocals(srf, logger) {
|
||||
|
||||
let localIp;
|
||||
try {
|
||||
localIp = ip.address();
|
||||
// Either use the configured IP address or call ip.address() to find it
|
||||
localIp = HTTP_IP || ip.address();
|
||||
} catch (err) {
|
||||
logger.error({err}, 'installSrfLocals - error detecting local ipv4 address');
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ const optimalDeepramModels = {
|
||||
tr: ['nova-2', 'nova-2'],
|
||||
uk: ['nova-2', 'nova-2']
|
||||
};
|
||||
|
||||
const selectDefaultDeepgramModel = (task, language) => {
|
||||
if (language in optimalDeepramModels) {
|
||||
const [gather, transcribe] = optimalDeepramModels[language];
|
||||
@@ -151,6 +150,24 @@ const selectDefaultDeepgramModel = (task, language) => {
|
||||
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) => {
|
||||
if (bufferedTranscripts.length === 1) return bufferedTranscripts[0];
|
||||
let totalConfidence = 0;
|
||||
@@ -497,9 +514,9 @@ module.exports = (logger) => {
|
||||
|
||||
if ('google' === vendor) {
|
||||
const useV2 = rOpts.googleOptions?.serviceVersion === 'v2';
|
||||
const model = task.name === TaskName.Gather ?
|
||||
(useV2 ? 'telephony_short' : 'command_and_search') :
|
||||
(useV2 ? 'long' : 'latest_long');
|
||||
const version = useV2 ? 'v2' : 'v1';
|
||||
let {model} = rOpts;
|
||||
model = model || selectDefaultGoogleModel(task, language, version);
|
||||
opts = {
|
||||
...opts,
|
||||
...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),
|
||||
|
||||
63
package-lock.json
generated
63
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@jambonz/speech-utils": "^0.1.15",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/time-series": "^0.2.9",
|
||||
"@jambonz/verb-specifications": "^0.0.79",
|
||||
"@jambonz/verb-specifications": "^0.0.76",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
||||
@@ -31,7 +31,7 @@
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.43",
|
||||
"drachtio-fsmrf": "^3.0.45",
|
||||
"drachtio-srf": "^4.5.35",
|
||||
"express": "^4.19.2",
|
||||
"express-validator": "^7.0.1",
|
||||
@@ -45,10 +45,10 @@
|
||||
"short-uuid": "^5.1.0",
|
||||
"sinon": "^17.0.1",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"undici": "^6.19.2",
|
||||
"undici": "^6.19.4",
|
||||
"uuid-random": "^1.3.2",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
"ws": "^8.17.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1575,9 +1575,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/verb-specifications": {
|
||||
"version": "0.0.79",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.79.tgz",
|
||||
"integrity": "sha512-SJpUfRivPaBBF16sUhkKPuXC4KFf2vE03LuSNYGhtjzZ03PnIGXbsuz16cK+XeQow5tkof+ptmxwFgfv6TM5RQ==",
|
||||
"version": "0.0.76",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.76.tgz",
|
||||
"integrity": "sha512-7s61qAsG07xLLaEAHW236rSYzEoh9Qg0aRWHPbTfxCsuTKDNeq+5EwGAShDU5R5ZpjgweZJLhArQm8Ym+4xJ2A==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"pino": "^8.8.0"
|
||||
@@ -3831,9 +3831,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/drachtio-fsmrf": {
|
||||
"version": "3.0.43",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.43.tgz",
|
||||
"integrity": "sha512-ZDgO4WCzZssmOpf+ng20P8r5pu7demWFiECqwRivbWadXqAm3LNabZwWOred2MdCRDvzgHWUwBZoJPzCqKBL5g==",
|
||||
"version": "3.0.45",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.45.tgz",
|
||||
"integrity": "sha512-YsrevTwGvg9v0OQXB4gZedlmzegB83fyd3NX3X2x7NXsKa5jO6TZCvZVHtBf/jR/ELE3B0aVLpxHw2YviRMIuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camel-case": "^4.1.2",
|
||||
"debug": "^2.6.9",
|
||||
@@ -8831,10 +8832,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.2.tgz",
|
||||
"integrity": "sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==",
|
||||
"license": "MIT",
|
||||
"version": "6.19.4",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.4.tgz",
|
||||
"integrity": "sha512-i3uaEUwNdkRq2qtTRRJb13moW5HWqviu7Vl7oYRYz++uPtGHJj+x7TGjcEuwS5Mt2P4nA0U9dhIX3DdB6JGY0g==",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
@@ -9193,10 +9193,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -10539,9 +10538,9 @@
|
||||
}
|
||||
},
|
||||
"@jambonz/verb-specifications": {
|
||||
"version": "0.0.79",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.79.tgz",
|
||||
"integrity": "sha512-SJpUfRivPaBBF16sUhkKPuXC4KFf2vE03LuSNYGhtjzZ03PnIGXbsuz16cK+XeQow5tkof+ptmxwFgfv6TM5RQ==",
|
||||
"version": "0.0.76",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.76.tgz",
|
||||
"integrity": "sha512-7s61qAsG07xLLaEAHW236rSYzEoh9Qg0aRWHPbTfxCsuTKDNeq+5EwGAShDU5R5ZpjgweZJLhArQm8Ym+4xJ2A==",
|
||||
"requires": {
|
||||
"debug": "^4.3.4",
|
||||
"pino": "^8.8.0"
|
||||
@@ -12255,9 +12254,9 @@
|
||||
}
|
||||
},
|
||||
"drachtio-fsmrf": {
|
||||
"version": "3.0.43",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.43.tgz",
|
||||
"integrity": "sha512-ZDgO4WCzZssmOpf+ng20P8r5pu7demWFiECqwRivbWadXqAm3LNabZwWOred2MdCRDvzgHWUwBZoJPzCqKBL5g==",
|
||||
"version": "3.0.45",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.45.tgz",
|
||||
"integrity": "sha512-YsrevTwGvg9v0OQXB4gZedlmzegB83fyd3NX3X2x7NXsKa5jO6TZCvZVHtBf/jR/ELE3B0aVLpxHw2YviRMIuQ==",
|
||||
"requires": {
|
||||
"camel-case": "^4.1.2",
|
||||
"debug": "^2.6.9",
|
||||
@@ -16056,9 +16055,9 @@
|
||||
}
|
||||
},
|
||||
"undici": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.2.tgz",
|
||||
"integrity": "sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA=="
|
||||
"version": "6.19.4",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.4.tgz",
|
||||
"integrity": "sha512-i3uaEUwNdkRq2qtTRRJb13moW5HWqviu7Vl7oYRYz++uPtGHJj+x7TGjcEuwS5Mt2P4nA0U9dhIX3DdB6JGY0g=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
@@ -16329,9 +16328,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"requires": {}
|
||||
},
|
||||
"xml2js": {
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jambonz-feature-server",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.2",
|
||||
"main": "app.js",
|
||||
"engines": {
|
||||
"node": ">= 18.x"
|
||||
@@ -34,7 +34,7 @@
|
||||
"@jambonz/speech-utils": "^0.1.15",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/time-series": "^0.2.9",
|
||||
"@jambonz/verb-specifications": "^0.0.79",
|
||||
"@jambonz/verb-specifications": "^0.0.76",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.43",
|
||||
"drachtio-fsmrf": "^3.0.45",
|
||||
"drachtio-srf": "^4.5.35",
|
||||
"express": "^4.19.2",
|
||||
"express-validator": "^7.0.1",
|
||||
@@ -61,10 +61,10 @@
|
||||
"short-uuid": "^5.1.0",
|
||||
"sinon": "^17.0.1",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"undici": "^6.19.2",
|
||||
"undici": "^6.19.4",
|
||||
"uuid-random": "^1.3.2",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
"ws": "^8.17.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user