Compare commits

..

4 Commits

Author SHA1 Message Date
Dave Horton
f7134d8fe7 more logging on restart of transcribing during gather 2024-05-10 14:12:34 -04:00
Dave Horton
a23dc50c20 lint 2024-05-03 08:56:55 -04:00
Dave Horton
888fddff37 possible fix for race condition in gather which ends but lets transcription continue 2024-05-03 08:55:16 -04:00
Dave Horton
e1497f90a8 update with various deepgram fixes, including for #700 2024-04-01 13:03:52 -04:00
48 changed files with 8555 additions and 7589 deletions

View File

@@ -6,17 +6,12 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 18
- run: npm ci - run: npm ci
- run: npm run jslint - run: npm run jslint
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
- run: docker pull drachtio/sipp - run: docker pull drachtio/sipp
- run: npm test - run: npm test
env: env:

3
.gitignore vendored
View File

@@ -42,5 +42,4 @@ ecosystem.config.js
test/credentials/*.json test/credentials/*.json
run-tests.sh run-tests.sh
run-coverage.sh run-coverage.sh
.vscode .vscode
.env

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2018-2024 FirstFive8, Inc. Copyright (c) 2021 Drachtio Communications Services, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -21,7 +21,6 @@ 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|

7
app.js
View File

@@ -100,13 +100,8 @@ createHttpListener(logger, srf)
}); });
setInterval(async() => { setInterval(() => {
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count); srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
// Checking system log level
const systemInformation = await srf.locals.dbHelpers.lookupSystemInformation();
if (systemInformation && systemInformation.log_level) {
logger.level = systemInformation.log_level;
}
}, 20000); }, 20000);
const disconnect = () => { const disconnect = () => {

View File

@@ -9,112 +9,7 @@
"can't take your call", "can't take your call",
"will get back to you", "will get back to you",
"I'll get back to you", "I'll get back to you",
"we are unable", "we are unable"
"Unable to take your call now",
"I'll reply soon",
"I'll call back",
"I'll reach out to you as soon as possible",
"Leave a message",
"Away from phone",
"Not available now",
"I'll return call",
"On another call",
"Currently on another call",
"I will return call later",
"Busy please leave message",
"Message will be returned promptly",
"Currently unavailable to answer",
"Planning to return your call soon",
"Apologies for missing your call",
"Not by the phone at the moment",
"Expecting to return your call",
"Currently not accessible",
"Intend to call back",
"Appreciate your patience!",
"Engaged in another conversation",
"I Will respond promptly",
"Kindly leave a message",
"Currently occupied leave a message",
"Unfortunately unable to answer right now",
"Occupied at the moment",
"Not present leave a message",
"Regrettably unavailable kindly leave a message",
"Will ensure a prompt response to your message",
"Currently engaged",
"Will return your call at the earliest opportunity",
"Your message will receive my prompt attention",
"I'll respond as soon as I can",
"Your message is important please leave it after the beep",
"Away from the phone at the moment",
"Unable to answer right now",
"Engaged in another task",
"Not by the phone presently",
"I'll respond at my earliest convenience",
"Away from the phone momentarily",
"I'll return your call shortly",
"Currently not able to answer",
"Your message is important please leave it after the tone",
"I'm unable to take your call right now",
"Please leave your message for me",
"I'll get back to you soon",
"Your call has been missed",
"Please leave a detailed message for me to respond to",
"Leave a message I'll make sure to respond",
"Feel free to leave a message",
"Your call is important to me",
"I'll get back to you shortly",
"Your message will be attended to promptly",
"Not available at the moment",
"I'll be sure to get back to you",
"I'll call you back soon",
"I'll ensure a prompt response",
"Sorry for the inconvenience",
"I'll return your call",
"I'll make sure to get back to you",
"I'll call you back shortly",
"I'll return your call as soon as possible",
"Apologies for the inconvenience leave your message",
"Your call is appreciated",
"I'm unavailable to answer",
"I'm currently away",
"I'll return your call as soon as I can",
"I'm away from the phone",
"I'm currently unavailable to take your call",
"Sorry for missing your call",
"I'll ensure it receives my immediate attention",
"I'm away from the phone momentarily",
"I'll reach out to you shortly",
"Apologies for the inconvenience",
"Currently occupied",
"Unable to answer your call at the moment",
"I'll make sure to follow up with you",
"Sorry for not being available",
"I'll reach out to you as soon as I can",
"I'm currently engaged",
"I'm currently busy",
"I'm currently unavailable",
"I'll respond to you at my earliest convenience",
"Your message is appreciated",
"I'll get back to you promptly",
"I'll get back to you without delay",
"Currently away from the phone",
"I'll return your call at my earliest opportunity",
"Sorry for the missed call",
"I'll make sure to address your concerns",
"Please provide your details for a callback",
"I'll make every effort to respond promptly",
"I'll ensure it's attended to promptly",
"Away from the phone temporarily",
"I'll get back to you as soon as I return",
"Currently not in a position to answer your call",
"Your call cannot be answered at the moment",
"I'll ensure to respond as soon as I'm able",
"Your call is important please leave a message",
"Unable to answer right now please leave your message",
"Currently not accessible intending to return your call",
"I'll respond promptly to your message",
"leave a memo",
"please leave a memo"
], ],
"es-ES": [ "es-ES": [
"le pasamos la llamada", "le pasamos la llamada",

View File

@@ -73,7 +73,6 @@ 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;
@@ -108,8 +107,6 @@ const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY;
const ANCHOR_MEDIA_ALWAYS = process.env.ANCHOR_MEDIA_ALWAYS; const ANCHOR_MEDIA_ALWAYS = process.env.ANCHOR_MEDIA_ALWAYS;
const VMD_HINTS_FILE = process.env.VMD_HINTS_FILE; const VMD_HINTS_FILE = process.env.VMD_HINTS_FILE;
const JAMBONES_AWS_TRANSCRIBE_USE_GRPC = process.env.JAMBONES_AWS_TRANSCRIBE_USE_GRPC;
/* security, secrets */ /* security, secrets */
const LEGACY_CRYPTO = !!process.env.LEGACY_CRYPTO; const LEGACY_CRYPTO = !!process.env.LEGACY_CRYPTO;
const JWT_SECRET = process.env.JWT_SECRET; const JWT_SECRET = process.env.JWT_SECRET;
@@ -173,7 +170,6 @@ 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,
@@ -192,7 +188,6 @@ module.exports = {
ANCHOR_MEDIA_ALWAYS, ANCHOR_MEDIA_ALWAYS,
VMD_HINTS_FILE, VMD_HINTS_FILE,
JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS, JAMBONES_FREESWITCH_MAX_CALL_DURATION_MINS,
JAMBONES_AWS_TRANSCRIBE_USE_GRPC,
LEGACY_CRYPTO, LEGACY_CRYPTO,
JWT_SECRET, JWT_SECRET,

View File

@@ -30,20 +30,6 @@ const appsMap = {
} }
] ]
}] }]
},
conference: {
// Dummy hook to follow later feature server logic.
call_hook: {
url: 'https://jambonz.org',
method: 'GET'
},
account_sid: '',
app_json: [{
verb: 'conference',
name: '',
beep: false,
startConferenceOnEnter: true
}]
} }
}; };
@@ -52,7 +38,6 @@ const createJambonzApp = (type, {account_sid, name, caller_id}) => {
app.account_sid = account_sid; app.account_sid = account_sid;
switch (type) { switch (type) {
case 'queue': case 'queue':
case 'conference':
app.app_json[0].name = name; app.app_json[0].name = name;
break; break;
case 'user': case 'user':

View File

@@ -97,8 +97,7 @@ 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) {
@@ -219,7 +218,7 @@ router.post('/',
} }
if (!app.notifier && app.call_status_hook) { if (!app.notifier && app.call_status_hook) {
app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret); app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
logger.debug({call_status_hook: app.call_status_hook}, 'creating http client for call status hook'); logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook');
} }
else if (!app.notifier) { else if (!app.notifier) {
logger.debug('creating null call status hook'); logger.debug('creating null call status hook');
@@ -258,8 +257,6 @@ router.post('/',
callId: inviteReq.get('Call-ID'), callId: inviteReq.get('Call-ID'),
accountSid, accountSid,
traceId: rootSpan.traceId traceId: rootSpan.traceId
}, {
...(account.enable_debug_log && {level: 'debug'})
}); });
app.requestor.logger = app.notifier.logger = sipLogger; app.requestor.logger = app.notifier.logger = sipLogger;
const callInfo = new CallInfo({ const callInfo = new CallInfo({
@@ -293,8 +290,6 @@ router.post('/',
}, },
cbProvisional: (prov) => { cbProvisional: (prov) => {
const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing; const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing;
// Update call-id for sbc outbound INVITE
cs.callInfo.sbcCallid = prov.get('X-CID');
if ([180, 183].includes(prov.status) && prov.body) connectStream(prov.body); if ([180, 183].includes(prov.status) && prov.body) connectStream(prov.body);
restDial.emit('callStatus', prov.status, !!prov.body); restDial.emit('callStatus', prov.status, !!prov.body);
cs.emit('callStatusChange', {callStatus, sipStatus: prov.status}); cs.emit('callStatusChange', {callStatus, sipStatus: prov.status});

View File

@@ -75,19 +75,13 @@ module.exports = function(srf, logger) {
req.locals.application_sid = application_sid; req.locals.application_sid = application_sid;
} }
// check for call to queue // check for call to queue
else if (uri.user?.startsWith('queue-') && req.locals.originatingUser && clientDb?.allow_direct_queue_calling) { if (uri.user?.startsWith('queue-') && req.locals.originatingUser && clientDb?.allow_direct_queue_calling) {
const queue_name = uri.user.match(/queue-(.*)/)[1]; const queue_name = uri.user.match(/queue-(.*)/)[1];
logger.debug(`got Queue from Request URI header: ${queue_name}`); logger.debug(`got Queue from Request URI header: ${queue_name}`);
req.locals.queue_name = queue_name; req.locals.queue_name = queue_name;
} }
// check for call to conference
else if (uri.user?.startsWith('conference-') && req.locals.originatingUser && clientDb?.allow_direct_app_calling) {
const conference_id = uri.user.match(/conference-(.*)/)[1];
logger.debug(`got Conference from Request URI header: ${conference_id}`);
req.locals.conference_id = conference_id;
}
// check for call to registered user // check for call to registered user
else if (!JAMBONES_DISABLE_DIRECT_P2P_CALL && req.locals.originatingUser && clientDb?.allow_direct_user_calling) { if (!JAMBONES_DISABLE_DIRECT_P2P_CALL && req.locals.originatingUser && clientDb?.allow_direct_user_calling) {
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser); const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
if (arr) { if (arr) {
const sipRealm = arr[2]; const sipRealm = arr[2];
@@ -187,20 +181,14 @@ module.exports = function(srf, logger) {
const {span} = rootSpan.startChildSpan('lookupAccountDetails'); const {span} = rootSpan.startChildSpan('lookupAccountDetails');
try { try {
const accountDetail = await lookupAccountDetails(account_sid); req.locals.accountInfo = await lookupAccountDetails(account_sid);
const account = accountDetail?.account; req.locals.service_provider_sid = req.locals.accountInfo?.account?.service_provider_sid;
req.locals.accountInfo = accountDetail;
req.locals.service_provider_sid = account?.service_provider_sid;
span.end(); span.end();
if (!account?.is_active) { if (!req.locals.accountInfo.account.is_active) {
logger.info(`Account is inactive or suspended ${account_sid}`); logger.info(`Account is inactive or suspended ${account_sid}`);
// TODO: alert // TODO: alert
return res.send(503, {headers: {'X-Reason': 'Account exists but is inactive'}}); return res.send(503, {headers: {'X-Reason': 'Account exists but is inactive'}});
} }
// Change the default log level to debug
if (account?.enable_debug_log) {
req.locals.logger.level = 'debug';
}
logger.debug({accountInfo: req.locals?.accountInfo?.account}, `retrieved account info for ${account_sid}`); logger.debug({accountInfo: req.locals?.accountInfo?.account}, `retrieved account info for ${account_sid}`);
next(); next();
} catch (err) { } catch (err) {
@@ -249,9 +237,6 @@ module.exports = function(srf, logger) {
logger.debug(`calling to registered user ${req.locals.called_user}, generating dial app`); logger.debug(`calling to registered user ${req.locals.called_user}, generating dial app`);
app = createJambonzApp('user', app = createJambonzApp('user',
{account_sid, name: req.locals.called_user, caller_id: req.locals.callingNumber}); {account_sid, name: req.locals.called_user, caller_id: req.locals.callingNumber});
} else if (req.locals.conference_id) {
logger.debug(`calling to conference ${req.locals.conference_id}, generating conference app`);
app = createJambonzApp('conference', {account_sid, name: req.locals.conference_id});
} else if (req.locals.application_sid) { } else if (req.locals.application_sid) {
app = await lookupAppBySid(req.locals.application_sid); app = await lookupAppBySid(req.locals.application_sid);
} else if (req.locals.originatingUser) { } else if (req.locals.originatingUser) {
@@ -358,17 +343,6 @@ module.exports = function(srf, logger) {
direction: CallDirection.Inbound, direction: CallDirection.Inbound,
traceId: rootSpan.traceId traceId: rootSpan.traceId
}); });
// if transferred call contains callInfo, let update original data to newly created callInfo in this instance.
if (app.transferredCall && app.callInfo) {
const {direction, callerName, from, to, originatingSipIp, originatingSipTrunkName} = app.callInfo;
req.locals.callInfo.direction = direction;
req.locals.callInfo.callerName = callerName;
req.locals.callInfo.from = from;
req.locals.callInfo.to = to;
req.locals.callInfo.originatingSipIp = originatingSipIp;
req.locals.callInfo.originatingSipTrunkName = originatingSipTrunkName;
delete app.callInfo;
}
next(); next();
} catch (err) { } catch (err) {
span.end(); span.end();
@@ -385,7 +359,7 @@ module.exports = function(srf, logger) {
const {rootSpan, siprec, application:app} = req.locals; const {rootSpan, siprec, application:app} = req.locals;
let span; let span;
try { try {
if (app.tasks && app.tasks?.length > 0 && !JAMBONES_MYSQL_REFRESH_TTL) { if (app.tasks && !JAMBONES_MYSQL_REFRESH_TTL) {
app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata)); app.tasks = normalizeJambones(logger, app.tasks).map((tdata) => makeTask(logger, tdata));
if (0 === app.tasks.length) throw new Error('no application provided'); if (0 === app.tasks.length) throw new Error('no application provided');
return next(); return next();

View File

@@ -32,7 +32,6 @@ class CallInfo {
this.sipStatus = 100; this.sipStatus = 100;
this.sipReason = 'Trying'; this.sipReason = 'Trying';
this.callStatus = CallStatus.Trying; this.callStatus = CallStatus.Trying;
this.sbcCallid = req.get('X-CID');
this.originatingSipIp = req.get('X-Forwarded-For'); this.originatingSipIp = req.get('X-Forwarded-For');
this.originatingSipTrunkName = req.get('X-Originating-Carrier'); this.originatingSipTrunkName = req.get('X-Originating-Carrier');
const {siprec} = req.locals; const {siprec} = req.locals;
@@ -130,7 +129,6 @@ class CallInfo {
from: this.from, from: this.from,
to: this.to, to: this.to,
callId: this.callId, callId: this.callId,
sbcCallid: this.sbcCallid,
sipStatus: this.sipStatus, sipStatus: this.sipStatus,
sipReason: this.sipReason, sipReason: this.sipReason,
callStatus: this.callStatus, callStatus: this.callStatus,

View File

@@ -14,13 +14,10 @@ const moment = require('moment');
const assert = require('assert'); const assert = require('assert');
const sessionTracker = require('./session-tracker'); const sessionTracker = require('./session-tracker');
const makeTask = require('../tasks/make_task'); const makeTask = require('../tasks/make_task');
const parseDecibels = require('../utils/parse-decibels');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const { normalizeJambones } = require('@jambonz/verb-specifications');
const listTaskNames = require('../utils/summarize-tasks'); const listTaskNames = require('../utils/summarize-tasks');
const HttpRequestor = require('../utils/http-requestor'); const HttpRequestor = require('../utils/http-requestor');
const WsRequestor = require('../utils/ws-requestor'); const WsRequestor = require('../utils/ws-requestor');
const ActionHookDelayProcessor = require('../utils/action-hook-delay');
const {parseUri} = require('drachtio-srf');
const { const {
JAMBONES_INJECT_CONTENT, JAMBONES_INJECT_CONTENT,
JAMBONES_EAGERLY_PRE_CACHE_AUDIO, JAMBONES_EAGERLY_PRE_CACHE_AUDIO,
@@ -114,19 +111,13 @@ class CallSession extends Emitter {
this.requestor.removeAllListeners(); this.requestor.removeAllListeners();
this.application.requestor = newRequestor; this.application.requestor = newRequestor;
this.requestor.on('command', this._onCommand.bind(this)); this.requestor.on('command', this._onCommand.bind(this));
this.logger.debug(`CallSession: ${this.callSid} listener count ${this.requestor.listenerCount('command')}`);
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this)); this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.requestor.on('handover', handover.bind(this)); this.requestor.on('handover', handover.bind(this));
this.requestor.on('reconnect-error', this._onSessionReconnectError.bind(this));
}; };
if (!this.isConfirmCallSession) { this.requestor.on('command', this._onCommand.bind(this));
this.requestor.on('command', this._onCommand.bind(this)); this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.logger.debug(`CallSession: ${this.callSid} listener count ${this.requestor.listenerCount('command')}`); this.requestor.on('handover', handover.bind(this));
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.requestor.on('handover', handover.bind(this));
this.requestor.on('reconnect-error', this._onSessionReconnectError.bind(this));
}
} }
/** /**
@@ -197,24 +188,6 @@ class CallSession extends Emitter {
this._synthesizer = synth; this._synthesizer = synth;
} }
/**
* ASR TTS fallback
*/
get hasFallbackAsr() {
return this._hasFallbackAsr || false;
}
set hasFallbackAsr(i) {
this._hasFallbackAsr = i;
}
get hasFallbackTts() {
return this._hasFallbackTts || false;
}
set hasFallbackTts(i) {
this._hasFallbackTts = i;
}
/** /**
* default vendor to use for speech synthesis if not provided in the app * default vendor to use for speech synthesis if not provided in the app
*/ */
@@ -340,29 +313,6 @@ class CallSession extends Emitter {
this.application.fallback_speech_recognizer_language = language; this.application.fallback_speech_recognizer_language = language;
} }
/**
* global referHook
*/
set referHook(hook) {
this._referHook = hook;
}
get referHook() {
return this._referHook;
}
/**
* Vad
*/
get vad() {
return this._vad;
}
set vad(v) {
this._vad = v;
}
/** /**
* indicates whether the call currently in progress * indicates whether the call currently in progress
*/ */
@@ -533,80 +483,14 @@ class CallSession extends Emitter {
this._actionHookDelayRetries = e; this._actionHookDelayRetries = e;
} }
get actionHookDelayProcessor() { get actionHookDelayActions() {
return this._actionHookDelayProcessor; return this._actionHookDelayActions;
} }
set actionHookDelayProperties(opts) { set actionHookDelayActions(e) {
if (this._actionHookDelayProcessor) { this._actionHookDelayActions = e;
this._actionHookDelayProcessor.stop();
if (!this._actionHookDelayProcessor.init(opts)) {
this._actionHookDelayProcessor.removeAllListeners();
this._actionHookDelayProcessor = null;
}
}
else {
try {
this._actionHookDelayProcessor = new ActionHookDelayProcessor(this.logger, opts, this, this.ep);
this._actionHookDelayProcessor.on('giveup', () => {
this.logger.info('CallSession: ActionHookDelayProcessor: giveup event - hanging up call');
this._jambonzHangup();
if (this.wakeupResolver) {
this.logger.debug('CallSession: Giveup timer expired - waking up');
this.wakeupResolver({reason: 'noResponseGiveUp'});
this.wakeupResolver = null;
}
});
this._actionHookDelayProcessor.on('giveupWithTasks', (tasks) => {
this.logger.info('CallSession: ActionHookDelayProcessor: giveupWithTasks event');
const giveUpTasks = normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata));
this.logger.info({tasks: listTaskNames(giveUpTasks)}, 'CallSession:giveupWithTasks task list');
// we need to clear the ahd, as we do not want to execute actionHookDelay actions again
this.clearActionHookDelayProcessor();
// replace the application with giveUpTasks
this.replaceApplication(giveUpTasks);
});
} catch (err) {
this.logger.error({err}, 'CallSession: Error creating ActionHookDelayProcessor');
}
}
} }
async clearOrRestoreActionHookDelayProcessor() {
if (this._actionHookDelayProcessor) {
await this._actionHookDelayProcessor.stop();
if (!this.popActionHookDelayProperties()) {
//this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - ahd settings');
//await this.clearActionHookDelayProcessor();
}
this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - say or play action completed');
}
}
async clearActionHookDelayProcessor() {
if (this._actionHookDelayProcessor) {
await this._actionHookDelayProcessor.stop();
this._actionHookDelayProcessor.removeAllListeners();
this._actionHookDelayProcessor = null;
}
}
stashActionHookDelayProperties() {
this._storedActionHookDelayProperties = this._actionHookDelayProcessor.properties;
}
popActionHookDelayProperties() {
if (this._storedActionHookDelayProperties) {
this._actionHookDelayProcessor.init(this._storedActionHookDelayProperties);
this._storedActionHookDelayProperties = null;
return true;
}
return false;
}
hasGlobalSttPunctuation() { hasGlobalSttPunctuation() {
return this._globalSttPunctuation !== undefined; return this._globalSttPunctuation !== undefined;
} }
@@ -627,18 +511,6 @@ class CallSession extends Emitter {
this.speechSynthesisVoice = this._origSynthesizerSettings.voice; this.speechSynthesisVoice = this._origSynthesizerSettings.voice;
} }
enableFillerNoise(opts) {
this._fillerNoise = opts;
}
disableFillerNoise() {
this._fillerNoise = null;
}
get fillerNoise() {
return this._fillerNoise;
}
async notifyRecordOptions(opts) { async notifyRecordOptions(opts) {
const {action} = opts; const {action} = opts;
this.logger.debug({opts}, 'CallSession:notifyRecordOptions'); this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
@@ -813,7 +685,7 @@ class CallSession extends Emitter {
task = await this.backgroundTaskManager.newTask('bargeIn', gather); task = await this.backgroundTaskManager.newTask('bargeIn', gather);
task.sticky = autoEnable; task.sticky = autoEnable;
// listen to the bargein-done from background manager // listen to the bargein-done from background manager
this.backgroundTaskManager.on('bargeIn-done', () => { this.backgroundTaskManager.once('bargeIn-done', () => {
if (this.requestor instanceof WsRequestor) { if (this.requestor instanceof WsRequestor) {
try { try {
this.kill(true); this.kill(true);
@@ -826,8 +698,6 @@ class CallSession extends Emitter {
} }
} }
async disableBotMode() { async disableBotMode() {
const task = this.backgroundTaskManager.getTask('bargeIn');
if (task) task.sticky = false;
this.backgroundTaskManager.stop('bargeIn'); this.backgroundTaskManager.stop('bargeIn');
} }
@@ -886,8 +756,7 @@ class CallSession extends Emitter {
writeAlerts({ writeAlerts({
alert_type: AlertType.TTS_FAILURE, alert_type: AlertType.TTS_FAILURE,
account_sid: this.accountSid, account_sid: this.accountSid,
vendor, vendor
target_sid: this.callSid
}).catch((err) => this.logger.error({err}, 'Error writing tts alert')); }).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
} }
} }
@@ -896,7 +765,6 @@ class CallSession extends Emitter {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
accessKeyId: credential.access_key_id, accessKeyId: credential.access_key_id,
secretAccessKey: credential.secret_access_key, secretAccessKey: credential.secret_access_key,
roleArn: credential.role_arn,
region: credential.aws_region || AWS_REGION region: credential.aws_region || AWS_REGION
}; };
} }
@@ -933,7 +801,6 @@ class CallSession extends Emitter {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key, api_key: credential.api_key,
deepgram_stt_uri: credential.deepgram_stt_uri, deepgram_stt_uri: credential.deepgram_stt_uri,
deepgram_tts_uri: credential.deepgram_tts_uri,
deepgram_stt_use_tls: credential.deepgram_stt_use_tls deepgram_stt_use_tls: credential.deepgram_stt_use_tls
}; };
} }
@@ -963,56 +830,23 @@ class CallSession extends Emitter {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
cobalt_server_uri: credential.cobalt_server_uri cobalt_server_uri: credential.cobalt_server_uri
}; };
} } else if ('elevenlabs' === vendor) {
else if ('elevenlabs' === vendor) {
return { return {
api_key: credential.api_key, api_key: credential.api_key,
model_id: credential.model_id, model_id: credential.model_id,
options: credential.options options: credential.options
}; };
} } else if ('assemblyai' === vendor) {
else if ('playht' === vendor) {
return {
api_key: credential.api_key,
user_id: credential.user_id,
voice_engine: credential.voice_engine,
options: credential.options
};
}
else if ('rimelabs' === vendor) {
return {
api_key: credential.api_key,
model_id: credential.model_id,
options: credential.options
};
}
else if ('assemblyai' === vendor) {
return { return {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key api_key: credential.api_key
}; };
} } else if ('whisper' === vendor) {
else if ('whisper' === vendor) {
return { return {
api_key: credential.api_key, api_key: credential.api_key,
model_id: credential.model_id model_id: credential.model_id
}; };
} } else if (vendor.startsWith('custom:')) {
else if ('verbio' === vendor) {
return {
client_id: credential.client_id,
client_secret: credential.client_secret,
engine_version: credential.engine_version
};
}
else if ('speechmatics' === vendor) {
this.logger.info({credential}, 'CallSession:getSpeechCredentials - speechmatics credential');
return {
api_key: credential.api_key,
speechmatics_stt_uri: credential.speechmatics_stt_uri,
};
}
else if (vendor.startsWith('custom:')) {
return { return {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
auth_token: credential.auth_token, auth_token: credential.auth_token,
@@ -1025,8 +859,7 @@ class CallSession extends Emitter {
writeAlerts({ writeAlerts({
alert_type: AlertType.STT_NOT_PROVISIONED, alert_type: AlertType.STT_NOT_PROVISIONED,
account_sid: this.accountSid, account_sid: this.accountSid,
vendor, vendor
target_sid: this.callSid
}).catch((err) => this.logger.error({err}, 'Error writing tts alert')); }).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
} }
} }
@@ -1050,6 +883,7 @@ class CallSession extends Emitter {
task.on('VerbHookSpanWaitForEnd', ({span}) => { task.on('VerbHookSpanWaitForEnd', ({span}) => {
this.verbHookSpan = span; this.verbHookSpan = span;
}); });
task.on('ActionHookDelayActionOptions', this._onActionHookDelayActions.bind(this));
try { try {
const resources = await this._evaluatePreconditions(task); const resources = await this._evaluatePreconditions(task);
let skip = false; let skip = false;
@@ -1095,15 +929,6 @@ class CallSession extends Emitter {
) { ) {
try { try {
await this._awaitCommandsOrHangup(); await this._awaitCommandsOrHangup();
//await this.clearOrRestoreActionHookDelayProcessor();
//TODO: remove filler noise code and simply create as action hook delay
if (this._isPlayingFillerNoise) {
this._isPlayingFillerNoise = false;
this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err, 'Error killing filler noise'));
}
if (this.callGone) break; if (this.callGone) break;
} catch (err) { } catch (err) {
this.logger.info(err, 'CallSession:exec - error waiting for new commands'); this.logger.info(err, 'CallSession:exec - error waiting for new commands');
@@ -1172,11 +997,6 @@ class CallSession extends Emitter {
this.wakeupResolver({reason: 'session ended'}); this.wakeupResolver({reason: 'session ended'});
this.wakeupResolver = null; this.wakeupResolver = null;
} }
if (this._maxCallDurationTimer) {
clearTimeout(this._maxCallDurationTimer);
this._maxCallDurationTimer = null;
}
} }
/** /**
@@ -1334,8 +1154,9 @@ class CallSession extends Emitter {
this.currentTask.kill(this); this.currentTask.kill(this);
} }
this._endVerbHookSpan(); this._endVerbHookSpan();
// clear all delay action hook timeout if there is
await this.clearOrRestoreActionHookDelayProcessor(); this._clearActionHookNoResponseGiveUpTimer();
this._clearActionHookNoResponseTimer();
} }
/** /**
@@ -1391,15 +1212,7 @@ class CallSession extends Emitter {
this.callInfo.customerData = tag; this.callInfo.customerData = tag;
} }
async _lccConferenceParticipantAction(opts) { async _lccMuteStatus(callSid, mute) {
const task = this.currentTask;
if (!task || TaskName.Conference !== task.name || !this.isInConference) {
return this.logger.info('CallSession:_lccConferenceParticipantAction - invalid cmd, call is not in conference');
}
task.doConferenceParticipantAction(this, opts);
}
async _lccMuteStatus(mute, callSid) {
// this whole thing requires us to be in a Dial or Conference verb // this whole thing requires us to be in a Dial or Conference verb
const task = this.currentTask; const task = this.currentTask;
if (!task || ![TaskName.Dial, TaskName.Conference].includes(task.name)) { if (!task || ![TaskName.Dial, TaskName.Conference].includes(task.name)) {
@@ -1531,93 +1344,6 @@ Duration=${duration} `
task.whisper(tasks, callSid).catch((err) => this.logger.error(err, 'CallSession:_lccWhisper')); task.whisper(tasks, callSid).catch((err) => this.logger.error(err, 'CallSession:_lccWhisper'));
} }
async _lccConfig(opts) {
this.logger.debug({opts}, 'CallSession:_lccConfig');
const t = normalizeJambones(this.logger, [
{
verb: 'config',
...opts
}
])
.map((tdata) => makeTask(this.logger, tdata));
const task = t[0];
const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`);
span.setAttributes({'verb.summary': task.summary});
task.span = span;
task.ctx = ctx;
try {
await task.exec(this, {ep: this.ep});
} catch (err) {
this.logger.error(err, 'CallSession:_lccConfig');
}
task.span.end();
}
async _lccDub(opts, callSid) {
this.logger.debug({opts}, `CallSession:_lccDub on call_sid ${callSid}`);
const t = normalizeJambones(this.logger, [
{
verb: 'dub',
...opts
}
])
.map((tdata) => makeTask(this.logger, tdata));
const task = t[0];
const ep = this.currentTask?.name === TaskName.Dial && callSid === this.currentTask?.callSid ?
this.currentTask.ep :
this.ep;
const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`);
span.setAttributes({'verb.summary': task.summary});
task.span = span;
task.ctx = ctx;
try {
await task.exec(this, {ep});
} catch (err) {
this.logger.error(err, 'CallSession:_lccDub');
}
task.span.end();
}
async _lccBoostAudioSignal(opts, callSid) {
const ep = this.currentTask?.name === TaskName.Dial && callSid === this.currentTask?.callSid ?
this.currentTask.ep :
this.ep;
const db = parseDecibels(opts);
this.logger.info(`_lccBoostAudioSignal: boosting audio signal by ${db} dB`);
const args = [ep.uuid, 'setGain', db];
const response = await ep.api('uuid_dub', args);
this.logger.info({response}, '_lccBoostAudioSignal: response from freeswitch');
}
_lccToolOutput(tool_call_id, opts, callSid) {
// only valid if we are in an LLM verb
const task = this.currentTask;
if (!task || !task.name.startsWith('Llm')) {
return this.logger.info('CallSession:_lccToolOutput - invalid command since we are not in an llm');
}
task.processToolOutput(tool_call_id, opts, callSid)
.catch((err) => this.logger.error(err, 'CallSession:_lccToolOutput'));
}
_lccLlmUpdate(opts, callSid) {
// only valid if we are in an LLM verb
const task = this.currentTask;
if (!task || !task.name.startsWith('Llm')) {
return this.logger.info('CallSession:_lccLlmUpdate - invalid command since we are not in an llm');
}
task.processLlmUpdate(opts, callSid)
.catch((err) => this.logger.error(err, 'CallSession:_lccLlmUpdate'));
}
/** /**
* perform call hangup by jambonz * perform call hangup by jambonz
*/ */
@@ -1648,7 +1374,7 @@ Duration=${duration} `
await this._lccTranscribeStatus(opts); await this._lccTranscribeStatus(opts);
} }
else if (opts.mute_status) { else if (opts.mute_status) {
await this._lccMuteStatus(opts.mute_status === 'mute', callSid); await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
} }
else if (opts.conf_hold_status) { else if (opts.conf_hold_status) {
await this._lccConfHoldStatus(opts); await this._lccConfHoldStatus(opts);
@@ -1668,21 +1394,6 @@ Duration=${duration} `
else if (opts.tag) { else if (opts.tag) {
return this._lccTag(opts); return this._lccTag(opts);
} }
else if (opts.conferenceParticipantAction) {
return this._lccConferenceParticipantAction(opts.conferenceParticipantAction);
}
else if (opts.dub) {
return this._lccDub(opts);
}
else if (opts.boostAudioSignal) {
return this._lccBoostAudioSignal(opts, callSid);
}
else if (opts.llm_tool_output) {
return this._lccToolOutput(opts.tool_call_id, opts.llm_tool_output, callSid);
}
else if (opts.llm_update) {
return this._lccLlmUpdate(opts.llm_update, callSid);
}
// whisper may be the only thing we are asked to do, or it may that // whisper may be the only thing we are asked to do, or it may that
// we are doing a whisper after having muted, paused recording etc.. // we are doing a whisper after having muted, paused recording etc..
@@ -1774,23 +1485,7 @@ Duration=${duration} `
} }
_preCacheAudio(newTasks) { _preCacheAudio(newTasks) {
/** for (const task of newTasks) {
* only precache audio for the a queued say if we have one or more non-Config verbs
* ahead of it in the queue. This is because the Config verb returns immediately
* and would not give us enough time to generate the audio. The point of precaching
* is to take advantage of getting the audio in advance of being needed, so we need
* to be confident we have some time before the say verb is executed, and the Config
* does not give us that confidence since it returns immediately.
*/
const haveQueuedNonConfig = this.tasks.findIndex((t) => t.name !== TaskName.Config) !== -1;
let tasks = haveQueuedNonConfig ? newTasks : [];
if (!haveQueuedNonConfig) {
const idxFirstNotConfig = newTasks.findIndex((t) => t.name !== TaskName.Config);
if (-1 === idxFirstNotConfig) return;
tasks = newTasks.slice(idxFirstNotConfig + 1);
}
for (const task of tasks) {
if (task.name === TaskName.Config && task.hasSynthesizer) { if (task.name === TaskName.Config && task.hasSynthesizer) {
/* if they change synthesizer settings don't try to precache */ /* if they change synthesizer settings don't try to precache */
break; break;
@@ -1862,24 +1557,7 @@ Duration=${duration} `
}, 'CallSession:_injectTasks - completed'); }, 'CallSession:_injectTasks - completed');
} }
async _onSessionReconnectError(err) { _onCommand({msgid, command, call_sid, queueCommand, data}) {
const {writeAlerts, AlertType} = this.srf.locals;
const sid = this.accountInfo.account.account_sid;
this.logger.info({err}, `_onSessionReconnectError for account ${sid}`);
try {
await writeAlerts({
alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE,
account_sid: this.accountSid,
detail: `Session:reconnect error ${err}`,
url: this.application.call_hook.url,
});
} catch (error) {
this.logger.error({error}, 'Error writing WEBHOOK_CONNECTION_FAILURE alert');
}
this._jambonzHangup();
}
async _onCommand({msgid, command, call_sid, queueCommand, tool_call_id, data}) {
this.logger.info({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command'); this.logger.info({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command');
let resolution; let resolution;
switch (command) { switch (command) {
@@ -1904,9 +1582,9 @@ Duration=${duration} `
} }
resolution = {reason: 'received command, new tasks', queue: queueCommand, command}; resolution = {reason: 'received command, new tasks', queue: queueCommand, command};
resolution.command = listTaskNames(t); resolution.command = listTaskNames(t);
// clear all delay action hook timeout if there is // clear all delay action hook timeout if there is
await this.clearOrRestoreActionHookDelayProcessor(); this._clearActionHookNoResponseGiveUpTimer();
this._clearActionHookNoResponseTimer();
} }
else this._lccCallHook(data); else this._lccCallHook(data);
break; break;
@@ -1915,24 +1593,16 @@ Duration=${duration} `
this._lccCallStatus(data); this._lccCallStatus(data);
break; break;
case 'config':
this._lccConfig(data, call_sid);
break;
case 'dial': case 'dial':
this._lccCallDial(data); this._lccCallDial(data);
break; break;
case 'dub':
this._lccDub(data, call_sid);
break;
case 'record': case 'record':
this.notifyRecordOptions(data); this.notifyRecordOptions(data);
break; break;
case 'mute:status': case 'mute:status':
this._lccMuteStatus(data, call_sid); this._lccMuteStatus(call_sid, data);
break; break;
case 'conf:mute-status': case 'conf:mute-status':
@@ -1943,10 +1613,6 @@ Duration=${duration} `
this._lccConfHoldStatus(data); this._lccConfHoldStatus(data);
break; break;
case 'conf:participant-action':
this._lccConferenceParticipantAction(data);
break;
case 'listen:status': case 'listen:status':
this._lccListenStatus(data); this._lccListenStatus(data);
break; break;
@@ -1973,21 +1639,6 @@ Duration=${duration} `
}); });
break; break;
case 'boostAudioSignal':
this._lccBoostAudioSignal(data, call_sid)
.catch((err) => {
this.logger.info({err, data}, 'CallSession:_onCommand - error boosting audio signal');
});
break;
case 'llm:tool-output':
this._lccToolOutput(tool_call_id, data, call_sid);
break;
case 'llm:update':
this._lccLlmUpdate(data, call_sid);
break;
default: default:
this.logger.info(`CallSession:_onCommand - invalid command ${command}`); this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
} }
@@ -2067,7 +1718,7 @@ Duration=${duration} `
}); });
//ep.cs = this; //ep.cs = this;
this.ep = ep; this.ep = ep;
this.logger.info(`allocated endpoint ${ep.uuid}`); this.logger.debug(`allocated endpoint ${ep.uuid}`);
this._configMsEndpoint(); this._configMsEndpoint();
@@ -2140,12 +1791,6 @@ Duration=${duration} `
this.logger.error('CallSession:replaceEndpoint cannot be called without stable dlg'); this.logger.error('CallSession:replaceEndpoint cannot be called without stable dlg');
return; return;
} }
// 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.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
this._configMsEndpoint(); this._configMsEndpoint();
@@ -2180,13 +1825,13 @@ Duration=${duration} `
this.rootSpan && this.rootSpan.end(); this.rootSpan && this.rootSpan.end();
// close all background tasks // close all background tasks
this.backgroundTaskManager.stopAll(); this.backgroundTaskManager.stopAll();
this.clearOrRestoreActionHookDelayProcessor().catch((err) => {}); this._clearActionHookNoResponseGiveUpTimer();
this._clearActionHookNoResponseTimer();
} }
/** /**
* called when the caller has hung up. Provided for subclasses to override * called when the caller has hung up. Provided for subclasses to override
* in order to apply logic at this point if needed. * in order to apply logic at this point if needed.
* return true if success fallback, return false if not
*/ */
_callerHungup() { _callerHungup() {
assert(false, 'subclass responsibility to override this method'); assert(false, 'subclass responsibility to override this method');
@@ -2247,10 +1892,6 @@ Duration=${duration} `
} }
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`); this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
} }
else {
this.logger.debug('CallSession:propagateAnswer - call already answered - re-anchor media with a reinvite');
await this.dlg.modify(this.ep.local.sdp);
}
} }
async _onRequestWithinDialog(req, res) { async _onRequestWithinDialog(req, res) {
@@ -2303,59 +1944,17 @@ Duration=${duration} `
} }
/** /**
* Handle incoming REFER * Handle incoming REFER if we are in a dial task
* @param {*} req * @param {*} req
* @param {*} res * @param {*} res
*/ */
_onRefer(req, res) { _onRefer(req, res) {
const task = this.currentTask; const task = this.currentTask;
const sd = task.sd; const sd = task.sd;
if (task && TaskName.Dial === task.name && sd && task.referHook) { if (task && TaskName.Dial === task.name && sd) {
task.handleRefer(this, req, res); task.handleRefer(this, req, res);
} }
else { else {
this._handleRefer(req, res);
}
}
async _handleRefer(req, res) {
if (this._referHook) {
try {
const to = parseUri(req.getParsedHeader('Refer-To').uri);
const by = parseUri(req.getParsedHeader('Referred-By').uri);
const b3 = this.b3;
const httpHeaders = b3 && {b3};
const json = await this.requestor.request('verb:hook', this._referHook, {
...(this.callInfo.toJSON()),
refer_details: {
sip_refer_to: req.get('Refer-To'),
sip_referred_by: req.get('Referred-By'),
sip_user_agent: req.get('User-Agent'),
refer_to_user: to.scheme === 'tel' ? to.number : to.user,
referred_by_user: by.scheme === 'tel' ? by.number : by.user,
referring_call_sid: this.callSid,
referred_call_sid: null,
}
}, httpHeaders);
if (json && Array.isArray(json)) {
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.logger.info('CallSession:handleRefer received REFER, get new tasks');
this.replaceApplication(tasks);
if (this.wakeupResolver) {
this.wakeupResolver({reason: 'CallSession: referHook new taks'});
this.wakeupResolver = null;
}
}
}
res.send(202);
this.logger.info('CallSession:handleRefer - sent 202 Accepted');
} catch (err) {
this.logger.error({err}, 'CallSession:handleRefer - error while asking referHook');
res.send(err.statusCode || 501);
}
} else {
res.send(501); res.send(501);
} }
} }
@@ -2630,30 +2229,6 @@ Duration=${duration} `
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.logger.info('_awaitCommandsOrHangup - waiting...'); this.logger.info('_awaitCommandsOrHangup - waiting...');
this.wakeupResolver = resolve; this.wakeupResolver = resolve;
if (this._actionHookDelayProcessor) {
this._actionHookDelayProcessor.start();
}
/**
* TODO: filler noise can be handled as an ActionHookDelayProcessor -
* it's just one specific scenario for action hook delay -
* remove the code below and simply implement filler noise as an action hook delay
*/
/* start filler noise if configured while we wait for new commands */
if (this.fillerNoise?.url && this.ep?.connected && !this.ep2) {
this.logger.debug('CallSession:_awaitCommandsOrHangup - playing filler noise');
this._isPlayingFillerNoise = true;
this.ep.play(this.fillerNoise.url);
this.ep.once('playback-start', (evt) => {
if (evt.file === this.fillerNoise.url && !this._isPlayingFillerNoise) {
this.logger.info('CallSession:_awaitCommandsOrHangup - filler noise started');
this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err, 'Error killing filler noise'));
}
});
}
}); });
} }
@@ -2685,26 +2260,68 @@ Duration=${duration} `
this.verbHookSpan = null; this.verbHookSpan = null;
} }
} }
// actionHook delay actions
_onActionHookDelayActions(options) {
this._actionHookDelayRetryCount = 0;
this._startActionHookNoResponseTimer(options);
this._startActionHookNoResponseGiveUpTimer(options);
}
async startMaxCallDurationTimer(timeLimit) { _startActionHookNoResponseTimer(options) {
if (!this._maxCallDurationTimer && timeLimit > 0) { this._clearActionHookNoResponseTimer();
this.timeLimit = timeLimit; if (options.noResponseTimeoutMs) {
this._maxCallDurationTimer = setTimeout(this._onMaxCallDuration.bind(this), timeLimit * 1000); this.logger.debug(`CallSession:_startActionHookNoResponseTimer ${options.noResponseTimeoutMs}`);
this.logger.debug(`CallSession:startMaxCallDurationTimer - started max call duration timer for ${timeLimit}s`); this._actionHookNoResponseTimer = setTimeout(() => {
if (this._actionHookDelayRetryCount >= options.retries) {
this._jambonzHangup();
}
const verb = options.actions[this._actionHookDelayRetryCount % options.actions.length];
// Inject verb to main stack
const t = normalizeJambones(this.logger, [verb])
.map((tdata) => makeTask(this.logger, tdata));
if (t.length) {
t[0].on('playDone', (err) => {
if (err) this.logger.error({err}, `Call-Session:exec Error delay action, play ${verb}`);
this._startActionHookNoResponseTimer(options);
});
}
this.tasks.push(...t);
if (this.wakeupResolver) {
this.wakeupResolver({reason: 'actionHook no response, applied delay actions', verb});
this.wakeupResolver = null;
}
this.logger.debug(`CallSession:_startActionHookNoResponseTimer, executing verb ${JSON.stringify(verb)}`);
this._actionHookDelayRetryCount++;
}, options.noResponseTimeoutMs);
} }
} }
/** _clearActionHookNoResponseTimer() {
* _onMaxCallDuration - called when the call has reached the maximum duration if (this._actionHookNoResponseTimer) {
*/ clearTimeout(this._actionHookNoResponseTimer);
_onMaxCallDuration() {
this.logger.info(`callSession:_onMaxCallDuration tearing down call as it has reached ${this.timeLimit}s`);
if (!this.dlg) {
this.logger.debug('CallSession:_onMaxCallDuration - no dialog, call already gone');
return;
} }
this._jambonzHangup('Max Call Duration'); this._actionHookNoResponseTimer = null;
this._maxCallDurationTimer = null; }
_startActionHookNoResponseGiveUpTimer(options) {
this._clearActionHookNoResponseGiveUpTimer();
if (options.noResponseGiveUpTimeoutMs) {
this.logger.debug(`CallSession:_startActionHookNoResponseGiveUpTimer ${options.noResponseGiveUpTimeoutMs}`);
this._actionHookNoResponseGiveUpTimer = setTimeout(() => {
this.logger.debug('CallSession:_startActionHookNoResponseGiveUpTimer Timeout');
this._jambonzHangup();
this._actionHookNoResponseGiveUpTimer = null;
}, options.noResponseGiveUpTimeoutMs);
}
}
_clearActionHookNoResponseGiveUpTimer() {
if (this._actionHookNoResponseGiveUpTimer) {
clearTimeout(this._actionHookNoResponseGiveUpTimer);
}
this._actionHookNoResponseGiveUpTimer = null;
} }
} }

View File

@@ -71,9 +71,7 @@ class InboundCallSession extends CallSession {
} }
_jambonzHangup() { _jambonzHangup() {
this.dlg?.destroy(); this._hangup();
// kill current task or wakeup the call session.
this._callReleased();
} }
_hangup(terminatedBy = 'jambonz') { _hangup(terminatedBy = 'jambonz') {
@@ -81,7 +79,6 @@ class InboundCallSession extends CallSession {
this.logger.info('InboundCallSession:_hangup - race condition, dlg cleared by app hangup'); this.logger.info('InboundCallSession:_hangup - race condition, dlg cleared by app hangup');
return; return;
} }
this.logger.info(`InboundCallSession: ${terminatedBy} hung up`);
assert(this.dlg.connectTime); assert(this.dlg.connectTime);
const duration = moment().diff(this.dlg.connectTime, 'seconds'); const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.rootSpan.setAttributes({'call.termination': `hangup by ${terminatedBy}`}); this.rootSpan.setAttributes({'call.termination': `hangup by ${terminatedBy}`});
@@ -90,6 +87,7 @@ class InboundCallSession extends CallSession {
callStatus: CallStatus.Completed, callStatus: CallStatus.Completed,
duration duration
}); });
this.logger.info(`InboundCallSession: ${terminatedBy} hung up`);
this._callReleased(); this._callReleased();
this.req.removeAllListeners('cancel'); this.req.removeAllListeners('cancel');
} }

View File

@@ -1,6 +1,7 @@
const CallSession = require('./call-session'); const CallSession = require('./call-session');
const {CallStatus} = require('../utils/constants'); const {CallStatus} = require('../utils/constants');
const moment = require('moment'); const moment = require('moment');
/** /**
* @classdesc Subclass of CallSession. This represents a CallSession that is * @classdesc Subclass of CallSession. This represents a CallSession that is
* created for an outbound call that is initiated via the REST API. * created for an outbound call that is initiated via the REST API.
@@ -41,10 +42,9 @@ class RestCallSession extends CallSession {
setDialog(dlg) { setDialog(dlg) {
this.dlg = dlg; this.dlg = dlg;
dlg.on('destroy', this._callerHungup.bind(this)); dlg.on('destroy', this._callerHungup.bind(this));
dlg.on('refer', this._onRefer.bind(this));
dlg.on('modify', this._onReinvite.bind(this));
this.wrapDialog(dlg); this.wrapDialog(dlg);
} }
/** /**
* This is invoked when the called party hangs up, in order to calculate the call duration. * This is invoked when the called party hangs up, in order to calculate the call duration.
*/ */
@@ -56,7 +56,7 @@ class RestCallSession extends CallSession {
this._hangup(); this._hangup();
} }
_hangup(terminatedBy = 'jambonz') { _hangup(terminatedBy = 'jamboz') {
if (this.restDialTask) { if (this.restDialTask) {
this.restDialTask.turnOffAmd(); this.restDialTask.turnOffAmd();
} }

View File

@@ -1,22 +0,0 @@
const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants');
/**
* Answer the call.
* Note: This is rarely used, as the call is typically answered automatically when required by the app,
* but it can be useful to force an answer before a pause in some cases
*/
class TaskAnswer extends Task {
constructor(logger, opts) {
super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
}
get name() { return TaskName.Answer; }
async exec(cs) {
super.exec(cs);
}
}
module.exports = TaskAnswer;

View File

@@ -6,7 +6,6 @@ const { normalizeJambones } = require('@jambonz/verb-specifications');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const bent = require('bent'); const bent = require('bent');
const assert = require('assert'); const assert = require('assert');
const HttpRequestor = require('../utils/http-requestor');
const WAIT = 'wait'; const WAIT = 'wait';
const JOIN = 'join'; const JOIN = 'join';
const START = 'start'; const START = 'start';
@@ -61,8 +60,6 @@ class Conference extends Task {
this.emitter = new Emitter(); this.emitter = new Emitter();
this.results = {}; this.results = {};
this.coaching = [];
this.speakOnlyTo = this.data.speakOnlyTo;
// transferred from another server in order to bridge to a local caller? // transferred from another server in order to bridge to a local caller?
if (this.data._ && this.data._.connectTime) { if (this.data._ && this.data._.connectTime) {
@@ -118,9 +115,7 @@ 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) {
// drachtio-fsmrf override esl::event::CUSTOM to conference join listerner, After finish the conference this.ep.conn.removeAllListeners('esl::event::CUSTOM::*');
// 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'));
} }
@@ -137,10 +132,15 @@ class Conference extends Task {
* @param {SipDialog} dlg * @param {SipDialog} dlg
*/ */
async _init(cs, dlg) { async _init(cs, dlg) {
const friendlyName = this.confName;
const {createHash, retrieveHash} = cs.srf.locals.dbHelpers; const {createHash, retrieveHash} = cs.srf.locals.dbHelpers;
this.friendlyName = this.confName;
this.confName = `conf:${cs.accountSid}:${this.confName}`; this.confName = `conf:${cs.accountSid}:${this.confName}`;
this.statusParams = Object.assign({
conferenceSid: this.confName,
friendlyName
}, cs.callInfo);
// check if conference is in progress // check if conference is in progress
const obj = await retrieveHash(this.confName); const obj = await retrieveHash(this.confName);
if (obj) { if (obj) {
@@ -348,29 +348,16 @@ class Conference extends Task {
Object.assign(opts, {flags: { Object.assign(opts, {flags: {
...(this.endConferenceOnExit && {endconf: true}), ...(this.endConferenceOnExit && {endconf: true}),
...(this.startConferenceOnEnter && {moderator: true}), ...(this.startConferenceOnEnter && {moderator: true}),
//https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod_conference_3965534/ ...(this.joinMuted && {joinMuted: true}),
// mute | Enter conference muted
...((this.joinMuted || this.speakOnlyTo) && {mute: true}),
}}); }});
/**
* Note on the above: if we are joining in "coaching" mode (ie only going to heard by a subset of participants)
* then we join muted temporarily, and then unmute ourselves once we have identified the subset of participants
* to whom we will be speaking.
*/
} }
try { try {
const {memberId, confUuid} = await this.ep.join(this.confName, opts); const {memberId, confUuid} = await this.ep.join(this.confName, opts);
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`); this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
this.memberId = parseInt(memberId, 10); this.memberId = memberId;
this.confUuid = confUuid; this.confUuid = confUuid;
// set a tag for this member, if provided
if (this.data.memberTag) {
this.setMemberTag(this.data.memberTag);
}
cs.setConferenceDetails(memberId, this.confName, confUuid); cs.setConferenceDetails(memberId, this.confName, confUuid);
const response = await this.ep.api('conference', [this.confName, 'get', 'count']); const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body); if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
@@ -397,9 +384,6 @@ class Conference extends Task {
.catch((err) => {}); .catch((err) => {});
} }
if (this.speakOnlyTo) {
this.setCoachMode(this.speakOnlyTo);
}
} catch (err) { } catch (err) {
this.logger.error(err, `Failed to join conference ${this.confName}`); this.logger.error(err, `Failed to join conference ${this.confName}`);
throw err; throw err;
@@ -444,15 +428,7 @@ class Conference extends Task {
} }
} }
doConferenceMute(cs, opts) { async doConferenceHold(cs, opts) {
assert (cs.isInConference);
const mute = opts.conf_mute_status === 'mute';
this.ep.api(`conference ${this.confName} ${mute ? 'mute' : 'unmute'} ${this.memberId}`)
.catch((err) => this.logger.info({err}, 'Error muting or unmuting participant'));
}
doConferenceHold(cs, opts) {
assert (cs.isInConference); assert (cs.isInConference);
const {conf_hold_status, wait_hook} = opts; const {conf_hold_status, wait_hook} = opts;
@@ -489,46 +465,6 @@ class Conference extends Task {
} }
} }
async doConferenceParticipantAction(cs, opts) {
const {action, tag, wait_hook } = opts;
switch (action) {
case 'tag':
await this.setMemberTag(tag);
break;
case 'untag':
await this.clearMemberTag();
break;
case 'coach':
await this.setCoachMode(tag);
break;
case 'uncoach':
await this.clearCoachMode();
break;
case 'hold':
this.doConferenceHold(cs, {
conf_hold_status: 'hold',
...(wait_hook && {wait_hook})
});
break;
case 'unhold':
this.doConferenceHold(cs, {conf_hold_status: 'unhold'});
break;
case 'mute':
this.doConferenceMute(cs, {conf_mute_status: 'mute'});
break;
case 'unmute':
this.doConferenceMute(cs, {conf_mute_status: 'unmute'});
break;
case 'kick':
this.kickMember(cs);
break;
default:
this.logger.info(`Conference:doConferenceParticipantAction - unhandled action ${action}`);
break;
}
}
async _doWaitHookWhileOnHold(cs, dlg, wait_hook) { async _doWaitHookWhileOnHold(cs, dlg, wait_hook) {
do { do {
try { try {
@@ -546,13 +482,6 @@ class Conference extends Task {
} while (!this.killed && this.conf_hold_status === 'hold'); } while (!this.killed && this.conf_hold_status === 'hold');
} }
/**
* mute or unmute side of the call
*/
mute(callSid, doMute) {
this.doConferenceMute(this.callSession, {conf_mute_status: doMute});
}
/** /**
* Add ourselves to the waitlist of sessions to be notified once * Add ourselves to the waitlist of sessions to be notified once
* the conference starts * the conference starts
@@ -582,7 +511,7 @@ class Conference extends Task {
_normalizeHook(cs, hook) { _normalizeHook(cs, hook) {
if (typeof hook === 'object') return hook; if (typeof hook === 'object') return hook;
const url = hook.startsWith('/') ? const url = hook.startsWith('/') ?
`${cs.application.requestor instanceof HttpRequestor ? cs.application.requestor.baseUrl : ''}${hook}` : `${cs.application.requestor.baseUrl}${hook}` :
hook; hook;
return { url } ; return { url } ;
@@ -601,7 +530,7 @@ class Conference extends Task {
const response = await this.ep.api('conference', [this.confName, 'get', 'count']); const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
if (response.body && confNoMatch(response.body)) this.participantCount = 0; if (response.body && confNoMatch(response.body)) this.participantCount = 0;
else if (response.body && /^\d+$/.test(response.body)) this.participantCount = parseInt(response.body) - 1; else if (response.body && /^\d+$/.test(response.body)) this.participantCount = parseInt(response.body) - 1;
this.logger.debug(`Conference:_doFinalMemberCheck conference count ${this.participantCount}`); this.logger.debug({response}, `Conference:_doFinalMemberCheck conference count ${this.participantCount}`);
} catch (err) { } catch (err) {
this.logger.info({err}, 'Conference:_doFinalMemberCheck error retrieving count (we were probably kicked'); this.logger.info({err}, 'Conference:_doFinalMemberCheck error retrieving count (we were probably kicked');
} }
@@ -611,7 +540,7 @@ class Conference extends Task {
* when we hang up as the last member, the current member count = 1 * when we hang up as the last member, the current member count = 1
* when we are kicked out of the call when the moderator leaves, the member count = 0 * when we are kicked out of the call when the moderator leaves, the member count = 0
*/ */
if (this.participantCount === 0 || this.endConferenceOnExit) { if (this.participantCount === 0) {
const {deleteKey} = cs.srf.locals.dbHelpers; const {deleteKey} = cs.srf.locals.dbHelpers;
try { try {
this._notifyConferenceEvent(cs, 'end'); this._notifyConferenceEvent(cs, 'end');
@@ -619,8 +548,7 @@ class Conference extends Task {
this.logger.info(`conf ${this.confName} deprovisioned: ${removed ? 'success' : 'failure'}`); this.logger.info(`conf ${this.confName} deprovisioned: ${removed ? 'success' : 'failure'}`);
} }
catch (err) { catch (err) {
this.logger.error(err, `Error deprovisioning conference ${this.confName}, this.logger.error(err, `Error deprovisioning conference ${this.confName}`);
might be the conference already cleaned by another moderator`);
} }
} }
} }
@@ -697,24 +625,8 @@ class Conference extends Task {
if (!params.time) params.time = (new Date()).toISOString(); if (!params.time) params.time = (new Date()).toISOString();
if (!params.members && typeof this.participantCount === 'number') params.members = this.participantCount; if (!params.members && typeof this.participantCount === 'number') params.members = this.participantCount;
cs.application.requestor cs.application.requestor
.request( .request('verb:hook', this.statusHook, Object.assign(params, this.statusParams, httpHeaders))
'verb:hook', .catch((err) => this.logger.info(err, 'Conference:notifyConferenceEvent - error'));
this.statusHook,
Object.assign(
params,
Object.assign(
{
conferenceSid: this.confName,
friendlyName: this.friendlyName,
},
cs.callInfo.toJSON()
),
httpHeaders
)
)
.catch((err) =>
this.logger.info(err, 'Conference:notifyConferenceEvent - error')
);
} }
} }
@@ -730,19 +642,11 @@ class Conference extends Task {
} }
// conference event handlers // conference event handlers
_onAddMember(logger, cs, evt) {
const memberId = parseInt(evt.getHeader('Member-ID')) ;
if (this.speakOnlyTo) {
logger.debug(`Conference:_onAddMember - member ${memberId} added to ${this.confName}, updating coaching mode`);
this.setCoachMode(this.speakOnlyTo).catch(() => {});
}
else logger.debug(`Conference:_onAddMember - member ${memberId} added to conference ${this.confName}`);
}
_onDelMember(logger, cs, evt) { _onDelMember(logger, cs, evt) {
const memberId = parseInt(evt.getHeader('Member-ID')) ; const memberId = parseInt(evt.getHeader('Member-ID')) ;
this.participantCount = parseInt(evt.getHeader('Conference-Size')); this.participantCount = parseInt(evt.getHeader('Conference-Size'));
if (memberId === this.memberId) { if (memberId === this.memberId) {
logger.info(`Conference:_onDelMember - I was dropped from conference ${this.confName}, task is complete`); this.logger.info(`Conference:_onDelMember - I was dropped from conference ${this.confName}, task is complete`);
this.replaceEndpointAndEnd(cs); this.replaceEndpointAndEnd(cs);
} }
} }
@@ -771,99 +675,6 @@ class Conference extends Task {
} }
} }
_onTag(logger, cs, evt) {
const memberId = parseInt(evt.getHeader('Member-ID')) ;
const tag = evt.getHeader('Tag') || '';
if (memberId !== this.memberId && this.speakOnlyTo) {
logger.info(`Conference:_onTag - member ${memberId} set tag to '${tag }'; updating coach mode accordingly`);
this.setCoachMode(this.speakOnlyTo).catch(() => {});
}
}
/**
* Set the conference to "coaching" mode, where the audio of the participant is only heard
* by a subset of the participants in the conference.
* We do this by first getting all of the members who do *not* have this tag, and then
* we configure this members audio to not be sent to them.
* @param {string} speakOnlyTo - tag of the members who should receive our audio
*
* N.B.: this feature requires jambonz patches to freeswitch mod_conference
*/
async setCoachMode(speakOnlyTo) {
this.speakOnlyTo = speakOnlyTo;
if (!this.memberId) {
this.logger.info('Conference:_setCoachMode: no member id yet');
return;
}
try {
const members = (await this.ep.getNonMatchingConfParticipants(this.confName, speakOnlyTo))
.filter((m) => m !== this.memberId);
if (members.length === 0) {
this.logger.info({members}, 'Conference:_setCoachMode: all participants have the tag, so all will hear me');
if (this.coaching.length) {
await this.ep.api('conference', [this.confName, 'relate', this.memberId, this.coaching.join(','), 'clear']);
this.coaching = [];
}
}
else {
const memberList = members.join(',');
this.logger.info(`Conference:_setCoachMode: my audio will NOT be sent to ${memberList}`);
await this.ep.api('conference', [this.confName, 'relate', this.memberId, memberList, 'nospeak']);
this.coaching = members;
}
} catch (err) {
this.logger.error({err, speakOnlyTo}, '_setCoachMode: Error');
}
}
async clearCoachMode() {
if (!this.memberId) return;
try {
if (this.coaching.length === 0) {
this.logger.info('Conference:_clearCoachMode: no coaching mode to clear');
}
else {
const memberList = this.coaching.join(',');
this.logger.info(`Conference:_clearCoachMode: now sending my audio to all, including ${memberList}`);
await this.ep.api('conference', [this.confName, 'relate', this.memberId, memberList, 'clear']);
}
this.speakOnlyTo = null;
this.coaching = [];
} catch (err) {
this.logger.error({err}, '_clearCoachMode: Error');
}
}
async setMemberTag(tag) {
try {
await this.ep.api('conference', [this.confName, 'tag', this.memberId, tag]);
this.logger.info(`Conference:setMemberTag: set tag for ${this.memberId} to ${tag}`);
this.memberTag = tag;
} catch (err) {
this.logger.error({err}, `Error setting tag for ${this.memberId} to ${tag}`);
}
}
async clearMemberTag() {
try {
await this.ep.api('conference', [this.confName, 'tag', this.memberId]);
this.logger.info(`Conference:setMemberTag: clearing tag for ${this.memberId}`);
this.memberTag = null;
} catch (err) {
this.logger.error({err}, `Error clearing tag for ${this.memberId}`);
}
}
async kickMember(cs) {
assert(cs.isInConference);
try {
await this.ep.api('conference', [this.confName, 'kick', this.memberId]);
this.logger.info(`Conference:kickMember: kick ${this.memberId} out of conference ${this.confName}`);
} catch (err) {
this.logger.error({err}, `Error kicking member out of conference for ${this.memberId}`);
}
}
} }
module.exports = Conference; module.exports = Conference;

View File

@@ -1,11 +1,9 @@
const Task = require('./task'); const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants'); const {TaskName, TaskPreconditions} = require('../utils/constants');
const parseDecibels = require('../utils/parse-decibels');
class TaskConfig extends Task { class TaskConfig extends Task {
constructor(logger, opts) { constructor(logger, opts) {
super(logger, opts); super(logger, opts);
[ [
'synthesizer', 'synthesizer',
'recognizer', 'recognizer',
@@ -13,10 +11,7 @@ class TaskConfig extends Task {
'record', 'record',
'listen', 'listen',
'transcribe', 'transcribe',
'fillerNoise', 'actionHookDelayAction'
'actionHookDelayAction',
'boostAudioSignal',
'vad'
].forEach((k) => this[k] = this.data[k] || {}); ].forEach((k) => this[k] = this.data[k] || {});
if ('notifyEvents' in this.data) { if ('notifyEvents' in this.data) {
@@ -34,8 +29,7 @@ class TaskConfig extends Task {
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits', 'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
'interDigitTimeout', 'bargein', 'dtmfBargein', 'minBargeinWordCount', 'actionHook' 'interDigitTimeout', 'bargein', 'dtmfBargein', 'minBargeinWordCount', 'actionHook'
].forEach((k) => { ].forEach((k) => {
const val = this.bargeIn[k]; if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
if (val !== undefined && val !== null) this.gatherOpts[k] = val;
}); });
} }
if (this.transcribe?.enable) { if (this.transcribe?.enable) {
@@ -56,7 +50,6 @@ class TaskConfig extends Task {
this.record?.action || this.record?.action ||
this.listen?.url || this.listen?.url ||
this.data.amd || this.data.amd ||
'boostAudioSignal' in this.data ||
this.transcribe?.enable) ? this.transcribe?.enable) ?
TaskPreconditions.Endpoint : TaskPreconditions.Endpoint :
TaskPreconditions.None; TaskPreconditions.None;
@@ -71,10 +64,6 @@ class TaskConfig extends Task {
get hasRecording() { return Object.keys(this.record).length; } get hasRecording() { return Object.keys(this.record).length; }
get hasListen() { return Object.keys(this.listen).length; } get hasListen() { return Object.keys(this.listen).length; }
get hasTranscribe() { return Object.keys(this.transcribe).length; } get hasTranscribe() { return Object.keys(this.transcribe).length; }
get hasDub() { return Object.keys(this.dub).length; }
get hasVad() { return Object.keys(this.vad).length; }
get hasFillerNoise() { return Object.keys(this.fillerNoise).length; }
get hasReferHook() { return Object.keys(this.data).includes('referHook'); }
get summary() { get summary() {
const phrase = []; const phrase = [];
@@ -84,13 +73,13 @@ class TaskConfig extends Task {
if (this.bargeIn.enable) phrase.push('enable barge-in'); if (this.bargeIn.enable) phrase.push('enable barge-in');
if (this.hasSynthesizer) { if (this.hasSynthesizer) {
const {vendor:v, language:l, voice, label} = this.synthesizer; const {vendor:v, language:l, voice} = this.synthesizer;
const s = `{${v},${l},${voice},${label || 'None'}}`; const s = `{${v},${l},${voice}}`;
phrase.push(`set synthesizer${s}`); phrase.push(`set synthesizer${s}`);
} }
if (this.hasRecognizer) { if (this.hasRecognizer) {
const {vendor:v, language:l, label} = this.recognizer; const {vendor:v, language:l} = this.recognizer;
const s = `{${v},${l},${label || 'None'}}`; const s = `{${v},${l}}`;
phrase.push(`set recognizer${s}`); phrase.push(`set recognizer${s}`);
} }
if (this.hasRecording) phrase.push(this.record.action); if (this.hasRecording) phrase.push(this.record.action);
@@ -100,12 +89,9 @@ class TaskConfig extends Task {
if (this.hasTranscribe) { if (this.hasTranscribe) {
phrase.push(this.transcribe.enable ? `transcribe ${this.transcribe.transcriptionHook}` : 'stop transcribe'); phrase.push(this.transcribe.enable ? `transcribe ${this.transcribe.transcriptionHook}` : 'stop transcribe');
} }
if (this.hasFillerNoise) phrase.push(`fillerNoise ${this.fillerNoise.enable ? 'on' : 'off'}`);
if (this.data.amd) phrase.push('enable amd'); if (this.data.amd) phrase.push('enable amd');
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`); if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
if (this.onHoldMusic) phrase.push(`onHoldMusic: ${this.onHoldMusic}`); if (this.onHoldMusic) phrase.push(`onHoldMusic: ${this.onHoldMusic}`);
if ('boostAudioSignal' in this.data) phrase.push(`setGain ${this.data.boostAudioSignal}`);
if (this.hasReferHook) phrase.push('set referHook');
return `${this.name}{${phrase.join(',')}}`; return `${this.name}{${phrase.join(',')}}`;
} }
@@ -144,8 +130,9 @@ class TaskConfig extends Task {
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default' cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
? this.synthesizer.vendor ? this.synthesizer.vendor
: cs.speechSynthesisVendor; : cs.speechSynthesisVendor;
cs.speechSynthesisLabel = this.synthesizer.label === 'default' cs.speechSynthesisLabel = this.synthesizer.label !== 'default'
? cs.speechSynthesisLabel : this.synthesizer.label; ? this.synthesizer.label
: cs.speechSynthesisLabel;
cs.speechSynthesisLanguage = this.synthesizer.language !== 'default' cs.speechSynthesisLanguage = this.synthesizer.language !== 'default'
? this.synthesizer.language ? this.synthesizer.language
: cs.speechSynthesisLanguage; : cs.speechSynthesisLanguage;
@@ -157,16 +144,15 @@ class TaskConfig extends Task {
cs.fallbackSpeechSynthesisVendor = this.synthesizer.fallbackVendor !== 'default' cs.fallbackSpeechSynthesisVendor = this.synthesizer.fallbackVendor !== 'default'
? this.synthesizer.fallbackVendor ? this.synthesizer.fallbackVendor
: cs.fallbackSpeechSynthesisVendor; : cs.fallbackSpeechSynthesisVendor;
cs.fallbackSpeechSynthesisLabel = this.synthesizer.fallbackLabel === 'default' cs.fallbackSpeechSynthesisLabel = this.synthesizer.fallbackLabel !== 'default'
? cs.fallbackSpeechSynthesisLabel : this.synthesizer.fallbackLabel; ? this.synthesizer.fallbackLabel
: cs.fallbackSpeechSynthesisLabel;
cs.fallbackSpeechSynthesisLanguage = this.synthesizer.fallbackLanguage !== 'default' cs.fallbackSpeechSynthesisLanguage = this.synthesizer.fallbackLanguage !== 'default'
? this.synthesizer.fallbackLanguage ? this.synthesizer.fallbackLanguage
: cs.fallbackSpeechSynthesisLanguage; : cs.fallbackSpeechSynthesisLanguage;
cs.fallbackSpeechSynthesisVoice = this.synthesizer.fallbackVoice !== 'default' cs.fallbackSpeechSynthesisVoice = this.synthesizer.fallbackVoice !== 'default'
? this.synthesizer.fallbackVoice ? this.synthesizer.fallbackVoice
: cs.fallbackSpeechSynthesisVoice; : cs.fallbackSpeechSynthesisVoice;
// new vendor is set, reset fallback vendor
cs.hasFallbackTts = false;
this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer'); this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer');
} }
if (this.hasRecognizer) { if (this.hasRecognizer) {
@@ -174,8 +160,9 @@ class TaskConfig extends Task {
cs.speechRecognizerVendor = this.recognizer.vendor !== 'default' cs.speechRecognizerVendor = this.recognizer.vendor !== 'default'
? this.recognizer.vendor ? this.recognizer.vendor
: cs.speechRecognizerVendor; : cs.speechRecognizerVendor;
cs.speechRecognizerLabel = this.recognizer.label === 'default' cs.speechRecognizerLabel = this.recognizer.label !== 'default'
? cs.speechRecognizerLabel : this.recognizer.label; ? this.recognizer.label
: cs.speechRecognizerLabel;
cs.speechRecognizerLanguage = this.recognizer.language !== 'default' cs.speechRecognizerLanguage = this.recognizer.language !== 'default'
? this.recognizer.language ? this.recognizer.language
: cs.speechRecognizerLanguage; : cs.speechRecognizerLanguage;
@@ -184,9 +171,9 @@ class TaskConfig extends Task {
cs.fallbackSpeechRecognizerVendor = this.recognizer.fallbackVendor !== 'default' cs.fallbackSpeechRecognizerVendor = this.recognizer.fallbackVendor !== 'default'
? this.recognizer.fallbackVendor ? this.recognizer.fallbackVendor
: cs.fallbackSpeechRecognizerVendor; : cs.fallbackSpeechRecognizerVendor;
cs.fallbackSpeechRecognizerLabel = this.recognizer.fallbackLabel === 'default' ? cs.fallbackSpeechRecognizerLabel = this.recognizer.fallbackLabel !== 'default'
cs.fallbackSpeechRecognizerLabel : ? this.recognizer.fallbackLabel
this.recognizer.fallbackLabel; : cs.fallbackSpeechRecognizerLabel;
cs.fallbackSpeechRecognizerLanguage = this.recognizer.fallbackLanguage !== 'default' cs.fallbackSpeechRecognizerLanguage = this.recognizer.fallbackLanguage !== 'default'
? this.recognizer.fallbackLanguage ? this.recognizer.fallbackLanguage
: cs.fallbackSpeechRecognizerLanguage; : cs.fallbackSpeechRecognizerLanguage;
@@ -210,8 +197,6 @@ class TaskConfig extends Task {
if ('punctuation' in this.recognizer) { if ('punctuation' in this.recognizer) {
cs.globalSttPunctuation = this.recognizer.punctuation; cs.globalSttPunctuation = this.recognizer.punctuation;
} }
// new vendor is set, reset fallback vendor
cs.hasFallbackAsr = false;
this.logger.info({ this.logger.info({
recognizer: this.recognizer, recognizer: this.recognizer,
isContinuousAsr: cs.isContinuousAsr isContinuousAsr: cs.isContinuousAsr
@@ -252,14 +237,12 @@ class TaskConfig extends Task {
} }
if (this.hasTranscribe) { if (this.hasTranscribe) {
if (this.transcribe.enable) { if (this.transcribe.enable) {
if (!this.transcribeOpts.recognizer) { this.transcribeOpts.recognizer = this.hasRecognizer ?
this.transcribeOpts.recognizer = this.hasRecognizer ? this.recognizer :
this.recognizer : {
{ vendor: cs.speechRecognizerVendor,
vendor: cs.speechRecognizerVendor, language: cs.speechRecognizerLanguage
language: cs.speechRecognizerLanguage };
};
}
this.logger.debug(this.transcribeOpts, 'Config: enabling transcribe'); this.logger.debug(this.transcribeOpts, 'Config: enabling transcribe');
cs.startBackgroundTask('transcribe', this.transcribeOpts); cs.startBackgroundTask('transcribe', this.transcribeOpts);
} else { } else {
@@ -267,44 +250,17 @@ class TaskConfig extends Task {
cs.stopBackgroundTask('transcribe'); cs.stopBackgroundTask('transcribe');
} }
} }
if (Object.keys(this.actionHookDelayAction).length !== 0) {
cs.actionHookDelayProperties = this.actionHookDelayAction; if (this.actionHookDelayAction) {
cs.actionHookDelayEnabled = this.actionHookDelayAction.enabled || false;
cs.actionHookNoResponseTimeout = this.actionHookDelayAction.noResponseTimeout || 0;
cs.actionHookNoResponseGiveUpTimeout = this.actionHookDelayAction.noResponseGiveUpTimeout || 0;
cs.actionHookDelayRetries = this.actionHookDelayAction.retries || 1;
cs.actionHookDelayActions = this.actionHookDelayAction.actions || [];
} }
if (this.data.sipRequestWithinDialogHook) { if (this.data.sipRequestWithinDialogHook) {
cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook; cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
} }
if ('boostAudioSignal' in this.data) {
const db = parseDecibels(this.data.boostAudioSignal);
this.logger.info(`Config: boosting audio signal by ${db} dB`);
const args = [ep.uuid, 'setGain', db];
ep.api('uuid_dub', args).catch((err) => {
this.logger.error(err, 'Error boosting audio signal');
});
}
if (this.hasFillerNoise) {
const {enable, ...opts} = this.fillerNoise;
this.logger.info({fillerNoise: this.fillerNoise}, 'Config: fillerNoise');
if (!enable) cs.disableFillerNoise();
else {
cs.enableFillerNoise(opts);
}
}
if (this.hasVad) {
cs.vad = {
enable: this.vad.enable || false,
voiceMs: this.vad.voiceMs || 250,
silenceMs: this.vad.silenceMs || 150,
strategy: this.vad.strategy || 'one-shot',
mode: (this.vad.mode !== undefined && this.vad.mode !== null) ? this.vad.mode : 2
};
}
if (this.hasReferHook) {
cs.referHook = this.data.referHook;
}
} }
async kill(cs) { async kill(cs) {

View File

@@ -14,7 +14,6 @@ const sessionTracker = require('../session/session-tracker');
const DtmfCollector = require('../utils/dtmf-collector'); const DtmfCollector = require('../utils/dtmf-collector');
const ConfirmCallSession = require('../session/confirm-call-session'); const ConfirmCallSession = require('../session/confirm-call-session');
const dbUtils = require('../utils/db-utils'); const dbUtils = require('../utils/db-utils');
const parseDecibels = require('../utils/parse-decibels');
const debug = require('debug')('jambonz:feature-server'); const debug = require('debug')('jambonz:feature-server');
const {parseUri} = require('drachtio-srf'); const {parseUri} = require('drachtio-srf');
const {ANCHOR_MEDIA_ALWAYS, JAMBONZ_DISABLE_DIAL_PAI_HEADER} = require('../config'); const {ANCHOR_MEDIA_ALWAYS, JAMBONZ_DISABLE_DIAL_PAI_HEADER} = require('../config');
@@ -82,8 +81,6 @@ function filterAndLimit(logger, tasks) {
return unique; return unique;
} }
const sleepFor = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
class TaskDial extends Task { class TaskDial extends Task {
constructor(logger, opts) { constructor(logger, opts) {
super(logger, opts); super(logger, opts);
@@ -104,7 +101,6 @@ class TaskDial extends Task {
this.dtmfHook = this.data.dtmfHook; this.dtmfHook = this.data.dtmfHook;
this.proxy = this.data.proxy; this.proxy = this.data.proxy;
this.tag = this.data.tag; this.tag = this.data.tag;
this.boostAudioSignal = this.data.boostAudioSignal;
if (this.dtmfHook) { if (this.dtmfHook) {
const {parentDtmfCollector, childDtmfCollector} = parseDtmfOptions(logger, this.data.dtmfCapture || {}); const {parentDtmfCollector, childDtmfCollector} = parseDtmfOptions(logger, this.data.dtmfCapture || {});
@@ -122,9 +118,6 @@ class TaskDial extends Task {
if (this.data.transcribe) { if (this.data.transcribe) {
this.transcribeTask = makeTask(logger, {'transcribe' : this.data.transcribe}, this); this.transcribeTask = makeTask(logger, {'transcribe' : this.data.transcribe}, this);
} }
if (this.data.dub && Array.isArray(this.data.dub) && this.data.dub.length > 0) {
this.dubTasks = this.data.dub.map((d) => makeTask(logger, {'dub': d}, this));
}
this.results = {}; this.results = {};
this.bridged = false; this.bridged = false;
@@ -156,7 +149,6 @@ class TaskDial extends Task {
this.cs.onHoldMusic || this.cs.onHoldMusic ||
ANCHOR_MEDIA_ALWAYS || ANCHOR_MEDIA_ALWAYS ||
this.listenTask || this.listenTask ||
this.dubTasks ||
this.transcribeTask || this.transcribeTask ||
this.startAmd; this.startAmd;
@@ -205,16 +197,7 @@ class TaskDial extends Task {
else { else {
this.epOther = cs.ep; this.epOther = cs.ep;
if (this.dialMusic && this.epOther && this.epOther.connected) { if (this.dialMusic && this.epOther && this.epOther.connected) {
(async() => { this.epOther.play(this.dialMusic).catch((err) => {});
do {
try {
await this.epOther.play(this.dialMusic);
} catch (err) {
this.logger.error(err, `TaskDial:exec error playing ${this.dialMusic}`);
await sleepFor(1000);
}
} while (!this.killed || !this.bridged);
})();
} }
} }
if (!this.killed) await this._attemptCalls(cs); if (!this.killed) await this._attemptCalls(cs);
@@ -340,17 +323,15 @@ class TaskDial extends Task {
const to = parseUri(req.getParsedHeader('Refer-To').uri); const to = parseUri(req.getParsedHeader('Refer-To').uri);
const by = parseUri(req.getParsedHeader('Referred-By').uri); const by = parseUri(req.getParsedHeader('Referred-By').uri);
const referredBy = req.get('Referred-By');
const userAgent = req.get('User-Agent');
this.logger.info({to}, 'refer to parsed'); this.logger.info({to}, 'refer to parsed');
const json = await cs.requestor.request('verb:hook', this.referHook, { const json = await cs.requestor.request('verb:hook', this.referHook, {
...(callInfo.toJSON()), ...(callInfo.toJSON()),
refer_details: { refer_details: {
sip_refer_to: req.get('Refer-To'), sip_refer_to: req.get('Refer-To'),
sip_referred_by: req.get('Referred-By'),
sip_user_agent: req.get('User-Agent'),
refer_to_user: to.scheme === 'tel' ? to.number : to.user, refer_to_user: to.scheme === 'tel' ? to.number : to.user,
...(referredBy && {sip_referred_by: referredBy}), referred_by_user: by.scheme === 'tel' ? by.number : by.user,
...(userAgent && {sip_user_agent: userAgent}),
...(by && {referred_by_user: by.scheme === 'tel' ? by.number : by.user}),
referring_call_sid, referring_call_sid,
referred_call_sid referred_call_sid
} }
@@ -381,7 +362,6 @@ class TaskDial extends Task {
res.send(202); res.send(202);
this.logger.info('DialTask:handleRefer - sent 202 Accepted'); this.logger.info('DialTask:handleRefer - sent 202 Accepted');
} catch (err) { } catch (err) {
this.logger.info({err}, 'DialTask:handleRefer - error processing incoming REFER');
res.send(err.statusCode || 501); res.send(err.statusCode || 501);
} }
} }
@@ -487,7 +467,7 @@ class TaskDial extends Task {
} }
async _attemptCalls(cs) { async _attemptCalls(cs) {
const {req, callInfo, direction, srf} = cs; const {req, srf} = cs;
const {getSBC} = srf.locals; const {getSBC} = srf.locals;
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers; const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
const {lookupCarrier, lookupCarrierByPhoneNumber} = dbUtils(this.logger, cs.srf); const {lookupCarrier, lookupCarrierByPhoneNumber} = dbUtils(this.logger, cs.srf);
@@ -499,11 +479,8 @@ class TaskDial extends Task {
this.headers = { this.headers = {
'X-Account-Sid': cs.accountSid, 'X-Account-Sid': cs.accountSid,
...(req && req.has('X-CID') && {'X-CID': req.get('X-CID')}), ...(req && req.has('X-CID') && {'X-CID': req.get('X-CID')}),
...(direction === 'outbound' && callInfo.sbcCallid && {'X-CID': callInfo.sbcCallid}), ...(req && req.has('P-Asserted-Identity') && !JAMBONZ_DISABLE_DIAL_PAI_HEADER &&
...(!JAMBONZ_DISABLE_DIAL_PAI_HEADER && req && { {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
...(req.has('P-Asserted-Identity') && {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
...(req.has('Privacy') && {'Privacy': req.get('Privacy')}),
}),
...(req && req.has('X-Voip-Carrier-Sid') && {'X-Voip-Carrier-Sid': req.get('X-Voip-Carrier-Sid')}), ...(req && req.has('X-Voip-Carrier-Sid') && {'X-Voip-Carrier-Sid': req.get('X-Voip-Carrier-Sid')}),
// Put headers at the end to make sure opt.headers override all default behavior. // Put headers at the end to make sure opt.headers override all default behavior.
...this.headers ...this.headers
@@ -574,9 +551,9 @@ class TaskDial extends Task {
const str = this.callerId || req.callingNumber || ''; const str = this.callerId || req.callingNumber || '';
const callingNumber = str.startsWith('+') ? str.substring(1) : str; const callingNumber = str.startsWith('+') ? str.substring(1) : str;
const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber); const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber);
this.logger.info(
`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested phone number: ${callingNumber}`);
if (voip_carrier_sid) { if (voip_carrier_sid) {
this.logger.info(
`Dial:_attemptCalls: selected voip_carrier_sid ${voip_carrier_sid} for callingNumber: ${callingNumber}`);
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid; opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
} }
} }
@@ -618,7 +595,6 @@ class TaskDial extends Task {
dialCallStatus: obj.callStatus, dialCallStatus: obj.callStatus,
dialSipStatus: obj.sipStatus, dialSipStatus: obj.sipStatus,
dialCallSid: sd.callSid, dialCallSid: sd.callSid,
dialSbcCallid: sd.callInfo.sbcCallid
}); });
} }
switch (obj.callStatus) { switch (obj.callStatus) {
@@ -654,8 +630,6 @@ class TaskDial extends Task {
await this._connectSingleDial(cs, sd); await this._connectSingleDial(cs, sd);
} catch (err) { } catch (err) {
this.logger.info({err}, 'Dial:_attemptCalls - Error calling _connectSingleDial '); this.logger.info({err}, 'Dial:_attemptCalls - Error calling _connectSingleDial ');
sd.removeAllListeners();
this.kill(cs);
} }
}) })
.on('decline', () => { .on('decline', () => {
@@ -805,17 +779,6 @@ class TaskDial extends Task {
dialCallSid: sd.callSid, dialCallSid: sd.callSid,
}); });
if (this.dubTasks) {
for (const dub of this.dubTasks) {
try {
await dub.exec(cs, {ep: sd.ep});
}
catch (err) {
this.logger.error({err}, 'Dial:_selectSingleDial - error executing dubTask');
}
}
}
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg); if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg); if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
if (cs.sipRequestWithinDialogHook) this._initSipIndialogRequestListener(cs, this.dlg); if (cs.sipRequestWithinDialogHook) this._initSipIndialogRequestListener(cs, this.dlg);
@@ -830,18 +793,6 @@ class TaskDial extends Task {
} }
} }
/* boost audio signal if requested */
if (this.boostAudioSignal) {
try {
const db = parseDecibels(this.boostAudioSignal);
this.logger.info(`Dial: boosting audio signal by ${db} dB`);
const args = [this.ep.uuid, 'setGain', db];
await this.ep.api('uuid_dub', args);
} catch (err) {
this.logger.info({err}, 'Dial:_selectSingleDial - Error boosting audio signal');
}
}
/* if we can release the media back to the SBC, do so now */ /* if we can release the media back to the SBC, do so now */
if (this.canReleaseMedia) setTimeout(this._releaseMedia.bind(this, cs, sd), 200); if (this.canReleaseMedia) setTimeout(this._releaseMedia.bind(this, cs, sd), 200);
} }

View File

@@ -1,144 +0,0 @@
const {TaskName} = require('../utils/constants');
const TtsTask = require('./tts-task');
const assert = require('assert');
const parseDecibels = require('../utils/parse-decibels');
/**
* Dub task: add or remove additional audio tracks into the call
*/
class TaskDub extends TtsTask {
constructor(logger, opts, parentTask) {
super(logger, opts, parentTask);
this.logger.debug({opts: this.data}, 'TaskDub constructor');
['action', 'track', 'play', 'say', 'loop'].forEach((prop) => {
this[prop] = this.data[prop];
});
this.gain = parseDecibels(this.data.gain);
assert.ok(this.action, 'TaskDub: action is required');
assert.ok(this.track, 'TaskDub: track is required');
}
get name() { return TaskName.Dub; }
async exec(cs, {ep}) {
super.exec(cs);
try {
switch (this.action) {
case 'addTrack':
await this._addTrack(cs, ep);
break;
case 'removeTrack':
await this._removeTrack(cs, ep);
break;
case 'silenceTrack':
await this._silenceTrack(cs, ep);
break;
case 'playOnTrack':
await this._playOnTrack(cs, ep);
break;
case 'sayOnTrack':
await this._sayOnTrack(cs, ep);
break;
default:
throw new Error(`TaskDub: unsupported action ${this.action}`);
}
} catch (err) {
this.logger.error(err, 'Error executing dub task');
}
}
async _addTrack(cs, ep) {
this.logger.info(`adding track: ${this.track}`);
await ep.dub({
action: 'addTrack',
track: this.track
});
if (this.play) await this._playOnTrack(cs, ep);
else if (this.say) await this._sayOnTrack(cs, ep);
}
async _removeTrack(_cs, ep) {
this.logger.info(`removing track: ${this.track}`);
await ep.dub({
action: 'removeTrack',
track: this.track
});
}
async _silenceTrack(_cs, ep) {
this.logger.info(`silencing track: ${this.track}`);
await ep.dub({
action: 'silenceTrack',
track: this.track
});
}
async _playOnTrack(_cs, ep) {
this.logger.info(`playing on track: ${this.track}`);
await ep.dub({
action: 'playOnTrack',
track: this.track,
play: this.play,
loop: this.loop ? 'loop' : 'once',
gain: this.gain
});
}
async _sayOnTrack(cs, ep) {
const text = this.say.text || this.say;
this.synthesizer = this.say.synthesizer || {};
if (Object.keys(this.synthesizer).length) {
this.logger.info({synthesizer: this.synthesizer},
`saying on track ${this.track}: ${text} with synthesizer options`);
}
else {
this.logger.info(`saying on track ${this.track}: ${text}`);
}
this.synthesizer = this.synthesizer || {};
this.text = [text];
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
this.synthesizer.vendor :
cs.speechSynthesisVendor;
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
this.synthesizer.language :
cs.speechSynthesisLanguage ;
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
this.synthesizer.voice :
cs.speechSynthesisVoice;
const label = this.synthesizer.label && this.synthesizer.label !== 'default' ?
this.synthesizer.label :
cs.speechSynthesisLabel;
const disableTtsStreaming = false;
const filepath = await this._synthesizeWithSpecificVendor(cs, ep, {
vendor, language, voice, label, disableTtsStreaming
});
assert.ok(filepath.length === 1, 'TaskDub: no filepath returned from synthesizer');
const path = filepath[0];
if (!path.startsWith('say:{')) {
/* we have a local file of mp3 or r8 of synthesized speech audio to play */
this.logger.info(`playing synthesized speech from file on track ${this.track}: ${path}`);
this.play = path;
await this._playOnTrack(cs, ep);
}
else {
this.logger.info(`doing actual text to speech file on track ${this.track}: ${path}`);
await ep.dub({
action: 'sayOnTrack',
track: this.track,
say: path,
gain: this.gain
});
}
}
}
module.exports = TaskDub;

View File

@@ -338,7 +338,6 @@ class TaskEnqueue extends Task {
this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`); this.logger.error({err}, `TaskEnqueue:_playHook error retrieving list info for queue ${this.queueName}`);
} }
const json = await cs.application.requestor.request('verb:hook', hook, params, httpHeaders); const json = await cs.application.requestor.request('verb:hook', hook, params, httpHeaders);
this.logger.debug({json}, 'TaskEnqueue:_playHook: received response from waitHook');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata)); const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
const allowedTasks = tasks.filter((t) => allowed.includes(t.name)); const allowedTasks = tasks.filter((t) => allowed.includes(t.name));

View File

@@ -10,10 +10,7 @@ const {
IbmTranscriptionEvents, IbmTranscriptionEvents,
NvidiaTranscriptionEvents, NvidiaTranscriptionEvents,
JambonzTranscriptionEvents, JambonzTranscriptionEvents,
AssemblyAiTranscriptionEvents, AssemblyAiTranscriptionEvents
VadDetection,
VerbioTranscriptionEvents,
SpeechmaticsTranscriptionEvents
} = require('../utils/constants.json'); } = require('../utils/constants.json');
const { const {
JAMBONES_GATHER_EARLY_HINTS_MATCH, JAMBONES_GATHER_EARLY_HINTS_MATCH,
@@ -23,7 +20,6 @@ const {
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const assert = require('assert'); const assert = require('assert');
const SttTask = require('./stt-task'); const SttTask = require('./stt-task');
const { SpeechCredentialError } = require('../utils/error');
class TaskGather extends SttTask { class TaskGather extends SttTask {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
@@ -31,7 +27,7 @@ class TaskGather extends SttTask {
[ [
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits', 'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein', 'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
'speechTimeout', 'timeout', 'say', 'play', 'actionHookDelayAction', 'fillerNoise', 'vad' 'speechTimeout', 'timeout', 'say', 'play', 'actionHookDelayAction'
].forEach((k) => this[k] = this.data[k]); ].forEach((k) => this[k] = this.data[k]);
// gather default input is digits // gather default input is digits
@@ -45,8 +41,7 @@ class TaskGather extends SttTask {
this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000; this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000;
this.interim = !!this.partialResultHook || this.bargein || (this.timeout > 0); this.interim = !!this.partialResultHook || this.bargein || (this.timeout > 0);
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true; this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
this.minBargeinWordCount = this.data.minBargeinWordCount !== undefined ? this.data.minBargeinWordCount : 1; this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
this._vadEnabled = this.minBargeinWordCount === 0;
if (this.data.recognizer) { if (this.data.recognizer) {
/* continuous ASR (i.e. compile transcripts until a special timeout or dtmf key) */ /* continuous ASR (i.e. compile transcripts until a special timeout or dtmf key) */
this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ? this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ?
@@ -96,18 +91,6 @@ class TaskGather extends SttTask {
(this.playTask && this.playTask.earlyMedia); (this.playTask && this.playTask.earlyMedia);
} }
get hasFillerNoise() {
return Object.keys(this.fillerNoise).length > 0 && this.fillerNoise.enabled !== false;
}
get fillerNoiseUrl() {
return this.fillerNoise.url;
}
get fillerNoiseStartDelaySecs() {
return this.fillerNoise.startDelaySecs;
}
get summary() { get summary() {
let s = `${this.name}{`; let s = `${this.name}{`;
if (this.input.length === 2) s += 'inputs=[speech,digits],'; if (this.input.length === 2) s += 'inputs=[speech,digits],';
@@ -119,39 +102,15 @@ class TaskGather extends SttTask {
} }
if (this.sayTask) s += ',with nested say task'; if (this.sayTask) s += ',with nested say task';
if (this.playTask) s += ',with nested play task'; if (this.playTask) s += ',with nested play task';
if (this.actionHookDelayAction) s += ',with actionHookDelayAction';
s += '}'; s += '}';
return s; return s;
} }
async exec(cs, obj) { async exec(cs, {ep}) {
try {
await this.handling(cs, obj);
} catch (error) {
if (error instanceof SpeechCredentialError) {
this.logger.info('Gather failed due to SpeechCredentialError, finished!');
this.notifyTaskDone();
return;
}
throw error;
}
}
async handling(cs, {ep}) {
this.logger.debug({options: this.data}, 'Gather:exec'); this.logger.debug({options: this.data}, 'Gather:exec');
await super.exec(cs, {ep}); await super.exec(cs, {ep});
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
this.fillerNoise = {
...(cs.fillerNoise || {}),
...(this.fillerNoise || {})
};
this.vad = {
...(cs.vad || {}),
...(this.vad || {})
};
if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) { if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints; const {hints, hintsBoost} = cs.globalSttHints;
const setOfHints = new Set((this.data.recognizer.hints || []) const setOfHints = new Set((this.data.recognizer.hints || [])
@@ -179,24 +138,27 @@ class TaskGather extends SttTask {
this.interim = true; this.interim = true;
this.logger.debug('Gather:exec - early hints match enabled'); this.logger.debug('Gather:exec - early hints match enabled');
} }
// actionHook delay
this._hookDelayEn = cs.actionHookDelayEnabled || !!this.actionHookDelayAction;
// if we have actionHook delay, and the session does as well, stash the session config this._hookDelayActions = this.actionHookDelayAction?.actions || cs.actionHookDelayActions || [];
if (this.actionHookDelayAction) {
if (cs.actionHookDelayProcessor) {
this.logger.debug('Gather:exec - stashing session-level ahd proprerties');
cs.stashActionHookDelayProperties();
}
cs.actionHookDelayProperties = this.actionHookDelayAction;
}
this._startVad(); // Only enable NoResponseTimeout if there is _hookDelayActions
this._hookNoResponseTimeout = (this._hookDelayActions?.length ?
(this.actionHookDelayAction?.noResponseTimeout || cs.actionHookNoResponseTimeout || 0)
: 0) * 1000;
const startDtmfListener = () => { this._hookNoResponseGiveUpTimeout = (this.actionHookDelayAction?.noResponseGiveUpTimeout ||
assert(!this._dtmfListenerStarted); cs.actionHookNoResponseGiveUpTimeout || 0) * 1000;
if (this.input.includes('digits') || this.dtmfBargein || this.asrDtmfTerminationDigit) {
ep.on('dtmf', this._onDtmf.bind(this, cs, ep)); this._hookDelayRetries = this.actionHookDelayAction?.retries || cs.actionHookDelayRetries || 1;
this._dtmfListenerStarted = true; this._hookDelayRetryCount = 0;
} this.hookDelayActionOpts = {
enabled: this._hookDelayEn,
actions: this._hookDelayActions,
noResponseTimeoutMs: this._hookNoResponseTimeout,
noResponseGiveUpTimeoutMs: this._hookNoResponseGiveUpTimeout,
retries: this._hookDelayRetries
}; };
const startListening = async(cs, ep) => { const startListening = async(cs, ep) => {
@@ -209,10 +171,16 @@ class TaskGather extends SttTask {
this.logger.info('Gather:exec - task was quickly killed so do not transcribe'); this.logger.info('Gather:exec - task was quickly killed so do not transcribe');
return; return;
} }
this.logger.debug('Gather:exec - going to start transcribing (startListening)');
this._startTranscribing(ep); this._startTranscribing(ep);
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
} catch (e) { } catch (e) {
await this._startFallback(cs, ep, {error: e}); if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
await this._fallback();
startListening(cs, ep);
} else {
this.logger.error({error: e}, 'error in initSpeech');
}
} }
} }
}; };
@@ -220,12 +188,13 @@ class TaskGather extends SttTask {
try { try {
if (this.sayTask) { if (this.sayTask) {
const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`); const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`);
const process = () => { this.sayTask.span = span;
this.sayTask.ctx = ctx;
this.sayTask.exec(cs, {ep}); // kicked off, _not_ waiting for it to complete
this.sayTask.on('playDone', (err) => {
span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
this.logger.debug('Gather: nested say task completed'); this.logger.debug('Gather: nested say task completed');
if (!this.listenDuringPrompt) {
startDtmfListener();
}
this._stopVad();
if (!this.killed) { if (!this.killed) {
startListening(cs, ep); startListening(cs, ep);
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) { if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
@@ -235,27 +204,17 @@ class TaskGather extends SttTask {
}); });
} }
} }
};
this.sayTask.span = span;
this.sayTask.ctx = ctx;
this.sayTask.exec(cs, {ep}) // kicked off, _not_ waiting for it to complete
.catch((err) => {
process();
});
this.sayTask.on('playDone', (err) => {
span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
process();
}); });
} }
else if (this.playTask) { else if (this.playTask) {
const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`); const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`);
const process = () => { this.playTask.span = span;
this.playTask.ctx = ctx;
this.playTask.exec(cs, {ep}); // kicked off, _not_ waiting for it to complete
this.playTask.on('playDone', (err) => {
span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing url');
this.logger.debug('Gather: nested play task completed'); this.logger.debug('Gather: nested play task completed');
if (!this.listenDuringPrompt) {
startDtmfListener();
}
this._stopVad();
if (!this.killed) { if (!this.killed) {
startListening(cs, ep); startListening(cs, ep);
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) { if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
@@ -265,17 +224,6 @@ class TaskGather extends SttTask {
}); });
} }
} }
};
this.playTask.span = span;
this.playTask.ctx = ctx;
this.playTask.exec(cs, {ep}) // kicked off, _not_ waiting for it to complete
.catch((err) => {
process();
});
this.playTask.on('playDone', (err) => {
span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing url');
process();
}); });
} }
else { else {
@@ -288,23 +236,17 @@ class TaskGather extends SttTask {
if (this.input.includes('speech') && this.listenDuringPrompt) { if (this.input.includes('speech') && this.listenDuringPrompt) {
await this._setSpeechHandlers(cs, ep); await this._setSpeechHandlers(cs, ep);
if (!this.resolved && !this.killed) { this.logger.debug('Gather:exec - going to start transcribing (listenDuringPrompt)');
this._startTranscribing(ep); this._startTranscribing(ep);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid) updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */}); .catch(() => {/*already logged error */});
}
else {
this.logger.info('Gather:exec - task was killed or resolved quickly, not starting transcription');
}
} }
// https://github.com/jambonz/jambonz-feature-server/issues/913 if (this.input.includes('digits') || this.dtmfBargein || this.asrDtmfTerminationDigit) {
if (this.listenDuringPrompt || (!this.sayTask && !this.playTask)) { ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
startDtmfListener();
} }
await this.awaitTaskDone(); await this.awaitTaskDone();
this._killAudio(cs);
} catch (err) { } catch (err) {
this.logger.error(err, 'TaskGather:exec error'); this.logger.error(err, 'TaskGather:exec error');
} }
@@ -314,13 +256,12 @@ class TaskGather extends SttTask {
kill(cs) { kill(cs) {
super.kill(cs); super.kill(cs);
this._killAudio(cs); this._killAudio(cs);
this._clearFillerNoiseTimer(); this._killActionHookDelayAction();
this.ep.removeAllListeners('dtmf'); this.ep.removeAllListeners('dtmf');
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearAsrTimer(); this._clearAsrTimer();
this.playTask?.span.end(); this.playTask?.span.end();
this.sayTask?.span.end(); this.sayTask?.span.end();
this._stopVad();
this._resolve('killed'); this._resolve('killed');
} }
@@ -340,9 +281,6 @@ class TaskGather extends SttTask {
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
let resolved = false; let resolved = false;
if (this.dtmfBargein) { if (this.dtmfBargein) {
if (!this.playComplete) {
this.notifyStatus({event: 'dtmf-bargein-detected', ...evt});
}
this._killAudio(cs); this._killAudio(cs);
this.emit('dtmf', evt); this.emit('dtmf', evt);
} }
@@ -401,19 +339,23 @@ class TaskGather extends SttTask {
ep, GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep, GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener( this.addCustomEventListener(
ep, GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep)); ep, GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
this.addCustomEventListener(
ep, GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break; break;
case 'aws': case 'aws':
case 'polly': case 'polly':
this.bugname = `${this.bugname_prefix}aws_transcribe`; this.bugname = `${this.bugname_prefix}aws_transcribe`;
this.addCustomEventListener(ep, AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); this.addCustomEventListener(ep, AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(ep, AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break; break;
case 'microsoft': case 'microsoft':
this.bugname = `${this.bugname_prefix}azure_transcribe`; this.bugname = `${this.bugname_prefix}azure_transcribe`;
this.addCustomEventListener( this.addCustomEventListener(
ep, AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep, AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
//this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected, this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected,
//this._onNoSpeechDetected.bind(this, cs, ep)); this._onNoSpeechDetected.bind(this, cs, ep));
this.addCustomEventListener(ep, AzureTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break; break;
case 'nuance': case 'nuance':
this.bugname = `${this.bugname_prefix}nuance_transcribe`; this.bugname = `${this.bugname_prefix}nuance_transcribe`;
@@ -423,6 +365,8 @@ class TaskGather extends SttTask {
this._onStartOfSpeech.bind(this, cs, ep)); this._onStartOfSpeech.bind(this, cs, ep));
this.addCustomEventListener(ep, NuanceTranscriptionEvents.TranscriptionComplete, this.addCustomEventListener(ep, NuanceTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep)); this._onTranscriptionComplete.bind(this, cs, ep));
this.addCustomEventListener(ep, NuanceTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
/* stall timers until prompt finishes playing */ /* stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) { if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
@@ -445,12 +389,6 @@ class TaskGather extends SttTask {
ep, SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep, SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
break; break;
case 'verbio':
this.bugname = `${this.bugname_prefix}verbio_transcribe`;
this.addCustomEventListener(
ep, VerbioTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
break;
case 'cobalt': case 'cobalt':
this.bugname = `${this.bugname_prefix}cobalt_transcribe`; this.bugname = `${this.bugname_prefix}cobalt_transcribe`;
this.addCustomEventListener( this.addCustomEventListener(
@@ -498,6 +436,8 @@ class TaskGather extends SttTask {
this._onStartOfSpeech.bind(this, cs, ep)); this._onStartOfSpeech.bind(this, cs, ep));
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.TranscriptionComplete, this.addCustomEventListener(ep, NvidiaTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep)); this._onTranscriptionComplete.bind(this, cs, ep));
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
/* I think nvidia has this (??) - stall timers until prompt finishes playing */ /* I think nvidia has this (??) - stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) { if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
@@ -515,24 +455,6 @@ class TaskGather extends SttTask {
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.ConnectFailure, this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.ConnectFailure,
this._onVendorConnectFailure.bind(this, cs, ep)); this._onVendorConnectFailure.bind(this, cs, ep));
break; break;
case 'speechmatics':
this.bugname = `${this.bugname_prefix}speechmatics_transcribe`;
this.addCustomEventListener(
ep, SpeechmaticsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.Info,
this._onSpeechmaticsInfo.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.RecognitionStarted,
this._onSpeechmaticsRecognitionStarted.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.Connect,
this._onVendorConnect.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.ConnectFailure,
this._onVendorConnectFailure.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.Error,
this._onSpeechmaticsErrror.bind(this, cs, ep));
break;
default: default:
if (this.vendor.startsWith('custom:')) { if (this.vendor.startsWith('custom:')) {
this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`; this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`;
@@ -582,8 +504,7 @@ class TaskGather extends SttTask {
account_sid: this.cs.accountSid, account_sid: this.cs.accountSid,
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
vendor: this.vendor, vendor: this.vendor,
detail: err.message, detail: err.message
target_sid: this.cs.callSid
}); });
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
} }
@@ -622,10 +543,7 @@ class TaskGather extends SttTask {
} }
_clearAsrTimer() { _clearAsrTimer() {
if (this._asrTimer) { if (this._asrTimer) clearTimeout(this._asrTimer);
this.logger.debug('_clearAsrTimer: asrTimer cleared');
clearTimeout(this._asrTimer);
}
this._asrTimer = null; this._asrTimer = null;
} }
@@ -634,6 +552,99 @@ class TaskGather extends SttTask {
this.cs.hangup(); this.cs.hangup();
} }
_actionHookDelaySayAction(verb) {
delete verb.verb;
this.logger.debug(`_actionHookDelaySayAction ${verb}`);
this._actionHookDelaySayTask = makeTask(this.logger, {say: verb}, this);
const {span, ctx} = this.startChildSpan(`actionHookDelayAction:${this._actionHookDelaySayTask.summary}`);
this._actionHookDelaySayTask.span = span;
this._actionHookDelaySayTask.ctx = ctx;
this._actionHookDelaySayTask.exec(this.cs, {ep: this.ep});
this._actionHookDelaySayTask.on('playDone', (err) => {
this._actionHookDelaySayTask = null;
span.end();
if (err) this.logger.error({err}, 'Gather:actionHookDelay Error playing tts');
});
}
_killActionHookDelayAction() {
this.logger.debug('_killActionHookDelayAction');
if (this._actionHookDelaySayTask && !this._actionHookDelaySayTask.killed) {
this._actionHookDelaySayTask.removeAllListeners('playDone');
this._actionHookDelaySayTask.kill(this.cs);
this._actionHookDelaySayTask.span.end();
this._actionHookDelaySayTask = null;
}
if (this._actionHookDelayPlayTask && !this._actionHookDelayPlayTask.killed) {
this._actionHookDelayPlayTask.removeAllListeners('playDone');
this._actionHookDelayPlayTask.kill(this.cs);
this._actionHookDelayPlayTask.span.end();
this._actionHookDelayPlayTask = null;
}
}
_actionHookDelayPlayAction(verb) {
delete verb.verb;
this.logger.debug(`_actionHookDelayPlayAction ${verb}`);
this._actionHookDelayPlayTask = makeTask(this.logger, {play: verb}, this);
const {span, ctx} = this.startChildSpan(`actionHookDelayAction:${this._actionHookDelayPlayTask.summary}`);
this._actionHookDelayPlayTask.span = span;
this._actionHookDelayPlayTask.ctx = ctx;
this._actionHookDelayPlayTask.exec(this.cs, {ep: this.ep});
this._actionHookDelayPlayTask.on('playDone', (err) => {
this._actionHookDelayPlayTask = null;
span.end();
if (err) this.logger.error({err}, 'Gather:actionHookDelay Error playing tts');
});
}
_startActionHookNoResponseTimer() {
assert(this._hookNoResponseTimeout > 0);
this._clearActionHookNoResponseTimer();
this.logger.debug('startActionHookNoResponseTimer');
this._actionHookNoResponseTimer = setTimeout(() => {
if (this._hookDelayRetryCount >= this._hookDelayRetries) {
this._hangupCall();
return;
}
const verb = this._hookDelayActions[this._hookDelayRetryCount % this._hookDelayActions.length];
if (verb.verb === 'say') {
this._actionHookDelaySayAction(verb);
} else if (verb.verb === 'play') {
this._actionHookDelayPlayAction(verb);
}
this._hookDelayRetryCount++;
this._startActionHookNoResponseTimer();
}, this._hookNoResponseTimeout);
}
_clearActionHookNoResponseTimer() {
if (this._actionHookNoResponseTimer) {
clearTimeout(this._actionHookNoResponseTimer);
}
this._actionHookNoResponseTimer = null;
}
_startActionHookNoResponseGiveUpTimer() {
assert(this._hookNoResponseGiveUpTimeout > 0);
this._clearActionHookNoResponseGiveUpTimer();
this.logger.debug('startActionHookNoResponseGiveUpTimer');
this._actionHookNoResponseGiveUpTimer = setTimeout(() => {
this._hangupCall();
}, this._hookNoResponseGiveUpTimeout);
}
_clearActionHookNoResponseGiveUpTimer() {
if (this._actionHookNoResponseGiveUpTimer) {
clearTimeout(this._actionHookNoResponseGiveUpTimer);
}
this._actionHookNoResponseGiveUpTimer = null;
}
_startFastRecognitionTimer(evt) { _startFastRecognitionTimer(evt) {
assert(this.fastRecognitionTimeout > 0); assert(this.fastRecognitionTimeout > 0);
this._clearFastRecognitionTimer(); this._clearFastRecognitionTimer();
@@ -665,65 +676,11 @@ class TaskGather extends SttTask {
this._finalAsrTimer = null; this._finalAsrTimer = null;
} }
_startVad() {
if (!this._vadStarted && this._vadEnabled) {
this.logger.debug('_startVad');
this.addCustomEventListener(this.ep, VadDetection.Detection, this._onVadDetected.bind(this, this.cs, this.ep));
this.ep?.startVadDetection(this.vad);
this._vadStarted = true;
}
}
_stopVad() {
if (this._vadStarted) {
this.logger.debug('_stopVad');
this.ep?.stopVadDetection(this.vad);
this.ep?.removeCustomEventListener(VadDetection.Detection, this._onVadDetected);
this._vadStarted = false;
}
}
_startFillerNoise() {
this.logger.debug('Gather:_startFillerNoise - playing filler noise');
this.ep?.play(this.fillerNoise.url);
this._fillerNoiseOn = true;
this.ep.once('playback-start', (evt) => {
if (evt.file === this.fillerNoise.url && !this._fillerNoiseOn) {
this.logger.info({evt}, 'Gather:_startFillerNoise - race condition - kill filler noise here');
this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err, 'Error killing filler noise'));
return;
} else this.logger.debug({evt}, 'Gather:_startFillerNoise - playback started');
});
}
_startFillerNoiseTimer() {
this._clearFillerNoiseTimer();
this._fillerNoiseTimer = setTimeout(() => {
this.logger.debug('Gather:_startFillerNoiseTimer - playing filler noise');
this._startFillerNoise();
}, this.fillerNoise.startDelaySecs * 1000);
}
_clearFillerNoiseTimer() {
if (this._fillerNoiseTimer) clearTimeout(this._fillerNoiseTimer);
this._fillerNoiseTimer = null;
}
_killFillerNoise() {
if (this._fillerNoiseTimer) {
this.logger.debug('Gather:_killFillerNoise');
this.ep?.api('uuid_break', this.ep.uuid);
}
}
_killAudio(cs) { _killAudio(cs) {
if (this.hasFillerNoise || (!this.sayTask && !this.playTask && this.bargein)) { if (!this.sayTask && !this.playTask && this.bargein) {
if (this.ep?.connected && (!this.playComplete || this.hasFillerNoise)) { if (this.ep?.connected && !this.playComplete) {
this.logger.debug('Gather:_killAudio: killing playback of any audio'); this.logger.debug('Gather:_killAudio: killing playback of any audio');
this.playComplete = true; this.playComplete = true;
this._fillerNoiseOn = false; // in a race, if we just started audio it may sneak through here
this.ep.api('uuid_break', this.ep.uuid) this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err, 'Error killing audio')); .catch((err) => this.logger.info(err, 'Error killing audio'));
} }
@@ -739,7 +696,6 @@ class TaskGather extends SttTask {
this.playTask.kill(cs); this.playTask.kill(cs);
this.playTask = null; this.playTask = null;
} }
this.playComplete = true;
} }
_onTranscription(cs, ep, evt, fsEvent) { _onTranscription(cs, ep, evt, fsEvent) {
@@ -748,7 +704,6 @@ class TaskGather extends SttTask {
const finished = fsEvent.getHeader('transcription-session-finished'); const finished = fsEvent.getHeader('transcription-session-finished');
this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription raw transcript'); this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription raw transcript');
if (bugname && this.bugname !== bugname) return; if (bugname && this.bugname !== bugname) return;
if (finished === 'true') return;
if (this.vendor === 'ibm' && evt?.state === 'listening') return; if (this.vendor === 'ibm' && evt?.state === 'listening') return;
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') { if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
@@ -771,22 +726,12 @@ class TaskGather extends SttTask {
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language, evt = this.normalizeTranscription(evt, this.vendor, 1, this.language,
this.shortUtterance, this.data.recognizer.punctuation); this.shortUtterance, this.data.recognizer.punctuation);
this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription normalized transcript'); //this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription normalized transcript');
if (evt.alternatives.length === 0) { if (evt.alternatives.length === 0) {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening'); this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
return; return;
} }
const confidence = evt.alternatives[0].confidence;
const minConfidence = this.data.recognizer?.minConfidence;
this.logger.debug({evt},
`TaskGather:_onTranscription - confidence (${confidence}), minConfidence (${minConfidence})`);
if (confidence && minConfidence && confidence < minConfidence) {
this.logger.info({evt},
'TaskGather:_onTranscription - Transcript confidence ' +
`(${confidence}) < minConfidence (${minConfidence})`);
return this._resolve('stt-low-confidence', evt);
}
/* fast path: our first partial transcript exactly matches an early hint */ /* fast path: our first partial transcript exactly matches an early hint */
if (this.earlyHintsMatch && evt.is_final === false && this.partialTranscriptsCount++ === 0) { if (this.earlyHintsMatch && evt.is_final === false && this.partialTranscriptsCount++ === 0) {
@@ -850,39 +795,43 @@ class TaskGather extends SttTask {
this._startAsrTimer(); this._startAsrTimer();
/* some STT engines will keep listening after a final response, so no need to restart */ /* some STT engines will keep listening after a final response, so no need to restart */
if (!['soniox', 'aws', 'microsoft', 'deepgram'].includes(this.vendor)) this._startTranscribing(ep); if (!['soniox', 'aws', 'microsoft', 'deepgram'].includes(this.vendor)) {
this.logger.debug('Gather:_onTranscription - going to start transcribing again (continuous asr)');
this._startTranscribing(ep);
}
} }
else { else {
/* this was removed to fix https://github.com/jambonz/jambonz-feature-server/issues/783 */
/*
if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) { if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) {
this.logger.debug({evt, words, bufferedWords}, this.logger.debug({evt, words, bufferedWords},
'TaskGather:_onTranscription - final transcript but < min barge words'); 'TaskGather:_onTranscription - final transcript but < min barge words');
if (!emptyTranscript) this._bufferedTranscripts.push(evt); this._bufferedTranscripts.push(evt);
if (!['soniox', 'aws', 'microsoft', 'deepgram'].includes(this.vendor)) this._startTranscribing(ep); if (!['soniox', 'aws', 'microsoft', 'deepgram'].includes(this.vendor)) {
this.logger.debug(
`Gather:_onTranscription - start transcribing again (min bargein words=${this.minBargeinWordCount}`);
this._startTranscribing(ep);
}
return; return;
} }
else { else {
*/ if (this.vendor === 'soniox') {
if (this.vendor === 'soniox') { /* compile transcripts into one */
/* compile transcripts into one */ this._sonioxTranscripts.push(evt.vendor.finalWords);
this._sonioxTranscripts.push(evt.vendor.finalWords); evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language);
evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language); this._sonioxTranscripts = [];
this._sonioxTranscripts = []; }
} else if (this.vendor === 'deepgram') {
else if (this.vendor === 'deepgram') { /* compile transcripts into one */
/* compile transcripts into one */ if (!emptyTranscript) this._bufferedTranscripts.push(evt);
if (!emptyTranscript) this._bufferedTranscripts.push(evt);
/* deepgram can send an empty and final transcript; only if we have any buffered should we resolve */ /* deepgram can send an empty and final transcript; only if we have any buffered should we resolve */
if (this._bufferedTranscripts.length === 0) return; if (this._bufferedTranscripts.length === 0) return;
evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language, this.vendor); evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language, this.vendor);
this._bufferedTranscripts = []; this._bufferedTranscripts = [];
} }
/* here is where we return a final transcript */ /* here is where we return a final transcript */
this._resolve('speech', evt); this._resolve('speech', evt);
/*}*/ }
} }
} }
else { else {
@@ -903,7 +852,6 @@ class TaskGather extends SttTask {
if (!this.playComplete) { if (!this.playComplete) {
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech'); this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
this.emit('vad'); this.emit('vad');
this.notifyStatus({event: 'speech-bargein-detected', ...evt});
} }
this._killAudio(cs); this._killAudio(cs);
} }
@@ -918,18 +866,12 @@ class TaskGather extends SttTask {
this.cs.callInfo, httpHeaders)); this.cs.callInfo, httpHeaders));
} }
if (this.vendor === 'soniox') { if (this.vendor === 'soniox') {
this._clearTimer();
if (evt.vendor.finalWords.length) { if (evt.vendor.finalWords.length) {
this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript'); this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript');
this._sonioxTranscripts.push(evt.vendor.finalWords); this._sonioxTranscripts.push(evt.vendor.finalWords);
} }
} }
// If transcription received, reset timeout timer.
if (this._timeoutTimer) {
this._startTimer();
}
/* restart asr timer if we get a partial transcript (only if the asr timer is already running) */
/* note: https://github.com/jambonz/jambonz-feature-server/issues/866 */
if (this.isContinuousAsr && this._asrTimer) this._startAsrTimer();
} }
} }
_onEndOfUtterance(cs, ep) { _onEndOfUtterance(cs, ep) {
@@ -948,6 +890,7 @@ class TaskGather extends SttTask {
* since we dont have a final transcript yet. * since we dont have a final transcript yet.
*/ */
if (!this.resolved && !this.killed && !this._bufferedTranscripts.length && this.wantsSingleUtterance) { if (!this.resolved && !this.killed && !this._bufferedTranscripts.length && this.wantsSingleUtterance) {
this.logger.debug('Gather:_onEndOfUtterance - start transcribing again (end of utterance/wantsSingleUtterance)');
this._startTranscribing(ep); this._startTranscribing(ep);
} }
} }
@@ -961,9 +904,9 @@ class TaskGather extends SttTask {
_onTranscriptionComplete(cs, ep) { _onTranscriptionComplete(cs, ep) {
this.logger.debug('TaskGather:_onTranscriptionComplete'); this.logger.debug('TaskGather:_onTranscriptionComplete');
} }
async _onJambonzError(cs, ep, evt) {
async _startFallback(cs, ep, evt) { this.logger.info({evt}, 'TaskGather:_onJambonzError');
if (this.canFallback) { if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
ep.stopTranscription({ ep.stopTranscription({
vendor: this.vendor, vendor: this.vendor,
bugname: this.bugname bugname: this.bugname
@@ -971,79 +914,41 @@ class TaskGather extends SttTask {
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`)); .catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
try { try {
this.logger.debug('gather:_startFallback'); await this._fallback();
this.notifyError({ msg: 'ASR error', await this._initSpeech(cs, ep);
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'in progress'}); this.logger.debug('Gather:_onJambonzError - going to start transcribing again (jambonz error)');
await this._initFallback();
this._speechHandlersSet = false;
await this._setSpeechHandlers(cs, ep);
this._startTranscribing(ep); this._startTranscribing(ep);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
return true; return;
} catch (error) { } catch (error) {
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`); this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
} }
} else {
this.logger.debug('gather:_startFallback no condition for falling back');
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
} }
return false;
}
async _onJambonzError(cs, ep, evt) {
if (this.vendor === 'google' && evt.error_code === 0) {
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError - ignoring google error code 0');
return;
}
this.logger.info({evt}, 'TaskGather:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals; const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') { if (this.vendor === 'nuance') {
const {code, error} = evt; const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout'); if (code === 404 && error === 'No speech') return this._resolve('timeout');
if (code === 413 && error === 'Too much speech') return this._resolve('timeout'); if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
} }
this.logger.info({evt}, 'TaskGather:_onJambonzError'); this.logger.info({evt}, 'TaskGather:_onJambonzError');
const errMessage = evt.error || evt.Message;
writeAlerts({ writeAlerts({
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${errMessage}`, message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor, vendor: this.vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
if (!(await this._startFallback(cs, ep, evt))) { this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
this.notifyTaskDone();
}
} }
async _onVendorConnectFailure(cs, _ep, evt) { _onVendorConnectFailure(cs, _ep, evt) {
super._onVendorConnectFailure(cs, _ep, evt); super._onVendorConnectFailure(cs, _ep, evt);
if (!(await this._startFallback(cs, _ep, evt))) { this.notifyTaskDone();
this.notifyTaskDone();
}
} }
async _onSpeechmaticsErrror(cs, _ep, evt) { _onVendorError(cs, _ep, evt) {
// eslint-disable-next-line no-unused-vars
const {message, ...e} = evt;
this._onVendorError(cs, _ep, {error: JSON.stringify(e)});
}
async _onVendorError(cs, _ep, evt) {
super._onVendorError(cs, _ep, evt); super._onVendorError(cs, _ep, evt);
if (!(await this._startFallback(cs, _ep, evt))) { this._resolve('stt-error', evt);
this._resolve('stt-error', evt);
}
}
async _onSpeechmaticsRecognitionStarted(_cs, _ep, evt) {
this.logger.debug({evt}, 'TaskGather:_onSpeechmaticsRecognitionStarted');
}
async _onSpeechmaticsInfo(_cs, _ep, evt) {
this.logger.debug({evt}, 'TaskGather:_onSpeechmaticsInfo');
} }
_onVadDetected(cs, ep) { _onVadDetected(cs, ep) {
@@ -1052,10 +957,6 @@ class TaskGather extends SttTask {
this._killAudio(cs); this._killAudio(cs);
this.emit('vad'); this.emit('vad');
} }
if (this.vad?.strategy === 'one-shot') {
this.ep?.removeCustomEventListener(VadDetection.Detection, this._onVadDetected);
this._vadStarted = false;
}
} }
_onNoSpeechDetected(cs, ep, evt, fsEvent) { _onNoSpeechDetected(cs, ep, evt, fsEvent) {
@@ -1065,7 +966,7 @@ class TaskGather extends SttTask {
this.logger.debug('TaskGather:_onNoSpeechDetected for old gather, ignoring'); this.logger.debug('TaskGather:_onNoSpeechDetected for old gather, ignoring');
} }
else { else {
this.logger.debug('TaskGather:_onNoSpeechDetected - listen again'); this.logger.debug('Gather:_onNoSpeechDetected - going to start transcribing again');
this._startTranscribing(ep); this._startTranscribing(ep);
} }
return; return;
@@ -1084,10 +985,7 @@ class TaskGather extends SttTask {
this.logger.error({err}, 'Error stopping transcription'); this.logger.error({err}, 'Error stopping transcription');
}); });
} }
if (this.resolved) { if (this.resolved) return;
this.logger.debug('TaskGather:_resolve - already resolved');
return;
}
this.resolved = true; this.resolved = true;
// If bargin is false and ws application return ack to verb:hook // If bargin is false and ws application return ack to verb:hook
@@ -1098,12 +996,9 @@ class TaskGather extends SttTask {
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearTimer(); this._clearTimer();
this._clearFastRecognitionTimer(); this._clearFastRecognitionTimer();
this._clearAsrTimer();
this._clearFinalAsrTimer();
this.span.setAttributes({ this.span.setAttributes({
channel: 1, channel: 1,
'stt.label': this.label || 'None',
'stt.resolve': reason, 'stt.resolve': reason,
'stt.result': JSON.stringify(evt) 'stt.result': JSON.stringify(evt)
}); });
@@ -1114,88 +1009,49 @@ class TaskGather extends SttTask {
return; return;
} }
// action hook delay // Enabled action Hook delay timer to applied actions
if (this.cs.actionHookDelayProcessor) { if (this._hookNoResponseTimeout > 0) {
this.logger.debug('TaskGather:_resolve - actionHookDelayProcessor exists - starting it'); this._startActionHookNoResponseTimer();
this.cs.actionHookDelayProcessor.start();
} }
// TODO: remove and implement as actionHookDelay if (this._hookNoResponseGiveUpTimeout > 0) {
if (this.hasFillerNoise && (reason.startsWith('dtmf') || reason.startsWith('speech'))) { this._startActionHookNoResponseGiveUpTimer();
if (this.fillerNoiseStartDelaySecs > 0) {
this._startFillerNoiseTimer();
}
else {
this.logger.debug(`TaskGather:_resolve - playing filler noise: ${this.fillerNoiseUrl}`);
this._startFillerNoise();
}
} }
let returnedVerbs = false;
try { try {
if (reason.startsWith('dtmf')) { if (reason.startsWith('dtmf')) {
if (this.parentTask) this.parentTask.emit('dtmf', evt); if (this.parentTask) this.parentTask.emit('dtmf', evt);
else { else {
this.emit('dtmf', evt); this.emit('dtmf', evt);
const payload = {digits: this.digitBuffer, reason: 'dtmfDetected'}; await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
this.logger.info({payload}, 'TaskGather:_resolve - sending dtmf event to app');
returnedVerbs = await this.performAction(payload);
} }
} }
else if (reason.startsWith('speech')) { else if (reason.startsWith('speech')) {
if (this.parentTask) this.parentTask.emit('transcription', evt); if (this.parentTask) this.parentTask.emit('transcription', evt);
else { else {
this.emit('transcription', evt); this.emit('transcription', evt);
const payload = {speech: evt, reason: 'speechDetected'}; await this.performAction({speech: evt, reason: 'speechDetected'});
this.logger.info({payload}, 'TaskGather:_resolve - sending speech transcript to app');
returnedVerbs = await this.performAction(payload);
this.logger.debug({returnedVerbs}, 'TaskGather:_resolve - back from performAction');
} }
} }
else if (reason.startsWith('timeout')) { else if (reason.startsWith('timeout')) {
if (this.parentTask) this.parentTask.emit('timeout', evt); if (this.parentTask) this.parentTask.emit('timeout', evt);
else { else {
this.emit('timeout', evt); this.emit('timeout', evt);
this.logger.info('TaskGather:_resolve - sending timeout event to app'); await this.performAction({reason: 'timeout'});
returnedVerbs = await this.performAction({reason: 'timeout'});
} }
} }
else if (reason.startsWith('stt-error')) { else if (reason.startsWith('stt-error')) {
if (this.parentTask) this.parentTask.emit('stt-error', evt); if (this.parentTask) this.parentTask.emit('stt-error', evt);
else { else {
this.emit('stt-error', evt); this.emit('stt-error', evt);
const payload = {reason: 'stt-error', details: evt.error}; await this.performAction({reason: 'error', details: evt.error});
this.logger.info({payload}, 'TaskGather:_resolve - sending stt-error event to app');
returnedVerbs = await this.performAction(payload);
}
} else if (reason.startsWith('stt-low-confidence')) {
if (this.parentTask) this.parentTask.emit('stt-low-confidence', evt);
else {
this.emit('stt-low-confidence', evt);
this.logger.info('TaskGather:_resolve - sending stt-low-confidence event to app');
returnedVerbs = await this.performAction({reason: 'stt-low-confidence'});
} }
} }
} catch (err) { /*already logged error*/ } } catch (err) { /*already logged error*/ }
// Gather got response from hook, cancel actionHookDelay processing // Gather got response from hook, cancel all delay timers if there is any
this.logger.debug('TaskGather:_resolve - checking ahd'); this._clearActionHookNoResponseTimer();
if (this.cs.actionHookDelayProcessor) { this._clearActionHookNoResponseGiveUpTimer();
if (returnedVerbs) {
this.logger.debug('TaskGather:_resolve - got response from action hook, cancelling actionHookDelay');
await this.cs.actionHookDelayProcessor.stop();
if (this.actionHookDelayAction && !this.cs.popActionHookDelayProperties()) {
// no session level ahd was running when this task started, so clear it
this.cs.clearActionHookDelayProcessor();
this.logger.debug('TaskGather:_resolve - clear ahd');
}
}
else {
this.logger.debug('TaskGather:_resolve - no response from action hook, continue actionHookDelay');
}
}
this._clearFillerNoiseTimer();
this.notifyTaskDone(); this.notifyTaskDone();
} }

View File

@@ -8,10 +8,6 @@ const DTMF_SPAN_NAME = 'dtmf';
class TaskListen extends Task { class TaskListen extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
super(logger, opts); super(logger, opts);
/**
* @deprecated
* use bidirectionalAudio.enabled
*/
this.disableBidirectionalAudio = opts.disableBidirectionalAudio; this.disableBidirectionalAudio = opts.disableBidirectionalAudio;
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
@@ -29,15 +25,6 @@ class TaskListen extends Task {
this.results = {}; this.results = {};
this.playAudioQueue = []; this.playAudioQueue = [];
this.isPlayingAudioFromQueue = false; this.isPlayingAudioFromQueue = false;
this.bidirectionalAudio = {
enabled: this.disableBidirectionalAudio === true ? false : true,
...(this.data['bidirectionalAudio']),
};
// From drachtio-version 3.0.40, forkAudioStart will send empty bugname, metadata together with
// bidirectionalAudio params that cause old version of freeswitch missunderstand between bugname and
// bidirectionalAudio params
this._bugname = 'audio_fork';
if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this); if (this.transcribe) this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe}, this);
} }
@@ -146,8 +133,7 @@ class TaskListen extends Task {
mixType: this.mixType, mixType: this.mixType,
sampling: this.sampleRate, sampling: this.sampleRate,
...(this._bugname && {bugname: this._bugname}), ...(this._bugname && {bugname: this._bugname}),
metadata, metadata
bidirectionalAudio: this.bidirectionalAudio || {}
}); });
this.recordStartTime = moment(); this.recordStartTime = moment();
if (this.maxLength) { if (this.maxLength) {
@@ -167,7 +153,7 @@ class TaskListen extends Task {
} }
/* support bi-directional audio */ /* support bi-directional audio */
if (this.bidirectionalAudio.enabled) { if (!this.disableBidirectionalAudio) {
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep)); ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
} }
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep)); ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));

View File

@@ -1,96 +0,0 @@
const Task = require('../task');
const {TaskPreconditions} = require('../../utils/constants');
const TaskLlmOpenAI_S2S = require('./llms/openai_s2s');
class TaskLlm extends Task {
constructor(logger, opts) {
super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
['vendor', 'model', 'auth', 'connectOptions'].forEach((prop) => {
this[prop] = this.data[prop];
});
this.eventHandlers = [];
// delegate to the specific llm model
this.llm = this.createSpecificLlm();
}
get name() { return this.llm.name ; }
get toolHook() { return this.llm?.toolHook; }
get eventHook() { return this.llm?.eventHook; }
get ep() { return this.cs.ep; }
async exec(cs, {ep}) {
await super.exec(cs, {ep});
await this.llm.exec(cs, {ep});
}
async kill(cs) {
super.kill(cs);
await this.llm.kill(cs);
}
createSpecificLlm() {
let llm;
switch (this.vendor) {
case 'openai':
case 'microsoft':
if (this.model.startsWith('gpt-4o-realtime')) {
llm = new TaskLlmOpenAI_S2S(this.logger, this.data, this);
}
break;
default:
throw new Error(`Unsupported vendor ${this.vendor} for LLM`);
}
if (!llm) {
throw new Error(`Unsupported vendor:model ${this.vendor}:${this.model}`);
}
return llm;
}
addCustomEventListener(ep, event, handler) {
this.eventHandlers.push({ep, event, handler});
ep.addCustomEventListener(event, handler);
}
removeCustomEventListeners() {
this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler));
}
async sendEventHook(data) {
await this.cs?.requestor.request('llm:event', this.eventHook, data);
}
async sendToolHook(tool_call_id, data) {
await this.cs?.requestor.request('llm:tool-call', this.toolHook, {tool_call_id, ...data});
}
async processToolOutput(tool_call_id, data) {
if (!this.ep.connected) {
this.logger.info('TaskLlm:processToolOutput - no connected endpoint');
return;
}
this.llm.processToolOutput(this.ep, tool_call_id, data);
}
async processLlmUpdate(data, callSid) {
if (this.ep.connected) {
if (typeof this.llm.processLlmUpdate === 'function') {
this.llm.processLlmUpdate(this.ep, data, callSid);
}
else {
const {vendor, model} = this.llm;
this.logger.info({data, callSid},
`TaskLlm:_processLlmUpdate: LLM ${vendor}:${model} does not support llm:update`);
}
}
}
}
module.exports = TaskLlm;

View File

@@ -1,357 +0,0 @@
const Task = require('../../task');
const TaskName = 'Llm_OpenAI_s2s';
const {LlmEvents_OpenAI} = require('../../../utils/constants');
const ClientEvent = 'client.event';
const SessionDelete = 'session.delete';
const openai_server_events = [
'error',
'session.created',
'session.updated',
'conversation.created',
'input_audio_buffer.committed',
'input_audio_buffer.cleared',
'input_audio_buffer.speech_started',
'input_audio_buffer.speech_stopped',
'conversation.item.created',
'conversation.item.input_audio_transcription.completed',
'conversation.item.input_audio_transcription.failed',
'conversation.item.truncated',
'conversation.item.deleted',
'response.created',
'response.done',
'response.output_item.added',
'response.output_item.done',
'response.content_part.added',
'response.content_part.done',
'response.text.delta',
'response.text.done',
'response.audio_transcript.delta',
'response.audio_transcript.done',
'response.audio.delta',
'response.audio.done',
'response.function_call_arguments.delta',
'response.function_call_arguments.done',
'rate_limits.updated',
'output_audio.playback_started',
'output_audio.playback_stopped',
];
const expandWildcards = (events) => {
const expandedEvents = [];
events.forEach((evt) => {
if (evt.endsWith('.*')) {
const prefix = evt.slice(0, -2); // Remove the wildcard ".*"
const matchingEvents = openai_server_events.filter((e) => e.startsWith(prefix));
expandedEvents.push(...matchingEvents);
} else {
expandedEvents.push(evt);
}
});
return expandedEvents;
};
class TaskLlmOpenAI_S2S extends Task {
constructor(logger, opts, parentTask) {
super(logger, opts, parentTask);
this.parent = parentTask;
this.vendor = this.parent.vendor;
this.model = this.parent.model;
this.auth = this.parent.auth;
this.connectionOptions = this.parent.connectOptions;
const {apiKey} = this.auth || {};
if (!apiKey) throw new Error('auth.apiKey is required for OpenAI S2S');
if (['openai', 'microsoft'].indexOf(this.vendor) === -1) {
throw new Error(`Invalid vendor ${this.vendor} for OpenAI S2S`);
}
if ('microsoft' === this.vendor && !this.connectionOptions?.host) {
throw new Error('connectionOptions.host is required for Microsoft OpenAI S2S');
}
this.apiKey = apiKey;
this.authType = 'microsoft' === this.vendor ? 'query' : 'bearer';
this.actionHook = this.data.actionHook;
this.eventHook = this.data.eventHook;
this.toolHook = this.data.toolHook;
const {response_create, session_update} = this.data.llmOptions;
if (typeof response_create !== 'object') {
throw new Error('llmOptions with an initial response.create is required for OpenAI S2S');
}
this.response_create = response_create;
this.session_update = session_update;
this.results = {
completionReason: 'normal conversation end'
};
/**
* only one of these will have items,
* if includeEvents, then these are the events to include
* if excludeEvents, then these are the events to exclude
*/
this.includeEvents = [];
this.excludeEvents = [];
/* default to all events if user did not specify */
this._populateEvents(this.data.events || openai_server_events);
this.addCustomEventListener = parentTask.addCustomEventListener.bind(parentTask);
this.removeCustomEventListeners = parentTask.removeCustomEventListeners.bind(parentTask);
}
get name() { return TaskName; }
get host() {
const {host} = this.connectionOptions || {};
return host || (this.vendor === 'openai' ? 'api.openai.com' : void 0);
}
get path() {
const {path} = this.connectionOptions || {};
if (path) return path;
switch (this.vendor) {
case 'openai':
return 'v1/realtime?model=gpt-4o-realtime-preview-2024-10-01';
case 'microsoft':
return 'openai/realtime?api-version=2024-10-01-preview&deployment=gpt-4o-realtime-preview-1001&';
}
}
async _api(ep, args) {
const res = await ep.api('uuid_openai_s2s', `^^|${args.join('|')}`);
if (!res.body?.startsWith('+OK')) {
throw new Error({args}, `Error calling uuid_openai_s2s: ${res.body}`);
}
}
async exec(cs, {ep}) {
await super.exec(cs);
await this._startListening(cs, ep);
await this.awaitTaskDone();
/* note: the parent llm verb started the span, which is why this is necessary */
await this.parent.performAction(this.results);
this._unregisterHandlers();
}
async kill(cs) {
super.kill(cs);
this._api(cs.ep, [cs.ep.uuid, SessionDelete])
.catch((err) => this.logger.info({err}, 'TaskLlmOpenAI_S2S:kill - error deleting session'));
this.notifyTaskDone();
}
/**
* Send function call output to the OpenAI server in the form of conversation.item.create
* per https://platform.openai.com/docs/guides/realtime/function-calls
*/
async processToolOutput(ep, tool_call_id, data) {
try {
this.logger.debug({tool_call_id, data}, 'TaskLlmOpenAI_S2S:processToolOutput');
if (!data.type || data.type !== 'conversation.item.create') {
this.logger.info({data},
'TaskLlmOpenAI_S2S:processToolOutput - invalid tool output, must be conversation.item.create');
}
else {
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)]);
// spec also recommends to send immediate response.create
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify({type: 'response.create'})]);
}
} catch (err) {
this.logger.info({err}, 'TaskLlmOpenAI_S2S:processToolOutput');
}
}
/**
* Send a session.update to the OpenAI server
* Note: creating and deleting conversation items also supported as well as interrupting the assistant
*/
async processLlmUpdate(ep, data, _callSid) {
try {
this.logger.debug({data, _callSid}, 'TaskLlmOpenAI_S2S:processLlmUpdate');
if (!data.type || ![
'session.update',
'conversation.item.create',
'conversation.item.delete',
'response.cancel'
].includes(data.type)) {
this.logger.info({data}, 'TaskLlmOpenAI_S2S:processLlmUpdate - invalid mid-call request');
}
else {
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)]);
}
} catch (err) {
this.logger.info({err}, 'TaskLlmOpenAI_S2S:processLlmUpdate');
}
}
async _startListening(cs, ep) {
this._registerHandlers(ep);
try {
const args = [ep.uuid, 'session.create', this.host, this.path, this.authType, this.apiKey];
await this._api(ep, args);
} catch (err) {
this.logger.error({err}, 'TaskLlmOpenAI_S2S:_startListening');
this.notifyTaskDone();
}
}
async _sendClientEvent(ep, obj) {
let ok = true;
this.logger.debug({obj}, 'TaskLlmOpenAI_S2S:_sendClientEvent');
try {
const args = [ep.uuid, ClientEvent, JSON.stringify(obj)];
await this._api(ep, args);
} catch (err) {
ok = false;
this.logger.error({err}, 'TaskLlmOpenAI_S2S:_sendClientEvent - Error');
}
return ok;
}
async _sendInitialMessage(ep) {
let obj = {type: 'response.create', response: this.response_create};
if (!await this._sendClientEvent(ep, obj)) {
this.notifyTaskDone();
}
/* send immediate session.update if present */
else if (this.session_update) {
obj = {type: 'session.update', session: this.session_update};
this.logger.debug({obj}, 'TaskLlmOpenAI_S2S:_sendInitialMessage - sending session.update');
if (!await this._sendClientEvent(ep, obj)) {
this.notifyTaskDone();
}
}
}
_registerHandlers(ep) {
this.addCustomEventListener(ep, LlmEvents_OpenAI.Connect, this._onConnect.bind(this, ep));
this.addCustomEventListener(ep, LlmEvents_OpenAI.ConnectFailure, this._onConnectFailure.bind(this, ep));
this.addCustomEventListener(ep, LlmEvents_OpenAI.Disconnect, this._onDisconnect.bind(this, ep));
this.addCustomEventListener(ep, LlmEvents_OpenAI.ServerEvent, this._onServerEvent.bind(this, ep));
}
_unregisterHandlers() {
this.removeCustomEventListeners();
}
_onError(ep, evt) {
this.logger.info({evt}, 'TaskLlmOpenAI_S2S:_onError');
this.notifyTaskDone();
}
_onConnect(ep) {
this.logger.debug('TaskLlmOpenAI_S2S:_onConnect');
this._sendInitialMessage(ep);
}
_onConnectFailure(_ep, evt) {
this.logger.info(evt, 'TaskLlmOpenAI_S2S:_onConnectFailure');
this.results = {completionReason: 'connection failure'};
this.notifyTaskDone();
}
_onDisconnect(_ep, evt) {
this.logger.info(evt, 'TaskLlmOpenAI_S2S:_onConnectFailure');
this.results = {completionReason: 'disconnect from remote end'};
this.notifyTaskDone();
}
async _onServerEvent(ep, evt) {
let endConversation = false;
const type = evt.type;
this.logger.info({evt}, 'TaskLlmOpenAI_S2S:_onServerEvent');
/* check for failures, such as rate limit exceeded, that should terminate the conversation */
if (type === 'response.done' && evt.response.status === 'failed') {
endConversation = true;
this.results = {
completionReason: 'server failure',
error: evt.response.status_details?.error
};
}
/* server errors of some sort */
else if (type === 'error') {
endConversation = true;
this.results = {
completionReason: 'server error',
error: evt.error
};
}
/* tool calls */
else if (type === 'response.output_item.done' && evt.item?.type === 'function_call') {
this.logger.debug({evt}, 'TaskLlmOpenAI_S2S:_onServerEvent - function_call');
if (!this.toolHook) {
this.logger.warn({evt}, 'TaskLlmOpenAI_S2S:_onServerEvent - no toolHook defined!');
}
else {
const {name, call_id} = evt.item;
const args = JSON.parse(evt.item.arguments);
try {
await this.parent.sendToolHook(call_id, {name, args});
} catch (err) {
this.logger.info({err, evt}, 'TaskLlmOpenAI - error calling function');
this.results = {
completionReason: 'client error calling function',
error: err
};
endConversation = true;
}
}
}
/* check whether we should notify on this event */
if (this.includeEvents.length > 0 ? this.includeEvents.includes(type) : !this.excludeEvents.includes(type)) {
this.parent.sendEventHook(evt)
.catch((err) => this.logger.info({err}, 'TaskLlmOpenAI_S2S:_onServerEvent - error sending event hook'));
}
if (endConversation) {
this.logger.info({results: this.results}, 'TaskLlmOpenAI_S2S:_onServerEvent - ending conversation due to error');
this.notifyTaskDone();
}
}
_populateEvents(events) {
if (events.includes('all')) {
/* work by excluding specific events */
const exclude = events
.filter((evt) => evt.startsWith('-'))
.map((evt) => evt.slice(1));
if (exclude.length === 0) this.includeEvents = openai_server_events;
else this.excludeEvents = expandWildcards(exclude);
}
else {
/* work by including specific events */
const include = events
.filter((evt) => !evt.startsWith('-'));
this.includeEvents = expandWildcards(include);
}
this.logger.debug({
includeEvents: this.includeEvents,
excludeEvents: this.excludeEvents
}, 'TaskLlmOpenAI_S2S:_populateEvents');
}
}
module.exports = TaskLlmOpenAI_S2S;

View File

@@ -14,9 +14,6 @@ function makeTask(logger, obj, parent) {
} }
validateVerb(name, data, logger); validateVerb(name, data, logger);
switch (name) { switch (name) {
case TaskName.Answer:
const TaskAnswer = require('./answer');
return new TaskAnswer(logger, data, parent);
case TaskName.SipDecline: case TaskName.SipDecline:
const TaskSipDecline = require('./sip_decline'); const TaskSipDecline = require('./sip_decline');
return new TaskSipDecline(logger, data, parent); return new TaskSipDecline(logger, data, parent);
@@ -44,9 +41,6 @@ function makeTask(logger, obj, parent) {
case TaskName.Dtmf: case TaskName.Dtmf:
const TaskDtmf = require('./dtmf'); const TaskDtmf = require('./dtmf');
return new TaskDtmf(logger, data, parent); return new TaskDtmf(logger, data, parent);
case TaskName.Dub:
const TaskDub = require('./dub');
return new TaskDub(logger, data, parent);
case TaskName.Enqueue: case TaskName.Enqueue:
const TaskEnqueue = require('./enqueue'); const TaskEnqueue = require('./enqueue');
return new TaskEnqueue(logger, data, parent); return new TaskEnqueue(logger, data, parent);
@@ -62,9 +56,6 @@ function makeTask(logger, obj, parent) {
case TaskName.Message: case TaskName.Message:
const TaskMessage = require('./message'); const TaskMessage = require('./message');
return new TaskMessage(logger, data, parent); return new TaskMessage(logger, data, parent);
case TaskName.Llm:
const TaskLlm = require('./llm');
return new TaskLlm(logger, data, parent);
case TaskName.Rasa: case TaskName.Rasa:
const TaskRasa = require('./rasa'); const TaskRasa = require('./rasa');
return new TaskRasa(logger, data, parent); return new TaskRasa(logger, data, parent);

View File

@@ -12,13 +12,11 @@ class TaskRestDial extends Task {
this.from = this.data.from; this.from = this.data.from;
this.callerName = this.data.callerName; this.callerName = this.data.callerName;
this.timeLimit = this.data.timeLimit;
this.fromHost = this.data.fromHost; this.fromHost = this.data.fromHost;
this.to = this.data.to; this.to = this.data.to;
this.call_hook = this.data.call_hook; this.call_hook = this.data.call_hook;
this.timeout = this.data.timeout || 60; this.timeout = this.data.timeout || 60;
this.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook; this.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
this.referHook = this.data.referHook;
this.on('connect', this._onConnect.bind(this)); this.on('connect', this._onConnect.bind(this));
this.on('callStatus', this._onCallStatus.bind(this)); this.on('callStatus', this._onCallStatus.bind(this));
@@ -40,9 +38,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();
@@ -66,10 +64,6 @@ class TaskRestDial extends Task {
this.canCancel = false; this.canCancel = false;
const cs = this.callSession; const cs = this.callSession;
cs.setDialog(dlg); cs.setDialog(dlg);
cs.referHook = this.referHook;
if (this.timeLimit) {
cs.startMaxCallDurationTimer(this.timeLimit);
}
this.logger.debug('TaskRestDial:_onConnect - call connected'); this.logger.debug('TaskRestDial:_onConnect - call connected');
if (this.sipRequestWithinDialogHook) this._initSipRequestWithinDialogHandler(cs, dlg); if (this.sipRequestWithinDialogHook) this._initSipRequestWithinDialogHandler(cs, dlg);
try { try {

View File

@@ -1,7 +1,6 @@
const TtsTask = require('./tts-task'); const Task = require('./task');
const {TaskName, TaskPreconditions} = require('../utils/constants'); const {TaskName, TaskPreconditions} = require('../utils/constants');
const pollySSMLSplit = require('polly-ssml-split'); const pollySSMLSplit = require('polly-ssml-split');
const { SpeechCredentialError } = require('../utils/error');
const breakLengthyTextIfNeeded = (logger, text) => { const breakLengthyTextIfNeeded = (logger, text) => {
const chunkSize = 1000; const chunkSize = 1000;
@@ -30,9 +29,9 @@ const parseTextFromSayString = (text) => {
return text.slice(closingBraceIndex + 1); return text.slice(closingBraceIndex + 1);
}; };
class TaskSay extends TtsTask { class TaskSay extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
super(logger, opts, parentTask); super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
this.text = (Array.isArray(this.data.text) ? this.data.text : [this.data.text]) this.text = (Array.isArray(this.data.text) ? this.data.text : [this.data.text])
@@ -40,6 +39,10 @@ class TaskSay extends TtsTask {
.flat(); .flat();
this.loop = this.data.loop || 1; this.loop = this.data.loop || 1;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
this.synthesizer = this.data.synthesizer || {};
this.disableTtsCache = this.data.disableTtsCache;
this.options = this.synthesizer.options || {};
this.isHandledByPrimaryProvider = true; this.isHandledByPrimaryProvider = true;
} }
@@ -62,41 +65,168 @@ class TaskSay extends TtsTask {
} }
} }
async exec(cs, obj) { async _synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label, preCache = false}) {
try { const {srf, accountSid:account_sid} = cs;
await this.handling(cs, obj); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
this.emit('playDone'); const {writeAlerts, AlertType, stats} = srf.locals;
} catch (error) { const {synthAudio} = srf.locals.dbHelpers;
if (error instanceof SpeechCredentialError) { const engine = this.synthesizer.engine || 'standard';
// if say failed due to speech credentials, alarm is writtern and error notification is sent const salt = cs.callSid;
// finished this say to move to next task.
this.logger.info('Say failed due to SpeechCredentialError, finished!'); let credentials = cs.getSpeechCredentials(vendor, 'tts', label);
this.emit('playDone'); /* parse Nuance voices into name and model */
return; let model;
if (vendor === 'nuance' && voice) {
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
if (arr) {
voice = arr[1];
model = arr[2];
} }
throw error; } else if (vendor === 'deepgram') {
model = voice;
} }
/* 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;
} 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;
}
ep.set({
tts_engine: vendor,
tts_voice: voice,
cache_speech_handles: 1,
}).catch((err) => this.logger.info({err}, 'Error setting tts_engine on endpoint'));
if (!preCache) this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
try {
if (!credentials) {
writeAlerts({
account_sid,
alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
this.notifyError({
msg: 'TTS error',
details:`No speech credentials provisioned for selected vendor ${vendor}`
});
throw new Error('no provisioned speech credentials for TTS');
}
// synthesize all of the text elements
let lastUpdated = false;
/* produce an audio segment from the provided text */
const generateAudio = async(text) => {
if (this.killed) return;
if (text.startsWith('silence_stream://')) return text;
/* otel: trace time for tts */
if (!preCache) {
const {span} = this.startChildSpan('tts-generation', {
'tts.vendor': vendor,
'tts.language': language,
'tts.voice': voice
});
this.otelSpan = span;
}
try {
const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
account_sid,
text,
vendor,
language,
voice,
engine,
model,
salt,
credentials,
options: this.options,
disableTtsCache : this.disableTtsCache,
preCache
});
if (!filePath.startsWith('say:')) {
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
if (filePath) cs.trackTmpFile(filePath);
if (this.otelSpan) {
this.otelSpan.setAttributes({'tts.cached': servedFromCache});
this.otelSpan.end();
this.otelSpan = null;
}
if (!servedFromCache && !lastUpdated) {
lastUpdated = true;
updateSpeechCredentialLastUsed(credentials.speech_credential_sid).catch(() => {/* logged error */});
}
if (!servedFromCache && rtt && !preCache) {
this.notifyStatus({
event: 'synthesized-audio',
vendor,
language,
characters: text.length,
elapsedTime: rtt
});
}
}
else {
this.logger.debug('a streaming tts api will be used');
const modifiedPath = filePath.replace('say:{', `say:{session-uuid=${ep.uuid},`);
return modifiedPath;
}
return filePath;
} catch (err) {
this.logger.info({err}, 'Error synthesizing tts');
if (this.otelSpan) this.otelSpan.end();
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.TTS_FAILURE,
vendor,
detail: err.message
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
this.notifyError({msg: 'TTS error', details: err.message || err});
throw err;
}
};
const arr = this.text.map((t) => (this._validateURL(t) ? t : generateAudio(t)));
return (await Promise.all(arr)).filter((fp) => fp && fp.length);
} catch (err) {
this.logger.info(err, 'TaskSay:exec error');
throw err;
}
} }
async handling(cs, {ep}) { async exec(cs, {ep}) {
const {srf, accountSid:account_sid, callSid:target_sid} = cs; const {srf, accountSid:account_sid} = cs;
const {writeAlerts, AlertType} = srf.locals; const {writeAlerts, AlertType} = srf.locals;
const {addFileToCache} = srf.locals.dbHelpers; const {addFileToCache} = srf.locals.dbHelpers;
const engine = this.synthesizer.engine || cs.synthesizer?.engine || 'neural'; const engine = this.synthesizer.engine || 'standard';
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
let vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ? const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
this.synthesizer.vendor : this.synthesizer.vendor :
cs.speechSynthesisVendor; cs.speechSynthesisVendor;
let language = this.synthesizer.language && this.synthesizer.language !== 'default' ? const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
this.synthesizer.language : this.synthesizer.language :
cs.speechSynthesisLanguage ; cs.speechSynthesisLanguage ;
let voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ? const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
this.synthesizer.voice : this.synthesizer.voice :
cs.speechSynthesisVoice; cs.speechSynthesisVoice;
let label = this.taskInlcudeSynthesizer ? this.synthesizer.label : cs.speechSynthesisLabel; const label = this.synthesizer.label && this.synthesizer.label !== 'default' ?
this.synthesizer.label :
cs.speechSynthesisLabel;
const fallbackVendor = this.synthesizer.fallbackVendor && this.synthesizer.fallbackVendor !== 'default' ? const fallbackVendor = this.synthesizer.fallbackVendor && this.synthesizer.fallbackVendor !== 'default' ?
this.synthesizer.fallbackVendor : this.synthesizer.fallbackVendor :
@@ -107,22 +237,16 @@ class TaskSay extends TtsTask {
const fallbackVoice = this.synthesizer.fallbackVoice && this.synthesizer.fallbackVoice !== 'default' ? const fallbackVoice = this.synthesizer.fallbackVoice && this.synthesizer.fallbackVoice !== 'default' ?
this.synthesizer.fallbackVoice : this.synthesizer.fallbackVoice :
cs.fallbackSpeechSynthesisVoice; cs.fallbackSpeechSynthesisVoice;
const fallbackLabel = this.taskInlcudeSynthesizer ? const fallbackLabel = this.synthesizer.fallbackLabel && this.synthesizer.fallbackLabel !== 'default' ?
this.synthesizer.fallbackLabel : cs.fallbackSpeechSynthesisLabel; this.synthesizer.fallbackLabel :
cs.fallbackSpeechSynthesisLabel;
if (cs.hasFallbackTts) { let filepath;
vendor = fallbackVendor; try {
language = fallbackLanguage; filepath = await this._synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label});
voice = fallbackVoice; } catch (error) {
label = fallbackLabel; if (fallbackVendor && this.isHandledByPrimaryProvider) {
}
const startFallback = async(error) => {
if (fallbackVendor && this.isHandledByPrimaryProvider && !cs.hasFallbackTts) {
this.notifyError(
{ msg: 'TTS error', details:`TTS vendor ${vendor} error: ${error}`, failover: 'in progress'});
this.isHandledByPrimaryProvider = false; this.isHandledByPrimaryProvider = false;
cs.hasFallbackTts = true;
this.logger.info(`Synthesize error, fallback to ${fallbackVendor}`); this.logger.info(`Synthesize error, fallback to ${fallbackVendor}`);
filepath = await this._synthesizeWithSpecificVendor(cs, ep, filepath = await this._synthesizeWithSpecificVendor(cs, ep,
{ {
@@ -132,16 +256,8 @@ class TaskSay extends TtsTask {
label: fallbackLabel label: fallbackLabel
}); });
} else { } else {
this.notifyError( throw error;
{ msg: 'TTS error', details:`TTS vendor ${vendor} error: ${error}`, failover: 'not available'});
throw new SpeechCredentialError(error.message);
} }
};
let filepath;
try {
filepath = await this._synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label});
} catch (error) {
await startFallback(error);
} }
this.notifyStatus({event: 'start-playback'}); this.notifyStatus({event: 'start-playback'});
@@ -153,35 +269,31 @@ class TaskSay extends TtsTask {
await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]); await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]);
} }
else { else {
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}, 'got playback-start');
if (this.otelSpan) { if (this.otelSpan) {
this._addStreamingTtsAttributes(this.otelSpan, evt); this._addStreamingTtsAttributes(this.otelSpan, evt);
this.otelSpan.end(); this.otelSpan.end();
this.otelSpan = null; this.otelSpan = null;
if (evt.variable_tts_cache_filename) { if (evt.variable_tts_cache_filename) cs.trackTmpFile(evt.variable_tts_cache_filename);
cs.trackTmpFile(evt.variable_tts_cache_filename);
}
} }
}); });
ep.once('playback-stop', (evt) => { ep.once('playback-stop', (evt) => {
this.logger.debug({evt}, 'Say got playback-stop'); this.logger.debug({evt}, 'got playback-stop');
if (evt.variable_tts_error) { if (evt.variable_tts_error) {
writeAlerts({ writeAlerts({
account_sid, account_sid,
alert_type: AlertType.TTS_FAILURE, alert_type: AlertType.TTS_FAILURE,
vendor, vendor,
detail: evt.variable_tts_error, detail: evt.variable_tts_error
target_sid
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
} }
if (evt.variable_tts_cache_filename && !this.killed) { if (evt.variable_tts_cache_filename) {
const text = parseTextFromSayString(this.text[segment]); const text = parseTextFromSayString(this.text[segment]);
addFileToCache(evt.variable_tts_cache_filename, { addFileToCache(evt.variable_tts_cache_filename, {
account_sid, account_sid,
@@ -192,45 +304,18 @@ class TaskSay extends TtsTask {
text text
}).catch((err) => this.logger.info({err}, 'Error adding file to cache')); }).catch((err) => this.logger.info({err}, 'Error adding file to cache'));
} }
if (this._playResolve) {
evt.variable_tts_error ? this._playReject(new Error(evt.variable_tts_error)) : this._playResolve();
}
}); });
// wait for playback-stop event received to confirm if the playback is successful await ep.play(filepath[segment]);
this._playPromise = new Promise((resolve, reject) => {
this._playResolve = resolve;
this._playReject = reject;
});
const r = await ep.play(filepath[segment]);
this.logger.debug({r}, 'Say:exec play result');
try {
// wait for playback-stop event received to confirm if the playback is successful
await this._playPromise;
} catch (err) {
try {
await startFallback(err);
continue;
} catch (err) {
this.logger.info({err}, 'Error waiting for playback-stop event');
throw err;
}
} finally {
this._playPromise = null;
this._playResolve = null;
this._playReject = null;
}
if (filepath[segment].startsWith('say:{')) { if (filepath[segment].startsWith('say:{')) {
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]); const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
if (arr) this.logger.debug(`Say:exec complete playing streaming tts request: ${arr[1].substring(0, 64)}..`); if (arr) this.logger.debug(`Say:exec complete playing streaming tts request: ${arr[1].substring(0, 64)}..`);
} else {
// This log will print spech credentials in say command for tts stream mode
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
} }
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
} }
segment++; segment++;
} }
} }
this.emit('playDone');
} }
async kill(cs) { async kill(cs) {
@@ -247,11 +332,6 @@ class TaskSay extends TtsTask {
} }
this.ep.removeAllListeners('playback-start'); this.ep.removeAllListeners('playback-start');
this.ep.removeAllListeners('playback-stop'); this.ep.removeAllListeners('playback-stop');
// if we are waiting for playback-stop event, resolve the promise
if (this._playResolve) {
this._playResolve();
this._playResolve = null;
}
} }
} }
@@ -261,10 +341,6 @@ class TaskSay extends TtsTask {
if (key.startsWith('variable_tts_')) { if (key.startsWith('variable_tts_')) {
let newKey = key.substring('variable_tts_'.length) let newKey = key.substring('variable_tts_'.length)
.replace('whisper_', 'whisper.') .replace('whisper_', 'whisper.')
.replace('deepgram_', 'deepgram.')
.replace('playht_', 'playht.')
.replace('rimelabs_', 'rimelabs.')
.replace('verbio_', 'verbio.')
.replace('elevenlabs_', 'elevenlabs.'); .replace('elevenlabs_', 'elevenlabs.');
if (spanMapping[newKey]) newKey = spanMapping[newKey]; if (spanMapping[newKey]) newKey = spanMapping[newKey];
attrs[newKey] = value; attrs[newKey] = value;
@@ -276,9 +352,6 @@ class TaskSay extends TtsTask {
} }
const spanMapping = { const spanMapping = {
// IMPORTANT!!! JAMBONZ WEBAPP WILL SHOW TEXT PERFECTLY IF THE SPAN NAME IS SMALLER OR EQUAL 25 CHARACTERS.
// EX: whisper.ratelim_reqs has length 20 <= 25 which is perfect
// Elevenlabs
'elevenlabs.reported_latency_ms': 'elevenlabs.latency_ms', 'elevenlabs.reported_latency_ms': 'elevenlabs.latency_ms',
'elevenlabs.request_id': 'elevenlabs.req_id', 'elevenlabs.request_id': 'elevenlabs.req_id',
'elevenlabs.history_item_id': 'elevenlabs.item_id', 'elevenlabs.history_item_id': 'elevenlabs.item_id',
@@ -286,37 +359,11 @@ const spanMapping = {
'elevenlabs.name_lookup_time_ms': 'name_lookup_ms', 'elevenlabs.name_lookup_time_ms': 'name_lookup_ms',
'elevenlabs.connect_time_ms': 'connect_ms', 'elevenlabs.connect_time_ms': 'connect_ms',
'elevenlabs.final_response_time_ms': 'final_response_ms', 'elevenlabs.final_response_time_ms': 'final_response_ms',
// Whisper
'whisper.reported_latency_ms': 'whisper.latency_ms', 'whisper.reported_latency_ms': 'whisper.latency_ms',
'whisper.request_id': 'whisper.req_id', 'whisper.request_id': 'whisper.req_id',
'whisper.reported_organization': 'whisper.organization',
'whisper.reported_ratelimit_requests': 'whisper.ratelimit',
'whisper.reported_ratelimit_remaining_requests': 'whisper.ratelimit_remain',
'whisper.reported_ratelimit_reset_requests': 'whisper.ratelimit_reset',
'whisper.name_lookup_time_ms': 'name_lookup_ms', 'whisper.name_lookup_time_ms': 'name_lookup_ms',
'whisper.connect_time_ms': 'connect_ms', 'whisper.connect_time_ms': 'connect_ms',
'whisper.final_response_time_ms': 'final_response_ms', 'whisper.final_response_time_ms': 'final_response_ms',
// Deepgram
'deepgram.request_id': 'deepgram.req_id',
'deepgram.reported_model_name': 'deepgram.model_name',
'deepgram.reported_model_uuid': 'deepgram.model_uuid',
'deepgram.reported_char_count': 'deepgram.char_count',
'deepgram.name_lookup_time_ms': 'name_lookup_ms',
'deepgram.connect_time_ms': 'connect_ms',
'deepgram.final_response_time_ms': 'final_response_ms',
// Playht
'playht.request_id': 'playht.req_id',
'playht.name_lookup_time_ms': 'name_lookup_ms',
'playht.connect_time_ms': 'connect_ms',
'playht.final_response_time_ms': 'final_response_ms',
// Rimelabs
'rimelabs.name_lookup_time_ms': 'name_lookup_ms',
'rimelabs.connect_time_ms': 'connect_ms',
'rimelabs.final_response_time_ms': 'final_response_ms',
// verbio
'verbio.name_lookup_time_ms': 'name_lookup_ms',
'verbio.connect_time_ms': 'connect_ms',
'verbio.final_response_time_ms': 'final_response_ms',
}; };
module.exports = TaskSay; module.exports = TaskSay;

View File

@@ -94,10 +94,7 @@ class TaskSipRefer extends Task {
} }
if (status >= 200) { if (status >= 200) {
this.referSpan.setAttributes({'refer.finalNotify': status}); this.referSpan.setAttributes({'refer.finalNotify': status});
await this.performAction({refer_status: 202, final_referred_call_status: status}) await this.performAction({refer_status: 202, final_referred_call_status: status});
.catch((err) => {
this.logger.error(err, 'TaskSipRefer:exec - error performing action finalNotify');
});
this.notifyTaskDone(); this.notifyTaskDone();
} }
} }

View File

@@ -2,8 +2,6 @@ const Task = require('./task');
const assert = require('assert'); const assert = require('assert');
const crypto = require('crypto'); const crypto = require('crypto');
const { TaskPreconditions, CobaltTranscriptionEvents } = require('../utils/constants'); const { TaskPreconditions, CobaltTranscriptionEvents } = require('../utils/constants');
const { SpeechCredentialError } = require('../utils/error');
const {JAMBONES_AWS_TRANSCRIBE_USE_GRPC} = require('../config');
class SttTask extends Task { class SttTask extends Task {
@@ -18,22 +16,14 @@ class SttTask extends Task {
normalizeTranscription, normalizeTranscription,
setSpeechCredentialsAtRuntime, setSpeechCredentialsAtRuntime,
compileSonioxTranscripts, compileSonioxTranscripts,
consolidateTranscripts, consolidateTranscripts
updateSpeechmaticsPayload
} = require('../utils/transcription-utils')(logger); } = require('../utils/transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt; this.setChannelVarsForStt = setChannelVarsForStt;
this.normalizeTranscription = normalizeTranscription; this.normalizeTranscription = normalizeTranscription;
this.compileSonioxTranscripts = compileSonioxTranscripts; this.compileSonioxTranscripts = compileSonioxTranscripts;
this.consolidateTranscripts = consolidateTranscripts; this.consolidateTranscripts = consolidateTranscripts;
this.updateSpeechmaticsPayload = updateSpeechmaticsPayload;
this.eventHandlers = []; this.eventHandlers = [];
this.isHandledByPrimaryProvider = true; this.isHandledByPrimaryProvider = true;
/**
* Task use taskIncludeRecognizer to identify
* if taskIncludeRecognizer === true, use label from verb.recognizer, even it's empty
* if taskIncludeRecognizer === false, use label from application.recognizer
*/
this.taskIncludeRecognizer = !!this.data.recognizer;
if (this.data.recognizer) { if (this.data.recognizer) {
const recognizer = this.data.recognizer; const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor; this.vendor = recognizer.vendor;
@@ -43,7 +33,7 @@ class SttTask extends Task {
//fallback //fallback
this.fallbackVendor = recognizer.fallbackVendor || 'default'; this.fallbackVendor = recognizer.fallbackVendor || 'default';
this.fallbackLanguage = recognizer.fallbackLanguage || 'default'; this.fallbackLanguage = recognizer.fallbackLanguage || 'default';
this.fallbackLabel = recognizer.fallbackLabel; this.fallbackLabel = recognizer.fallbackLabel || 'default';
/* let credentials be supplied in the recognizer object at runtime */ /* let credentials be supplied in the recognizer object at runtime */
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer); this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
@@ -66,20 +56,24 @@ class SttTask extends Task {
super.exec(cs); super.exec(cs);
this.ep = ep; this.ep = ep;
this.ep2 = ep2; this.ep2 = ep2;
// copy all value from config verb to this object.
// use session preferences if we don't have specific verb-level settings.
if (cs.recognizer) { if (cs.recognizer) {
for (const k in cs.recognizer) { for (const k in cs.recognizer) {
const newValue = this.data.recognizer && this.data.recognizer[k] !== undefined ? if (Array.isArray(this.data.recognizer[k]) ||
this.data.recognizer[k] : Array.isArray(cs.recognizer[k])) {
cs.recognizer[k]; this.data.recognizer[k] = [
...this.data.recognizer[k],
if (Array.isArray(newValue)) { ...cs.recognizer[k]
this.data.recognizer[k] = [...(this.data.recognizer[k] || []), ...cs.recognizer[k]]; ];
} else if (typeof newValue === 'object' && newValue !== null) { } else if (typeof this.data.recognizer[k] === 'object' ||
this.data.recognizer[k] = { ...(this.data.recognizer[k] || {}), ...cs.recognizer[k] }; typeof cs.recognizer[k] === 'object'
) {
this.data.recognizer[k] = {
...this.data.recognizer[k],
...cs.recognizer[k]
};
} else { } else {
this.data.recognizer[k] = newValue; this.data.recognizer[k] = cs.recognizer[k] || this.data.recognizer[k];
} }
} }
} }
@@ -91,7 +85,7 @@ class SttTask extends Task {
this.language = cs.speechRecognizerLanguage; this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language; if (this.data.recognizer) this.data.recognizer.language = this.language;
} }
if (!this.taskIncludeRecognizer) { if ('default' === this.label || !this.label) {
this.label = cs.speechRecognizerLabel; this.label = cs.speechRecognizerLabel;
if (this.data.recognizer) this.data.recognizer.label = this.label; if (this.data.recognizer) this.data.recognizer.label = this.label;
} }
@@ -104,22 +98,10 @@ class SttTask extends Task {
this.fallbackLanguage = cs.fallbackSpeechRecognizerLanguage; this.fallbackLanguage = cs.fallbackSpeechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.fallbackLanguage = this.fallbackLanguage; if (this.data.recognizer) this.data.recognizer.fallbackLanguage = this.fallbackLanguage;
} }
if (!this.taskIncludeRecognizer) { if ('default' === this.fallbackLabel || !this.fallbackLabel) {
this.fallbackLabel = cs.fallbackSpeechRecognizerLabel; this.fallbackLabel = cs.fallbackSpeechRecognizerLabel;
if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel; if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel;
} }
if (cs.hasFallbackAsr) {
if (this.taskIncludeRecognizer) {
// reset fallback ASR from previous run if this verb contains data.recognizer.
cs.hasFallbackAsr = false;
} else {
this.logger.debug('Call session has fallback to 2nd ASR, use 2nd recognizer configuration');
this.vendor = this.fallbackVendor;
this.language = this.fallbackLanguage;
this.label = this.fallbackLabel;
}
}
if (!this.data.recognizer.vendor) { if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor; this.data.recognizer.vendor = this.vendor;
} }
@@ -137,19 +119,9 @@ class SttTask extends Task {
try { try {
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label); this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
} catch (error) { } catch (error) {
if (this.canFallback) { if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
this.notifyError( await this._fallback();
{
msg: 'ASR error', details:`Invalid vendor ${this.vendor}, Error: ${error}`,
failover: 'in progress'
});
await this._initFallback();
} else { } else {
this.notifyError(
{
msg: 'ASR error', details:`Invalid vendor ${this.vendor}, Error: ${error}`,
failover: 'not available'
});
throw error; throw error;
} }
} }
@@ -181,7 +153,7 @@ class SttTask extends Task {
} }
async _initSpeechCredentials(cs, vendor, label) { async _initSpeechCredentials(cs, vendor, label) {
const {getNuanceAccessToken, getIbmAccessToken, getAwsAuthToken, getVerbioAccessToken} = cs.srf.locals.dbHelpers; const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
let credentials = cs.getSpeechCredentials(vendor, 'stt', label); let credentials = cs.getSpeechCredentials(vendor, 'stt', label);
if (!credentials) { if (!credentials) {
@@ -190,11 +162,15 @@ class SttTask extends Task {
writeAlerts({ writeAlerts({
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED, alert_type: AlertType.STT_NOT_PROVISIONED,
vendor, vendor
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
// the ASR might have fallback configuration, should not done task here. // Notify application that STT vender is wrong.
throw new SpeechCredentialError(`No speech-to-text service credentials for ${vendor} have been configured`); this.notifyError({
msg: 'ASR error',
details: `No speech-to-text service credentials for ${vendor} have been configured`
});
this.notifyTaskDone();
throw new Error(`No speech-to-text service credentials for ${vendor} have been configured`);
} }
if (vendor === 'nuance' && credentials.client_id) { if (vendor === 'nuance' && credentials.client_id) {
@@ -210,54 +186,21 @@ class SttTask extends Task {
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key); const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
this.logger.debug({stt_api_key}, `got ibm access token ${servedFromCache ? 'from cache' : ''}`); this.logger.debug({stt_api_key}, `got ibm access token ${servedFromCache ? 'from cache' : ''}`);
credentials = {...credentials, access_token, stt_region}; credentials = {...credentials, access_token, stt_region};
} else if (['aws', 'polly'].includes(vendor) && credentials.roleArn) {
/* get aws access token */
const {roleArn, region} = credentials;
const {accessKeyId, secretAccessKey, sessionToken, servedFromCache} =
await getAwsAuthToken({
region,
roleArn
});
this.logger.debug({roleArn}, `(roleArn) got aws access token ${servedFromCache ? 'from cache' : ''}`);
credentials = {...credentials, accessKeyId, secretAccessKey, sessionToken};
} }
else if (vendor === 'verbio' && credentials.client_id && credentials.client_secret) {
const {access_token, servedFromCache} = await getVerbioAccessToken(credentials);
this.logger.debug({client_id: credentials.client_id},
`got verbio access token ${servedFromCache ? 'from cache' : ''}`);
credentials.access_token = access_token;
}
else if (vendor == 'aws' && !JAMBONES_AWS_TRANSCRIBE_USE_GRPC) {
/* get AWS access token */
const {accessKeyId, secretAccessKey, securityToken, region } = credentials;
if (!securityToken) {
const { servedFromCache, ...newCredentials} = await getAwsAuthToken({accessKeyId, secretAccessKey, region});
this.logger.debug({newCredentials}, `got aws security token ${servedFromCache ? 'from cache' : ''}`);
credentials = {...newCredentials, region};
}
}
return credentials; return credentials;
} }
get canFallback() { async _fallback() {
return this.fallbackVendor && this.isHandledByPrimaryProvider && !this.cs.hasFallbackAsr;
}
async _initFallback() {
assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration'); assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration');
this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
this.isHandledByPrimaryProvider = false; this.isHandledByPrimaryProvider = false;
this.cs.hasFallbackAsr = true; this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
this.vendor = this.cs.fallbackSpeechRecognizerVendor = this.fallbackVendor; this.vendor = this.fallbackVendor;
this.language = this.cs.fallbackSpeechRecognizerLanguage = this.fallbackLanguage; this.language = this.fallbackLanguage;
this.label = this.cs.fallbackSpeechRecognizerLabel = this.fallbackLabel; this.label = this.fallbackLabel;
this.data.recognizer.vendor = this.vendor; this.data.recognizer.vendor = this.vendor;
this.data.recognizer.language = this.language; this.data.recognizer.language = this.language;
this.data.recognizer.label = this.label; this.data.recognizer.label = this.label;
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label); this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
// cleanup previous listener from previous vendor
this.removeCustomEventListeners();
} }
async compileHintsForCobalt(ep, hostport, model, token, hints) { async compileHintsForCobalt(ep, hostport, model, token, hints) {
@@ -301,20 +244,6 @@ class SttTask extends Task {
_doContinuousAsrWithDeepgram(asrTimeout) { _doContinuousAsrWithDeepgram(asrTimeout) {
/* deepgram has an utterance_end_ms property that simplifies things */ /* deepgram has an utterance_end_ms property that simplifies things */
assert(this.vendor === 'deepgram'); assert(this.vendor === 'deepgram');
if (asrTimeout < 1000) {
this.notifyError({
msg: 'ASR error',
details:`asrTimeout ${asrTimeout} is too short for deepgram; setting it to 1000ms`
});
asrTimeout = 1000;
}
else if (asrTimeout > 5000) {
this.notifyError({
msg: 'ASR error',
details:`asrTimeout ${asrTimeout} is too long for deepgram; setting it to 5000ms`
});
asrTimeout = 5000;
}
this.logger.debug(`_doContinuousAsrWithDeepgram - setting utterance_end_ms to ${asrTimeout}`); this.logger.debug(`_doContinuousAsrWithDeepgram - setting utterance_end_ms to ${asrTimeout}`);
const dgOptions = this.data.recognizer.deepgramOptions = this.data.recognizer.deepgramOptions || {}; const dgOptions = this.data.recognizer.deepgramOptions = this.data.recognizer.deepgramOptions || {};
dgOptions.utteranceEndMs = dgOptions.utteranceEndMs || asrTimeout; dgOptions.utteranceEndMs = dgOptions.utteranceEndMs || asrTimeout;
@@ -333,8 +262,8 @@ class SttTask extends Task {
message: 'STT failure reported by vendor', message: 'STT failure reported by vendor',
detail: evt.error, detail: evt.error,
vendor: this.vendor, vendor: this.vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`)); }).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${evt.error}`});
} }
_onVendorConnectFailure(cs, _ep, evt) { _onVendorConnectFailure(cs, _ep, evt) {
@@ -346,8 +275,8 @@ class SttTask extends Task {
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`, message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
vendor: this.vendor, vendor: this.vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`)); }).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${reason}`});
} }
} }

View File

@@ -45,10 +45,6 @@ class Task extends Emitter {
return this.name; return this.name;
} }
set disableTracing(val) {
this._disableTracing = val;
}
toJSON() { toJSON() {
return this.data; return this.data;
} }
@@ -181,16 +177,15 @@ class Task extends Emitter {
// If actionHook delay action is configured, and ws application have not responded yet any verb for actionHook // If actionHook delay action is configured, and ws application have not responded yet any verb for actionHook
// We have to transfer the task to call-session to await on next ws command verbs, and also run action Hook // We have to transfer the task to call-session to await on next ws command verbs, and also run action Hook
// delay actions // delay actions
//if (this.hookDelayActionOpts) { if (this.hookDelayActionOpts) {
// this.emit('ActionHookDelayActionOptions', this.hookDelayActionOpts); this.emit('ActionHookDelayActionOptions', this.hookDelayActionOpts);
//} }
} }
if (expectResponse && json && Array.isArray(json)) { if (expectResponse && json && Array.isArray(json)) {
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata)); const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) { if (tasks && tasks.length > 0) {
this.callSession.replaceApplication(tasks); this.callSession.replaceApplication(tasks);
return true;
} }
} }
} catch (err) { } catch (err) {
@@ -198,7 +193,6 @@ class Task extends Emitter {
span.end(); span.end();
throw err; throw err;
} }
return false;
} }
} }
@@ -278,7 +272,6 @@ class Task extends Emitter {
delete obj.requestor; delete obj.requestor;
delete obj.notifier; delete obj.notifier;
obj.tasks = cs.getRemainingTaskData(); obj.tasks = cs.getRemainingTaskData();
obj.callInfo = cs.callInfo.toJSON();
if (opts && obj.tasks.length > 0) { if (opts && obj.tasks.length > 0) {
const key = Object.keys(obj.tasks[0])[0]; const key = Object.keys(obj.tasks[0])[0];
Object.assign(obj.tasks[0][key], {_: opts}); Object.assign(obj.tasks[0][key], {_: opts});

View File

@@ -12,13 +12,10 @@ const {
NvidiaTranscriptionEvents, NvidiaTranscriptionEvents,
JambonzTranscriptionEvents, JambonzTranscriptionEvents,
TranscribeStatus, TranscribeStatus,
AssemblyAiTranscriptionEvents, AssemblyAiTranscriptionEvents
VerbioTranscriptionEvents,
SpeechmaticsTranscriptionEvents
} = require('../utils/constants.json'); } = require('../utils/constants.json');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const { normalizeJambones } = require('@jambonz/verb-specifications');
const SttTask = require('./stt-task'); const SttTask = require('./stt-task');
const { SpeechCredentialError } = require('../utils/error');
const STT_LISTEN_SPAN_NAME = 'stt-listen'; const STT_LISTEN_SPAN_NAME = 'stt-listen';
@@ -35,22 +32,8 @@ class TaskTranscribe extends SttTask {
} }
/* for nested transcribe in dial, unless the app explicitly says so we want to transcribe both legs */ /* for nested transcribe in dial, unless the app explicitly says so we want to transcribe both legs */
if (this.parentTask?.name === TaskName.Dial) { if (this.parentTask?.name === TaskName.Dial && this.separateRecognitionPerChannel !== false) {
if (this.data.channel === 1 || this.data.channel === 2) { this.separateRecognitionPerChannel = true;
/* transcribe only the channel specified */
this.separateRecognitionPerChannel = false;
this.channel = this.data.channel;
logger.debug(`TaskTranscribe: transcribing only channel ${this.channel} in the Dial verb`);
}
else if (this.separateRecognitionPerChannel !== false) {
this.separateRecognitionPerChannel = true;
}
else {
this.channel = 1;
}
}
else {
this.channel = 1;
} }
this.childSpan = [null, null]; this.childSpan = [null, null];
@@ -61,35 +44,14 @@ class TaskTranscribe extends SttTask {
this.isContinuousAsr = true; this.isContinuousAsr = true;
} }
/* buffer speech for continuous asr */ /* buffer speech for continuous asr */
this._bufferedTranscripts = [ [], [] ]; // for channel 1 and 2 this._bufferedTranscripts = [];
this.bugname_prefix = 'transcribe_'; this.bugname_prefix = 'transcribe_';
this.paused = false; this.paused = false;
} }
get name() { return TaskName.Transcribe; } get name() { return TaskName.Transcribe; }
get transcribing1() { async exec(cs, {ep, ep2}) {
return this.channel === 1 || this.separateRecognitionPerChannel;
}
get transcribing2() {
return this.channel === 2 || this.separateRecognitionPerChannel && this.ep2;
}
async exec(cs, obj) {
try {
await this.handling(cs, obj);
} catch (error) {
if (error instanceof SpeechCredentialError) {
this.logger.info('Transcribe failed due to SpeechCredentialError, finished!');
this.notifyTaskDone();
return;
}
throw error;
}
}
async handling(cs, {ep, ep2}) {
await super.exec(cs, {ep, ep2}); await super.exec(cs, {ep, ep2});
if (this.data.recognizer.vendor === 'nuance') { if (this.data.recognizer.vendor === 'nuance') {
@@ -111,30 +73,25 @@ class TaskTranscribe extends SttTask {
} }
try { try {
if (this.transcribing1) { await this._startTranscribing(cs, ep, 1);
await this._startTranscribing(cs, ep, 1); if (this.separateRecognitionPerChannel && ep2) {
}
if (this.transcribing2) {
await this._startTranscribing(cs, ep2, 2); await this._startTranscribing(cs, ep2, 2);
} }
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid) updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */}); .catch(() => {/*already logged error */});
await this.awaitTaskDone();
} catch (err) { } catch (err) {
if (!(await this._startFallback(cs, ep, {error: err}))) { this.logger.info(err, 'TaskTranscribe:exec - error');
this.logger.info(err, 'TaskTranscribe:exec - error'); this.parentTask && this.parentTask.emit('error', err);
this.parentTask && this.parentTask.emit('error', err);
this.removeCustomEventListeners();
return;
}
} }
await this.awaitTaskDone();
this.removeCustomEventListeners(); this.removeCustomEventListeners();
} }
async _stopTranscription() { async _stopTranscription() {
let stopTranscription = false; let stopTranscription = false;
if (this.transcribing1 && this.ep?.connected) { if (this.ep?.connected) {
stopTranscription = true; stopTranscription = true;
this.ep.stopTranscription({ this.ep.stopTranscription({
vendor: this.vendor, vendor: this.vendor,
@@ -142,7 +99,7 @@ class TaskTranscribe extends SttTask {
}) })
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill')); .catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
} }
if (this.transcribing2 && this.ep2?.connected) { if (this.separateRecognitionPerChannel && this.ep2 && this.ep2.connected) {
stopTranscription = true; stopTranscription = true;
this.ep2.stopTranscription({vendor: this.vendor, bugname: this.bugname}) this.ep2.stopTranscription({vendor: this.vendor, bugname: this.bugname})
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill')); .catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
@@ -171,8 +128,10 @@ class TaskTranscribe extends SttTask {
break; break;
case TranscribeStatus.Resume: case TranscribeStatus.Resume:
this.paused = false; this.paused = false;
if (this.transcribing1) await this._startTranscribing(this.cs, this.ep, 1); await this._startTranscribing(this.cs, this.ep, 1);
if (this.transcribing2) await this._startTranscribing(this.cs, this.ep2, 2); if (this.separateRecognitionPerChannel && this.ep2) {
await this._startTranscribing(this.cs, this.ep2, 2);
}
break; break;
} }
} }
@@ -213,8 +172,8 @@ class TaskTranscribe extends SttTask {
this.bugname = `${this.bugname_prefix}azure_transcribe`; this.bugname = `${this.bugname_prefix}azure_transcribe`;
this.addCustomEventListener(ep, AzureTranscriptionEvents.Transcription, this.addCustomEventListener(ep, AzureTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
//this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected, this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected,
// this._onNoAudio.bind(this, cs, ep, channel)); this._onNoAudio.bind(this, cs, ep, channel));
break; break;
case 'nuance': case 'nuance':
this.bugname = `${this.bugname_prefix}nuance_transcribe`; this.bugname = `${this.bugname_prefix}nuance_transcribe`;
@@ -231,7 +190,7 @@ class TaskTranscribe extends SttTask {
this._onVendorConnectFailure.bind(this, cs, ep, channel)); this._onVendorConnectFailure.bind(this, cs, ep, channel));
/* if app sets deepgramOptions.utteranceEndMs they essentially want continuous asr */ /* if app sets deepgramOptions.utteranceEndMs they essentially want continuous asr */
//if (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true; if (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true;
break; break;
case 'soniox': case 'soniox':
@@ -239,13 +198,6 @@ class TaskTranscribe extends SttTask {
this.addCustomEventListener(ep, SonioxTranscriptionEvents.Transcription, this.addCustomEventListener(ep, SonioxTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
break; break;
case 'verbio':
this.bugname = `${this.bugname_prefix}verbio_transcribe`;
this.addCustomEventListener(
ep, VerbioTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
break;
case 'cobalt': case 'cobalt':
this.bugname = `${this.bugname_prefix}cobalt_transcribe`; this.bugname = `${this.bugname_prefix}cobalt_transcribe`;
this.addCustomEventListener(ep, CobaltTranscriptionEvents.Transcription, this.addCustomEventListener(ep, CobaltTranscriptionEvents.Transcription,
@@ -303,22 +255,6 @@ class TaskTranscribe extends SttTask {
this._onVendorConnectFailure.bind(this, cs, ep, channel)); this._onVendorConnectFailure.bind(this, cs, ep, channel));
break; break;
case 'speechmatics':
this.bugname = `${this.bugname_prefix}speechmatics_transcribe`;
this.addCustomEventListener(
ep, SpeechmaticsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.Info,
this._onSpeechmaticsInfo.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.RecognitionStarted,
this._onSpeechmaticsRecognitionStarted.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.Connect,
this._onVendorConnect.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.ConnectFailure,
this._onVendorConnectFailure.bind(this, cs, ep));
this.addCustomEventListener(ep, SpeechmaticsTranscriptionEvents.Error,
this._onSpeechmaticsError.bind(this, cs, ep));
break;
default: default:
if (this.vendor.startsWith('custom:')) { if (this.vendor.startsWith('custom:')) {
this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`; this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`;
@@ -358,7 +294,7 @@ class TaskTranscribe extends SttTask {
vendor: this.vendor, vendor: this.vendor,
interim: this.interim ? true : false, interim: this.interim ? true : false,
locale: this.language, locale: this.language,
channels: 1, channels: /*this.separateRecognitionPerChannel ? 2 : */ 1,
bugname: this.bugname, bugname: this.bugname,
hostport: this.hostport hostport: this.hostport
}); });
@@ -368,7 +304,6 @@ class TaskTranscribe extends SttTask {
// make sure this is not a transcript from answering machine detection // make sure this is not a transcript from answering machine detection
const bugname = fsEvent.getHeader('media-bugname'); const bugname = fsEvent.getHeader('media-bugname');
const finished = fsEvent.getHeader('transcription-session-finished'); const finished = fsEvent.getHeader('transcription-session-finished');
const bufferedTranscripts = this._bufferedTranscripts[channel - 1];
if (bugname && this.bugname !== bugname) return; if (bugname && this.bugname !== bugname) return;
if (this.paused) { if (this.paused) {
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - paused, ignoring transcript'); this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - paused, ignoring transcript');
@@ -378,20 +313,14 @@ class TaskTranscribe extends SttTask {
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') { if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
/* we will only get this when we have set utterance_end_ms */ /* we will only get this when we have set utterance_end_ms */
if (this._bufferedTranscripts.length === 0) {
/* DH: send a speech event when we get UtteranceEnd if they want interim events */
if (this.interim) {
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram, sending speech event');
this._resolve(channel, evt);
}
if (bufferedTranscripts.length === 0) {
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram but no buffered transcripts'); this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram but no buffered transcripts');
} }
else { else {
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram, return buffered transcript'); this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram, return buffered transcript');
evt = this.consolidateTranscripts(bufferedTranscripts, channel, this.language, this.vendor); evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language, this.vendor);
evt.is_final = true; evt.is_final = true;
this._bufferedTranscripts[channel - 1] = []; this._bufferedTranscripts = [];
this._resolve(channel, evt); this._resolve(channel, evt);
} }
return; return;
@@ -408,11 +337,11 @@ class TaskTranscribe extends SttTask {
let emptyTranscript = false; let emptyTranscript = false;
if (evt.is_final) { if (evt.is_final) {
if (evt.alternatives.length === 0 || evt.alternatives[0].transcript === '' && !cs.callGone && !this.killed) { if (evt.alternatives[0].transcript === '' && !cs.callGone && !this.killed) {
emptyTranscript = true; emptyTranscript = true;
if (finished === 'true' && if (finished === 'true' &&
['microsoft', 'deepgram'].includes(this.vendor) && ['microsoft', 'deepgram'].includes(this.vendor) &&
bufferedTranscripts.length === 0) { this._bufferedTranscripts.length === 0) {
this.logger.debug({evt}, 'TaskGather:_onTranscription - got empty transcript from old gather, disregarding'); this.logger.debug({evt}, 'TaskGather:_onTranscription - got empty transcript from old gather, disregarding');
return; return;
} }
@@ -425,7 +354,7 @@ class TaskTranscribe extends SttTask {
'TaskGather:_onTranscription - got empty deepgram transcript during continous asr, continue listening'); 'TaskGather:_onTranscription - got empty deepgram transcript during continous asr, continue listening');
return; return;
} }
else if (this.vendor === 'deepgram' && bufferedTranscripts.length > 0) { else if (this.vendor === 'deepgram' && this._bufferedTranscripts.length > 0) {
this.logger.info({evt}, this.logger.info({evt},
'TaskGather:_onTranscription - got empty transcript from deepgram, return the buffered transcripts'); 'TaskGather:_onTranscription - got empty transcript from deepgram, return the buffered transcripts');
} }
@@ -441,7 +370,7 @@ class TaskTranscribe extends SttTask {
} }
} }
this.logger.info({evt}, 'TaskGather:_onTranscription - got transcript during continous asr'); this.logger.info({evt}, 'TaskGather:_onTranscription - got transcript during continous asr');
bufferedTranscripts.push(evt); this._bufferedTranscripts.push(evt);
this._startAsrTimer(channel); this._startAsrTimer(channel);
/* some STT engines will keep listening after a final response, so no need to restart */ /* some STT engines will keep listening after a final response, so no need to restart */
@@ -457,24 +386,20 @@ class TaskTranscribe extends SttTask {
} }
else if (this.vendor === 'deepgram') { else if (this.vendor === 'deepgram') {
/* compile transcripts into one */ /* compile transcripts into one */
if (!emptyTranscript) bufferedTranscripts.push(evt); if (!emptyTranscript) this._bufferedTranscripts.push(evt);
/* deepgram can send an empty and final transcript; only if we have any buffered should we resolve */ /* deepgram can send an empty and final transcript; only if we have any buffered should we resolve */
if (bufferedTranscripts.length === 0) return; if (this._bufferedTranscripts.length === 0) return;
evt = this.consolidateTranscripts(bufferedTranscripts, channel, this.language); evt = this.consolidateTranscripts(this._bufferedTranscripts, channel, this.language);
this._bufferedTranscripts[channel - 1] = []; this._bufferedTranscripts = [];
} }
/* here is where we return a final transcript */ /* here is where we return a final transcript */
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - sending final transcript'); this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - sending final transcript');
this._resolve(channel, evt); this._resolve(channel, evt);
/* some STT engines will keep listening after a final response, so no need to restart */ /* some STT engines will keep listening after a final response, so no need to restart */
if (!['soniox', 'aws', 'microsoft', 'deepgram', 'google'].includes(this.vendor) && if (!['soniox', 'aws', 'microsoft', 'deepgram', 'google']
!this.vendor.startsWith('custom:')) { .includes(this.vendor)) this._startTranscribing(cs, ep, channel);
this.logger.debug('TaskTranscribe:_onTranscription - restarting transcribe');
this._startTranscribing(cs, ep, channel);
}
} }
} }
else { else {
@@ -485,7 +410,7 @@ class TaskTranscribe extends SttTask {
const originalEvent = evt.vendor.evt; const originalEvent = evt.vendor.evt;
if (originalEvent.is_final && evt.alternatives[0].transcript !== '') { if (originalEvent.is_final && evt.alternatives[0].transcript !== '') {
this.logger.debug({evt}, 'Gather:_onTranscription - buffering a completed (partial) deepgram transcript'); this.logger.debug({evt}, 'Gather:_onTranscription - buffering a completed (partial) deepgram transcript');
bufferedTranscripts.push(evt); this._bufferedTranscripts.push(evt);
} }
} }
@@ -497,32 +422,26 @@ class TaskTranscribe extends SttTask {
} }
async _resolve(channel, evt) { async _resolve(channel, evt) {
if (evt.is_final) { /* we've got a transcript, so end the otel child span for this channel */
/* we've got a final transcript, so end the otel child span for this channel */ if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) { this.childSpan[channel - 1].span.setAttributes({
this.childSpan[channel - 1].span.setAttributes({ channel,
channel, 'stt.resolve': 'transcript',
'stt.label': this.label || 'None', 'stt.result': JSON.stringify(evt)
'stt.resolve': 'transcript', });
'stt.result': JSON.stringify(evt) this.childSpan[channel - 1].span.end();
});
this.childSpan[channel - 1].span.end();
}
} }
if (this.transcriptionHook) { if (this.transcriptionHook) {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
const payload = {
...this.cs.callInfo,
...httpHeaders,
...(evt.alternatives && {speech: evt}),
...(evt.type && {speechEvent: evt})
};
try { try {
this.logger.debug({payload}, 'sending transcriptionHook'); const json = await this.cs.requestor.request('verb:hook', this.transcriptionHook, {
const json = await this.cs.requestor.request('verb:hook', this.transcriptionHook, payload); ...this.cs.callInfo,
this.logger.info({json}, 'completed transcriptionHook'); ...httpHeaders,
speech: evt
});
this.logger.info({json}, 'sent transcriptionHook');
if (json && Array.isArray(json) && !this.parentTask) { if (json && Array.isArray(json) && !this.parentTask) {
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata)); const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
@@ -543,7 +462,7 @@ class TaskTranscribe extends SttTask {
this._clearTimer(); this._clearTimer();
this.notifyTaskDone(); this.notifyTaskDone();
} }
else if (evt.is_final) { else {
/* start another child span for this channel */ /* start another child span for this channel */
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`); const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
this.childSpan[channel - 1] = {span, ctx}; this.childSpan[channel - 1] = {span, ctx};
@@ -556,8 +475,7 @@ class TaskTranscribe extends SttTask {
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) { if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({ this.childSpan[channel - 1].span.setAttributes({
channel, channel,
'stt.resolve': 'timeout', 'stt.resolve': 'timeout'
'stt.label': this.label || 'None',
}); });
this.childSpan[channel - 1].span.end(); this.childSpan[channel - 1].span.end();
} }
@@ -574,8 +492,7 @@ class TaskTranscribe extends SttTask {
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) { if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({ this.childSpan[channel - 1].span.setAttributes({
channel, channel,
'stt.resolve': 'max duration exceeded', 'stt.resolve': 'max duration exceeded'
'stt.label': this.label || 'None',
}); });
this.childSpan[channel - 1].span.end(); this.childSpan[channel - 1].span.end();
} }
@@ -594,8 +511,10 @@ class TaskTranscribe extends SttTask {
} }
} }
async _startFallback(cs, _ep, evt) { async _onJambonzError(cs, _ep, evt) {
if (this.canFallback) { this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
if (this.paused) return;
if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
_ep.stopTranscription({ _ep.stopTranscription({
vendor: this.vendor, vendor: this.vendor,
bugname: this.bugname bugname: this.bugname
@@ -603,84 +522,46 @@ class TaskTranscribe extends SttTask {
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`)); .catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
try { try {
this.notifyError({ msg: 'ASR error', await this._fallback();
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'in progress'});
await this._initFallback();
let channel = 1; let channel = 1;
if (this.ep !== _ep) { if (this.ep !== _ep) {
channel = 2; channel = 2;
} }
this[`_speechHandlersSet_${channel}`] = false;
this._startTranscribing(cs, _ep, channel); this._startTranscribing(cs, _ep, channel);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
return true; return;
} catch (error) { } catch (error) {
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`); this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
} }
} else { } else {
this.logger.debug('transcribe:_startFallback no condition for falling back'); const {writeAlerts, AlertType} = cs.srf.locals;
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
}
return false;
}
async _onJambonzError(cs, _ep, evt) { if (this.vendor === 'nuance') {
if (this.vendor === 'google' && evt.error_code === 0) { const {code, error} = evt;
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError - ignoring google error code 0'); if (code === 404 && error === 'No speech') return this._resolve('timeout');
return; if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
} }
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError'); this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
if (this.paused) return; writeAlerts({
const {writeAlerts, AlertType} = cs.srf.locals; account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
if (this.vendor === 'nuance') { message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
const {code, error} = evt; vendor: this.vendor,
if (code === 404 && error === 'No speech') return this._resolve('timeout'); }).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
if (code === 413 && error === 'Too much speech') return this._resolve('timeout'); this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
}
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
if (!(await this._startFallback(cs, _ep, evt))) {
this.notifyTaskDone();
} }
} }
async _onVendorConnectFailure(cs, _ep, channel, evt) { _onVendorConnectFailure(cs, _ep, channel, evt) {
super._onVendorConnectFailure(cs, _ep, evt); super._onVendorConnectFailure(cs, _ep, evt);
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) { if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({ this.childSpan[channel - 1].span.setAttributes({
channel, channel,
'stt.resolve': 'connection failure', 'stt.resolve': 'connection failure'
'stt.label': this.label || 'None',
}); });
this.childSpan[channel - 1].span.end(); this.childSpan[channel - 1].span.end();
} }
if (!(await this._startFallback(cs, _ep, evt))) { this.notifyTaskDone();
this.notifyTaskDone();
}
}
async _onSpeechmaticsRecognitionStarted(_cs, _ep, evt) {
this.logger.debug({evt}, 'TaskGather:_onSpeechmaticsRecognitionStarted');
}
async _onSpeechmaticsInfo(_cs, _ep, evt) {
this.logger.debug({evt}, 'TaskGather:_onSpeechmaticsInfo');
}
async _onSpeechmaticsErrror(cs, _ep, evt) {
// eslint-disable-next-line no-unused-vars
const {message, ...e} = evt;
this._onVendorError(cs, _ep, {error: JSON.stringify(e)});
} }
_startAsrTimer(channel) { _startAsrTimer(channel) {
@@ -689,9 +570,8 @@ class TaskTranscribe extends SttTask {
this._clearAsrTimer(channel); this._clearAsrTimer(channel);
this._asrTimer = setTimeout(() => { this._asrTimer = setTimeout(() => {
this.logger.debug(`TaskTranscribe:_startAsrTimer - asr timer went off for channel: ${channel}`); this.logger.debug(`TaskTranscribe:_startAsrTimer - asr timer went off for channel: ${channel}`);
const evt = this.consolidateTranscripts( const evt = this.consolidateTranscripts(this._bufferedTranscripts, channel, this.language, this.vendor);
this._bufferedTranscripts[channel - 1], channel, this.language, this.vendor); this._bufferedTranscripts = [];
this._bufferedTranscripts[channel - 1] = [];
this._resolve(channel, evt); this._resolve(channel, evt);
}, this.asrTimeout); }, this.asrTimeout);
this.logger.debug(`TaskTranscribe:_startAsrTimer: set for ${this.asrTimeout}ms for channel ${channel}`); this.logger.debug(`TaskTranscribe:_startAsrTimer: set for ${this.asrTimeout}ms for channel ${channel}`);

View File

@@ -1,225 +0,0 @@
const Task = require('./task');
const { TaskPreconditions } = require('../utils/constants');
const { SpeechCredentialError } = require('../utils/error');
class TtsTask extends Task {
constructor(logger, data, parentTask) {
super(logger, data);
this.parentTask = parentTask;
this.preconditions = TaskPreconditions.Endpoint;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
/**
* Task use taskInlcudeSynthesizer to identify
* if taskInlcudeSynthesizer === true, use label from verb.synthesizer, even it's empty
* if taskInlcudeSynthesizer === false, use label from application.synthesizer
*/
this.taskInlcudeSynthesizer = !!this.data.synthesizer;
this.synthesizer = this.data.synthesizer || {};
this.disableTtsCache = this.data.disableTtsCache;
this.options = this.synthesizer.options || {};
}
async exec(cs) {
super.exec(cs);
if (cs.synthesizer) {
this.options = {...cs.synthesizer.options, ...this.options};
this.data.synthesizer = this.data.synthesizer || {};
for (const k in cs.synthesizer) {
const newValue = this.data.synthesizer && this.data.synthesizer[k] !== undefined ?
this.data.synthesizer[k] :
cs.synthesizer[k];
if (Array.isArray(newValue)) {
this.data.synthesizer[k] = [...(this.data.synthesizer[k] || []), ...cs.synthesizer[k]];
} else if (typeof newValue === 'object' && newValue !== null) {
this.data.synthesizer[k] = { ...(this.data.synthesizer[k] || {}), ...cs.synthesizer[k] };
} else {
this.data.synthesizer[k] = newValue;
}
}
}
}
async _synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label, preCache = false}) {
const {srf, accountSid:account_sid} = cs;
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
const {writeAlerts, AlertType, stats} = srf.locals;
const {synthAudio} = srf.locals.dbHelpers;
const engine = this.synthesizer.engine || cs.synthesizer?.engine || 'neural';
const salt = cs.callSid;
let credentials = cs.getSpeechCredentials(vendor, 'tts', label);
if (!credentials) {
throw new SpeechCredentialError(
`No text-to-speech service credentials for ${vendor} with labels: ${label} have been configured`);
}
/* parse Nuance voices into name and model */
let model;
if (vendor === 'nuance' && voice) {
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
if (arr) {
voice = arr[1];
model = arr[2];
}
} else if (vendor === 'deepgram') {
model = voice;
}
/* 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;
} 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;
} else if (vendor === 'rimelabs') {
credentials = credentials || {};
credentials.model_id = this.options.model_id || credentials.model_id;
} else if (vendor === 'whisper') {
credentials = credentials || {};
credentials.model_id = this.options.model_id || credentials.model_id;
} else if (vendor === 'verbio') {
credentials = credentials || {};
credentials.engine_version = this.options.engine_version || credentials.engine_version;
} else if (vendor === 'playht') {
credentials = credentials || {};
credentials.voice_engine = this.options.voice_engine || credentials.voice_engine;
}
/**
* note on cache_speech_handles. This was found to be risky.
* It can cause a crash in the following sequence on a single call:
* 1. Stream tts on vendor A with cache_speech_handles=1, then
* 2. Stream tts on vendor B with cache_speech_handles=1
*
* we previously tried to track when vendors were switched and manage the flag accordingly,
* but it difficult to track all the scenarios and the benefit (slightly faster start to tts playout)
* is probably minimal. DH.
*/
ep.set({
tts_engine: vendor.startsWith('custom:') ? 'custom' : vendor,
tts_voice: voice,
//cache_speech_handles: !cs.currentTtsVendor || cs.currentTtsVendor === vendor ? 1 : 0,
cache_speech_handles: 0,
}).catch((err) => this.logger.info({err}, 'Error setting tts_engine on endpoint'));
// set the current vendor on the call session
// If vendor is changed from the previous one, then reset the cache_speech_handles flag
//cs.currentTtsVendor = vendor;
if (!preCache && !this._disableTracing) this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
try {
if (!credentials) {
writeAlerts({
account_sid,
alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
throw new SpeechCredentialError('no provisioned speech credentials for TTS');
}
// synthesize all of the text elements
let lastUpdated = false;
/* produce an audio segment from the provided text */
const generateAudio = async(text) => {
if (this.killed) return;
if (text.startsWith('silence_stream://')) return text;
/* otel: trace time for tts */
if (!preCache && !this._disableTracing) {
const {span} = this.startChildSpan('tts-generation', {
'tts.vendor': vendor,
'tts.language': language,
'tts.voice': voice,
'tts.label': label || 'None',
});
this.otelSpan = span;
}
try {
const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
account_sid,
text,
vendor,
language,
voice,
engine,
model,
salt,
credentials,
options: this.options,
disableTtsCache : this.disableTtsCache,
renderForCaching: preCache
});
if (!filePath.startsWith('say:')) {
this.logger.debug(`Say: file ${filePath}, served from cache ${servedFromCache}`);
if (filePath) cs.trackTmpFile(filePath);
if (this.otelSpan) {
this.otelSpan.setAttributes({'tts.cached': servedFromCache});
this.otelSpan.end();
this.otelSpan = null;
}
if (!servedFromCache && !lastUpdated) {
lastUpdated = true;
updateSpeechCredentialLastUsed(credentials.speech_credential_sid).catch(() => {/* logged error */});
}
if (!servedFromCache && rtt && !preCache && !this._disableTracing) {
this.notifyStatus({
event: 'synthesized-audio',
vendor,
language,
characters: text.length,
elapsedTime: rtt
});
}
}
else {
this.logger.debug('Say: a streaming tts api will be used');
const modifiedPath = filePath.replace('say:{', `say:{session-uuid=${ep.uuid},`);
return modifiedPath;
}
return filePath;
} catch (err) {
this.logger.info({err}, 'Error synthesizing tts');
if (this.otelSpan) this.otelSpan.end();
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.TTS_FAILURE,
vendor,
detail: err.message,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
throw err;
}
};
const arr = this.text.map((t) => (this._validateURL(t) ? t : generateAudio(t)));
return (await Promise.all(arr)).filter((fp) => fp && fp.length);
} catch (err) {
this.logger.info(err, 'TaskSay:exec error');
throw err;
}
}
_validateURL(urlString) {
try {
new URL(urlString);
return true;
} catch (e) {
return false;
}
}
}
module.exports = TtsTask;

View File

@@ -1,182 +0,0 @@
const makeTask = require('../tasks/make_task');
const Emitter = require('events');
const { normalizeJambones } = require('@jambonz/verb-specifications');
const {TaskName} = require('../utils/constants');
/**
* ActionHookDelayProcessor
* @extends Emitter
*
* @param {Object} logger - logger instance
* @param {Object} opts - options
* @param {Object} cs - call session
* @param {Object} ep - endpoint
*
* @emits {Event} 'giveup' - when associated giveup timer expires
*
* Ref:https://www.jambonz.org/docs/supporting-articles/handling-action-hook-delays/
*/
class ActionHookDelayProcessor extends Emitter {
constructor(logger, opts, cs) {
super();
this.logger = logger;
this.cs = cs;
this._active = false;
const enabled = this.init(opts);
if (enabled && this.noResponseTimeout &&
(!this.actions || !Array.isArray(this.actions) || this.actions.length === 0)) {
throw new Error('ActionHookDelayProcessor: no actions specified');
}
else if (enabled && this.actions &&
this.actions.some((a) => !a.verb || ![TaskName.Say, TaskName.Play].includes(a.verb))) {
throw new Error(`ActionHookDelayProcessor: invalid actions specified: ${JSON.stringify(this.actions)}`);
}
}
get properties() {
return {
actions: this.actions,
retries: this.retries,
noResponseTimeout: this.noResponseTimeout,
noResponseGiveUpTimeout: this.noResponseGiveUpTimeout
};
}
get ep() {
return this.cs.ep;
}
init(opts) {
this.logger.debug({opts}, 'ActionHookDelayProcessor#init');
this.actions = opts.actions;
this.retries = opts.retries || 0;
this.noResponseTimeout = opts.noResponseTimeout;
this.noResponseGiveUpTimeout = opts.noResponseGiveUpTimeout;
this.giveUpActions = opts.giveUpActions;
// return false if these options actually disable the ahdp
return ('enable' in opts && opts.enable === true) ||
('enabled' in opts && opts.enabled === true) ||
(!('enable' in opts) && !('enabled' in opts));
}
start() {
this.logger.debug('ActionHookDelayProcessor#start');
if (this._active) {
this.logger.debug('ActionHookDelayProcessor#start: already started due to prior gather which is continuing');
return;
}
this._active = true;
this._retryCount = 0;
if (this.noResponseTimeout > 0) {
const timeoutMs = this.noResponseTimeout * 1000;
this._noResponseTimer = setTimeout(this._onNoResponseTimer.bind(this), timeoutMs);
} else {
this.logger.debug(
'ActionHookDelayProcessor#start: noResponseTimeout is 0 or undefined hence not calling _onNoResponseTimer'
);
}
if (this.noResponseGiveUpTimeout > 0) {
const timeoutMs = this.noResponseGiveUpTimeout * 1000;
this._noResponseGiveUpTimer = setTimeout(this._onNoResponseGiveUpTimer.bind(this), timeoutMs);
}
}
async stop() {
this._active = false;
if (this._noResponseTimer) {
clearTimeout(this._noResponseTimer);
this._noResponseTimer = null;
}
if (this._noResponseGiveUpTimer) {
clearTimeout(this._noResponseGiveUpTimer);
this._noResponseGiveUpTimer = null;
}
if (this._taskInProgress) {
this.logger.debug(`ActionHookDelayProcessor#stop: stopping ${this._taskInProgress.name}`);
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');
}
_onNoResponseTimer() {
this.logger.debug('ActionHookDelayProcessor#_onNoResponseTimer');
this._noResponseTimer = null;
/* get the next play or say action */
const verb = this.actions[this._retryCount % this.actions.length];
const t = normalizeJambones(this.logger, [verb]);
this.logger.debug({verb}, 'ActionHookDelayProcessor#_onNoResponseTimer: starting action');
try {
this._taskInProgress = makeTask(this.logger, t[0]);
this._taskInProgress.disableTracing = true;
this._taskInProgress.exec(this.cs, {ep: this.ep});
} catch (err) {
this.logger.info(err, 'ActionHookDelayProcessor#_onNoResponseTimer: error starting action');
this._taskInProgress = null;
return;
}
this.ep.once('playback-start', (evt) => {
this.logger.debug({evt}, 'got playback-start');
if (!this._active) {
this.logger.info({evt}, 'ActionHookDelayProcessor#_onNoResponseTimer: killing audio immediately');
/* note: in race condition we may have just hung up and cs.ep cleared */
this.ep?.api('uuid_break', this.ep?.uuid)
.catch((err) => this.logger.info(err,
'ActionHookDelayProcessor#_onNoResponseTimer Error killing audio'));
}
});
this.ep.once('playback-stop', (evt) => {
this._taskInProgress = null;
if (this._sayResolver) {
/* we were waiting for the play to finish before continuing to next task */
this.logger.debug({evt}, 'ActionHookDelayProcessor#_onNoResponseTimer got playback-stop');
this._sayResolver();
this._sayResolver = null;
}
else {
/* possibly start the no response timer again */
if (this._active && this.retries > 0 && this._retryCount < this.retries && this.noResponseTimeout > 0) {
this.logger.debug({evt}, 'ActionHookDelayProcessor#_onNoResponseTimer: playback-stop on play/say action');
const timeoutMs = this.noResponseTimeout * 1000;
this._noResponseTimer = setTimeout(this._onNoResponseTimer.bind(this), timeoutMs);
}
}
});
this._retryCount++;
}
_onNoResponseGiveUpTimer() {
this._active = false;
if (!this.giveUpActions) {
this.logger.info('ActionHookDelayProcessor#_onNoResponseGiveUpTimer');
this.stop().catch((err) => {});
this.emit('giveup');
} else {
this.logger.info('ActionHookDelayProcessor#_onNoResponseGiveUpTimer - giveUpActions');
this.emit('giveupWithTasks', this.giveUpActions);
}
}
}
module.exports = ActionHookDelayProcessor;

View File

@@ -210,8 +210,7 @@ module.exports = (logger) => {
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
vendor: vendor, vendor: vendor,
detail: err.message, detail: err.message
target_sid: cs.callSid
}); });
}).catch((err) => logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => logger.info({err}, 'Error generating alert for tts failure'));
@@ -246,10 +245,7 @@ module.exports = (logger) => {
const amd = ep.amd = new Amd(logger, cs, opts); const amd = ep.amd = new Amd(logger, cs, opts);
const {vendor, language} = amd; const {vendor, language} = amd;
let sttCredentials = amd.sttCredentials; let sttCredentials = amd.sttCredentials;
// hints from configuration might be too long for specific language and vendor that make transcribe freeswitch const hints = voicemailHints[language] || [];
// modules cannot connect to the vendor. hints is used in next step to validate if the transcription
// matchs voice mail hints.
const hints = [];
if (vendor === 'nuance' && sttCredentials.client_id) { if (vendor === 'nuance' && sttCredentials.client_id) {
/* get nuance access token */ /* get nuance access token */

View File

@@ -26,25 +26,25 @@ class BackgroundTaskManager extends Emitter {
return this.tasks.size; return this.tasks.size;
} }
async newTask(type, opts, sticky = false) { async newTask(type, taskOpts) {
this.logger.info({opts}, `initiating Background task ${type}`); this.logger.info({taskOpts}, `initiating Background task ${type}`);
if (this.tasks.has(type)) { if (this.tasks.has(type)) {
this.logger.info(`Background task ${type} is running, skipped`); this.logger.info(`Background task ${type} is running, skiped`);
return; return;
} }
let task; let task;
switch (type) { switch (type) {
case 'listen': case 'listen':
task = await this._initListen(opts); task = await this._initListen(taskOpts);
break; break;
case 'bargeIn': case 'bargeIn':
task = await this._initBargeIn(opts); task = await this._initBargeIn(taskOpts);
break; break;
case 'record': case 'record':
task = await this._initRecord(); task = await this._initRecord();
break; break;
case 'transcribe': case 'transcribe':
task = await this._initTranscribe(opts); task = await this._initTranscribe(taskOpts);
break; break;
default: default:
break; break;
@@ -52,7 +52,6 @@ class BackgroundTaskManager extends Emitter {
if (task) { if (task) {
this.tasks.set(type, task); this.tasks.set(type, task);
} }
if (task && sticky) task.sticky = true;
return task; return task;
} }
@@ -65,6 +64,8 @@ class BackgroundTaskManager extends Emitter {
task.kill(); task.kill();
// Remove task from managed List // Remove task from managed List
this.tasks.delete(type); this.tasks.delete(type);
} else {
this.logger.debug(`stopping background task, ${type} is not running, skipped`);
} }
} }
@@ -117,8 +118,7 @@ class BackgroundTaskManager extends Emitter {
this._taskCompleted('bargeIn', task); this._taskCompleted('bargeIn', task);
if (task.sticky && !this.cs.callGone && !this.cs._stopping) { if (task.sticky && !this.cs.callGone && !this.cs._stopping) {
this.logger.info('BackgroundTaskManager:_initBargeIn: restarting background bargeIn'); this.logger.info('BackgroundTaskManager:_initBargeIn: restarting background bargeIn');
this._bargeInHandled = false; this.newTask('bargeIn', opts);
this.newTask('bargeIn', opts, true);
} }
return; return;
}) })
@@ -187,8 +187,6 @@ class BackgroundTaskManager extends Emitter {
} }
_bargeInTaskCompleted(evt) { _bargeInTaskCompleted(evt) {
if (this._bargeInHandled) return;
this._bargeInHandled = true;
this.logger.debug({evt}, this.logger.debug({evt},
'BackgroundTaskManager:_bargeInTaskCompleted on event from background bargeIn, emitting bargein-done event'); 'BackgroundTaskManager:_bargeInTaskCompleted on event from background bargeIn, emitting bargein-done event');
this.emit('bargeIn-done', evt); this.emit('bargeIn-done', evt);

View File

@@ -1,20 +1,18 @@
{ {
"TaskName": { "TaskName": {
"Answer": "answer", "Cognigy": "cognigy",
"Conference": "conference", "Conference": "conference",
"Config": "config", "Config": "config",
"Dequeue": "dequeue", "Dequeue": "dequeue",
"Dial": "dial", "Dial": "dial",
"Dialogflow": "dialogflow", "Dialogflow": "dialogflow",
"Dtmf": "dtmf", "Dtmf": "dtmf",
"Dub": "dub",
"Enqueue": "enqueue", "Enqueue": "enqueue",
"Gather": "gather", "Gather": "gather",
"Hangup": "hangup", "Hangup": "hangup",
"Leave": "leave", "Leave": "leave",
"Lex": "lex", "Lex": "lex",
"Listen": "listen", "Listen": "listen",
"Llm": "llm",
"Message": "message", "Message": "message",
"Pause": "pause", "Pause": "pause",
"Play": "play", "Play": "play",
@@ -31,7 +29,7 @@
"Tag": "tag", "Tag": "tag",
"Transcribe": "transcribe" "Transcribe": "transcribe"
}, },
"AllowedSipRecVerbs": ["answer", "config", "gather", "transcribe", "listen", "tag"], "AllowedSipRecVerbs": ["config", "gather", "transcribe", "listen", "tag"],
"AllowedConfirmSessionVerbs": ["config", "gather", "plays", "say", "tag"], "AllowedConfirmSessionVerbs": ["config", "gather", "plays", "say", "tag"],
"CallStatus": { "CallStatus": {
"Trying": "trying", "Trying": "trying",
@@ -98,10 +96,6 @@
"Transcription": "soniox_transcribe::transcription", "Transcription": "soniox_transcribe::transcription",
"Error": "soniox_transcribe::error" "Error": "soniox_transcribe::error"
}, },
"VerbioTranscriptionEvents": {
"Transcription": "verbio_transcribe::transcription",
"Error": "verbio_transcribe::error"
},
"CobaltTranscriptionEvents": { "CobaltTranscriptionEvents": {
"Transcription": "cobalt_speech::transcription", "Transcription": "cobalt_speech::transcription",
"CompileContext": "cobalt_speech::compile_context_response", "CompileContext": "cobalt_speech::compile_context_response",
@@ -127,14 +121,6 @@
"NoSpeechDetected": "azure_transcribe::no_speech_detected", "NoSpeechDetected": "azure_transcribe::no_speech_detected",
"VadDetected": "azure_transcribe::vad_detected" "VadDetected": "azure_transcribe::vad_detected"
}, },
"SpeechmaticsTranscriptionEvents": {
"Transcription": "speechmatics_transcribe::transcription",
"Info": "speechmatics_transcribe::info",
"RecognitionStarted": "speechmatics_transcribe::recognition_started",
"ConnectFailure": "speechmatics_transcribe::connect_failed",
"Connect": "speechmatics_transcribe::connect",
"Error": "speechmatics_transcribe::error"
},
"JambonzTranscriptionEvents": { "JambonzTranscriptionEvents": {
"Transcription": "jambonz_transcribe::transcription", "Transcription": "jambonz_transcribe::transcription",
"ConnectFailure": "jambonz_transcribe::connect_failed", "ConnectFailure": "jambonz_transcribe::connect_failed",
@@ -147,9 +133,6 @@
"ConnectFailure": "assemblyai_transcribe::connect_failed", "ConnectFailure": "assemblyai_transcribe::connect_failed",
"Connect": "assemblyai_transcribe::connect" "Connect": "assemblyai_transcribe::connect"
}, },
"VadDetection": {
"Detection": "vad_detect:detection"
},
"ListenEvents": { "ListenEvents": {
"Connect": "mod_audio_fork::connect", "Connect": "mod_audio_fork::connect",
"ConnectFailure": "mod_audio_fork::connect_failed", "ConnectFailure": "mod_audio_fork::connect_failed",
@@ -167,13 +150,6 @@
"StandbyEnter": "standby-enter", "StandbyEnter": "standby-enter",
"StandbyExit": "standby-exit" "StandbyExit": "standby-exit"
}, },
"LlmEvents_OpenAI": {
"Error": "error",
"Connect": "openai_s2s::connect",
"ConnectFailure": "openai_s2s::connect_failed",
"Disconnect": "openai_s2s::disconnect",
"ServerEvent": "openai_s2s::server_event"
},
"QueueResults": { "QueueResults": {
"Bridged": "bridged", "Bridged": "bridged",
"Error": "error", "Error": "error",
@@ -194,14 +170,11 @@
"session:new", "session:new",
"session:reconnect", "session:reconnect",
"session:redirect", "session:redirect",
"session:adulting",
"call:status", "call:status",
"queue:status", "queue:status",
"dial:confirm", "dial:confirm",
"verb:hook", "verb:hook",
"verb:status", "verb:status",
"llm:event",
"llm:tool-call",
"jambonz:error" "jambonz:error"
], ],
"RecordState": { "RecordState": {

View File

@@ -41,7 +41,6 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id; obj.access_key_id = o.access_key_id;
obj.secret_access_key = o.secret_access_key; obj.secret_access_key = o.secret_access_key;
obj.role_arn = o.role_arn;
obj.aws_region = o.aws_region; obj.aws_region = o.aws_region;
} }
else if ('microsoft' === obj.vendor) { else if ('microsoft' === obj.vendor) {
@@ -77,7 +76,6 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key; obj.api_key = o.api_key;
obj.deepgram_stt_uri = o.deepgram_stt_uri; obj.deepgram_stt_uri = o.deepgram_stt_uri;
obj.deepgram_tts_uri = o.deepgram_tts_uri;
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls; obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
} }
else if ('soniox' === obj.vendor) { else if ('soniox' === obj.vendor) {
@@ -91,47 +89,19 @@ const speechMapper = (cred) => {
else if ('cobalt' === obj.vendor) { else if ('cobalt' === obj.vendor) {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.cobalt_server_uri = o.cobalt_server_uri; obj.cobalt_server_uri = o.cobalt_server_uri;
} } else if ('elevenlabs' === obj.vendor) {
else if ('elevenlabs' === obj.vendor) {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key; obj.api_key = o.api_key;
obj.model_id = o.model_id; obj.model_id = o.model_id;
obj.options = o.options; obj.options = o.options;
} } else if ('assemblyai' === obj.vendor) {
else if ('playht' === obj.vendor) {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key; obj.api_key = o.api_key;
obj.user_id = o.user_id; } else if ('whisper' === obj.vendor) {
obj.voice_engine = o.voice_engine;
obj.options = o.options;
}
else if ('rimelabs' === obj.vendor) {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key; obj.api_key = o.api_key;
obj.model_id = o.model_id; obj.model_id = o.model_id;
obj.options = o.options; } else if (obj.vendor.startsWith('custom:')) {
}
else if ('assemblyai' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
}
else if ('whisper' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
obj.model_id = o.model_id;
}
else if ('verbio' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.client_secret = o.client_secret;
obj.engine_version = o.engine_version;
}
else if ('speechmatics' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
obj.speechmatics_stt_uri = o.speechmatics_stt_uri;
}
else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.auth_token = o.auth_token; obj.auth_token = o.auth_token;
obj.custom_stt_url = o.custom_stt_url; obj.custom_stt_url = o.custom_stt_url;

View File

@@ -1,9 +0,0 @@
class SpeechCredentialError extends Error {
constructor(msg) {
super(msg);
}
}
module.exports = {
SpeechCredentialError
};

View File

@@ -1,5 +1,5 @@
const Mrf = require('drachtio-fsmrf'); const Mrf = require('drachtio-fsmrf');
const os = require('os'); const ip = require('ip');
const { const {
JAMBONES_MYSQL_HOST, JAMBONES_MYSQL_HOST,
JAMBONES_MYSQL_USER, JAMBONES_MYSQL_USER,
@@ -12,25 +12,11 @@ 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');
const assert = require('assert'); const assert = require('assert');
function getLocalIp() {
const interfaces = os.networkInterfaces();
for (const interfaceName in interfaces) {
const interface = interfaces[interfaceName];
for (const iface of interface) {
if (iface.family === 'IPv4' && !iface.internal) {
return iface.address;
}
}
}
return '127.0.0.1'; // Fallback to localhost if no suitable interface found
}
function initMS(logger, wrapper, ms) { function initMS(logger, wrapper, ms) {
Object.assign(wrapper, {ms, active: true, connects: 1}); Object.assign(wrapper, {ms, active: true, connects: 1});
logger.info(`connected to freeswitch at ${ms.address}`); logger.info(`connected to freeswitch at ${ms.address}`);
@@ -152,8 +138,7 @@ function installSrfLocals(srf, logger) {
lookupAccountBySid, lookupAccountBySid,
lookupAccountCapacitiesBySid, lookupAccountCapacitiesBySid,
lookupSmppGateways, lookupSmppGateways,
lookupClientByAccountAndUsername, lookupClientByAccountAndUsername
lookupSystemInformation
} = require('@jambonz/db-helpers')({ } = require('@jambonz/db-helpers')({
host: JAMBONES_MYSQL_HOST, host: JAMBONES_MYSQL_HOST,
user: JAMBONES_MYSQL_USER, user: JAMBONES_MYSQL_USER,
@@ -186,7 +171,7 @@ function installSrfLocals(srf, logger) {
retrieveFromSortedSet, retrieveFromSortedSet,
retrieveByPatternSortedSet, retrieveByPatternSortedSet,
sortedSetLength, sortedSetLength,
sortedSetPositionByPattern, sortedSetPositionByPattern
} = require('@jambonz/realtimedb-helpers')({}, logger, tracer); } = require('@jambonz/realtimedb-helpers')({}, logger, tracer);
const registrar = new Registrar(logger, client); const registrar = new Registrar(logger, client);
const { const {
@@ -194,8 +179,6 @@ function installSrfLocals(srf, logger) {
addFileToCache, addFileToCache,
getNuanceAccessToken, getNuanceAccessToken,
getIbmAccessToken, getIbmAccessToken,
getAwsAuthToken,
getVerbioAccessToken
} = require('@jambonz/speech-utils')({}, logger); } = require('@jambonz/speech-utils')({}, logger);
const { const {
writeAlerts, writeAlerts,
@@ -208,8 +191,7 @@ function installSrfLocals(srf, logger) {
let localIp; let localIp;
try { try {
// Either use the configured IP address or discover it localIp = ip.address();
localIp = HTTP_IP || getLocalIp();
} catch (err) { } catch (err) {
logger.error({err}, 'installSrfLocals - error detecting local ipv4 address'); logger.error({err}, 'installSrfLocals - error detecting local ipv4 address');
} }
@@ -229,13 +211,11 @@ function installSrfLocals(srf, logger) {
lookupAccountCapacitiesBySid, lookupAccountCapacitiesBySid,
lookupSmppGateways, lookupSmppGateways,
lookupClientByAccountAndUsername, lookupClientByAccountAndUsername,
lookupSystemInformation,
updateCallStatus, updateCallStatus,
retrieveCall, retrieveCall,
listCalls, listCalls,
deleteCall, deleteCall,
synthAudio, synthAudio,
getAwsAuthToken,
addFileToCache, addFileToCache,
createHash, createHash,
retrieveHash, retrieveHash,
@@ -257,8 +237,7 @@ function installSrfLocals(srf, logger) {
retrieveFromSortedSet, retrieveFromSortedSet,
retrieveByPatternSortedSet, retrieveByPatternSortedSet,
sortedSetLength, sortedSetLength,
sortedSetPositionByPattern, sortedSetPositionByPattern
getVerbioAccessToken
}, },
parentLogger: logger, parentLogger: logger,
getSBC, getSBC,

View File

@@ -1,18 +0,0 @@
const parseDecibels = (db) => {
if (!db) return 0;
if (typeof db === 'number') {
return db;
}
else if (typeof db === 'string') {
const match = db.match(/([+-]?\d+(\.\d+)?)\s*db/i);
if (match) {
return Math.trunc(parseFloat(match[1]));
} else {
return 0;
}
} else {
return 0;
}
};
module.exports = parseDecibels;

View File

@@ -213,8 +213,6 @@ class SingleDialer extends Emitter {
}, },
cbProvisional: (prov) => { cbProvisional: (prov) => {
const status = {sipStatus: prov.status, sipReason: prov.reason}; const status = {sipStatus: prov.status, sipReason: prov.reason};
// Update call-id for sbc outbound INVITE
this.callInfo.sbcCallid = prov.get('X-CID');
if ([180, 183].includes(prov.status) && prov.body) { if ([180, 183].includes(prov.status) && prov.body) {
if (status.callStatus !== CallStatus.EarlyMedia) { if (status.callStatus !== CallStatus.EarlyMedia) {
status.callStatus = CallStatus.EarlyMedia; status.callStatus = CallStatus.EarlyMedia;
@@ -297,17 +295,17 @@ class SingleDialer extends Emitter {
if (err.status === 487) status.callStatus = CallStatus.NoAnswer; if (err.status === 487) status.callStatus = CallStatus.NoAnswer;
else if ([486, 600].includes(err.status)) status.callStatus = CallStatus.Busy; else if ([486, 600].includes(err.status)) status.callStatus = CallStatus.Busy;
this.logger.info(`SingleDialer:exec outdial failure ${err.status}`); this.logger.info(`SingleDialer:exec outdial failure ${err.status}`);
inviteSpan?.setAttributes({'invite.status_code': err.status}); inviteSpan.setAttributes({'invite.status_code': err.status});
inviteSpan?.end(); inviteSpan.end();
} }
else { else {
this.logger.error(err, 'SingleDialer:exec'); this.logger.error(err, 'SingleDialer:exec');
status.sipStatus = 500; status.sipStatus = 500;
inviteSpan?.setAttributes({ inviteSpan.setAttributes({
'invite.status_code': 500, 'invite.status_code': 500,
'invite.err': err.message 'invite.err': err.message
}); });
inviteSpan?.end(); inviteSpan.end();
} }
this.emit('callStatusChange', status); this.emit('callStatusChange', status);
if (this.ep) this.ep.destroy(); if (this.ep) this.ep.destroy();
@@ -415,7 +413,6 @@ class SingleDialer extends Emitter {
const app = {...application}; const app = {...application};
if ('WS' === app.call_hook?.method || if ('WS' === app.call_hook?.method ||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) { app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
if (app.call_hook?.url) app.call_hook.url += '/adulting';
const requestor = new WsRequestor(logger, this.accountInfo.account.account_sid, const requestor = new WsRequestor(logger, this.accountInfo.account.account_sid,
app.call_hook, this.accountInfo.account.webhook_secret); app.call_hook, this.accountInfo.account.webhook_secret);
app.requestor = requestor; app.requestor = requestor;
@@ -441,16 +438,7 @@ class SingleDialer extends Emitter {
tasks, tasks,
rootSpan rootSpan
}); });
app.requestor.request('session:adulting', '/adulting', {
...cs.callInfo.toJSON(),
parentCallInfo: this.parentCallInfo.toJSON()
}).catch((err) => {
newLogger.error({err}, 'doAdulting: error sending adulting request');
});
cs.req = this.req; cs.req = this.req;
// fixed hangup an adulting session does not send status callback Completed
cs.wrapDialog(this.dlg);
cs.exec().catch((err) => newLogger.error({err}, 'doAdulting: error executing session')); cs.exec().catch((err) => newLogger.error({err}, 'doAdulting: error executing session'));
return cs; return cs;
} }

View File

@@ -1,4 +1,7 @@
const {TaskName} = require('./constants.json'); const {
TaskName,
} = require('./constants.json');
const stickyVars = { const stickyVars = {
google: [ google: [
'GOOGLE_SPEECH_HINTS', 'GOOGLE_SPEECH_HINTS',
@@ -42,19 +45,12 @@ const stickyVars = {
'DEEPGRAM_SPEECH_ENDPOINTING', 'DEEPGRAM_SPEECH_ENDPOINTING',
'DEEPGRAM_SPEECH_UTTERANCE_END_MS', 'DEEPGRAM_SPEECH_UTTERANCE_END_MS',
'DEEPGRAM_SPEECH_VAD_TURNOFF', 'DEEPGRAM_SPEECH_VAD_TURNOFF',
'DEEPGRAM_SPEECH_TAG', 'DEEPGRAM_SPEECH_TAG'
'DEEPGRAM_SPEECH_MODEL_VERSION'
], ],
aws: [ aws: [
'AWS_VOCABULARY_NAME', 'AWS_VOCABULARY_NAME',
'AWS_VOCABULARY_FILTER_METHOD', 'AWS_VOCABULARY_FILTER_METHOD',
'AWS_VOCABULARY_FILTER_NAME', 'AWS_VOCABULARY_FILTER_NAME'
'AWS_LANGUAGE_MODEL_NAME',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'AWS_REGION',
'AWS_SECURITY_TOKEN',
'AWS_PII_ENTITY_TYPES',
], ],
nuance: [ nuance: [
'NUANCE_ACCESS_TOKEN', 'NUANCE_ACCESS_TOKEN',
@@ -103,12 +99,6 @@ const stickyVars = {
assemblyai: [ assemblyai: [
'ASSEMBLYAI_API_KEY', 'ASSEMBLYAI_API_KEY',
'ASSEMBLYAI_WORD_BOOST' 'ASSEMBLYAI_WORD_BOOST'
],
speechmatics: [
'SPEECHMATICS_API_KEY',
'SPEECHMATICS_HOST',
'SPEECHMATICS_PATH',
'SPEECHMATICS_SPEECH_HINTS',
] ]
}; };
@@ -151,6 +141,7 @@ 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];
@@ -159,29 +150,6 @@ const selectDefaultDeepgramModel = (task, language) => {
return 'base'; return 'base';
}; };
const optimalGoogleModels = {
'v1' : {
'en-IN':['telephony', 'telephony'],
'es-DO':['default', 'default'],
'es-MX':['default', 'default'],
'en-AU':['telephony', 'telephony'],
'en-GB':['telephony', 'telephony'],
'en-NZ':['telephony', 'telephony']
},
'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;
@@ -302,7 +270,7 @@ const normalizeDeepgram = (evt, channel, language, shortUtterance) => {
language_code: language, language_code: language,
channel_tag: channel, channel_tag: channel,
is_final: shortUtterance ? evt.is_final : evt.speech_final, is_final: shortUtterance ? evt.is_final : evt.speech_final,
alternatives: alternatives.length ? [alternatives[0]] : [], alternatives: [alternatives[0]],
vendor: { vendor: {
name: 'deepgram', name: 'deepgram',
evt: copy evt: copy
@@ -348,10 +316,8 @@ const normalizeIbm = (evt, channel, language) => {
const normalizeGoogle = (evt, channel, language) => { const normalizeGoogle = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt)); const copy = JSON.parse(JSON.stringify(evt));
const language_code = evt.language_code || language;
return { return {
language_code: language_code, language_code: language,
channel_tag: channel, channel_tag: channel,
is_final: evt.is_final, is_final: evt.is_final,
alternatives: [evt.alternatives[0]], alternatives: [evt.alternatives[0]],
@@ -410,20 +376,6 @@ const normalizeNuance = (evt, channel, language) => {
}; };
}; };
const normalizeVerbio = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: evt.alternatives,
vendor: {
name: 'verbio',
evt: copy
}
};
};
const normalizeMicrosoft = (evt, channel, language, punctuation = true) => { const normalizeMicrosoft = (evt, channel, language, punctuation = true) => {
const copy = JSON.parse(JSON.stringify(evt)); const copy = JSON.parse(JSON.stringify(evt));
const nbest = evt.NBest; const nbest = evt.NBest;
@@ -455,41 +407,16 @@ const normalizeMicrosoft = (evt, channel, language, punctuation = true) => {
const normalizeAws = (evt, channel, language) => { const normalizeAws = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt)); const copy = JSON.parse(JSON.stringify(evt));
const isGrpcPayload = Array.isArray(evt); return {
if (isGrpcPayload) { language_code: language,
/* legacy grpc api */ channel_tag: channel,
return { is_final: evt[0].is_final,
language_code: language, alternatives: evt[0].alternatives,
channel_tag: channel, vendor: {
is_final: evt[0].is_final, name: 'aws',
alternatives: evt[0].alternatives, evt: copy
vendor: { }
name: 'aws', };
evt: copy
}
};
}
else {
/* websocket api */
const alternatives = evt.Transcript?.Results[0]?.Alternatives.map((alt) => {
const items = alt.Items.filter((item) => item.Type === 'pronunciation' && 'Confidence' in item);
const confidence = items.reduce((acc, item) => acc + item.Confidence, 0) / items.length;
return {
transcript: alt.Transcript,
confidence
};
});
return {
language_code: language,
channel_tag: channel,
is_final: evt.Transcript?.Results[0].IsPartial === false,
alternatives,
vendor: {
name: 'aws',
evt: copy
}
};
}
}; };
const normalizeAssemblyAi = (evt, channel, language) => { const normalizeAssemblyAi = (evt, channel, language) => {
@@ -505,37 +432,12 @@ const normalizeAssemblyAi = (evt, channel, language) => {
} }
], ],
vendor: { vendor: {
name: 'assemblyai', name: 'ASSEMBLYAI',
evt: copy evt: copy
} }
}; };
}; };
const normalizeSpeechmatics = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
const is_final = evt.message === 'AddTranscript';
const words = evt.results?.filter((r) => r.type === 'word') || [];
const confidence = words.length > 0 ?
words.reduce((acc, word) => acc + word.alternatives[0].confidence, 0) / words.length :
0;
const alternative = {
confidence,
transcript: evt.metadata?.transcript
};
const obj = {
language_code: language,
channel_tag: channel,
is_final,
alternatives: [alternative],
vendor: {
name: 'speechmatics',
evt: copy
}
};
return obj;
};
module.exports = (logger) => { module.exports = (logger) => {
const normalizeTranscription = (evt, vendor, channel, language, shortUtterance, punctuation) => { const normalizeTranscription = (evt, vendor, channel, language, shortUtterance, punctuation) => {
@@ -561,10 +463,6 @@ module.exports = (logger) => {
return normalizeCobalt(evt, channel, language); return normalizeCobalt(evt, channel, language);
case 'assemblyai': case 'assemblyai':
return normalizeAssemblyAi(evt, channel, language, shortUtterance); return normalizeAssemblyAi(evt, channel, language, shortUtterance);
case 'verbio':
return normalizeVerbio(evt, channel, language);
case 'speechmatics':
return normalizeSpeechmatics(evt, channel, language);
default: default:
if (vendor.startsWith('custom:')) { if (vendor.startsWith('custom:')) {
return normalizeCustom(evt, channel, language, vendor); return normalizeCustom(evt, channel, language, vendor);
@@ -576,13 +474,27 @@ module.exports = (logger) => {
const setChannelVarsForStt = (task, sttCredentials, language, rOpts = {}) => { const setChannelVarsForStt = (task, sttCredentials, language, rOpts = {}) => {
let opts = {}; let opts = {};
const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {};
const vad = {enable, voiceMs, mode};
const vendor = rOpts.vendor; const vendor = rOpts.vendor;
/* voice activity detection works across vendors */
opts = {
...opts,
...(vad.enable && {START_RECOGNIZING_ON_VAD: 1}),
...(vad.enable && vad.voiceMs && {RECOGNIZER_VAD_VOICE_MS: vad.voiceMs}),
...(vad.enable && typeof vad.mode === 'number' && {RECOGNIZER_VAD_MODE: vad.mode}),
};
if ('google' === vendor) { if ('google' === vendor) {
const useV2 = rOpts.googleOptions?.serviceVersion === 'v2'; const model = task.name === TaskName.Gather ? 'command_and_search' : 'latest_long';
const version = useV2 ? 'v2' : 'v1'; /**
let {model} = rOpts; * When we support google v2 the models are different and we will want something like:
model = model || selectDefaultGoogleModel(task, language, version); * const useV2 = sttCredentials?.credentials?.project_id; //TODO: v2 pref should be set in googleOptions
* const model = task.name === TaskName.Gather ?
* (useV2 ? 'telephony_short' : 'command_and_search') :
* (useV2 ? 'long' : 'latest_long');
*/
opts = { opts = {
...opts, ...opts,
...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}), ...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),
@@ -615,52 +527,25 @@ module.exports = (logger) => {
...{GOOGLE_SPEECH_MODEL: rOpts.model || model}, ...{GOOGLE_SPEECH_MODEL: rOpts.model || model},
...(rOpts.naicsCode > 0 && {GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}), ...(rOpts.naicsCode > 0 && {GOOGLE_SPEECH_METADATA_INDUSTRY_NAICS_CODE: rOpts.naicsCode}),
GOOGLE_SPEECH_METADATA_RECORDING_DEVICE_TYPE: 'phone_line', GOOGLE_SPEECH_METADATA_RECORDING_DEVICE_TYPE: 'phone_line',
/*
...(useV2 && { ...(useV2 && {
GOOGLE_SPEECH_RECOGNIZER_PARENT: `projects/${sttCredentials.credentials.project_id}/locations/global`, GOOGLE_SPEECH_RECOGNIZER_PARENT: `projects/${sttCredentials.credentials.project_id}/locations/global`,
GOOGLE_SPEECH_CLOUD_SERVICES_VERSION: 'v2', GOOGLE_SPEECH_CLOUD_SERVICES_VERSION: 'v2'
...(rOpts.googleOptions?.speechStartTimeoutMs && { }),
GOOGLE_SPEECH_START_TIMEOUT_MS: rOpts.googleOptions.speechStartTimeoutMs */
}),
...(rOpts.googleOptions?.speechEndTimeoutMs && {
GOOGLE_SPEECH_END_TIMEOUT_MS: rOpts.googleOptions.speechEndTimeoutMs
}),
...(rOpts.googleOptions?.transcriptNormalization && {
GOOGLE_SPEECH_TRANSCRIPTION_NORMALIZATION: JSON.stringify(rOpts.googleOptions.transcriptNormalization)
}),
...(rOpts.googleOptions?.enableVoiceActivityEvents && {
GOOGLE_SPEECH_ENABLE_VOICE_ACTIVITY_EVENTS: rOpts.googleOptions.enableVoiceActivityEvents
}),
...(rOpts.sgoogleOptions?.recognizerId) && {GOOGLE_SPEECH_RECOGNIZER_ID: rOpts.googleOptions.recognizerId},
...(rOpts.googleOptions?.enableVoiceActivityEvents && {
GOOGLE_SPEECH_ENABLE_VOICE_ACTIVITY_EVENTS: rOpts.googleOptions.enableVoiceActivityEvents
}),
}),
}; };
} }
else if (['aws', 'polly'].includes(vendor)) { else if (['aws', 'polly'].includes(vendor)) {
const {awsOptions = {}} = rOpts;
const vocabularyName = awsOptions.vocabularyName || rOpts.vocabularyName;
const vocabularyFilterName = awsOptions.vocabularyFilterName || rOpts.vocabularyFilterName;
const filterMethod = awsOptions.vocabularyFilterMethod || rOpts.filterMethod;
opts = { opts = {
...opts, ...opts,
...(vocabularyName && {AWS_VOCABULARY_NAME: vocabularyName}), ...(rOpts.vocabularyName && {AWS_VOCABULARY_NAME: rOpts.vocabularyName}),
...(vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: vocabularyFilterName}), ...(rOpts.vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: rOpts.vocabularyFilterName}),
...(filterMethod && {AWS_VOCABULARY_FILTER_METHOD: filterMethod}), ...(rOpts.filterMethod && {AWS_VOCABULARY_FILTER_METHOD: rOpts.filterMethod}),
...(sttCredentials && { ...(sttCredentials && {
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId, AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey, AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
AWS_REGION: sttCredentials.region, AWS_REGION: sttCredentials.region
AWS_SECURITY_TOKEN: sttCredentials.securityToken
}), }),
...(awsOptions.accessKey && {AWS_ACCESS_KEY_ID: awsOptions.accessKey}),
...(awsOptions.secretKey && {AWS_SECRET_ACCESS_KEY: awsOptions.secretKey}),
...(awsOptions.region && {AWS_REGION: awsOptions.region}),
...(awsOptions.securityToken && {AWS_SECURITY_TOKEN: awsOptions.securityToken}),
...(awsOptions.languageModelName && {AWS_LANGUAGE_MODEL_NAME: awsOptions.languageModelName}),
...(awsOptions.piiEntityTypes?.length && {AWS_PII_ENTITY_TYPES: awsOptions.piiEntityTypes.join(',')}),
...(awsOptions.piiIdentifyEntities && {AWS_PII_IDENTIFY_ENTITIES: true}),
...(awsOptions.languageModelName && {AWS_LANGUAGE_MODEL_NAME: awsOptions.languageModelName}),
}; };
} }
else if ('microsoft' === vendor) { else if ('microsoft' === vendor) {
@@ -688,8 +573,6 @@ module.exports = (logger) => {
{AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS: azureOptions.speechSegmentationSilenceTimeoutMs}), {AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS: azureOptions.speechSegmentationSilenceTimeoutMs}),
...(azureOptions.languageIdMode && ...(azureOptions.languageIdMode &&
{AZURE_LANGUAGE_ID_MODE: azureOptions.languageIdMode}), {AZURE_LANGUAGE_ID_MODE: azureOptions.languageIdMode}),
...(azureOptions.postProcessing &&
{AZURE_POST_PROCESSING_OPTION: azureOptions.postProcessing}),
...(sttCredentials && { ...(sttCredentials && {
...(sttCredentials.api_key && {AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key}), ...(sttCredentials.api_key && {AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key}),
...(sttCredentials.region && {AZURE_REGION: sttCredentials.region}), ...(sttCredentials.region && {AZURE_REGION: sttCredentials.region}),
@@ -797,17 +680,13 @@ module.exports = (logger) => {
...(deepgramOptions.keywords) && ...(deepgramOptions.keywords) &&
{DEEPGRAM_SPEECH_KEYWORDS: deepgramOptions.keywords.join(',')}, {DEEPGRAM_SPEECH_KEYWORDS: deepgramOptions.keywords.join(',')},
...('endpointing' in deepgramOptions) && ...('endpointing' in deepgramOptions) &&
{DEEPGRAM_SPEECH_ENDPOINTING: deepgramOptions.endpointing === false ? 'false' : deepgramOptions.endpointing, {DEEPGRAM_SPEECH_ENDPOINTING: deepgramOptions.endpointing === false ? 'false' : deepgramOptions.endpointing},
// default DEEPGRAM_SPEECH_UTTERANCE_END_MS is 1000, will be override by user settings later if there is.
DEEPGRAM_SPEECH_UTTERANCE_END_MS: 1000},
...(deepgramOptions.utteranceEndMs) && ...(deepgramOptions.utteranceEndMs) &&
{DEEPGRAM_SPEECH_UTTERANCE_END_MS: deepgramOptions.utteranceEndMs}, {DEEPGRAM_SPEECH_UTTERANCE_END_MS: deepgramOptions.utteranceEndMs},
...(deepgramOptions.vadTurnoff) && ...(deepgramOptions.vadTurnoff) &&
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff}, {DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff},
...(deepgramOptions.tag) && ...(deepgramOptions.tag) &&
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag}, {DEEPGRAM_SPEECH_TAG: deepgramOptions.tag}
...(deepgramOptions.version) &&
{DEEPGRAM_SPEECH_MODEL_VERSION: deepgramOptions.version}
}; };
} }
else if ('soniox' === vendor) { else if ('soniox' === vendor) {
@@ -906,8 +785,7 @@ module.exports = (logger) => {
...(cobaltOptions.enableConfusionNetwork && {COBALT_ENABLE_CONFUSION_NETWORK: 1}), ...(cobaltOptions.enableConfusionNetwork && {COBALT_ENABLE_CONFUSION_NETWORK: 1}),
...(cobaltOptions.compiledContextData && {COBALT_COMPILED_CONTEXT_DATA: cobaltOptions.compiledContextData}), ...(cobaltOptions.compiledContextData && {COBALT_COMPILED_CONTEXT_DATA: cobaltOptions.compiledContextData}),
}; };
} } else if ('assemblyai' === vendor) {
else if ('assemblyai' === vendor) {
opts = { opts = {
...opts, ...opts,
...(sttCredentials.api_key) && ...(sttCredentials.api_key) &&
@@ -916,37 +794,8 @@ module.exports = (logger) => {
{ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)}) {ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)})
}; };
} }
else if ('verbio' === vendor) {
const {verbioOptions = {}} = rOpts;
opts = {
...opts,
...(sttCredentials.access_token && { VERBIO_ACCESS_TOKEN: sttCredentials.access_token}),
...(sttCredentials.engine_version && {VERBIO_ENGINE_VERSION: sttCredentials.engine_version}),
...(language && {VERBIO_LANGUAGE: language}),
...(verbioOptions.enable_formatting && {VERBIO_ENABLE_FORMATTING: verbioOptions.enable_formatting}),
...(verbioOptions.enable_diarization && {VERBIO_ENABLE_DIARIZATION: verbioOptions.enable_diarization}),
...(verbioOptions.topic && {VERBIO_TOPIC: verbioOptions.topic}),
...(verbioOptions.inline_grammar && {VERBIO_INLINE_GRAMMAR: verbioOptions.inline_grammar}),
...(verbioOptions.grammar_uri && {VERBIO_GRAMMAR_URI: verbioOptions.grammar_uri}),
...(verbioOptions.label && {VERBIO_LABEL: verbioOptions.label}),
...(verbioOptions.recognition_timeout && {VERBIO_RECOGNITION_TIMEOUT: verbioOptions.recognition_timeout}),
...(verbioOptions.speech_complete_timeout &&
{VERBIO_SPEECH_COMPLETE_TIMEOUT: verbioOptions.speech_complete_timeout}),
...(verbioOptions.speech_incomplete_timeout &&
{VERBIO_SPEECH_INCOMPLETE_TIMEOUT: verbioOptions.speech_incomplete_timeout}),
};
}
else if ('speechmatics' === vendor) {
opts = {
...opts,
...(sttCredentials.api_key) && {SPEECHMATICS_API_KEY: sttCredentials.api_key},
...(sttCredentials.speechmatics_stt_uri) && {SPEECHMATICS_HOST: sttCredentials.speechmatics_stt_uri},
...(rOpts.hints?.length > 0 && {SPEECHMATICS_SPEECH_HINTS: rOpts.hints.join(',')}),
};
}
else if (vendor.startsWith('custom:')) { else if (vendor.startsWith('custom:')) {
let {options = {}} = rOpts.customOptions || {}; let {options = {}} = rOpts;
const {sampleRate} = rOpts.customOptions || {};
const {auth_token, custom_stt_url} = sttCredentials; const {auth_token, custom_stt_url} = sttCredentials;
options = { options = {
...options, ...options,
@@ -954,15 +803,14 @@ module.exports = (logger) => {
{hints: rOpts.hints}), {hints: rOpts.hints}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' && ...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
{hints: JSON.stringify(rOpts.hints)}), {hints: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost}), ...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost})
...(task.cs?.callSid && {callSid: task.cs.callSid})
}; };
opts = { opts = {
...opts, ...opts,
...(auth_token && {JAMBONZ_STT_API_KEY: auth_token}), ...(auth_token && {JAMBONZ_STT_API_KEY: auth_token}),
JAMBONZ_STT_URL: custom_stt_url, JAMBONZ_STT_URL: custom_stt_url,
...(Object.keys(options).length > 0 && {JAMBONZ_STT_OPTIONS: JSON.stringify(options)}), ...(Object.keys(options).length > 0 && {JAMBONZ_STT_OPTIONS: JSON.stringify(options)}),
...(sampleRate && {JAMBONZ_STT_SAMPLING: sampleRate})
}; };
} }
@@ -1012,6 +860,6 @@ module.exports = (logger) => {
setChannelVarsForStt, setChannelVarsForStt,
setSpeechCredentialsAtRuntime, setSpeechCredentialsAtRuntime,
compileSonioxTranscripts, compileSonioxTranscripts,
consolidateTranscripts, consolidateTranscripts
}; };
}; };

View File

@@ -44,7 +44,7 @@ class WsRequestor extends BaseRequestor {
async request(type, hook, params, httpHeaders = {}) { async request(type, hook, params, httpHeaders = {}) {
assert(HookMsgTypes.includes(type)); assert(HookMsgTypes.includes(type));
const url = hook.url || hook; const url = hook.url || hook;
const wantsAck = !['call:status', 'verb:status', 'jambonz:error', 'llm:event', 'llm:tool-call'].includes(type); const wantsAck = !['call:status', 'verb:status', 'jambonz:error'].includes(type);
if (this.maliciousClient) { if (this.maliciousClient) {
this.logger.info({url: this.url}, 'WsRequestor:request - discarding msg to malicious client'); this.logger.info({url: this.url}, 'WsRequestor:request - discarding msg to malicious client');
@@ -56,12 +56,6 @@ class WsRequestor extends BaseRequestor {
} }
if (type === 'session:new') this.call_sid = params.callSid; if (type === 'session:new') this.call_sid = params.callSid;
if (type === 'session:reconnect') {
this._reconnectPromise = new Promise((resolve, reject) => {
this._reconnectResolve = resolve;
this._reconnectReject = reject;
});
}
/* if we have an absolute url, and it is http then do a standard webhook */ /* if we have an absolute url, and it is http then do a standard webhook */
if (this._isAbsoluteUrl(url) && url.startsWith('http')) { if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
@@ -77,23 +71,20 @@ class WsRequestor extends BaseRequestor {
} }
/* connect if necessary */ /* connect if necessary */
const queueMsg = () => {
this.logger.debug(
`WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`);
if (wantsAck) {
const p = new Promise((resolve, reject) => {
this.queuedMsg.push({type, hook, params, httpHeaders, promise: {resolve, reject}});
});
return p;
}
else {
this.queuedMsg.push({type, hook, params, httpHeaders});
}
return;
};
if (!this.ws) { if (!this.ws) {
if (this.connectInProgress) { if (this.connectInProgress) {
return queueMsg(); this.logger.debug(
`WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`);
if (wantsAck) {
const p = new Promise((resolve, reject) => {
this.queuedMsg.push({type, hook, params, httpHeaders, promise: {resolve, reject}});
});
return p;
}
else {
this.queuedMsg.push({type, hook, params, httpHeaders});
}
return;
} }
this.connectInProgress = true; this.connectInProgress = true;
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection for ${type}`); this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection for ${type}`);
@@ -111,10 +102,6 @@ class WsRequestor extends BaseRequestor {
return Promise.reject(err); return Promise.reject(err);
} }
} }
// If jambonz wait for ack from reconnect, queue the msg until reconnect is acked
if (type !== 'session:reconnect' && this._reconnectPromise) {
return queueMsg();
}
assert(this.ws); assert(this.ws);
/* prepare and send message */ /* prepare and send message */
@@ -132,7 +119,7 @@ class WsRequestor extends BaseRequestor {
type, type,
msgid, msgid,
call_sid: this.call_sid, call_sid: this.call_sid,
hook: ['verb:hook', 'session:redirect', 'llm:event', 'llm:tool-call'].includes(type) ? url : undefined, hook: type === 'verb:hook' ? url : undefined,
data: {...payload}, data: {...payload},
...b3 ...b3
}; };
@@ -152,18 +139,6 @@ class WsRequestor extends BaseRequestor {
} }
}; };
const rejectQueuedMsgs = (err) => {
if (this.queuedMsg.length > 0) {
for (const {promise} of this.queuedMsg) {
this.logger.debug(`WsRequestor:request - preparing queued ${type} for rejectQueuedMsgs`);
if (promise) {
promise.reject(err);
}
}
this.queuedMsg.length = 0;
}
};
//this.logger.debug({obj}, `websocket: sending (${url})`); //this.logger.debug({obj}, `websocket: sending (${url})`);
/* special case: reconnecting before we received ack to session:new */ /* special case: reconnecting before we received ack to session:new */
@@ -204,37 +179,16 @@ class WsRequestor extends BaseRequestor {
this.logger.debug({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`); this.logger.debug({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`);
this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']); this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']);
resolve(response); resolve(response);
if (this._reconnectResolve) {
this._reconnectResolve();
}
}, },
failure: (err) => { failure: (err) => {
if (this._reconnectReject) {
this._reconnectReject(err);
}
clearTimeout(timer); clearTimeout(timer);
reject(err); reject(err);
} }
}); });
/* send the message */ /* send the message */
this.ws.send(JSON.stringify(obj), async() => { this.ws.send(JSON.stringify(obj), () => {
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`); this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
// If session:reconnect is waiting for ack, hold here until ack to send queuedMsgs
if (this._reconnectPromise) {
try {
await this._reconnectPromise;
} catch (err) {
// bad thing happened to session:recconnect
rejectQueuedMsgs(err);
this.emit('reconnect-error');
return;
} finally {
this._reconnectPromise = null;
this._reconnectResolve = null;
this._reconnectReject = null;
}
}
sendQueuedMsgs(); sendQueuedMsgs();
}); });
}); });
@@ -392,10 +346,7 @@ class WsRequestor extends BaseRequestor {
/* messages must be JSON format */ /* messages must be JSON format */
try { try {
const obj = JSON.parse(content); const obj = JSON.parse(content);
this.logger.debug({obj}, 'WsRequestor:_onMessage - received message'); const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj;
//const {type, msgid, command, call_sid = this.call_sid, queueCommand = false, data} = obj;
const {type, msgid, command, queueCommand = false, tool_call_id, data} = obj;
const call_sid = obj.callSid || this.call_sid;
//this.logger.debug({obj}, 'WsRequestor:request websocket: received'); //this.logger.debug({obj}, 'WsRequestor:request websocket: received');
assert.ok(type, 'type property not supplied'); assert.ok(type, 'type property not supplied');
@@ -408,8 +359,8 @@ class WsRequestor extends BaseRequestor {
case 'command': case 'command':
assert.ok(command, 'command property not supplied'); assert.ok(command, 'command property not supplied');
assert.ok(data || command === 'llm:tool-output', 'data property not supplied'); assert.ok(data, 'data property not supplied');
this._recvCommand(msgid, command, call_sid, queueCommand, tool_call_id, data); this._recvCommand(msgid, command, call_sid, queueCommand, data);
break; break;
default: default:
@@ -433,10 +384,10 @@ class WsRequestor extends BaseRequestor {
success && success(data); success && success(data);
} }
_recvCommand(msgid, command, call_sid, queueCommand, tool_call_id, data) { _recvCommand(msgid, command, call_sid, queueCommand, data) {
// TODO: validate command // TODO: validate command
this.logger.debug({msgid, command, call_sid, queueCommand, data}, 'received command'); this.logger.debug({msgid, command, call_sid, queueCommand, data}, 'received command');
this.emit('command', {msgid, command, call_sid, queueCommand, tool_call_id, data}); this.emit('command', {msgid, command, call_sid, queueCommand, data});
} }
} }

11916
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "jambonz-feature-server", "name": "jambonz-feature-server",
"version": "0.9.2", "version": "0.8.6",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 18.x" "node": ">= 18.x"
@@ -25,56 +25,57 @@
"jslint:fix": "eslint app.js tracer.js lib --fix" "jslint:fix": "eslint app.js tracer.js lib --fix"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-auto-scaling": "^3.549.0", "@aws-sdk/client-auto-scaling": "^3.360.0",
"@aws-sdk/client-sns": "^3.549.0", "@aws-sdk/client-sns": "^3.360.0",
"@jambonz/db-helpers": "^0.9.6", "@jambonz/db-helpers": "^0.9.3",
"@jambonz/http-health-check": "^0.0.1", "@jambonz/http-health-check": "^0.0.1",
"@jambonz/mw-registrar": "^0.2.7", "@jambonz/mw-registrar": "^0.2.4",
"@jambonz/realtimedb-helpers": "^0.8.8", "@jambonz/realtimedb-helpers": "^0.8.7",
"@jambonz/speech-utils": "^0.1.20", "@jambonz/speech-utils": "^0.0.42",
"@jambonz/stats-collector": "^0.1.10", "@jambonz/stats-collector": "^0.1.9",
"@jambonz/verb-specifications": "^0.0.91", "@jambonz/time-series": "^0.2.8",
"@jambonz/time-series": "^0.2.13", "@jambonz/verb-specifications": "^0.0.63",
"@opentelemetry/api": "^1.8.0", "@opentelemetry/api": "^1.4.0",
"@opentelemetry/exporter-jaeger": "^1.23.0", "@opentelemetry/exporter-jaeger": "^1.9.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0", "@opentelemetry/exporter-trace-otlp-http": "^0.35.0",
"@opentelemetry/exporter-zipkin": "^1.23.0", "@opentelemetry/exporter-zipkin": "^1.9.0",
"@opentelemetry/instrumentation": "^0.50.0", "@opentelemetry/instrumentation": "^0.35.0",
"@opentelemetry/resources": "^1.23.0", "@opentelemetry/resources": "^1.9.0",
"@opentelemetry/sdk-trace-base": "^1.23.0", "@opentelemetry/sdk-trace-base": "^1.9.0",
"@opentelemetry/sdk-trace-node": "^1.23.0", "@opentelemetry/sdk-trace-node": "^1.9.0",
"@opentelemetry/semantic-conventions": "^1.23.0", "@opentelemetry/semantic-conventions": "^1.9.0",
"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.45", "drachtio-fsmrf": "^3.0.38",
"drachtio-srf": "^4.5.35", "drachtio-srf": "^4.5.31",
"express": "^4.19.2", "express": "^4.18.2",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
"moment": "^2.30.1", "ip": "^1.1.9",
"parse-url": "^9.2.0", "moment": "^2.29.4",
"pino": "^8.20.0", "parse-url": "^8.1.0",
"pino": "^8.8.0",
"polly-ssml-split": "^0.1.0", "polly-ssml-split": "^0.1.0",
"proxyquire": "^2.1.3", "proxyquire": "^2.1.3",
"sdp-transform": "^2.14.2", "sdp-transform": "^2.14.1",
"short-uuid": "^5.1.0", "short-uuid": "^4.2.2",
"sinon": "^17.0.1", "sinon": "^15.0.1",
"to-snake-case": "^1.0.0", "to-snake-case": "^1.0.0",
"undici": "^6.20.0", "undici": "^5.28.3",
"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.18.0", "ws": "^8.9.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
"devDependencies": { "devDependencies": {
"clear-module": "^4.1.2", "clear-module": "^4.1.2",
"eslint": "7.32.0", "eslint": "^7.32.0",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^4.3.1",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"tape": "^5.7.5" "tape": "^5.6.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"bufferutil": "^4.0.8", "bufferutil": "^4.0.6",
"utf-8-validate": "^6.0.3" "utf-8-validate": "^5.0.8"
} }
} }

View File

@@ -222,62 +222,3 @@ test('test create-call app_json', async(t) => {
t.error(err); t.error(err);
} }
}); });
test('test create-call timeLimit', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let from = 'create-call-app-json';
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 startTime = Date.now();
const app_json = `[
{
"verb": "pause",
"length": 7
}
]`;
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"
},
app_json,
"from": from,
"to": {
"type": "phone",
"number": "15583084809"
},
"timeLimit": 1,
"speech_recognizer_vendor": "google",
"speech_recognizer_language": "en"
});
//THEN
await p;
const endTime = Date.now();
t.ok(endTime - startTime < 2000, 'create-call: timeLimit is respected');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,5 +1,4 @@
/* SQLEditor (MySQL (2))*/ /* SQLEditor (MySQL (2))*/
SET FOREIGN_KEY_CHECKS=0; SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS account_static_ips; DROP TABLE IF EXISTS account_static_ips;
@@ -54,8 +53,6 @@ DROP TABLE IF EXISTS signup_history;
DROP TABLE IF EXISTS smpp_addresses; DROP TABLE IF EXISTS smpp_addresses;
DROP TABLE IF EXISTS google_custom_voices;
DROP TABLE IF EXISTS speech_credentials; DROP TABLE IF EXISTS speech_credentials;
DROP TABLE IF EXISTS system_information; DROP TABLE IF EXISTS system_information;
@@ -139,9 +136,6 @@ account_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT 1,
username VARCHAR(64), username VARCHAR(64),
password VARCHAR(1024), password VARCHAR(1024),
allow_direct_app_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_queue_calling BOOLEAN NOT NULL DEFAULT 1,
allow_direct_user_calling BOOLEAN NOT NULL DEFAULT 1,
PRIMARY KEY (client_sid) PRIMARY KEY (client_sid)
); );
@@ -344,23 +338,11 @@ label VARCHAR(64),
PRIMARY KEY (speech_credential_sid) PRIMARY KEY (speech_credential_sid)
); );
CREATE TABLE google_custom_voices
(
google_custom_voice_sid CHAR(36) NOT NULL UNIQUE ,
speech_credential_sid CHAR(36) NOT NULL,
model VARCHAR(512) NOT NULL,
reported_usage ENUM('REPORTED_USAGE_UNSPECIFIED','REALTIME','OFFLINE') DEFAULT 'REALTIME',
name VARCHAR(64) NOT NULL,
PRIMARY KEY (google_custom_voice_sid)
);
CREATE TABLE system_information CREATE TABLE system_information
( (
domain_name VARCHAR(255), domain_name VARCHAR(255),
sip_domain_name VARCHAR(255), sip_domain_name VARCHAR(255),
monitoring_domain_name VARCHAR(255), monitoring_domain_name VARCHAR(255)
private_network_cidr VARCHAR(8192),
log_level ENUM('info', 'debug') NOT NULL DEFAULT 'info'
); );
CREATE TABLE users CREATE TABLE users
@@ -455,14 +437,11 @@ CREATE TABLE sip_gateways
sip_gateway_sid CHAR(36), sip_gateway_sid CHAR(36),
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.', ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.',
netmask INTEGER NOT NULL DEFAULT 32, netmask INTEGER NOT NULL DEFAULT 32,
port INTEGER COMMENT 'sip signaling port', port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port',
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway', inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway',
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN', outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
voip_carrier_sid CHAR(36) NOT NULL, voip_carrier_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT 1,
send_options_ping BOOLEAN NOT NULL DEFAULT 0,
use_sips_scheme BOOLEAN NOT NULL DEFAULT 0,
pad_crypto BOOLEAN NOT NULL DEFAULT 0,
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol', protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
PRIMARY KEY (sip_gateway_sid) PRIMARY KEY (sip_gateway_sid)
) COMMENT='A whitelisted sip gateway used for origination/termination'; ) COMMENT='A whitelisted sip gateway used for origination/termination';
@@ -499,19 +478,11 @@ messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
app_json TEXT, app_json TEXT,
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google', speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US', speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
speech_synthesis_voice VARCHAR(256), speech_synthesis_voice VARCHAR(64),
speech_synthesis_label VARCHAR(64), speech_synthesis_label VARCHAR(64),
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google', speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US', speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
speech_recognizer_label VARCHAR(64), speech_recognizer_label VARCHAR(64),
use_for_fallback_speech BOOLEAN DEFAULT false,
fallback_speech_synthesis_vendor VARCHAR(64),
fallback_speech_synthesis_language VARCHAR(12),
fallback_speech_synthesis_voice VARCHAR(256),
fallback_speech_synthesis_label VARCHAR(64),
fallback_speech_recognizer_vendor VARCHAR(64),
fallback_speech_recognizer_language VARCHAR(64),
fallback_speech_recognizer_label VARCHAR(64),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
record_all_calls BOOLEAN NOT NULL DEFAULT false, record_all_calls BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (application_sid) PRIMARY KEY (application_sid)
@@ -554,7 +525,6 @@ siprec_hook_sid CHAR(36),
record_all_calls BOOLEAN NOT NULL DEFAULT false, record_all_calls BOOLEAN NOT NULL DEFAULT false,
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3', record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service', bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
enable_debug_log BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (account_sid) PRIMARY KEY (account_sid)
) COMMENT='An enterprise that uses the platform for comm services'; ) COMMENT='An enterprise that uses the platform for comm services';
@@ -649,10 +619,6 @@ ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (ser
CREATE INDEX account_sid_idx ON speech_credentials (account_sid); CREATE INDEX account_sid_idx ON speech_credentials (account_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX google_custom_voice_sid_idx ON google_custom_voices (google_custom_voice_sid);
CREATE INDEX speech_credential_sid_idx ON google_custom_voices (speech_credential_sid);
ALTER TABLE google_custom_voices ADD FOREIGN KEY speech_credential_sid_idxfk (speech_credential_sid) REFERENCES speech_credentials (speech_credential_sid) ON DELETE CASCADE;
CREATE INDEX user_sid_idx ON users (user_sid); CREATE INDEX user_sid_idx ON users (user_sid);
CREATE INDEX email_idx ON users (email); CREATE INDEX email_idx ON users (email);
CREATE INDEX phone_idx ON users (phone); CREATE INDEX phone_idx ON users (phone);
@@ -738,5 +704,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid); ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid); ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
SET FOREIGN_KEY_CHECKS=1;
SET FOREIGN_KEY_CHECKS=1;

View File

@@ -57,7 +57,7 @@ services:
condition: service_healthy condition: service_healthy
freeswitch: freeswitch:
image: drachtio/drachtio-freeswitch-mrf:latest image: drachtio/drachtio-freeswitch-mrf:0.6.2
restart: always restart: always
command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100 command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100
environment: environment: