Compare commits

..

1 Commits

Author SHA1 Message Date
Dave Horton
f975338232 #412 - dont delay sending call status when stopping background listen 2023-08-02 12:37:23 -04:00
56 changed files with 1497 additions and 6873 deletions

View File

@@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 16
- run: npm ci - run: npm ci
- run: npm run jslint - run: npm run jslint
- run: docker pull drachtio/sipp - run: docker pull drachtio/sipp

View File

@@ -1,4 +1,4 @@
# jambonz-feature-server [![CI](https://github.com/jambonz/jambonz-feature-server/actions/workflows/build.yml/badge.svg)](https://github.com/jambonz/jambonz-feature-server/actions/workflows/build.yml) # jambones-feature-server ![Build Status](https://github.com/jambonz/jambonz-feature-server/workflows/CI/badge.svg)
This application implements the core feature server of the jambones platform. This application implements the core feature server of the jambones platform.
@@ -37,11 +37,6 @@ Configuration is provided via environment variables:
|STATS_PORT| listening port for metrics host|no| |STATS_PORT| listening port for metrics host|no|
|STATS_PROTOCOL| 'tcp' or 'udp'|no| |STATS_PROTOCOL| 'tcp' or 'udp'|no|
|STATS_TELEGRAF| if 1, metrics will be generated in telegraf format|no| |STATS_TELEGRAF| if 1, metrics will be generated in telegraf format|no|
|JAMBONZ_RECORD_WS_BASE_URL| recording websocket URL to send the recording audio|no|
|JAMBONZ_RECORD_WS_USERNAME| recording websocket username|no|
|JAMBONZ_RECORD_WS_PASSWORD| recording websocket password|no|
|ANCHOR_MEDIA_ALWAYS| keep media on media server|no|
|JAMBONZ_DISABLE_DIAL_PAI_HEADER| control P-Asserted-Identity header on B-Leg|no|
### running under pm2 ### running under pm2
Typically, this application runs under [pm2](https://pm2.io) using an [ecosystem.config.js](https://pm2.keymetrics.io/docs/usage/application-declaration/) file similar to this: Typically, this application runs under [pm2](https://pm2.io) using an [ecosystem.config.js](https://pm2.keymetrics.io/docs/usage/application-declaration/) file similar to this:

2
app.js
View File

@@ -109,7 +109,7 @@ const disconnect = () => {
httpServer?.on('close', resolve); httpServer?.on('close', resolve);
httpServer?.close(); httpServer?.close();
srf.disconnect(); srf.disconnect();
srf.locals.mediaservers?.forEach((ms) => ms.disconnect()); srf.locals.mediaservers.forEach((ms) => ms.disconnect());
}); });
}; };

0
bin/k8s-pre-stop-hook.js Normal file → Executable file
View File

View File

@@ -45,7 +45,7 @@ The GCP credential is the JSON service key in stringified format.
#### Install Docker #### Install Docker
The test suite also requires [Docker](https://www.docker.com/) and docker-compose to be installed on your laptop. Docker is used to set up a network with all of the elements required to test the jambonz-feature-server in a black-box type of fashion. The test suite ralso equires [Docker](https://www.docker.com/) and docker-compose to be installed on your laptop. Docker is used to set up a network with all of the elements required to test the jambonz-feature-server in a black-box type of fashion.
Once you have docker installed, you can optionally make sure everything Docker-wise is working properly by running this command from the project folder: Once you have docker installed, you can optionally make sure everything Docker-wise is working properly by running this command from the project folder:

View File

@@ -25,9 +25,13 @@ const JAMBONES_MYSQL_USER = process.env.JAMBONES_MYSQL_USER;
const JAMBONES_MYSQL_PASSWORD = process.env.JAMBONES_MYSQL_PASSWORD; const JAMBONES_MYSQL_PASSWORD = process.env.JAMBONES_MYSQL_PASSWORD;
const JAMBONES_MYSQL_DATABASE = process.env.JAMBONES_MYSQL_DATABASE; const JAMBONES_MYSQL_DATABASE = process.env.JAMBONES_MYSQL_DATABASE;
const JAMBONES_MYSQL_PORT = parseInt(process.env.JAMBONES_MYSQL_PORT, 10) || 3306; const JAMBONES_MYSQL_PORT = parseInt(process.env.JAMBONES_MYSQL_PORT, 10) || 3306;
const JAMBONES_MYSQL_REFRESH_TTL = parseInt(process.env.JAMBONES_MYSQL_REFRESH_TTL, 10) || 0; const JAMBONES_MYSQL_REFRESH_TTL = process.env.JAMBONES_MYSQL_REFRESH_TTL;
const JAMBONES_MYSQL_CONNECTION_LIMIT = parseInt(process.env.JAMBONES_MYSQL_CONNECTION_LIMIT, 10) || 10; const JAMBONES_MYSQL_CONNECTION_LIMIT = parseInt(process.env.JAMBONES_MYSQL_CONNECTION_LIMIT, 10) || 10;
/* redis */
const JAMBONES_REDIS_HOST = process.env.JAMBONES_REDIS_HOST;
const JAMBONES_REDIS_PORT = parseInt(process.env.JAMBONES_REDIS_PORT, 10) || 6379;
/* gather and hints */ /* gather and hints */
const JAMBONES_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONES_GATHER_EARLY_HINTS_MATCH; const JAMBONES_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONES_GATHER_EARLY_HINTS_MATCH;
const JAMBONZ_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONZ_GATHER_EARLY_HINTS_MATCH; const JAMBONZ_GATHER_EARLY_HINTS_MATCH = process.env.JAMBONZ_GATHER_EARLY_HINTS_MATCH;
@@ -117,20 +121,32 @@ const HTTP_POOL = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
const HTTP_POOLSIZE = parseInt(process.env.HTTP_POOLSIZE, 10) || 10; const HTTP_POOLSIZE = parseInt(process.env.HTTP_POOLSIZE, 10) || 10;
const HTTP_PIPELINING = parseInt(process.env.HTTP_PIPELINING, 10) || 1; const HTTP_PIPELINING = parseInt(process.env.HTTP_PIPELINING, 10) || 1;
const HTTP_TIMEOUT = 10000; const HTTP_TIMEOUT = 10000;
const HTTP_PROXY_IP = process.env.JAMBONES_HTTP_PROXY_IP;
const HTTP_PROXY_PORT = process.env.JAMBONES_HTTP_PROXY_PORT;
const HTTP_PROXY_PROTOCOL = process.env.JAMBONES_HTTP_PROXY_PROTOCOL || 'http';
const HTTP_USER_AGENT_HEADER = process.env.JAMBONES_HTTP_USER_AGENT_HEADER || 'jambonz';
const OPTIONS_PING_INTERVAL = parseInt(process.env.OPTIONS_PING_INTERVAL, 10) || 30000; const OPTIONS_PING_INTERVAL = parseInt(process.env.OPTIONS_PING_INTERVAL, 10) || 30000;
const JAMBONZ_RECORD_WS_BASE_URL = process.env.JAMBONZ_RECORD_WS_BASE_URL || process.env.JAMBONES_RECORD_WS_BASE_URL; const JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
const JAMBONZ_RECORD_WS_USERNAME = process.env.JAMBONZ_RECORD_WS_USERNAME || process.env.JAMBONES_RECORD_WS_USERNAME; sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
const JAMBONZ_RECORD_WS_PASSWORD = process.env.JAMBONZ_RECORD_WS_PASSWORD || process.env.JAMBONES_RECORD_WS_PASSWORD; let host, port = 26379;
const JAMBONZ_DISABLE_DIAL_PAI_HEADER = process.env.JAMBONZ_DISABLE_DIAL_PAI_HEADER || false; if (sentinel.includes(':')) {
const JAMBONES_DISABLE_DIRECT_P2P_CALL = process.env.JAMBONES_DISABLE_DIRECT_P2P_CALL || false; const arr = sentinel.split(':');
host = arr[0];
const JAMBONES_EAGERLY_PRE_CACHE_AUDIO = process.env.JAMBONES_EAGERLY_PRE_CACHE_AUDIO; port = parseInt(arr[1], 10);
} else {
host = sentinel;
}
return {host, port};
}),
name: process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
...(process.env.JAMBONES_REDIS_SENTINEL_PASSWORD && {
password: process.env.JAMBONES_REDIS_SENTINEL_PASSWORD
}),
...(process.env.JAMBONES_REDIS_SENTINEL_USERNAME && {
username: process.env.JAMBONES_REDIS_SENTINEL_USERNAME
})
} : null;
const JAMBONZ_RECORD_WS_BASE_URL = process.env.JAMBONZ_RECORD_WS_BASE_URL;
const JAMBONZ_RECORD_WS_USERNAME = process.env.JAMBONZ_RECORD_WS_USERNAME;
const JAMBONZ_RECORD_WS_PASSWORD = process.env.JAMBONZ_RECORD_WS_PASSWORD;
module.exports = { module.exports = {
JAMBONES_MYSQL_HOST, JAMBONES_MYSQL_HOST,
@@ -149,12 +165,14 @@ module.exports = {
JAMBONZ_GATHER_EARLY_HINTS_MATCH, JAMBONZ_GATHER_EARLY_HINTS_MATCH,
JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS, JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS,
JAMBONES_FREESWITCH, JAMBONES_FREESWITCH,
JAMBONES_REDIS_HOST,
JAMBONES_REDIS_PORT,
JAMBONES_REDIS_SENTINELS,
SMPP_URL, SMPP_URL,
JAMBONES_NETWORK_CIDR, JAMBONES_NETWORK_CIDR,
JAMBONES_API_BASE_URL, JAMBONES_API_BASE_URL,
JAMBONES_TIME_SERIES_HOST, JAMBONES_TIME_SERIES_HOST,
JAMBONES_INJECT_CONTENT, JAMBONES_INJECT_CONTENT,
JAMBONES_EAGERLY_PRE_CACHE_AUDIO,
JAMBONES_ESL_LISTEN_ADDRESS, JAMBONES_ESL_LISTEN_ADDRESS,
JAMBONES_SBCS, JAMBONES_SBCS,
JAMBONES_OTEL_ENABLED, JAMBONES_OTEL_ENABLED,
@@ -194,10 +212,6 @@ module.exports = {
HTTP_POOLSIZE, HTTP_POOLSIZE,
HTTP_PIPELINING, HTTP_PIPELINING,
HTTP_TIMEOUT, HTTP_TIMEOUT,
HTTP_PROXY_IP,
HTTP_PROXY_PORT,
HTTP_PROXY_PROTOCOL,
HTTP_USER_AGENT_HEADER,
OPTIONS_PING_INTERVAL, OPTIONS_PING_INTERVAL,
RESPONSE_TIMEOUT_MS, RESPONSE_TIMEOUT_MS,
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS, JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
@@ -211,7 +225,5 @@ module.exports = {
DEEPGRAM_API_KEY, DEEPGRAM_API_KEY,
JAMBONZ_RECORD_WS_BASE_URL, JAMBONZ_RECORD_WS_BASE_URL,
JAMBONZ_RECORD_WS_USERNAME, JAMBONZ_RECORD_WS_USERNAME,
JAMBONZ_RECORD_WS_PASSWORD, JAMBONZ_RECORD_WS_PASSWORD
JAMBONZ_DISABLE_DIAL_PAI_HEADER,
JAMBONES_DISABLE_DIRECT_P2P_CALL
}; };

View File

@@ -1,54 +0,0 @@
const appsMap = {
queue: {
// Dummy hook to follow later feature server logic.
call_hook: {
url: 'https://jambonz.org',
method: 'GET'
},
account_sid: '',
app_json: [{
verb: 'dequeue',
name: '',
timeout: 5
}]
},
user: {
// Dummy hook to follow later feature server logic.
call_hook: {
url: 'https://jambonz.org',
method: 'GET'
},
account_sid: '',
app_json: [{
verb: 'dial',
callerId: '',
answerOnBridge: true,
target: [
{
type: 'user',
name: ''
}
]
}]
}
};
const createJambonzApp = (type, {account_sid, name, caller_id}) => {
const app = {...appsMap[type]};
app.account_sid = account_sid;
switch (type) {
case 'queue':
app.app_json[0].name = name;
break;
case 'user':
app.app_json[0].callerId = caller_id;
app.app_json[0].target[0].name = name;
break;
}
app.app_json = JSON.stringify(app.app_json);
return app;
};
module.exports = {
createJambonzApp
};

View File

@@ -5,337 +5,276 @@ const CallInfo = require('../../session/call-info');
const {CallDirection, CallStatus} = require('../../utils/constants'); const {CallDirection, CallStatus} = require('../../utils/constants');
const uuidv4 = require('uuid-random'); const uuidv4 = require('uuid-random');
const SipError = require('drachtio-srf').SipError; const SipError = require('drachtio-srf').SipError;
const { validationResult, body } = require('express-validator');
const { validate } = require('@jambonz/verb-specifications');
const sysError = require('./error'); const sysError = require('./error');
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 RootSpan = require('../../utils/call-tracer'); const RootSpan = require('../../utils/call-tracer');
const dbUtils = require('../../utils/db-utils'); const dbUtils = require('../../utils/db-utils');
const { mergeSdpMedia, extractSdpMedia } = require('../../utils/sdp-utils');
const { createCallSchema, customSanitizeFunction } = require('../schemas/create-call');
const removeNullProperties = (obj) => (Object.keys(obj).forEach((key) => obj[key] === null && delete obj[key]), obj); router.post('/', async(req, res) => {
const removeNulls = (req, res, next) => { const {logger} = req.app.locals;
req.body = removeNullProperties(req.body); const accountSid = req.body.account_sid;
next(); const {srf} = require('../../..');
};
router.post('/', logger.debug({body: req.body}, 'got createCall request');
removeNulls, try {
createCallSchema, let uri, cs, to;
body('tag').custom((value) => { // app_json is creaeted by only api-server.
if (value) { // if it available, take it and delete before creating task
customSanitizeFunction(value); const app_json = req.body.app_json;
} delete req.body.app_json;
return true; const restDial = makeTask(logger, {'rest:dial': req.body});
}), restDial.appJson = app_json;
async(req, res) => { const {lookupAccountDetails, lookupCarrierByPhoneNumber, lookupCarrier} = dbUtils(logger, srf);
const {logger} = req.app.locals; const {
const errors = validationResult(req); lookupAppBySid
if (!errors.isEmpty()) { } = srf.locals.dbHelpers;
logger.info({errors: errors.array()}, 'POST /Calls: validation errors'); const {getSBC, getFreeswitch} = srf.locals;
return res.status(400).json({ errors: errors.array() }); const sbcAddress = getSBC();
} if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
const accountSid = req.body.account_sid; const target = restDial.to;
const {srf} = require('../../..'); const opts = {
callingNumber: restDial.from,
const app_json = req.body['app_json']; ...(restDial.callerName && {callingName: restDial.callerName}),
try { headers: req.body.headers || {}
// app_json is created only by api-server. };
if (app_json) {
// if available, delete from req before creating task
delete req.body.app_json;
// validate possible app_json via verb-specifications
validate(logger, JSON.parse(app_json));
}
} catch (err) {
logger.debug({ err }, `invalid app_json: ${err.message}`);
}
logger.debug({body: req.body}, 'got createCall request');
try {
let uri, cs, to;
const restDial = makeTask(logger, { 'rest:dial': req.body });
restDial.appJson = app_json;
const {lookupAccountDetails, lookupCarrierByPhoneNumber, lookupCarrier} = dbUtils(logger, srf);
const {
lookupAppBySid
} = srf.locals.dbHelpers;
const {getSBC, getFreeswitch} = srf.locals;
const sbcAddress = getSBC();
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
const target = restDial.to;
const opts = {
callingNumber: restDial.from,
...(restDial.callerName && {callingName: restDial.callerName}),
headers: req.body.headers || {}
};
const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers; const {lookupTeamsByAccount, lookupAccountBySid} = srf.locals.dbHelpers;
const account = await lookupAccountBySid(req.body.account_sid); const account = await lookupAccountBySid(req.body.account_sid);
const accountInfo = await lookupAccountDetails(req.body.account_sid); const accountInfo = await lookupAccountDetails(req.body.account_sid);
const callSid = uuidv4(); const callSid = uuidv4();
const application = req.body.application_sid ? await lookupAppBySid(req.body.application_sid) : null; const application = req.body.application_sid ? await lookupAppBySid(req.body.application_sid) : null;
const record_all_calls = account.record_all_calls || (application && application.record_all_calls); const record_all_calls = account.record_all_calls || (application && application.record_all_calls);
const recordOutputFormat = account.record_format || 'mp3'; const recordOutputFormat = account.record_format || 'mp3';
const rootSpan = new RootSpan('rest-call', {
callSid,
accountSid,
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid})
});
opts.headers = { opts.headers = {
...opts.headers, ...opts.headers,
'X-Jambonz-Routing': target.type, 'X-Jambonz-Routing': target.type,
'X-Jambonz-FS-UUID': srf.locals.fsUUID, 'X-Jambonz-FS-UUID': srf.locals.fsUUID,
'X-Call-Sid': callSid, 'X-Call-Sid': callSid,
'X-Account-Sid': accountSid, 'X-Account-Sid': accountSid,
'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}) };
};
switch (target.type) { switch (target.type) {
case 'phone': case 'phone':
case 'teams': case 'teams':
uri = `sip:${target.number}@${sbcAddress}`; uri = `sip:${target.number}@${sbcAddress}`;
to = target.number; to = target.number;
if ('teams' === target.type) { if ('teams' === target.type) {
const obj = await lookupTeamsByAccount(accountSid); const obj = await lookupTeamsByAccount(accountSid);
if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info'); if (!obj) throw new Error('dial to ms teams not allowed; account must first be configured with teams info');
Object.assign(opts.headers, { Object.assign(opts.headers, {
'X-MS-Teams-FQDN': obj.ms_teams_fqdn, 'X-MS-Teams-FQDN': obj.ms_teams_fqdn,
'X-MS-Teams-Tenant-FQDN': target.tenant || obj.tenant_fqdn 'X-MS-Teams-Tenant-FQDN': target.tenant || obj.tenant_fqdn
}); });
if (target.vmail === true) uri = `${uri};opaque=app:voicemail`; if (target.vmail === true) uri = `${uri};opaque=app:voicemail`;
}
break;
case 'user':
uri = `sip:${target.name}`;
to = target.name;
if (target.overrideTo) {
Object.assign(opts.headers, {
'X-Override-To': target.overrideTo
});
}
break;
case 'sip':
uri = target.sipUri;
to = uri;
break;
}
if (target.type === 'phone' && target.trunk) {
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
logger.info(
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
if (voip_carrier_sid) {
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
} }
} break;
case 'user':
uri = `sip:${target.name}`;
to = target.name;
if (target.overrideTo) {
Object.assign(opts.headers, {
'X-Override-To': target.overrideTo
});
}
break;
case 'sip':
uri = target.sipUri;
to = uri;
break;
}
/** if (target.type === 'phone' && target.trunk) {
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
logger.info(
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
if (voip_carrier_sid) {
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
}
}
/**
* trunk isn't specified, * trunk isn't specified,
* check if from-number matches any existing numbers on Jambonz * check if from-number matches any existing numbers on Jambonz
* */ * */
if (target.type === 'phone' && !target.trunk) { if (target.type === 'phone' && !target.trunk) {
const str = restDial.from || ''; const str = restDial.from || '';
const callingNumber = str.startsWith('+') ? str.substring(1) : str; const callingNumber = str.startsWith('+') ? str.substring(1) : str;
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber); const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber);
logger.info( logger.info(
`createCall: selected ${voip_carrier_sid} for requested phone number: ${callingNumber || 'unspecified'})`); `createCall: selected ${voip_carrier_sid} for requested phone number: ${callingNumber || 'unspecified'})`);
if (voip_carrier_sid) { if (voip_carrier_sid) {
opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid; opts.headers['X-Requested-Carrier-Sid'] = voip_carrier_sid;
}
} }
}
/* create endpoint for outdial */ /* create endpoint for outdial */
const ms = getFreeswitch(); const ms = getFreeswitch();
if (!ms) throw new Error('no available Freeswitch for outbound call creation'); if (!ms) throw new Error('no available Freeswitch for outbound call creation');
const ep = await ms.createEndpoint(); const ep = await ms.createEndpoint();
logger.debug(`createCall: successfully allocated endpoint, sending INVITE to ${sbcAddress}`); logger.debug(`createCall: successfully allocated endpoint, sending INVITE to ${sbcAddress}`);
/* launch outdial */ /* launch outdial */
let sdp, sipLogger; let sdp, sipLogger;
let dualEp; const connectStream = async(remoteSdp) => {
let localSdp = ep.local.sdp; if (remoteSdp !== sdp) {
ep.modify(sdp = remoteSdp);
if (req.body.dual_streams) { return true;
dualEp = await ms.createEndpoint();
localSdp = mergeSdpMedia(localSdp, dualEp.local.sdp);
} }
return false;
const connectStream = async(remoteSdp) => { };
if (remoteSdp !== sdp) { Object.assign(opts, {
sdp = remoteSdp; proxy: `sip:${sbcAddress}`,
if (req.body.dual_streams) { localSdp: ep.local.sdp
const [sdpLegA, sdpLebB] = extractSdpMedia(remoteSdp); });
if (target.auth) opts.auth = target.auth;
await ep.modify(sdpLegA);
await dualEp.modify(sdpLebB);
await ep.bridge(dualEp);
} else {
ep.modify(sdp);
}
return true;
}
return false;
};
Object.assign(opts, {
proxy: `sip:${sbcAddress}`,
localSdp
});
if (target.auth) opts.auth = target.auth;
/** /**
* create our application object - * create our application object -
* not from the database as per an inbound call, * not from the database as per an inbound call,
* but from the provided params in the request * but from the provided params in the request
*/ */
const app = req.body; const app = req.body;
/** /**
* attach our requestor and notifier objects * attach our requestor and notifier objects
* these will be used for all http requests we make during this call * these will be used for all http requests we make during this call
*/ */
if ('WS' === app.call_hook?.method || /^wss?:/.test(app.call_hook.url)) { if ('WS' === app.call_hook?.method || /^wss?:/.test(app.call_hook.url)) {
logger.debug({call_hook: app.call_hook}, 'creating websocket for call hook'); logger.debug({call_hook: app.call_hook}, 'creating websocket for call hook');
app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ; app.requestor = new WsRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret) ;
if (app.call_hook.url === app.call_status_hook.url || !app.call_status_hook?.url) { if (app.call_hook.url === app.call_status_hook.url || !app.call_status_hook?.url) {
logger.debug('reusing websocket for call status hook'); logger.debug('reusing websocket for call status hook');
app.notifier = app.requestor; app.notifier = app.requestor;
}
}
else {
logger.debug({call_hook: app.call_hook}, 'creating http client for call hook');
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
}
if (!app.notifier && app.call_status_hook) {
app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook');
}
else if (!app.notifier) {
logger.debug('creating null call status hook');
app.notifier = {request: () => {}, close: () => {}};
} }
}
else {
logger.debug({call_hook: app.call_hook}, 'creating http client for call hook');
app.requestor = new HttpRequestor(logger, account.account_sid, app.call_hook, account.webhook_secret);
}
if (!app.notifier && app.call_status_hook) {
app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook');
}
else if (!app.notifier) {
logger.debug('creating null call status hook');
app.notifier = {request: () => {}, close: () => {}};
}
/* now launch the outdial */ /* now launch the outdial */
try { try {
const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, { const dlg = await srf.createUAC(uri, {...opts, followRedirects: true, keepUriOnRedirect: true}, {
cbRequest: (err, inviteReq) => { cbRequest: (err, inviteReq) => {
/* in case of 302 redirect, this gets called twice, ignore the second /* in case of 302 redirect, this gets called twice, ignore the second
except to update the req so that it can later be canceled if need be except to update the req so that it can later be canceled if need be
*/ */
if (res.headersSent) { if (res.headersSent) {
logger.info(`create-call: got redirect, updating request to new call-id ${req.get('Call-ID')}`); logger.info(`create-call: got redirect, updating request to new call-id ${req.get('Call-ID')}`);
if (cs) cs.req = inviteReq; if (cs) cs.req = inviteReq;
return; return;
}
if (err) {
logger.error(err, 'createCall Error creating call');
res.status(500).send('Call Failure');
return;
}
inviteReq.srf = srf;
inviteReq.locals = {
...(inviteReq || {}),
callSid,
application_sid: app.application_sid
};
/* ok our outbound INVITE is in flight */
const tasks = [restDial];
sipLogger = logger.child({
callSid,
callId: inviteReq.get('Call-ID'),
accountSid,
traceId: rootSpan.traceId
});
app.requestor.logger = app.notifier.logger = sipLogger;
const callInfo = new CallInfo({
direction: CallDirection.Outbound,
req: inviteReq,
to,
tag: app.tag,
callSid,
accountSid: req.body.account_sid,
applicationSid: app.application_sid,
traceId: rootSpan.traceId
});
cs = new RestCallSession({
logger: sipLogger,
application: app,
srf,
req: inviteReq,
ep,
ep2: dualEp,
tasks,
callInfo,
accountInfo,
rootSpan
});
cs.exec(req);
res.status(201).json({sid: cs.callSid, callId: inviteReq.get('Call-ID')});
sipLogger.info({sid: cs.callSid, callId: inviteReq.get('Call-ID')},
`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
},
cbProvisional: (prov) => {
const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing;
if ([180, 183].includes(prov.status) && prov.body) connectStream(prov.body);
restDial.emit('callStatus', prov.status, !!prov.body);
cs.emit('callStatusChange', {callStatus, sipStatus: prov.status});
} }
});
connectStream(dlg.remote.sdp); if (err) {
cs.emit('callStatusChange', { logger.error(err, 'createCall Error creating call');
callStatus: CallStatus.InProgress, res.status(500).send('Call Failure');
sipStatus: 200, return;
sipReason: 'OK' }
}); inviteReq.srf = srf;
restDial.emit('callStatus', 200); inviteReq.locals = {
restDial.emit('connect', dlg); ...(inviteReq || {}),
} callSid,
catch (err) { application_sid: app.application_sid
let callStatus = CallStatus.Failed; };
if (err instanceof SipError) { /* ok our outbound INVITE is in flight */
if ([486, 603].includes(err.status)) callStatus = CallStatus.Busy;
else if (487 === err.status) callStatus = CallStatus.NoAnswer; const tasks = [restDial];
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`); const rootSpan = new RootSpan('rest-call', inviteReq);
else console.log(`REST outdial failed with ${err.status}`); sipLogger = logger.child({
if (cs) cs.emit('callStatusChange', { callSid,
callStatus, callId: inviteReq.get('Call-ID'),
sipStatus: err.status, accountSid,
sipReason: err.reason traceId: rootSpan.traceId
}); });
cs.callGone = true; app.requestor.logger = app.notifier.logger = sipLogger;
} const callInfo = new CallInfo({
else { direction: CallDirection.Outbound,
if (cs) cs.emit('callStatusChange', { req: inviteReq,
callStatus, to,
sipStatus: 500, tag: app.tag,
sipReason: 'Internal Server Error' callSid,
accountSid: req.body.account_sid,
applicationSid: app.application_sid,
traceId: rootSpan.traceId
}); });
if (sipLogger) sipLogger.error({err}, 'REST outdial failed'); cs = new RestCallSession({
else console.error(err); logger: sipLogger,
application: app,
srf,
req: inviteReq,
ep,
tasks,
callInfo,
accountInfo,
rootSpan
});
cs.exec(req);
res.status(201).json({sid: cs.callSid, callId: inviteReq.get('Call-ID')});
sipLogger.info({sid: cs.callSid, callId: inviteReq.get('Call-ID')},
`outbound REST call attempt to ${JSON.stringify(target)} has been sent`);
},
cbProvisional: (prov) => {
const callStatus = prov.body ? CallStatus.EarlyMedia : CallStatus.Ringing;
if ([180, 183].includes(prov.status) && prov.body) connectStream(prov.body);
restDial.emit('callStatus', prov.status, !!prov.body);
cs.emit('callStatusChange', {callStatus, sipStatus: prov.status});
} }
ep.destroy(); });
if (dualEp) { connectStream(dlg.remote.sdp);
dualEp.destroy(); cs.emit('callStatusChange', {
} callStatus: CallStatus.InProgress,
setTimeout(restDial.kill.bind(restDial, cs), 5000); sipStatus: 200,
} sipReason: 'OK'
} catch (err) { });
sysError(logger, res, err); restDial.emit('callStatus', 200);
restDial.emit('connect', dlg);
} }
}); catch (err) {
let callStatus = CallStatus.Failed;
if (err instanceof SipError) {
if ([486, 603].includes(err.status)) callStatus = CallStatus.Busy;
else if (487 === err.status) callStatus = CallStatus.NoAnswer;
if (sipLogger) sipLogger.info(`REST outdial failed with ${err.status}`);
else console.log(`REST outdial failed with ${err.status}`);
if (cs) cs.emit('callStatusChange', {
callStatus,
sipStatus: err.status,
sipReason: err.reason
});
cs.callGone = true;
}
else {
if (cs) cs.emit('callStatusChange', {
callStatus,
sipStatus: 500,
sipReason: 'Internal Server Error'
});
if (sipLogger) sipLogger.error({err}, 'REST outdial failed');
else console.error(err);
}
ep.destroy();
setTimeout(restDial.kill.bind(restDial), 5000);
}
} catch (err) {
sysError(logger, res, err);
}
});
module.exports = router; module.exports = router;

View File

@@ -9,29 +9,25 @@ const {CallStatus, CallDirection} = require('../../utils/constants');
*/ */
function retrieveCallSession(callSid, opts) { function retrieveCallSession(callSid, opts) {
if (opts.call_status_hook && !opts.call_hook) { if (opts.call_status_hook && !opts.call_hook) {
throw new DbErrorBadRequest( throw new DbErrorBadRequest('call_status_hook can be updated only when call_hook is also being updated');
`call_status_hook can be updated only when call_hook is also being updated for call_sid ${callSid}`);
} }
const cs = sessionTracker.get(callSid); const cs = sessionTracker.get(callSid);
if (!cs) { if (!cs) {
throw new DbErrorUnprocessableRequest(`call session is gone for call_sid ${callSid}`); throw new DbErrorUnprocessableRequest('call session is gone');
} }
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) { if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
throw new DbErrorUnprocessableRequest( throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
`current call state is incompatible with requested action for call_sid ${callSid}`);
} }
else if (opts.call_status === CallStatus.NoAnswer) { else if (opts.call_status === CallStatus.NoAnswer) {
if (cs.direction === CallDirection.Outbound) { if (cs.direction === CallDirection.Outbound) {
if (!cs.isOutboundCallRinging) { if (!cs.isOutboundCallRinging) {
throw new DbErrorUnprocessableRequest( throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
`current call state is incompatible with requested action for call_sid ${callSid}`);
} }
} }
else { else {
if (cs.isInboundCallAnswered) { if (cs.isInboundCallAnswered) {
throw new DbErrorUnprocessableRequest( throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
`current call state is incompatible with requested action for call_sid ${callSid}`);
} }
} }
} }
@@ -45,7 +41,7 @@ function retrieveCallSession(callSid, opts) {
router.post('/:callSid', async(req, res) => { router.post('/:callSid', async(req, res) => {
const logger = req.app.locals.logger; const logger = req.app.locals.logger;
const callSid = req.params.callSid; const callSid = req.params.callSid;
logger.debug({body: req.body}, 'got updateCall request'); logger.debug({body: req.body}, 'got upateCall request');
try { try {
const cs = retrieveCallSession(callSid, req.body); const cs = retrieveCallSession(callSid, req.body);
if (!cs) { if (!cs) {

View File

@@ -1,134 +0,0 @@
const { checkSchema } = require('express-validator');
/**
* @path api-server {{base_url}}/v1/Accounts/:account_sid/Calls
* @see https://api.jambonz.org/#243a2edd-7999-41db-bd0d-08082bbab401
*/
const createCallSchema = checkSchema({
application_sid: {
isString: true,
optional: true,
isLength: { options: { min: 36, max: 36 } },
errorMessage: 'Invalid application_sid',
},
answerOnBridge: {
isBoolean: true,
optional: true,
errorMessage: 'Invalid answerOnBridge',
},
from: {
errorMessage: 'Invalid from',
isString: true,
isLength: {
options: { min: 1, max: 256 },
},
},
fromHost: {
isString: true,
optional: true,
errorMessage: 'Invalid fromHost',
},
to: {
errorMessage: 'Invalid to',
isObject: true,
},
callerName: {
isString: true,
optional: true,
errorMessage: 'Invalid callerName',
},
amd: {
isObject: true,
optional: true,
},
tag: {
isObject: true,
optional: true,
errorMessage: 'Invalid tag',
},
app_json: {
isString: true,
optional: true,
errorMessage: 'Invalid app_json',
},
account_sid: {
isString: true,
optional: true,
errorMessage: 'Invalid account_sid',
isLength: { options: { min: 36, max: 36 } },
},
timeout: {
isInt: true,
optional: true,
errorMessage: 'Invalid timeout',
},
timeLimit: {
isInt: true,
optional: true,
errorMessage: 'Invalid timeLimit',
},
call_hook: {
isObject: true,
optional: true,
errorMessage: 'Invalid call_hook',
},
call_status_hook: {
isObject: true,
optional: true,
errorMessage: 'Invalid call_status_hook',
},
speech_synthesis_vendor: {
isString: true,
optional: true,
errorMessage: 'Invalid speech_synthesis_vendor',
},
speech_synthesis_language: {
isString: true,
optional: true,
errorMessage: 'Invalid speech_synthesis_language',
},
speech_synthesis_voice: {
isString: true,
optional: true,
errorMessage: 'Invalid speech_synthesis_voice',
},
speech_recognizer_vendor: {
isString: true,
optional: true,
errorMessage: 'Invalid speech_recognizer_vendor',
},
speech_recognizer_language: {
isString: true,
optional: true,
errorMessage: 'Invalid speech_recognizer_language',
}
}, ['body']);
const customSanitizeFunction = (value) => {
try {
if (Array.isArray(value)) {
value = value.map((item) => customSanitizeFunction(item));
} else if (typeof value === 'object') {
Object.keys(value).forEach((key) => {
value[key] = customSanitizeFunction(value[key]);
});
} else if (typeof value === 'string') {
/* trims characters at the beginning and at the end of a string */
value = value.trim();
/* Verify strings including 'http' via new URL */
if (value.includes('http')) {
value = new URL(value).toString();
}
}
} catch (error) {
value = `Error: ${error.message}`;
}
return value;
};
module.exports = {
createCallSchema,
customSanitizeFunction
};

View File

@@ -11,10 +11,8 @@ const dbUtils = require('./utils/db-utils');
const RootSpan = require('./utils/call-tracer'); const RootSpan = require('./utils/call-tracer');
const listTaskNames = require('./utils/summarize-tasks'); const listTaskNames = require('./utils/summarize-tasks');
const { const {
JAMBONES_MYSQL_REFRESH_TTL, JAMBONES_MYSQL_REFRESH_TTL
JAMBONES_DISABLE_DIRECT_P2P_CALL
} = require('./config'); } = require('./config');
const { createJambonzApp } = require('./dynamic-apps');
module.exports = function(srf, logger) { module.exports = function(srf, logger) {
const { const {
@@ -22,21 +20,17 @@ module.exports = function(srf, logger) {
lookupAppByRegex, lookupAppByRegex,
lookupAppBySid, lookupAppBySid,
lookupAppByRealm, lookupAppByRealm,
lookupAppByTeamsTenant, lookupAppByTeamsTenant
registrar,
lookupClientByAccountAndUsername
} = srf.locals.dbHelpers; } = srf.locals.dbHelpers;
const { const {
writeAlerts, writeAlerts,
AlertType AlertType
} = srf.locals; } = srf.locals;
const {lookupAccountDetails, lookupGoogleCustomVoice} = dbUtils(logger, srf); const {lookupAccountDetails} = dbUtils(logger, srf);
async function initLocals(req, res, next) { function initLocals(req, res, next) {
const callId = req.get('Call-ID'); const callId = req.get('Call-ID');
const uri = parseUri(req.uri);
logger.info({ logger.info({
uri,
callId, callId,
callingNumber: req.callingNumber, callingNumber: req.callingNumber,
calledNumber: req.calledNumber calledNumber: req.calledNumber
@@ -48,52 +42,12 @@ module.exports = function(srf, logger) {
const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4(); const callSid = req.has('X-Retain-Call-Sid') ? req.get('X-Retain-Call-Sid') : uuidv4();
const account_sid = req.get('X-Account-Sid'); const account_sid = req.get('X-Account-Sid');
req.locals = {callSid, account_sid, callId}; req.locals = {callSid, account_sid, callId};
if (req.has('X-Application-Sid')) {
let clientDb = null;
if (req.has('X-Authenticated-User')) {
req.locals.originatingUser = req.get('X-Authenticated-User');
let clientSettings;
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
if (arr) {
[clientSettings] = await lookupClientByAccountAndUsername(account_sid, arr[1]);
}
clientDb = await registrar.query(req.locals.originatingUser);
clientDb = {
...clientDb,
...clientSettings,
};
}
// check for call to application
if (uri.user?.startsWith('app-') && req.locals.originatingUser && clientDb?.allow_direct_app_calling) {
const application_sid = uri.user.match(/app-(.*)/)[1];
logger.debug(`got application from Request URI header: ${application_sid}`);
req.locals.application_sid = application_sid;
} else if (req.has('X-Application-Sid')) {
const application_sid = req.get('X-Application-Sid'); const application_sid = req.get('X-Application-Sid');
logger.debug(`got application from X-Application-Sid header: ${application_sid}`); logger.debug(`got application from X-Application-Sid header: ${application_sid}`);
req.locals.application_sid = application_sid; req.locals.application_sid = application_sid;
} }
// check for call to queue if (req.has('X-Authenticated-User')) req.locals.originatingUser = req.get('X-Authenticated-User');
if (uri.user?.startsWith('queue-') && req.locals.originatingUser && clientDb?.allow_direct_queue_calling) {
const queue_name = uri.user.match(/queue-(.*)/)[1];
logger.debug(`got Queue from Request URI header: ${queue_name}`);
req.locals.queue_name = queue_name;
}
// check for call to registered user
if (!JAMBONES_DISABLE_DIRECT_P2P_CALL && req.locals.originatingUser && clientDb?.allow_direct_user_calling) {
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
if (arr) {
const sipRealm = arr[2];
const called_user = `${req.calledNumber}@${sipRealm}`;
const reg = await registrar.query(called_user);
if (reg) {
logger.debug(`got called Number is a registered user: ${called_user}`);
req.locals.called_user = called_user;
}
}
}
if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN'); if (req.has('X-MS-Teams-Tenant-FQDN')) req.locals.msTeamsTenant = req.get('X-MS-Teams-Tenant-FQDN');
if (req.has('X-Cisco-Recording-Participant')) { if (req.has('X-Cisco-Recording-Participant')) {
const ciscoParticipants = req.get('X-Cisco-Recording-Participant'); const ciscoParticipants = req.get('X-Cisco-Recording-Participant');
@@ -166,7 +120,7 @@ module.exports = function(srf, logger) {
}; };
logger.info({callId, metadata, sdp}, 'successfully parsed SIPREC payload'); logger.info({callId, metadata, sdp}, 'successfully parsed SIPREC payload');
} catch (err) { } catch (err) {
logger.info({err, callId}, 'Error parsing multipart payload'); logger.info({callId}, 'Error parsing multipart payload');
return res.send(503); return res.send(503);
} }
} }
@@ -230,24 +184,15 @@ module.exports = function(srf, logger) {
const {span} = rootSpan.startChildSpan('lookupApplication'); const {span} = rootSpan.startChildSpan('lookupApplication');
try { try {
let app; let app;
if (req.locals.queue_name) { if (req.locals.application_sid) app = await lookupAppBySid(req.locals.application_sid);
logger.debug(`calling to queue ${req.locals.queue_name}, generating queue app`); else if (req.locals.originatingUser) {
app = createJambonzApp('queue', {account_sid, name: req.locals.queue_name});
} else if (req.locals.called_user) {
logger.debug(`calling to registered user ${req.locals.called_user}, generating dial app`);
app = createJambonzApp('user',
{account_sid, name: req.locals.called_user, caller_id: req.locals.callingNumber});
} else if (req.locals.application_sid) {
app = await lookupAppBySid(req.locals.application_sid);
} else if (req.locals.originatingUser) {
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser); const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
if (arr) { if (arr) {
const sipRealm = arr[2]; const sipRealm = arr[2];
logger.debug(`looking for device calling app for realm ${sipRealm}`); logger.debug(`looking for device calling app for realm ${sipRealm}`);
app = await lookupAppByRealm(sipRealm); app = await lookupAppByRealm(sipRealm);
if (app) { if (app) logger.debug({app}, `retrieved device calling app for realm ${sipRealm}`);
logger.debug({app}, `retrieved device calling app for realm ${sipRealm}`);
}
} }
} }
else if (req.locals.msTeamsTenant) { else if (req.locals.msTeamsTenant) {
@@ -312,22 +257,7 @@ module.exports = function(srf, logger) {
app2.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret); app2.requestor = new HttpRequestor(logger, account_sid, app.call_hook, accountInfo.account.webhook_secret);
if (app.call_status_hook) app2.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook, if (app.call_status_hook) app2.notifier = new HttpRequestor(logger, account_sid, app.call_status_hook,
accountInfo.account.webhook_secret); accountInfo.account.webhook_secret);
else app2.notifier = {request: () => {}, close: () => {}}; else app2.notifier = {request: () => {}};
}
// Resolve application.speech_synthesis_voice if it's custom voice
if (app2.speech_synthesis_vendor === 'google' && app2.speech_synthesis_voice.startsWith('custom_')) {
const arr = /custom_(.*)/.exec(app2.speech_synthesis_voice);
if (arr) {
const google_custom_voice_sid = arr[1];
const [custom_voice] = await lookupGoogleCustomVoice(google_custom_voice_sid);
if (custom_voice) {
app2.speech_synthesis_voice = {
reportedUsage: custom_voice.reported_usage,
model: custom_voice.model
};
}
}
} }
req.locals.application = app2; req.locals.application = app2;
@@ -369,30 +299,22 @@ module.exports = function(srf, logger) {
if (app.app_json) { if (app.app_json) {
json = JSON.parse(app.app_json); json = JSON.parse(app.app_json);
} else { } else {
const defaults = {
synthesizer: {
vendor: app.speech_synthesis_vendor,
...(app.speech_synthesis_label && {label: app.speech_synthesis_label}),
language: app.speech_synthesis_language,
voice: app.speech_synthesis_voice,
...(app.fallback_speech_synthesis_vendor && {fallback_vendor: app.fallback_speech_synthesis_vendor}),
...(app.fallback_speech_synthesis_label && {fallback_label: app.fallback_speech_synthesis_label}),
...(app.fallback_speech_synthesis_language && {fallback_language: app.fallback_speech_synthesis_language}),
...(app.fallback_speech_synthesis_voice && {fallback_voice: app.fallback_speech_synthesis_voice})
},
recognizer: {
vendor: app.speech_recognizer_vendor,
...(app.speech_recognizer_label && {label: app.speech_recognizer_label}),
language: app.speech_recognizer_language,
...(app.fallback_speech_recognizer_vendor && {fallback_vendor: app.fallback_speech_recognizer_vendor}),
...(app.fallback_speech_recognizer_label && {fallback_label: app.fallback_speech_recognizer_label}),
...(app.fallback_speech_recognizer_language && {fallback_language: app.fallback_speech_recognizer_language})
}
};
const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? { sip: req.msg } : {}, const params = Object.assign(['POST', 'WS'].includes(app.call_hook.method) ? { sip: req.msg } : {},
req.locals.callInfo, req.locals.callInfo,
{ service_provider_sid: req.locals.service_provider_sid }, { service_provider_sid: req.locals.service_provider_sid },
{ defaults }); {
defaults: {
synthesizer: {
vendor: app.speech_synthesis_vendor,
language: app.speech_synthesis_language,
voice: app.speech_synthesis_voice
},
recognizer: {
vendor: app.speech_recognizer_vendor,
language: app.speech_recognizer_language
}
}
});
logger.debug({ params }, 'sending initial webhook'); logger.debug({ params }, 'sending initial webhook');
const obj = rootSpan.startChildSpan('performAppWebhook'); const obj = rootSpan.startChildSpan('performAppWebhook');
span = obj.span; span = obj.span;

View File

@@ -1,6 +1,4 @@
const CallSession = require('./call-session'); const CallSession = require('./call-session');
const {CallStatus} = require('../utils/constants');
const moment = require('moment');
/** /**
* @classdesc Subclass of CallSession. Represents a CallSession * @classdesc Subclass of CallSession. Represents a CallSession
@@ -21,14 +19,12 @@ class AdultingCallSession extends CallSession {
rootSpan rootSpan
}); });
this.sd = singleDialer; this.sd = singleDialer;
this.req = callInfo.req;
this.sd.dlg.on('destroy', () => { this.sd.dlg.on('destroy', () => {
this.logger.info('AdultingCallSession: called party hung up'); this.logger.info('AdultingCallSession: called party hung up');
this._callReleased(); this._callReleased();
}); });
this.sd.emit('adulting'); this.sd.emit('adulting');
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
} }
get dlg() { get dlg() {
@@ -53,18 +49,6 @@ class AdultingCallSession extends CallSession {
} }
_callerHungup() { _callerHungup() {
if (this.dlg.connectTime) {
const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.rootSpan.setAttributes({'call.termination': 'hangup by caller'});
this.callInfo.callTerminationBy = 'caller';
this.emit('callStatusChange', {
callStatus: CallStatus.Completed,
duration
});
}
this.logger.info('InboundCallSession: caller hung up');
this._callReleased();
this.req.removeAllListeners('cancel');
} }
} }

View File

@@ -19,10 +19,11 @@ const HttpRequestor = require('../utils/http-requestor');
const WsRequestor = require('../utils/ws-requestor'); const WsRequestor = require('../utils/ws-requestor');
const { const {
JAMBONES_INJECT_CONTENT, JAMBONES_INJECT_CONTENT,
JAMBONES_EAGERLY_PRE_CACHE_AUDIO,
AWS_REGION, AWS_REGION,
JAMBONZ_RECORD_WS_BASE_URL,
JAMBONZ_RECORD_WS_USERNAME,
JAMBONZ_RECORD_WS_PASSWORD,
} = require('../config'); } = require('../config');
const BackgroundTaskManager = require('../utils/background-task-manager');
const BADPRECONDITIONS = 'preconditions not met'; const BADPRECONDITIONS = 'preconditions not met';
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction'; const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
@@ -65,11 +66,6 @@ class CallSession extends Emitter {
this.callGone = false; this.callGone = false;
this.notifiedComplete = false; this.notifiedComplete = false;
this.rootSpan = rootSpan; this.rootSpan = rootSpan;
this.backgroundTaskManager = new BackgroundTaskManager({
cs: this,
logger,
rootSpan
});
this._origRecognizerSettings = { this._origRecognizerSettings = {
vendor: this.application?.speech_recognizer_vendor, vendor: this.application?.speech_recognizer_vendor,
@@ -140,7 +136,8 @@ class CallSession extends Emitter {
} }
get isBackGroundListen() { get isBackGroundListen() {
return this.backgroundTaskManager.isTaskRunning('listen'); return !(this.backgroundListenTask === null ||
this.backgroundListenTask === undefined);
} }
/** /**
@@ -173,18 +170,6 @@ class CallSession extends Emitter {
return this.application.notifier; return this.application.notifier;
} }
/**
* syntheizer
*/
get synthesizer() {
return this._synthesizer;
}
set synthesizer(synth) {
this._synthesizer = synth;
}
/** /**
* default vendor to use for speech synthesis if not provided in the app * default vendor to use for speech synthesis if not provided in the app
*/ */
@@ -194,30 +179,6 @@ class CallSession extends Emitter {
set speechSynthesisVendor(vendor) { set speechSynthesisVendor(vendor) {
this.application.speech_synthesis_vendor = vendor; this.application.speech_synthesis_vendor = vendor;
} }
get fallbackSpeechSynthesisVendor() {
return this.application.fallback_speech_synthesis_vendor;
}
set fallbackSpeechSynthesisVendor(vendor) {
this.application.fallback_speech_synthesis_vendor = vendor;
}
/**
* default label to use for speech synthesis if not provided in the app
*/
get speechSynthesisLabel() {
return this.application.speech_synthesis_label;
}
set speechSynthesisLabel(label) {
this.application.speech_synthesis_label = label;
}
get fallbackSpeechSynthesisLabel() {
return this.application.fallback_speech_synthesis_label;
}
set fallbackSpeechSynthesisLabel(label) {
this.application.fallback_speech_synthesis_label = label;
}
/** /**
* default voice to use for speech synthesis if not provided in the app * default voice to use for speech synthesis if not provided in the app
*/ */
@@ -227,13 +188,6 @@ class CallSession extends Emitter {
set speechSynthesisVoice(voice) { set speechSynthesisVoice(voice) {
this.application.speech_synthesis_voice = voice; this.application.speech_synthesis_voice = voice;
} }
get fallbackSpeechSynthesisVoice() {
return this.application.fallback_speech_synthesis_voice;
}
set fallbackSpeechSynthesisVoice(voice) {
this.application.fallback_speech_synthesis_voice = voice;
}
/** /**
* default language to use for speech synthesis if not provided in the app * default language to use for speech synthesis if not provided in the app
*/ */
@@ -244,13 +198,6 @@ class CallSession extends Emitter {
this.application.speech_synthesis_language = language; this.application.speech_synthesis_language = language;
} }
get fallbackSpeechSynthesisLanguage() {
return this.application.fallback_speech_synthesis_language;
}
set fallbackSpeechSynthesisLanguage(language) {
this.application.fallback_speech_synthesis_language = language;
}
/** /**
* default vendor to use for speech recognition if not provided in the app * default vendor to use for speech recognition if not provided in the app
*/ */
@@ -260,39 +207,6 @@ class CallSession extends Emitter {
set speechRecognizerVendor(vendor) { set speechRecognizerVendor(vendor) {
this.application.speech_recognizer_vendor = vendor; this.application.speech_recognizer_vendor = vendor;
} }
get fallbackSpeechRecognizerVendor() {
return this.application.fallback_speech_recognizer_vendor;
}
set fallbackSpeechRecognizerVendor(vendor) {
this.application.fallback_speech_recognizer_vendor = vendor;
}
/**
* recognizer
*/
get recognizer() {
return this._recognizer;
}
set recognizer(rec) {
this._recognizer = rec;
}
/**
* default vendor to use for speech recognition if not provided in the app
*/
get speechRecognizerLabel() {
return this.application.speech_recognizer_label;
}
set speechRecognizerLabel(label) {
this.application.speech_recognizer_label = label;
}
get fallbackSpeechRecognizerLabel() {
return this.application.fallback_speech_recognizer_label;
}
set fallbackSpeechRecognizerLabel(label) {
this.application.fallback_speech_recognizer_label = label;
}
/** /**
* default language to use for speech recognition if not provided in the app * default language to use for speech recognition if not provided in the app
*/ */
@@ -303,13 +217,6 @@ class CallSession extends Emitter {
this.application.speech_recognizer_language = language; this.application.speech_recognizer_language = language;
} }
get fallbackSpeechRecognizerLanguage() {
return this.application.fallback_speech_recognizer_language;
}
set fallbackSpeechRecognizerLanguage(language) {
this.application.fallback_speech_recognizer_language = language;
}
/** /**
* indicates whether the call currently in progress * indicates whether the call currently in progress
*/ */
@@ -382,11 +289,11 @@ class CallSession extends Emitter {
} }
get isBotModeEnabled() { get isBotModeEnabled() {
return this.backgroundTaskManager.isTaskRunning('bargeIn'); return this.backgroundGatherTask;
} }
get isListenEnabled() { get isListenEnabled() {
return this.backgroundTaskManager.isTaskRunning('listen'); return this.backgroundListenTask;
} }
get b3() { get b3() {
@@ -431,22 +338,6 @@ class CallSession extends Emitter {
return this._globalSttPunctuation; return this._globalSttPunctuation;
} }
get onHoldMusic() {
return this._onHoldMusic;
}
set onHoldMusic(url) {
this._onHoldMusic = url;
}
get sipRequestWithinDialogHook() {
return this._sipRequestWithinDialogHook;
}
set sipRequestWithinDialogHook(url) {
this._sipRequestWithinDialogHook = url;
}
hasGlobalSttPunctuation() { hasGlobalSttPunctuation() {
return this._globalSttPunctuation !== undefined; return this._globalSttPunctuation !== undefined;
} }
@@ -615,41 +506,117 @@ class CallSession extends Emitter {
} }
} }
async startBackgroundListen(opts, bugname) {
if (this.isListenEnabled) {
this.logger.info('CallSession:startBackgroundListen - listen is already enabled, ignoring request');
return;
}
try {
this.logger.debug({opts}, 'CallSession:startBackgroundListen');
const t = normalizeJambones(this.logger, [opts]);
this.backgroundListenTask = makeTask(this.logger, t[0]);
this.backgroundListenTask.bugname = bugname;
// Remove unneeded customer data to be sent to api server.
this.backgroundListenTask.ignoreCustomerData = true;
const resources = await this._evaluatePreconditions(this.backgroundListenTask);
const {span, ctx} = this.rootSpan.startChildSpan(`background-listen:${this.backgroundListenTask.summary}`);
this.backgroundListenTask.span = span;
this.backgroundListenTask.ctx = ctx;
this.backgroundListenTask.exec(this, resources)
.then(() => {
this.logger.info('CallSession:startBackgroundListen: listen completed');
this.backgroundListenTask && this.backgroundListenTask.removeAllListeners();
this.backgroundListenTask && this.backgroundListenTask.span.end();
this.backgroundListenTask = null;
return;
})
.catch((err) => {
this.logger.info({err}, 'CallSession:startBackgroundListen: listen threw error');
this.backgroundListenTask && this.backgroundListenTask.removeAllListeners();
this.backgroundListenTask && this.backgroundListenTask.span.end();
this.backgroundListenTask = null;
});
} catch (err) {
this.logger.info({err, opts}, 'CallSession:startBackgroundListen - Error creating listen task');
}
}
async stopBackgroundListen() {
this.logger.debug('CallSession:stopBackgroundListen');
try {
if (this.backgroundListenTask) {
this.backgroundListenTask.removeAllListeners();
this.backgroundListenTask.kill().catch(() => {});
}
} catch (err) {
this.logger.info({err}, 'CallSession:stopBackgroundListen - Error stopping listen task');
}
}
async enableBotMode(gather, autoEnable) { async enableBotMode(gather, autoEnable) {
try { try {
let task; const t = normalizeJambones(this.logger, [gather]);
const task = makeTask(this.logger, t[0]);
if (this.isBotModeEnabled) { if (this.isBotModeEnabled) {
task = this.backgroundTaskManager.getTask('bargeIn'); const currInput = this.backgroundGatherTask.input;
const currInput = task.input; const newInput = task.input;
const t = normalizeJambones(this.logger, [gather]);
const tmpTask = makeTask(this.logger, t[0]);
const newInput = tmpTask.input;
if (JSON.stringify(currInput) === JSON.stringify(newInput)) { if (JSON.stringify(currInput) === JSON.stringify(newInput)) {
this.logger.info('CallSession:enableBotMode - bot mode currently enabled, ignoring request to start again'); this.logger.info('CallSession:enableBotMode - bot mode currently enabled, ignoring request to start again');
return; return;
} else { }
else {
this.logger.info({currInput, newInput}, this.logger.info({currInput, newInput},
'CallSession:enableBotMode - restarting background bargeIn to apply new input type'); 'CallSession:enableBotMode - restarting background gather to apply new input type');
task.sticky = false; this.backgroundGatherTask.sticky = false;
await this.disableBotMode(); await this.disableBotMode();
} }
} }
task = await this.backgroundTaskManager.newTask('bargeIn', gather); this.backgroundGatherTask = task;
task.sticky = autoEnable; this._bargeInEnabled = true;
task.once('bargeIn-done', () => { this.backgroundGatherTask
if (this.requestor instanceof WsRequestor) { .once('dtmf', this._clearTasks.bind(this, this.backgroundGatherTask))
try { .once('vad', this._clearTasks.bind(this, this.backgroundGatherTask))
this.kill(true); .once('transcription', this._clearTasks.bind(this, this.backgroundGatherTask))
} catch (err) {} .once('timeout', this._clearTasks.bind(this, this.backgroundGatherTask));
} this.logger.info({gather}, 'CallSession:enableBotMode - starting background gather');
}); const resources = await this._evaluatePreconditions(this.backgroundGatherTask);
this.logger.info({gather}, 'CallSession:enableBotMode - starting background bargeIn'); const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundGatherTask.summary}`);
this.backgroundGatherTask.span = span;
this.backgroundGatherTask.ctx = ctx;
this.backgroundGatherTask.sticky = autoEnable;
this.backgroundGatherTask.exec(this, resources)
.then(() => {
this.logger.info('CallSession:enableBotMode: gather completed');
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
this.backgroundGatherTask && this.backgroundGatherTask.span.end();
const sticky = this.backgroundGatherTask?.sticky;
this.backgroundGatherTask = null;
if (sticky && !this.callGone && !this._stopping && this._bargeInEnabled) {
this.logger.info('CallSession:enableBotMode: restarting background gather');
setImmediate(() => this.enableBotMode(gather, true));
}
return;
})
.catch((err) => {
this.logger.info({err}, 'CallSession:enableBotMode: gather threw error');
this.backgroundGatherTask && this.backgroundGatherTask.removeAllListeners();
this.backgroundGatherTask && this.backgroundGatherTask.span.end();
this.backgroundGatherTask = null;
});
} catch (err) { } catch (err) {
this.logger.info({err, gather}, 'CallSession:enableBotMode - Error creating bargeIn task'); this.logger.info({err, gather}, 'CallSession:enableBotMode - Error creating gather task');
} }
} }
async disableBotMode() { async disableBotMode() {
this.backgroundTaskManager.stop('bargeIn'); this._bargeInEnabled = false;
if (this.backgroundGatherTask) {
try {
this.backgroundGatherTask.removeAllListeners();
await this.backgroundGatherTask.kill();
} catch (err) {}
this.backgroundGatherTask = null;
}
} }
setConferenceDetails(memberId, confName, confUuid) { setConferenceDetails(memberId, confName, confUuid) {
@@ -673,28 +640,15 @@ class CallSession extends Emitter {
* Check for speech credentials for the specified vendor * Check for speech credentials for the specified vendor
* @param {*} vendor - google or aws * @param {*} vendor - google or aws
*/ */
getSpeechCredentials(vendor, type, label = null) { getSpeechCredentials(vendor, type) {
const {writeAlerts, AlertType} = this.srf.locals; const {writeAlerts, AlertType} = this.srf.locals;
if (this.accountInfo.speech && this.accountInfo.speech.length > 0) { if (this.accountInfo.speech && this.accountInfo.speech.length > 0) {
// firstly check if account level has expected credential const credential = this.accountInfo.speech.find((s) => s.vendor === vendor);
let credential = this.accountInfo.speech.find((s) => s.vendor === vendor &&
s.label === label && s.account_sid);
if (!credential) {
// check if SP level has expected credential
credential = this.accountInfo.speech.find((s) => s.vendor === vendor &&
s.label === label && !s.account_sid);
}
if (credential && ( if (credential && (
(type === 'tts' && credential.use_for_tts) || (type === 'tts' && credential.use_for_tts) ||
(type === 'stt' && credential.use_for_stt) (type === 'stt' && credential.use_for_stt)
)) { )) {
this.logger.info(
`Speech vendor: ${credential.vendor} ${credential.label ? `, label: ${credential.label}` : ''} selected`);
if ('google' === vendor) { if ('google' === vendor) {
if (type === 'tts' && !credential.tts_tested_ok ||
type === 'stt' && !credential.stt_tested_ok) {
return;
}
try { try {
const cred = JSON.parse(credential.service_key.replace(/\n/g, '\\n')); const cred = JSON.parse(credential.service_key.replace(/\n/g, '\\n'));
return { return {
@@ -726,10 +680,8 @@ class CallSession extends Emitter {
region: credential.region, region: credential.region,
use_custom_stt: credential.use_custom_stt, use_custom_stt: credential.use_custom_stt,
custom_stt_endpoint: credential.custom_stt_endpoint, custom_stt_endpoint: credential.custom_stt_endpoint,
custom_stt_endpoint_url: credential.custom_stt_endpoint_url,
use_custom_tts: credential.use_custom_tts, use_custom_tts: credential.use_custom_tts,
custom_tts_endpoint: credential.custom_tts_endpoint, custom_tts_endpoint: credential.custom_tts_endpoint
custom_tts_endpoint_url: credential.custom_tts_endpoint_url
}; };
} }
else if ('wellsaid' === vendor) { else if ('wellsaid' === vendor) {
@@ -774,28 +726,7 @@ class CallSession extends Emitter {
riva_server_uri: credential.riva_server_uri riva_server_uri: credential.riva_server_uri
}; };
} }
else if ('cobalt' === vendor) { else if (vendor.startsWith('custom:')) {
return {
speech_credential_sid: credential.speech_credential_sid,
cobalt_server_uri: credential.cobalt_server_uri
};
} else if ('elevenlabs' === vendor) {
return {
api_key: credential.api_key,
model_id: credential.model_id,
options: credential.options
};
} else if ('assemblyai' === vendor) {
return {
speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key
};
} else if ('whisper' === vendor) {
return {
api_key: credential.api_key,
model_id: credential.model_id
};
} 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,
@@ -833,7 +764,7 @@ class CallSession extends Emitter {
let skip = false; let skip = false;
this.currentTask = task; this.currentTask = task;
if (TaskName.Gather === task.name && this.isBotModeEnabled) { if (TaskName.Gather === task.name && this.isBotModeEnabled) {
if (this.backgroundTaskManager.getTask('bargeIn').updateTaskInProgress(task) !== false) { if (this.backgroundGatherTask.updateTaskInProgress(task) !== false) {
this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`); this.logger.info(`CallSession:exec skipping #${stackNum}:${taskNum}: ${task.name}`);
skip = true; skip = true;
} }
@@ -868,8 +799,7 @@ class CallSession extends Emitter {
if (0 === this.tasks.length && if (0 === this.tasks.length &&
this.requestor instanceof WsRequestor && this.requestor instanceof WsRequestor &&
!this.requestor.closedGracefully && !this.requestor.closedGracefully &&
!this.callGone && !this.callGone
!this.isConfirmCallSession
) { ) {
try { try {
await this._awaitCommandsOrHangup(); await this._awaitCommandsOrHangup();
@@ -1054,39 +984,6 @@ class CallSession extends Emitter {
listenTask.updateListen(opts.listen_status); listenTask.updateListen(opts.listen_status);
} }
/**
* perform live call control -- change Transcribe status
* @param {object} opts
* @param {string} opts.transcribe_status - 'pause' or 'resume'
*/
async _lccTranscribeStatus(opts) {
const task = this.currentTask;
if (!task || ![TaskName.Dial, TaskName.Transcribe].includes(task.name)) {
return this.logger.info(`CallSession:_lccTranscribeStatus - invalid transcribe_status in task ${task.name}`);
}
const transcribeTask = task.name === TaskName.Transcribe ? task : task.transcribeTask;
if (!transcribeTask) {
return this.logger
.info('CallSession:_lccTranscribeStatus - invalid transcribe_status: Dial does not have a Transcribe');
}
transcribeTask.updateTranscribe(opts.transcribe_status);
}
/**
* perform live call control -- update customer data
* @param {object} opts
* @param {object} opts.tag - customer data
*/
_lccTag(opts) {
const {tag} = opts;
if (typeof tag !== 'object' || Array.isArray(tag) || tag === null) {
this.logger.info('CallSession:_lccTag - invalid tag data');
return;
}
this.logger.debug({customerData: tag}, 'CallSession:_lccTag set customer data in callInfo');
this.callInfo.customerData = tag;
}
async _lccMuteStatus(callSid, mute) { async _lccMuteStatus(callSid, mute) {
// 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;
@@ -1137,38 +1034,6 @@ class CallSession extends Emitter {
} }
} }
/**
* perform live call control - send RFC 2833 DTMF
* @param {obj} opts
* @param {string} opts.dtmf.digit - DTMF digit
* @param {string} opts.dtmf.duration - Optional, Duration
*/
async _lccDtmf(opts, callSid) {
const {dtmf} = opts;
const {digit, duration = 250} = dtmf;
if (!this.hasStableDialog) {
this.logger.info('CallSession:_lccDtmf - invalid command as we do not have a stable call');
return;
}
try {
const dlg = callSid === this.callSid ? this.dlg : this.currentTask.dlg;
const res = await dlg.request({
method: 'INFO',
headers: {
'Content-Type': 'application/dtmf',
'X-Reason': 'Dtmf'
},
body: `Signal=${digit}
Duration=${duration} `
});
this.logger.debug({res}, `CallSession:_lccDtmf
got response to INFO DTMF digit=${digit} and duration=${duration}`);
return res;
} catch (err) {
this.logger.error({err}, 'CallSession:_lccDtmf - error sending INFO RFC 2833 DTMF');
}
}
/** /**
* perform live call control -- whisper to one party or the other on a call * perform live call control -- whisper to one party or the other on a call
* @param {array} opts - array of play or say tasks * @param {array} opts - array of play or say tasks
@@ -1223,7 +1088,7 @@ Duration=${duration} `
/** /**
* perform live call control * perform live call control
* @param {object} opts - update instructions * @param {object} opts - update instructions
* @param {string} callSid - identifies call to update * @param {string} callSid - identifies call toupdate
*/ */
async updateCall(opts, callSid) { async updateCall(opts, callSid) {
this.logger.debug(opts, 'CallSession:updateCall'); this.logger.debug(opts, 'CallSession:updateCall');
@@ -1237,9 +1102,6 @@ Duration=${duration} `
if (opts.listen_status) { if (opts.listen_status) {
await this._lccListenStatus(opts); await this._lccListenStatus(opts);
} }
if (opts.transcribe_status) {
await this._lccTranscribeStatus(opts);
}
else if (opts.mute_status) { else if (opts.mute_status) {
await this._lccMuteStatus(callSid, opts.mute_status === 'mute'); await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
} }
@@ -1252,18 +1114,13 @@ Duration=${duration} `
else if (opts.sip_request) { else if (opts.sip_request) {
const res = await this._lccSipRequest(opts, callSid); const res = await this._lccSipRequest(opts, callSid);
return {status: res.status, reason: res.reason}; return {status: res.status, reason: res.reason};
} else if (opts.dtmf) {
await this._lccDtmf(opts, callSid);
} }
else if (opts.record) { else if (opts.record) {
await this.notifyRecordOptions(opts.record); await this.notifyRecordOptions(opts.record);
} }
else if (opts.tag) {
return this._lccTag(opts);
}
// 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 reccording etc..
if (opts.whisper) { if (opts.whisper) {
return this._lccWhisper(opts, callSid); return this._lccWhisper(opts, callSid);
} }
@@ -1321,6 +1178,7 @@ Duration=${duration} `
get the full transcription. get the full transcription.
*/ */
delete t.bargeIn.enable; delete t.bargeIn.enable;
this._bargeInEnabled = false;
this.logger.info('CallSession:kill - found bargein disabled in the stack, clearing to that point'); this.logger.info('CallSession:kill - found bargein disabled in the stack, clearing to that point');
break; break;
} }
@@ -1331,35 +1189,6 @@ Duration=${duration} `
this.taskIdx = 0; this.taskIdx = 0;
} }
_preCacheAudio(newTasks) {
for (const task of newTasks) {
if (task.name === TaskName.Config && task.hasSynthesizer) {
/* if they change synthesizer settings don't try to precache */
break;
}
if (task.name === TaskName.Say) {
/* identify vendor language, voice, and label */
const vendor = task.synthesizer.vendor && task.synthesizer.vendor !== 'default' ?
task.synthesizer.vendor :
this.speechSynthesisVendor;
const language = task.synthesizer.language && task.synthesizer.language !== 'default' ?
task.synthesizer.language :
this.speechSynthesisLanguage ;
const voice = task.synthesizer.voice && task.synthesizer.voice !== 'default' ?
task.synthesizer.voice :
this.speechSynthesisVoice;
const label = task.synthesizer.label && task.synthesizer.label !== 'default' ?
task.synthesizer.label :
this.speechSynthesisLabel;
this.logger.info({vendor, language, voice, label},
'CallSession:_preCacheAudio - precaching audio for future prompt');
task._synthesizeWithSpecificVendor(this, this.ep, {vendor, language, voice, label, preCache: true})
.catch((err) => this.logger.error(err, 'CallSession:_preCacheAudio - error precaching audio'));
}
}
}
/** /**
* Append tasks to the current execution stack UNLESS there is a gather in the stack. * Append tasks to the current execution stack UNLESS there is a gather in the stack.
* in that case, insert the tasks before the gather AND if the tasks include * in that case, insert the tasks before the gather AND if the tasks include
@@ -1417,12 +1246,10 @@ Duration=${duration} `
this.replaceApplication(t); this.replaceApplication(t);
} }
else if (JAMBONES_INJECT_CONTENT) { else if (JAMBONES_INJECT_CONTENT) {
if (JAMBONES_EAGERLY_PRE_CACHE_AUDIO) this._preCacheAudio(t);
this._injectTasks(t); this._injectTasks(t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list'); this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
} }
else { else {
if (JAMBONES_EAGERLY_PRE_CACHE_AUDIO) this._preCacheAudio(t);
this.tasks.push(...t); this.tasks.push(...t);
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list'); this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
} }
@@ -1451,10 +1278,6 @@ Duration=${duration} `
this._lccListenStatus(data); this._lccListenStatus(data);
break; break;
case 'transcribe:status':
this._lccTranscribeStatus(data);
break;
case 'whisper': case 'whisper':
this._lccWhisper(data, call_sid); this._lccWhisper(data, call_sid);
break; break;
@@ -1466,13 +1289,6 @@ Duration=${duration} `
}); });
break; break;
case 'dtmf':
this._lccDtmf(data, call_sid)
.catch((err) => {
this.logger.info({err, data}, `CallSession:_onCommand - error sending RFC 2833 DTMF ${data}`);
});
break;
default: default:
this.logger.info(`CallSession:_onCommand - invalid command ${command}`); this.logger.info(`CallSession:_onCommand - invalid command ${command}`);
} }
@@ -1554,8 +1370,6 @@ Duration=${duration} `
this.ep = ep; this.ep = ep;
this.logger.debug(`allocated endpoint ${ep.uuid}`); this.logger.debug(`allocated endpoint ${ep.uuid}`);
this._configMsEndpoint();
this.ep.on('destroy', () => { this.ep.on('destroy', () => {
this.logger.debug(`endpoint was destroyed!! ${this.ep.uuid}`); this.logger.debug(`endpoint was destroyed!! ${this.ep.uuid}`);
}); });
@@ -1626,7 +1440,6 @@ Duration=${duration} `
return; return;
} }
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp}); this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
this._configMsEndpoint();
await this.dlg.modify(this.ep.local.sdp); await this.dlg.modify(this.ep.local.sdp);
this.logger.debug('CallSession:replaceEndpoint completed'); this.logger.debug('CallSession:replaceEndpoint completed');
@@ -1657,8 +1470,6 @@ Duration=${duration} `
this.notifier && this.notifier.close(); this.notifier && this.notifier.close();
this.rootSpan && this.rootSpan.end(); this.rootSpan && this.rootSpan.end();
// close all background tasks
this.backgroundTaskManager.stopAll();
} }
/** /**
@@ -1710,32 +1521,10 @@ Duration=${duration} `
} }
this.dlg.on('modify', this._onReinvite.bind(this)); this.dlg.on('modify', this._onReinvite.bind(this));
this.dlg.on('refer', this._onRefer.bind(this)); this.dlg.on('refer', this._onRefer.bind(this));
if (this.sipRequestWithinDialogHook) {
this.dlg.on('info', this._onRequestWithinDialog.bind(this));
this.dlg.on('message', this._onRequestWithinDialog.bind(this));
}
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`); this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
} }
} }
async _onRequestWithinDialog(req, res) {
if (!this.sipRequestWithinDialogHook) {
return;
}
const sip_method = req.method;
if (sip_method === 'INFO') {
res.send(200);
} else if (sip_method === 'MESSAGE') {
res.send(202);
} else {
this.logger.info(`CallSession:_onRequestWithinDialog unsported method: ${req.method}`);
res.send(501);
return;
}
const params = {sip_method, sip_body: req.body, sip_headers: req.headers};
this.currentTask.performHook(this, this.sipRequestWithinDialogHook, params);
}
async _onReinvite(req, res) { async _onReinvite(req, res) {
try { try {
if (this.ep) { if (this.ep) {
@@ -1744,14 +1533,9 @@ Duration=${duration} `
res.send(200, {body: this.ep.local.sdp}); res.send(200, {body: this.ep.local.sdp});
} }
else { else {
if (this.currentTask.name === TaskName.Dial && this.currentTask.isOnHold) { const newSdp = await this.ep.modify(req.body);
this.logger.info('onholdMusic reINVITE after media has been released'); res.send(200, {body: newSdp});
await this.currentTask.handleReinviteAfterMediaReleased(req, res); this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
} else {
const newSdp = await this.ep.modify(req.body);
res.send(200, {body: newSdp});
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
}
} }
} }
else if (this.currentTask && this.currentTask.name === TaskName.Dial) { else if (this.currentTask && this.currentTask.name === TaskName.Dial) {
@@ -1798,7 +1582,6 @@ Duration=${duration} `
} }
if (!this.ep) { if (!this.ep) {
this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body}); this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
this._configMsEndpoint();
} }
return {ms: this.ms, ep: this.ep}; return {ms: this.ms, ep: this.ep};
} }
@@ -1915,7 +1698,7 @@ Duration=${duration} `
wrapDialog(dlg) { wrapDialog(dlg) {
dlg.connectTime = moment(); dlg.connectTime = moment();
const origDestroy = dlg.destroy.bind(dlg); const origDestroy = dlg.destroy.bind(dlg);
dlg.destroy = (opts) => { dlg.destroy = () => {
if (dlg.connected) { if (dlg.connected) {
dlg.connected = false; dlg.connected = false;
dlg.destroy = origDestroy; dlg.destroy = origDestroy;
@@ -1924,7 +1707,7 @@ Duration=${duration} `
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration}); this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
this.logger.debug('CallSession: call terminated by jambonz'); this.logger.debug('CallSession: call terminated by jambonz');
this.rootSpan.setAttributes({'call.termination': 'hangup by jambonz'}); this.rootSpan.setAttributes({'call.termination': 'hangup by jambonz'});
origDestroy(opts).catch((err) => this.logger.info({err}, 'CallSession - error destroying dialog')); origDestroy().catch((err) => this.logger.info({err}, 'CallSession - error destroying dialog'));
if (this.wakeupResolver) { if (this.wakeupResolver) {
this.wakeupResolver({reason: 'session ended'}); this.wakeupResolver({reason: 'session ended'});
this.wakeupResolver = null; this.wakeupResolver = null;
@@ -1953,7 +1736,6 @@ Duration=${duration} `
'X-Reason': 'anchor-media' 'X-Reason': 'anchor-media'
} }
}); });
this._configMsEndpoint();
} }
async handleReinviteAfterMediaReleased(req, res) { async handleReinviteAfterMediaReleased(req, res) {
@@ -1975,14 +1757,12 @@ Duration=${duration} `
async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) { async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
if (this.callMoved) return; if (this.callMoved) return;
// manage record all call.
if (callStatus === CallStatus.InProgress) { if (callStatus === CallStatus.InProgress) {
if (this.accountInfo.account.record_all_calls || // nice, call is in progress, good time to enable record
this.application.record_all_calls) { await this.enableRecordAllCall();
this.backgroundTaskManager.newTask('record'); } else if (callStatus == CallStatus.Completed && this.isBackGroundListen) {
} this.stopBackgroundListen().catch((err) => this.logger.error(
} else if (callStatus == CallStatus.Completed) { {err}, 'CallSession:_notifyCallStatusChange - error stopping background listen'));
this.backgroundTaskManager.stop('record');
} }
/* race condition: we hang up at the same time as the caller */ /* race condition: we hang up at the same time as the caller */
@@ -2019,9 +1799,20 @@ Duration=${duration} `
} }
} }
_configMsEndpoint() { async enableRecordAllCall() {
if (this.onHoldMusic) { if (this.accountInfo.account.record_all_calls || this.application.record_all_calls) {
this.ep.set({hold_music: `shout://${this.onHoldMusic.replace(/^https?:\/\//, '')}`}); const listenOpts = {
url: `${JAMBONZ_RECORD_WS_BASE_URL}/record/${this.accountInfo.account.bucket_credential.vendor}`,
wsAuth: {
username: JAMBONZ_RECORD_WS_USERNAME,
password: JAMBONZ_RECORD_WS_PASSWORD
},
mixType : 'stereo',
passDtmf: true
};
this.logger.debug({listenOpts}, 'Record all calls: enabling listen');
await this.startBackgroundListen({verb: 'listen', ...listenOpts}, 'jambonz-session-record');
} }
} }
@@ -2061,18 +1852,6 @@ Duration=${duration} `
} catch (err) {} } catch (err) {}
} }
} }
/**
* startBackgroundTask - Start background task
*/
async startBackgroundTask(type, opts) {
await this.backgroundTaskManager.newTask(type, opts);
}
stopBackgroundTask(type) {
this.backgroundTaskManager.stop(type);
}
} }
module.exports = CallSession; module.exports = CallSession;

View File

@@ -8,7 +8,7 @@ const moment = require('moment');
* @extends CallSession * @extends CallSession
*/ */
class RestCallSession extends CallSession { class RestCallSession extends CallSession {
constructor({logger, application, srf, req, ep, ep2, tasks, callInfo, accountInfo, rootSpan}) { constructor({logger, application, srf, req, ep, tasks, callInfo, accountInfo, rootSpan}) {
super({ super({
logger, logger,
application, application,
@@ -21,7 +21,6 @@ class RestCallSession extends CallSession {
}); });
this.req = req; this.req = req;
this.ep = ep; this.ep = ep;
this.ep2 = ep2;
// keep restDialTask reference for closing AMD // keep restDialTask reference for closing AMD
if (tasks.length) { if (tasks.length) {
this.restDialTask = tasks[0]; this.restDialTask = tasks[0];
@@ -50,6 +49,7 @@ class RestCallSession extends CallSession {
*/ */
_callerHungup() { _callerHungup() {
if (this.restDialTask) { if (this.restDialTask) {
this.logger.info('RestCallSession: releasing AMD');
this.restDialTask.turnOffAmd(); this.restDialTask.turnOffAmd();
} }
this.callInfo.callTerminationBy = 'caller'; this.callInfo.callTerminationBy = 'caller';

View File

@@ -48,7 +48,7 @@ class Conference extends Task {
this.confName = this.data.name; this.confName = this.data.name;
[ [
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted', 'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook', 'endConferenceDuration' 'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook'
].forEach((attr) => this[attr] = this.data[attr]); ].forEach((attr) => this[attr] = this.data[attr]);
this.record = this.data.record || {}; this.record = this.data.record || {};
this.statusEvents = []; this.statusEvents = [];
@@ -344,13 +344,9 @@ class Conference extends Task {
} }
const opts = {}; const opts = {};
if (this.endConferenceOnExit || this.startConferenceOnEnter || this.joinMuted) { if (this.endConferenceOnExit) Object.assign(opts, {flags: {endconf: true}});
Object.assign(opts, {flags: { if (this.startConferenceOnEnter) Object.assign(opts, {flags: {moderator: true}});
...(this.endConferenceOnExit && {endconf: true}), if (this.joinMuted) Object.assign(opts, {flags: {mute: true}});
...(this.startConferenceOnEnter && {moderator: true}),
...(this.joinMuted && {joinMuted: true}),
}});
}
try { try {
const {memberId, confUuid} = await this.ep.join(this.confName, opts); const {memberId, confUuid} = await this.ep.join(this.confName, opts);
@@ -393,11 +389,6 @@ class Conference extends Task {
this.ep.api('conference', `${this.confName} set max_members ${this.maxParticipants}`) this.ep.api('conference', `${this.confName} set max_members ${this.maxParticipants}`)
.catch((err) => this.logger.error(err, `Error setting max participants to ${this.maxParticipants}`)); .catch((err) => this.logger.error(err, `Error setting max participants to ${this.maxParticipants}`));
} }
if (typeof this.endConferenceDuration === 'number' && this.endConferenceDuration >= 0) {
this.ep.api('conference', `${this.confName} set endconference_grace_time ${this.endConferenceDuration}`)
.catch((err) => this.logger.error(err, `Error setting end conference time to ${this.endConferenceDuration}`));
}
} }
/** /**

View File

@@ -9,8 +9,7 @@ class TaskConfig extends Task {
'recognizer', 'recognizer',
'bargeIn', 'bargeIn',
'record', 'record',
'listen', 'listen'
'transcribe'
].forEach((k) => this[k] = this.data[k] || {}); ].forEach((k) => this[k] = this.data[k] || {});
if ('notifyEvents' in this.data) { if ('notifyEvents' in this.data) {
@@ -31,13 +30,6 @@ class TaskConfig extends Task {
if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k]; if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
}); });
} }
if (this.transcribe?.enable) {
this.transcribeOpts = {
verb: 'transcribe',
...this.transcribe
};
delete this.transcribeOpts.enable;
}
if (this.data.reset) { if (this.data.reset) {
if (typeof this.data.reset === 'string') this.data.reset = [this.data.reset]; if (typeof this.data.reset === 'string') this.data.reset = [this.data.reset];
@@ -45,15 +37,9 @@ class TaskConfig extends Task {
else this.data.reset = []; else this.data.reset = [];
if (this.bargeIn.sticky) this.autoEnable = true; if (this.bargeIn.sticky) this.autoEnable = true;
this.preconditions = (this.bargeIn.enable || this.preconditions = (this.bargeIn.enable || this.record?.action || this.listen?.url || this.data.amd) ?
this.record?.action ||
this.listen?.url ||
this.data.amd ||
this.transcribe?.enable) ?
TaskPreconditions.Endpoint : TaskPreconditions.Endpoint :
TaskPreconditions.None; TaskPreconditions.None;
this.onHoldMusic = this.data.onHoldMusic;
} }
get name() { return TaskName.Config; } get name() { return TaskName.Config; }
@@ -62,7 +48,6 @@ class TaskConfig extends Task {
get hasRecognizer() { return Object.keys(this.recognizer).length; } get hasRecognizer() { return Object.keys(this.recognizer).length; }
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 summary() { get summary() {
const phrase = []; const phrase = [];
@@ -85,12 +70,8 @@ class TaskConfig extends Task {
if (this.hasListen) { if (this.hasListen) {
phrase.push(this.listen.enable ? `listen ${this.listen.url}` : 'stop listen'); phrase.push(this.listen.enable ? `listen ${this.listen.url}` : 'stop listen');
} }
if (this.hasTranscribe) {
phrase.push(this.transcribe.enable ? `transcribe ${this.transcribe.transcriptionHook}` : 'stop transcribe');
}
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}`);
return `${this.name}{${phrase.join(',')}}`; return `${this.name}{${phrase.join(',')}}`;
} }
@@ -102,10 +83,6 @@ class TaskConfig extends Task {
cs.notifyEvents = !!this.data.notifyEvents; cs.notifyEvents = !!this.data.notifyEvents;
} }
if (this.onHoldMusic) {
cs.onHoldMusic = this.onHoldMusic;
}
if (this.data.amd) { if (this.data.amd) {
this.startAmd = cs.startAmd; this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd; this.stopAmd = cs.stopAmd;
@@ -125,58 +102,24 @@ class TaskConfig extends Task {
}); });
if (this.hasSynthesizer) { if (this.hasSynthesizer) {
cs.synthesizer = this.synthesizer;
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'
? 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;
cs.speechSynthesisVoice = this.synthesizer.voice !== 'default' cs.speechSynthesisVoice = this.synthesizer.voice !== 'default'
? this.synthesizer.voice ? this.synthesizer.voice
: cs.speechSynthesisVoice; : cs.speechSynthesisVoice;
// fallback vendor
cs.fallbackSpeechSynthesisVendor = this.synthesizer.fallbackVendor !== 'default'
? this.synthesizer.fallbackVendor
: cs.fallbackSpeechSynthesisVendor;
cs.fallbackSpeechSynthesisLabel = this.synthesizer.fallbackLabel !== 'default'
? this.synthesizer.fallbackLabel
: cs.fallbackSpeechSynthesisLabel;
cs.fallbackSpeechSynthesisLanguage = this.synthesizer.fallbackLanguage !== 'default'
? this.synthesizer.fallbackLanguage
: cs.fallbackSpeechSynthesisLanguage;
cs.fallbackSpeechSynthesisVoice = this.synthesizer.fallbackVoice !== 'default'
? this.synthesizer.fallbackVoice
: cs.fallbackSpeechSynthesisVoice;
this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer'); this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer');
} }
if (this.hasRecognizer) { if (this.hasRecognizer) {
cs.recognizer = this.recognizer;
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'
? 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;
//fallback
cs.fallbackSpeechRecognizerVendor = this.recognizer.fallbackVendor !== 'default'
? this.recognizer.fallbackVendor
: cs.fallbackSpeechRecognizerVendor;
cs.fallbackSpeechRecognizerLabel = this.recognizer.fallbackLabel !== 'default'
? this.recognizer.fallbackLabel
: cs.fallbackSpeechRecognizerLabel;
cs.fallbackSpeechRecognizerLanguage = this.recognizer.fallbackLanguage !== 'default'
? this.recognizer.fallbackLanguage
: cs.fallbackSpeechRecognizerLanguage;
cs.isContinuousAsr = typeof this.recognizer.asrTimeout === 'number' ? true : false; cs.isContinuousAsr = typeof this.recognizer.asrTimeout === 'number' ? true : false;
if (cs.isContinuousAsr) { if (cs.isContinuousAsr) {
cs.asrTimeout = this.recognizer.asrTimeout; cs.asrTimeout = this.recognizer.asrTimeout;
@@ -228,30 +171,12 @@ class TaskConfig extends Task {
const {enable, ...opts} = this.listen; const {enable, ...opts} = this.listen;
if (enable) { if (enable) {
this.logger.debug({opts}, 'Config: enabling listen'); this.logger.debug({opts}, 'Config: enabling listen');
cs.startBackgroundTask('listen', {verb: 'listen', ...opts}); cs.startBackgroundListen({verb: 'listen', ...opts});
} else { } else {
this.logger.info('Config: disabling listen'); this.logger.info('Config: disabling listen');
cs.stopBackgroundTask('listen'); cs.stopBackgroundListen();
} }
} }
if (this.hasTranscribe) {
if (this.transcribe.enable) {
this.transcribeOpts.recognizer = this.hasRecognizer ?
this.recognizer :
{
vendor: cs.speechRecognizerVendor,
language: cs.speechRecognizerLanguage
};
this.logger.debug(this.transcribeOpts, 'Config: enabling transcribe');
cs.startBackgroundTask('transcribe', this.transcribeOpts);
} else {
this.logger.info('Config: disabling transcribe');
cs.stopBackgroundTask('transcribe');
}
}
if (this.data.sipRequestWithinDialogHook) {
cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
}
} }
async kill(cs) { async kill(cs) {

View File

@@ -12,13 +12,10 @@ const assert = require('assert');
const placeCall = require('../utils/place-outdial'); const placeCall = require('../utils/place-outdial');
const sessionTracker = require('../session/session-tracker'); 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 dbUtils = require('../utils/db-utils'); const dbUtils = require('../utils/db-utils');
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} = require('../config');
const { isOnhold, isOpusFirst } = require('../utils/sdp-utils');
const { normalizeJambones } = require('@jambonz/verb-specifications');
function parseDtmfOptions(logger, dtmfCapture) { function parseDtmfOptions(logger, dtmfCapture) {
let parentDtmfCollector, childDtmfCollector; let parentDtmfCollector, childDtmfCollector;
@@ -138,10 +135,6 @@ class TaskDial extends Task {
get name() { return TaskName.Dial; } get name() { return TaskName.Dial; }
get isOnHold() {
return this.isIncomingLegHold || this.isOutgoingLegHold;
}
get canReleaseMedia() { get canReleaseMedia() {
const keepAnchor = this.data.anchorMedia || const keepAnchor = this.data.anchorMedia ||
this.cs.isBackGroundListen || this.cs.isBackGroundListen ||
@@ -204,7 +197,6 @@ class TaskDial extends Task {
await this.performAction(this.results, this.killReason !== KillReason.Replaced); await this.performAction(this.results, this.killReason !== KillReason.Replaced);
this._removeDtmfDetection(cs.dlg); this._removeDtmfDetection(cs.dlg);
this._removeDtmfDetection(this.dlg); this._removeDtmfDetection(this.dlg);
this._removeSipIndialogRequestListener(this.dlg);
} catch (err) { } catch (err) {
this.logger.error({err}, 'TaskDial:exec terminating with error'); this.logger.error({err}, 'TaskDial:exec terminating with error');
this.kill(cs); this.kill(cs);
@@ -233,7 +225,7 @@ class TaskDial extends Task {
} }
this._removeDtmfDetection(cs.dlg); this._removeDtmfDetection(cs.dlg);
this._removeDtmfDetection(this.dlg); this._removeDtmfDetection(this.dlg);
await this._killOutdials(); this._killOutdials();
if (this.sd) { if (this.sd) {
this.sd.kill(); this.sd.kill();
this.sd.removeAllListeners(); this.sd.removeAllListeners();
@@ -322,41 +314,18 @@ 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);
this.logger.info({to}, 'refer to parsed'); this.logger.info({to}, 'refer to parsed');
const json = await cs.requestor.request('verb:hook', this.referHook, { await cs.requestor.request('verb:hook', this.referHook, {
...(callInfo.toJSON()), ...callInfo,
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_referred_by: req.get('Referred-By'),
sip_user_agent: req.get('User-Agent'), sip_user_agent: req.get('User-Agent'),
refer_to_user: to.scheme === 'tel' ? to.number : to.user, refer_to_user: to.user,
referred_by_user: by.scheme === 'tel' ? by.number : by.user, referred_by_user: by.user,
referring_call_sid, referring_call_sid,
referred_call_sid referred_call_sid
} }
}, httpHeaders); }, httpHeaders);
if (json && Array.isArray(json)) {
try {
const logger = isChild ? this.logger : this.sd.logger;
const tasks = normalizeJambones(logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
const legs = isChild ? ['child', 'parent'] : ['parent', 'child'];
logger.info(`Dial:handleRefer received REFER on ${legs[0]} leg, setting new app on ${legs[1]} leg`);
if (isChild) this.redirect(cs, tasks);
else {
logger.info({tasks: json}, 'Dial:handleRefer - new application for for child leg');
const adultingSession = await this.sd.doAdulting({
logger,
application: cs.application,
tasks
});
/* need to update the callSid of the child with its own (new) AdultingCallSession */
sessionTracker.add(adultingSession.callSid, adultingSession);
}
}
} catch (err) {
this.logger.info(err, 'Dial:handleRefer - error setting new application after receiving REFER');
}
}
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) {
@@ -377,16 +346,11 @@ class TaskDial extends Task {
sd.removeAllListeners('callCreateFail'); sd.removeAllListeners('callCreateFail');
} }
async _killOutdials() { _killOutdials() {
for (const [callSid, sd] of Array.from(this.dials)) { for (const [callSid, sd] of Array.from(this.dials)) {
this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`); this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`);
try { sd.kill().catch((err) => this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`));
await sd.kill();
} catch (err) {
this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`);
}
this._removeHandlers(sd); this._removeHandlers(sd);
this.logger.debug(`Dial:_killOutdials killed callSid ${callSid}`);
} }
this.dials.clear(); this.dials.clear();
} }
@@ -399,14 +363,8 @@ class TaskDial extends Task {
} }
_onInfo(cs, dlg, req, res) { _onInfo(cs, dlg, req, res) {
// SIP Indialog will be handled by another handler
if (cs.sipRequestWithinDialogHook) {
return;
}
res.send(200); res.send(200);
if (req.get('Content-Type') !== 'application/dtmf-relay') { if (req.get('Content-Type') !== 'application/dtmf-relay') return;
return;
}
const dtmfDetector = dlg === cs.dlg ? this.parentDtmfCollector : this.childDtmfCollector; const dtmfDetector = dlg === cs.dlg ? this.parentDtmfCollector : this.childDtmfCollector;
if (!dtmfDetector) return; if (!dtmfDetector) return;
@@ -435,20 +393,6 @@ class TaskDial extends Task {
} }
} }
_initSipIndialogRequestListener(cs, dlg) {
dlg.on('info', this._onRequestWithinDialog.bind(this, cs));
dlg.on('message', this._onRequestWithinDialog.bind(this, cs));
}
_removeSipIndialogRequestListener(dlg) {
dlg && dlg.removeAllListeners('message');
dlg && dlg.removeAllListeners('info');
}
async _onRequestWithinDialog(cs, req, res) {
cs._onRequestWithinDialog(req, res);
}
async _initializeInbound(cs) { async _initializeInbound(cs) {
const {ep} = await cs._evalEndpointPrecondition(this); const {ep} = await cs._evalEndpointPrecondition(this);
this.epOther = ep; this.epOther = ep;
@@ -477,8 +421,7 @@ 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')}),
...(req && req.has('P-Asserted-Identity') && !JAMBONZ_DISABLE_DIAL_PAI_HEADER && ...(req && req.has('P-Asserted-Identity') && {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
{'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
...(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
@@ -488,8 +431,7 @@ class TaskDial extends Task {
headers: this.headers, headers: this.headers,
proxy: `sip:${sbcAddress}`, proxy: `sip:${sbcAddress}`,
callingNumber: this.callerId || req.callingNumber, callingNumber: this.callerId || req.callingNumber,
...(this.callerName && {callingName: this.callerName}), ...(this.callerName && {callingName: this.callerName})
opusFirst: isOpusFirst(this.cs.ep.remote.sdp)
}; };
const t = this.target.find((t) => t.type === 'teams'); const t = this.target.find((t) => t.type === 'teams');
@@ -500,14 +442,10 @@ class TaskDial extends Task {
} }
const ms = await cs.getMS(); const ms = await cs.getMS();
this.timerRing = setTimeout(async() => { this.timerRing = setTimeout(() => {
this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`); this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`);
this.timerRing = null; this.timerRing = null;
try { this._killOutdials();
await this._killOutdials();
} catch (err) {
this.logger.info(err, 'Dial:_attemptCall - error killing outdials');
}
this.result = { this.result = {
dialCallStatus: CallStatus.NoAnswer, dialCallStatus: CallStatus.NoAnswer,
dialSipStatus: 487 dialSipStatus: 487
@@ -569,8 +507,7 @@ class TaskDial extends Task {
callInfo: cs.callInfo, callInfo: cs.callInfo,
accountInfo: cs.accountInfo, accountInfo: cs.accountInfo,
rootSpan: cs.rootSpan, rootSpan: cs.rootSpan,
startSpan: this.startSpan.bind(this), startSpan: this.startSpan.bind(this)
dialTask: this
}); });
this.dials.set(sd.callSid, sd); this.dials.set(sd.callSid, sd);
@@ -586,8 +523,7 @@ class TaskDial extends Task {
} }
}) })
.on('callStatusChange', (obj) => { .on('callStatusChange', (obj) => {
if (this.results.dialCallStatus !== CallStatus.Completed && if (this.results.dialCallStatus !== CallStatus.Completed) {
this.results.dialCallStatus !== CallStatus.NoAnswer) {
Object.assign(this.results, { Object.assign(this.results, {
dialCallStatus: obj.callStatus, dialCallStatus: obj.callStatus,
dialSipStatus: obj.sipStatus, dialSipStatus: obj.sipStatus,
@@ -640,7 +576,11 @@ class TaskDial extends Task {
} }
}) })
.on('reinvite', (req, res) => { .on('reinvite', (req, res) => {
this._onReinvite(req, res); try {
cs.handleReinviteAfterMediaReleased(req, res);
} catch (err) {
this.logger.error(err, 'Error in dial einvite from B leg');
}
}) })
.on('refer', (callInfo, req, res) => { .on('refer', (callInfo, req, res) => {
@@ -676,35 +616,6 @@ class TaskDial extends Task {
this._killOutdials(); // NB: order is important this._killOutdials(); // NB: order is important
} }
async _onReinvite(req, res) {
try {
let isHandled = false;
if (this.cs.onHoldMusic) {
if (isOnhold(req.body) && !this.epOther && !this.ep) {
await this.cs.handleReinviteAfterMediaReleased(req, res);
// Onhold but media is already released
// reconnect A Leg and Response B leg
await this.reAnchorMedia(this.cs, this.sd);
this.isOutgoingLegHold = true;
isHandled = true;
this._onHoldHook();
} else if (!isOnhold(req.body) && this.epOther && this.ep && this.isOutgoingLegHold && this.canReleaseMedia) {
// Offhold, time to release media
const newSdp = await this.ep.modify(req.body);
await res.send(200, {body: newSdp});
await this._releaseMedia(this.cs, this.sd);
isHandled = true;
this.isOutgoingLegHold = false;
}
}
if (!isHandled) {
this.cs.handleReinviteAfterMediaReleased(req, res);
}
} catch (err) {
this.logger.error(err, 'Error in dial einvite from B leg');
}
}
_onMaxCallDuration(cs) { _onMaxCallDuration(cs) {
this.logger.info(`Dial:_onMaxCallDuration tearing down call as it has reached ${this.timeLimit}s`); this.logger.info(`Dial:_onMaxCallDuration tearing down call as it has reached ${this.timeLimit}s`);
this.ep && this.ep.unbridge(); this.ep && this.ep.unbridge();
@@ -757,7 +668,6 @@ class TaskDial extends Task {
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 (this.transcribeTask) this.transcribeTask.exec(cs, {ep: this.epOther, ep2:this.ep}); if (this.transcribeTask) this.transcribeTask.exec(cs, {ep: this.epOther, ep2:this.ep});
if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther}); if (this.listenTask) this.listenTask.exec(cs, {ep: this.epOther});
@@ -791,11 +701,9 @@ class TaskDial extends Task {
assert(cs.ep && sd.ep); assert(cs.ep && sd.ep);
try { try {
// Wait until we got new SDP from B leg to ofter to A Leg
const aLegSdp = cs.ep.remote.sdp; const aLegSdp = cs.ep.remote.sdp;
await sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp);
const bLegSdp = sd.dlg.remote.sdp; const bLegSdp = sd.dlg.remote.sdp;
await cs.releaseMediaToSBC(bLegSdp); await Promise.all[sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp), cs.releaseMediaToSBC(bLegSdp)];
this.epOther = null; this.epOther = null;
this.logger.info('Dial:_releaseMedia - successfully released media from freewitch'); this.logger.info('Dial:_releaseMedia - successfully released media from freewitch');
} catch (err) { } catch (err) {
@@ -812,29 +720,9 @@ class TaskDial extends Task {
} }
async handleReinviteAfterMediaReleased(req, res) { async handleReinviteAfterMediaReleased(req, res) {
let isHandled = false; const sdp = await this.dlg.modify(req.body);
if (isOnhold(req.body) && !this.epOther && !this.ep) { this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
const sdp = await this.dlg.modify(req.body); res.send(200, {body: sdp});
res.send(200, {body: sdp});
// Onhold but media is already released
await this.reAnchorMedia(this.cs, this.sd);
isHandled = true;
this.isIncomingLegHold = true;
this._onHoldHook();
} else if (!isOnhold(req.body) && this.epOther && this.ep && this.isIncomingLegHold && this.canReleaseMedia) {
// Offhold, time to release media
const newSdp = await this.epOther.modify(req.body);
await res.send(200, {body: newSdp});
await this._releaseMedia(this.cs, this.sd);
isHandled = true;
this.isIncomingLegHold = false;
}
if (!isHandled) {
const sdp = await this.dlg.modify(req.body);
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
res.send(200, {body: sdp});
}
} }
_onAmdEvent(cs, evt) { _onAmdEvent(cs, evt) {
@@ -845,48 +733,6 @@ class TaskDial extends Task {
this.logger.error({err}, 'Dial:_onAmdEvent - error calling actionHook'); this.logger.error({err}, 'Dial:_onAmdEvent - error calling actionHook');
}); });
} }
async _onHoldHook(allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) {
if (this.data.onHoldHook) {
// send silence for keep Voice quality
await this.epOther.play('silence_stream://500');
let allowedTasks;
do {
try {
const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3};
const json = await this.cs.application.requestor.
request('verb:hook', this.data.onHoldHook, this.cs.callInfo.toJSON(), httpHeaders);
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
allowedTasks = tasks.filter((t) => allowed.includes(t.name));
if (tasks.length !== allowedTasks.length) {
this.logger.debug({tasks, allowedTasks}, 'unsupported task');
throw new Error(`unsupported verb in enqueue waitHook: only ${JSON.stringify(allowed)}`);
}
this.logger.debug(`DialTask:_onHoldHook: executing ${tasks.length} tasks`);
if (tasks.length) {
this._playSession = new ConfirmCallSession({
logger: this.logger,
application: this.cs.application,
dlg: this.isIncomingLegHold ? this.dlg : this.cs.dlg,
ep: this.isIncomingLegHold ? this.ep : this.cs.ep,
callInfo: this.cs.callInfo,
accountInfo: this.cs.accountInfo,
tasks,
rootSpan: this.cs.rootSpan
});
await this._playSession.exec();
this._playSession = null;
}
} catch (error) {
this.logger.info(error, 'DialTask:_onHoldHook: failed retrieving waitHook');
this._playSession = null;
break;
}
} while (allowedTasks && allowedTasks.length > 0 && !this.killed && this.isOnHold);
this.logger.info('Finish onHoldHook');
}
}
} }
module.exports = TaskDial; module.exports = TaskDial;

View File

@@ -58,13 +58,6 @@ class Dialogflow extends Task {
this.vendor = this.data.tts.vendor || 'default'; this.vendor = this.data.tts.vendor || 'default';
this.language = this.data.tts.language || 'default'; this.language = this.data.tts.language || 'default';
this.voice = this.data.tts.voice || 'default'; this.voice = this.data.tts.voice || 'default';
this.speechSynthesisLabel = this.data.tts.label;
// fallback tts
this.fallbackVendor = this.data.tts.fallbackVendor || 'default';
this.fallbackLanguage = this.data.tts.fallbackLanguage || 'default';
this.fallbackVoice = this.data.tts.fallbackLanguage || 'default';
this.fallbackLabel = this.data.tts.fallbackLabel;
} }
this.bargein = this.data.bargein; this.bargein = this.data.bargein;
} }
@@ -125,15 +118,8 @@ class Dialogflow extends Task {
this.vendor = cs.speechSynthesisVendor; this.vendor = cs.speechSynthesisVendor;
this.language = cs.speechSynthesisLanguage; this.language = cs.speechSynthesisLanguage;
this.voice = cs.speechSynthesisVoice; this.voice = cs.speechSynthesisVoice;
this.speechSynthesisLabel = cs.speechSynthesisLabel;
} }
if (this.fallbackVendor === 'default') { this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
this.fallbackVendor = cs.fallbackSpeechSynthesisVendor;
this.fallbackLanguage = cs.fallbackSpeechSynthesisLanguage;
this.fallbackVoice = cs.fallbackSpeechSynthesisVoice;
this.fallbackLabel = cs.fallbackSpeechSynthesisLabel;
}
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts', this.speechSynthesisLabel);
this.ep.addCustomEventListener('dialogflow::intent', this._onIntent.bind(this, ep, cs)); this.ep.addCustomEventListener('dialogflow::intent', this._onIntent.bind(this, ep, cs));
this.ep.addCustomEventListener('dialogflow::transcription', this._onTranscription.bind(this, ep, cs)); this.ep.addCustomEventListener('dialogflow::transcription', this._onTranscription.bind(this, ep, cs));
@@ -235,8 +221,19 @@ class Dialogflow extends Task {
} }
try { try {
const {filePath} = await this._fallbackSynthAudio(cs, intent, stats, synthAudio); const obj = {
account_sid: cs.accountSid,
text: intent.fulfillmentText,
vendor: this.vendor,
language: this.language,
voice: this.voice,
salt: cs.callSid,
credentials: this.ttsCredentials
};
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via tts');
const {filePath, servedFromCache} = await synthAudio(stats, obj);
if (filePath) cs.trackTmpFile(filePath); if (filePath) cs.trackTmpFile(filePath);
if (!this.ttsCredentials && !servedFromCache) cs.billForTts(intent.fulfillmentText.length);
if (this.playInProgress) { if (this.playInProgress) {
await ep.api('uuid_break', ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio')); await ep.api('uuid_break', ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
@@ -280,46 +277,6 @@ class Dialogflow extends Task {
} }
} }
async _fallbackSynthAudio(cs, intent, stats, synthAudio) {
try {
const obj = {
account_sid: cs.accountSid,
text: intent.fulfillmentText,
vendor: this.vendor,
language: this.language,
voice: this.voice,
salt: cs.callSid,
credentials: this.ttsCredentials
};
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via tts');
return await synthAudio(stats, obj);
} catch (error) {
this.logger.info({error}, 'Failed to synthesize audio from primary vendor');
try {
if (this.fallbackVendor) {
const credentials = cs.getSpeechCredentials(this.fallbackVendor, 'tts', this.fallbackLabel);
const obj = {
account_sid: cs.accountSid,
text: intent.fulfillmentText,
vendor: this.fallbackVendor,
language: this.fallbackLanguage,
voice: this.fallbackVoice,
salt: cs.callSid,
credentials
};
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via fallback tts');
return await synthAudio(stats, obj);
}
} catch (err) {
this.logger.info({err}, 'Failed to synthesize audio from falllback vendor');
throw err;
}
throw error;
}
}
/** /**
* A transcription - either interim or final - has been returned. * A transcription - either interim or final - has been returned.
* If we are doing barge-in based on hotword detection, check for the hotword or phrase. * If we are doing barge-in based on hotword detection, check for the hotword or phrase.

View File

@@ -311,8 +311,7 @@ class TaskEnqueue extends Task {
} }
} }
async _playHook(cs, dlg, hook, async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave]) {
allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave, TaskName.Tag]) {
const {sortedSetLength, sortedSetPositionByPattern} = cs.srf.locals.dbHelpers; const {sortedSetLength, sortedSetPositionByPattern} = cs.srf.locals.dbHelpers;
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
@@ -332,7 +331,6 @@ class TaskEnqueue extends Task {
queuePosition: queuePosition.length ? queuePosition[0] : 0, queuePosition: queuePosition.length ? queuePosition[0] : 0,
callSid: this.cs.callSid, callSid: this.cs.callSid,
callId: this.cs.callId, callId: this.cs.callId,
customerData: this.cs.callInfo.customerData
}); });
} catch (err) { } catch (err) {
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}`);

View File

@@ -1,17 +1,17 @@
const Task = require('./task');
const { const {
TaskName, TaskName,
TaskPreconditions,
GoogleTranscriptionEvents, GoogleTranscriptionEvents,
NuanceTranscriptionEvents, NuanceTranscriptionEvents,
AwsTranscriptionEvents, AwsTranscriptionEvents,
AzureTranscriptionEvents, AzureTranscriptionEvents,
DeepgramTranscriptionEvents, DeepgramTranscriptionEvents,
SonioxTranscriptionEvents, SonioxTranscriptionEvents,
CobaltTranscriptionEvents,
IbmTranscriptionEvents, IbmTranscriptionEvents,
NvidiaTranscriptionEvents, NvidiaTranscriptionEvents,
JambonzTranscriptionEvents, JambonzTranscriptionEvents
AssemblyAiTranscriptionEvents } = require('../utils/constants');
} = require('../utils/constants.json');
const { const {
JAMBONES_GATHER_EARLY_HINTS_MATCH, JAMBONES_GATHER_EARLY_HINTS_MATCH,
JAMBONZ_GATHER_EARLY_HINTS_MATCH, JAMBONZ_GATHER_EARLY_HINTS_MATCH,
@@ -19,11 +19,34 @@ const {
} = require('../config'); } = require('../config');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const assert = require('assert'); const assert = require('assert');
const SttTask = require('./stt-task');
class TaskGather extends SttTask { const compileTranscripts = (logger, evt, arr) => {
if (!Array.isArray(arr) || arr.length === 0) return;
let t = '';
for (const a of arr) {
t += ` ${a.alternatives[0].transcript}`;
}
t += ` ${evt.alternatives[0].transcript}`;
evt.alternatives[0].transcript = t.trim();
};
class TaskGather extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
super(logger, opts, parentTask); super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
const {
setChannelVarsForStt,
normalizeTranscription,
removeSpeechListeners,
setSpeechCredentialsAtRuntime,
compileSonioxTranscripts
} = require('../utils/transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt;
this.normalizeTranscription = normalizeTranscription;
this.removeSpeechListeners = removeSpeechListeners;
this.compileSonioxTranscripts = compileSonioxTranscripts;
[ [
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits', 'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein', 'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
@@ -39,23 +62,27 @@ class TaskGather extends SttTask {
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true; this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
this.minBargeinWordCount = this.data.minBargeinWordCount || 1; this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
if (this.data.recognizer) { if (this.data.recognizer) {
const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor;
this.language = recognizer.language;
/* let credentials be supplied in the recognizer object at runtime */
this.sttCredentials = setSpeechCredentialsAtRuntime(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 recognizer.asrTimeout === 'number' ? recognizer.asrTimeout * 1000 : 0;
this.data.recognizer.asrTimeout * 1000 : 0; if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit;
if (this.asrTimeout > 0) { this.isContinuousAsr = this.asrTimeout > 0;
this.isContinuousAsr = true;
this.asrDtmfTerminationDigit = this.data.recognizer.asrDtmfTerminationDigit;
}
if (Array.isArray(this.data.recognizer.hints) && if (Array.isArray(this.data.recognizer.hints) &&
0 == this.data.recognizer.hints.length && JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS) { 0 == this.data.recognizer.hints.length && JAMBONES_GATHER_CLEAR_GLOBAL_HINTS_ON_EMPTY_HINTS) {
logger.debug('Gather: an empty hints array was supplied, so we will mask global hints'); logger.debug('Gather: an empty hints array was supplied, so we will mask global hints');
this.maskGlobalSttHints = true; this.maskGlobalSttHints = true;
} }
// fast Recognition, fire event after a specified time after the last hypothesis. this.data.recognizer.hints = this.data.recognizer.hints || [];
this.fastRecognitionTimeout = typeof this.data.recognizer.fastRecognitionTimeout === 'number' ? this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || [];
this.data.recognizer.fastRecognitionTimeout * 1000 : 0;
} }
else this.data.recognizer = {hints: [], altLanguages: []};
this.digitBuffer = ''; this.digitBuffer = '';
this._earlyMedia = this.data.earlyMedia === true; this._earlyMedia = this.data.earlyMedia === true;
@@ -70,8 +97,12 @@ class TaskGather extends SttTask {
/* buffer speech for continuous asr */ /* buffer speech for continuous asr */
this._bufferedTranscripts = []; this._bufferedTranscripts = [];
/* buffer for soniox transcripts */
this._sonioxTranscripts = [];
this.parentTask = parentTask;
this.partialTranscriptsCount = 0; this.partialTranscriptsCount = 0;
this.bugname_prefix = 'gather_';
} }
get name() { return TaskName.Gather; } get name() { return TaskName.Gather; }
@@ -104,12 +135,13 @@ class TaskGather extends SttTask {
async exec(cs, {ep}) { async exec(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);
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
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
.concat(hints) .concat(hints)
.filter((h) => typeof h === 'string' && h.length > 0)); .filter((h) => typeof h === 'string' && h.length > 0));
this.data.recognizer.hints = [...setOfHints]; this.data.recognizer.hints = [...setOfHints];
@@ -117,6 +149,14 @@ class TaskGather extends SttTask {
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost}, this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
'Gather:exec - applying global sttHints'); 'Gather:exec - applying global sttHints');
} }
if (cs.hasAltLanguages) {
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages},
'Gather:exec - applying altLanguages');
}
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
this.data.recognizer.punctuation = cs.globalSttPunctuation;
}
if (!this.isContinuousAsr && cs.isContinuousAsr) { if (!this.isContinuousAsr && cs.isContinuousAsr) {
this.isContinuousAsr = true; this.isContinuousAsr = true;
this.asrTimeout = cs.asrTimeout * 1000; this.asrTimeout = cs.asrTimeout * 1000;
@@ -134,26 +174,67 @@ 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');
} }
const startListening = async(cs, ep) => {
this.ep = ep;
if ('default' === this.vendor || !this.vendor) {
this.vendor = cs.speechRecognizerVendor;
if (this.data.recognizer) this.data.recognizer.vendor = this.vendor;
}
if ('default' === this.language || !this.language) {
this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language;
}
if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor;
}
if (this.needsStt && !this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
if (this.needsStt && !this.sttCredentials) {
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
// Notify application that STT vender is wrong.
this.notifyError({
msg: 'ASR error',
details: `No speech-to-text service credentials for ${this.vendor} have been configured`
});
this.notifyTaskDone();
throw new Error(`No speech-to-text service credentials for ${this.vendor} have been configured`);
}
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
/* get nuance access token */
const {client_id, secret} = this.sttCredentials;
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
this.logger.debug({client_id}, `Gather:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token};
}
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
/* get ibm access token */
const {stt_api_key, stt_region} = this.sttCredentials;
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
}
const startListening = (cs, ep) => {
this._startTimer(); this._startTimer();
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer(); if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
if (this.input.includes('speech') && !this.listenDuringPrompt) { if (this.input.includes('speech') && !this.listenDuringPrompt) {
try { this._initSpeech(cs, ep)
await this._setSpeechHandlers(cs, ep); .then(() => {
if (this.killed) { if (this.killed) {
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._startTranscribing(ep); this._startTranscribing(ep);
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
} catch (e) { })
if (this.fallbackVendor && this.isHandledByPrimaryProvider) { .catch((err) => {
await this._fallback(); this.logger.error({err}, 'error in initSpeech');
startListening(cs, ep); });
} else {
this.logger.error({error: e}, 'error in initSpeech');
}
}
} }
}; };
@@ -207,7 +288,7 @@ 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._initSpeech(cs, ep);
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 */});
@@ -221,7 +302,7 @@ class TaskGather extends SttTask {
} catch (err) { } catch (err) {
this.logger.error(err, 'TaskGather:exec error'); this.logger.error(err, 'TaskGather:exec error');
} }
this.removeCustomEventListeners(); this.removeSpeechListeners(ep);
} }
kill(cs) { kill(cs) {
@@ -259,16 +340,6 @@ class TaskGather extends SttTask {
this._resolve('dtmf-terminator-key'); this._resolve('dtmf-terminator-key');
} }
else if (this.input.includes('digits')) { else if (this.input.includes('digits')) {
if (this.digitBuffer.length === 0 && this.needsStt) {
// DTMF is higher priority than STT.
this.removeCustomEventListeners();
ep.stopTranscription({
vendor: this.vendor,
bugname: this.bugname,
})
.catch((err) => this.logger.error({err},
` Received DTMF, Error stopping transcription for vendor ${this.vendor}`));
}
this.digitBuffer += evt.dtmf; this.digitBuffer += evt.dtmf;
const len = this.digitBuffer.length; const len = this.digitBuffer.length;
if (len === this.numDigits || len === this.maxDigits) { if (len === this.numDigits || len === this.maxDigits) {
@@ -291,51 +362,38 @@ class TaskGather extends SttTask {
} }
} }
async _setSpeechHandlers(cs, ep) { async _initSpeech(cs, ep) {
if (this._speechHandlersSet) return; const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
this._speechHandlersSet = true;
/* some special deepgram logic */
if (this.vendor === 'deepgram') {
if (this.isContinuousAsr) this._doContinuousAsrWithDeepgram(this.asrTimeout);
if (this.data.recognizer?.deepgramOptions?.shortUtterance) this.shortUtterance = true;
}
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.language, this.data.recognizer);
switch (this.vendor) { switch (this.vendor) {
case 'google': case 'google':
this.bugname = `${this.bugname_prefix}google_transcribe`; this.bugname = 'google_transcribe';
this.addCustomEventListener( ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep, GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
this.addCustomEventListener( ep.addCustomEventListener(GoogleTranscriptionEvents.VadDetected, this._onVadDetected.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 = 'aws_transcribe';
this.addCustomEventListener(ep, AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(ep, AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep)); ep.addCustomEventListener(AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break; break;
case 'microsoft': case 'microsoft':
this.bugname = `${this.bugname_prefix}azure_transcribe`; this.bugname = 'azure_transcribe';
this.addCustomEventListener( ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep, AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep.addCustomEventListener(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)); ep.addCustomEventListener(AzureTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break; break;
case 'nuance': case 'nuance':
this.bugname = `${this.bugname_prefix}nuance_transcribe`; this.bugname = 'nuance_transcribe';
this.addCustomEventListener(ep, NuanceTranscriptionEvents.Transcription, ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep)); this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(ep, NuanceTranscriptionEvents.StartOfSpeech, ep.addCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep)); this._onStartOfSpeech.bind(this, cs, ep));
this.addCustomEventListener(ep, NuanceTranscriptionEvents.TranscriptionComplete, ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep)); this._onTranscriptionComplete.bind(this, cs, ep));
this.addCustomEventListener(ep, NuanceTranscriptionEvents.VadDetected, ep.addCustomEventListener(NuanceTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep)); this._onVadDetected.bind(this, cs, ep));
/* stall timers until prompt finishes playing */ /* stall timers until prompt finishes playing */
@@ -345,71 +403,35 @@ class TaskGather extends SttTask {
break; break;
case 'deepgram': case 'deepgram':
this.bugname = `${this.bugname_prefix}deepgram_transcribe`; this.bugname = 'deepgram_transcribe';
this.addCustomEventListener( ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep, DeepgramTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep.addCustomEventListener(DeepgramTranscriptionEvents.Connect, this._onDeepgramConnect.bind(this, cs, ep));
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep)); ep.addCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure,
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.ConnectFailure, this._onDeepGramConnectFailure.bind(this, cs, ep));
this._onVendorConnectFailure.bind(this, cs, ep));
/* if app sets deepgramOptions.utteranceEndMs they essentially want continuous asr */
if (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true;
break; break;
case 'soniox': case 'soniox':
this.bugname = `${this.bugname_prefix}soniox_transcribe`; this.bugname = 'soniox_transcribe';
this.addCustomEventListener( ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep, SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
break;
case 'cobalt':
this.bugname = `${this.bugname_prefix}cobalt_transcribe`;
this.addCustomEventListener(
ep, CobaltTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
/* cobalt doesnt have language, it has model, which is required */
if (!this.data.recognizer.model) {
throw new Error('Cobalt requires a model to be specified');
}
this.language = this.data.recognizer.model;
/* special case: if using hints with cobalt we need to compile them */
this.hostport = opts.COBALT_SERVER_URI;
if (this.vendor === 'cobalt' && opts.COBALT_SPEECH_HINTS) {
try {
const context = await this.compileHintsForCobalt(
ep,
this.hostport,
this.data.recognizer.model,
opts.COBALT_CONTEXT_TOKEN,
opts.COBALT_SPEECH_HINTS
);
if (context) opts.COBALT_COMPILED_CONTEXT_DATA = context;
delete opts.COBALT_SPEECH_HINTS;
} catch (err) {
this.logger.error({err}, 'Error compiling hints for cobalt');
}
}
delete opts.COBALT_SERVER_URI;
break; break;
case 'ibm': case 'ibm':
this.bugname = `${this.bugname_prefix}ibm_transcribe`; this.bugname = 'ibm_transcribe';
this.addCustomEventListener(ep, IbmTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(ep, IbmTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep)); ep.addCustomEventListener(IbmTranscriptionEvents.Connect, this._onIbmConnect.bind(this, cs, ep));
this.addCustomEventListener(ep, IbmTranscriptionEvents.ConnectFailure, ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
this._onVendorConnectFailure.bind(this, cs, ep)); this._onIbmConnectFailure.bind(this, cs, ep));
break; break;
case 'nvidia': case 'nvidia':
this.bugname = `${this.bugname_prefix}nvidia_transcribe`; this.bugname = 'nvidia_transcribe';
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.Transcription, ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep)); this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.StartOfSpeech, ep.addCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep)); this._onStartOfSpeech.bind(this, cs, ep));
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.TranscriptionComplete, ep.addCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep)); this._onTranscriptionComplete.bind(this, cs, ep));
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.VadDetected, ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep)); 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 */
@@ -418,24 +440,13 @@ class TaskGather extends SttTask {
} }
break; break;
case 'assemblyai':
this.bugname = `${this.bugname_prefix}assemblyai_transcribe`;
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(
ep, AssemblyAiTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.ConnectFailure,
this._onVendorConnectFailure.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.vendor}_transcribe`;
this.addCustomEventListener( ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
ep, JambonzTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep.addCustomEventListener(JambonzTranscriptionEvents.Connect, this._onJambonzConnect.bind(this, cs, ep));
this.addCustomEventListener(ep, JambonzTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep)); ep.addCustomEventListener(JambonzTranscriptionEvents.ConnectFailure,
this.addCustomEventListener(ep, JambonzTranscriptionEvents.ConnectFailure, this._onJambonzConnectFailure.bind(this, cs, ep));
this._onVendorConnectFailure.bind(this, cs, ep));
break; break;
} }
else { else {
@@ -446,7 +457,7 @@ class TaskGather extends SttTask {
} }
/* common handler for all stt engine errors */ /* common handler for all stt engine errors */
this.addCustomEventListener(ep, JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep)); ep.addCustomEventListener(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep));
await ep.set(opts) await ep.set(opts)
.catch((err) => this.logger.info(err, 'Error setting channel variables')); .catch((err) => this.logger.info(err, 'Error setting channel variables'));
} }
@@ -458,18 +469,11 @@ class TaskGather extends SttTask {
interim: this.interim, interim: this.interim,
bugname: this.bugname bugname: this.bugname
}, 'Gather:_startTranscribing'); }, 'Gather:_startTranscribing');
/**
* Note: we don't need to ask deepgram for interim results, because they
* already send us words as they are finalized (is_final=true) even before
* the utterance is finalized (speech_final=true)
*/
ep.startTranscription({ ep.startTranscription({
vendor: this.vendor, vendor: this.vendor,
locale: this.language, locale: this.language,
interim: this.interim, interim: this.interim,
bugname: this.bugname, bugname: this.bugname,
hostport: this.hostport,
}).catch((err) => { }).catch((err) => {
const {writeAlerts, AlertType} = this.cs.srf.locals; const {writeAlerts, AlertType} = this.cs.srf.locals;
this.logger.error(err, 'TaskGather:_startTranscribing error'); this.logger.error(err, 'TaskGather:_startTranscribing error');
@@ -487,11 +491,7 @@ class TaskGather extends SttTask {
this._clearTimer(); this._clearTimer();
this._timeoutTimer = setTimeout(() => { this._timeoutTimer = setTimeout(() => {
if (this.isContinuousAsr) this._startAsrTimer(); if (this.isContinuousAsr) this._startAsrTimer();
else if (this.interDigitTimeout <= 0 || else this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
this.digitBuffer.length < this.minDigits ||
this.needsStt && this.digitBuffer.length === 0) {
this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
}
}, this.timeout); }, this.timeout);
} }
@@ -503,13 +503,11 @@ class TaskGather extends SttTask {
} }
_startAsrTimer() { _startAsrTimer() {
if (this.vendor === 'deepgram') return; // no need
assert(this.isContinuousAsr); assert(this.isContinuousAsr);
this._clearAsrTimer(); this._clearAsrTimer();
this._asrTimer = setTimeout(() => { this._asrTimer = setTimeout(() => {
this.logger.debug('_startAsrTimer - asr timer went off'); this.logger.debug('_startAsrTimer - asr timer went off');
const evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language); this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout');
this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout', evt);
}, this.asrTimeout); }, this.asrTimeout);
this.logger.debug(`_startAsrTimer: set for ${this.asrTimeout}ms`); this.logger.debug(`_startAsrTimer: set for ${this.asrTimeout}ms`);
} }
@@ -519,28 +517,11 @@ class TaskGather extends SttTask {
this._asrTimer = null; this._asrTimer = null;
} }
_startFastRecognitionTimer(evt) {
assert(this.fastRecognitionTimeout > 0);
this._clearFastRecognitionTimer();
this._fastRecognitionTimer = setTimeout(() => {
evt.is_final = true;
this._resolve('speech', evt);
}, this.fastRecognitionTimeout);
}
_clearFastRecognitionTimer() {
if (this._fastRecognitionTimer) {
clearTimeout(this._fastRecognitionTimer);
}
this._fastRecognitionTimer = null;
}
_startFinalAsrTimer() { _startFinalAsrTimer() {
this._clearFinalAsrTimer(); this._clearFinalAsrTimer();
this._finalAsrTimer = setTimeout(() => { this._finalAsrTimer = setTimeout(() => {
this.logger.debug('_startFinalAsrTimer - final asr timer went off'); this.logger.debug('_startFinalAsrTimer - final asr timer went off');
const evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language); this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout');
this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout', evt);
}, 1000); }, 1000);
this.logger.debug('_startFinalAsrTimer: set for 1 second'); this.logger.debug('_startFinalAsrTimer: set for 1 second');
} }
@@ -579,24 +560,11 @@ class TaskGather extends SttTask {
this.logger.debug({evt, bugname, finished}, `Gather:_onTranscription for vendor ${this.vendor}`); this.logger.debug({evt, bugname, finished}, `Gather:_onTranscription for vendor ${this.vendor}`);
if (bugname && this.bugname !== bugname) return; if (bugname && this.bugname !== bugname) return;
if (this.vendor === 'ibm' && evt?.state === 'listening') return; if (this.vendor === 'ibm') {
if (evt?.state === 'listening') return;
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
/* we will only get this when we have set utterance_end_ms */
if (this._bufferedTranscripts.length === 0) {
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram but no buffered transcripts');
}
else {
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram, return buffered transcript');
evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language);
this._bufferedTranscripts = [];
this._resolve('speech', evt);
}
return;
} }
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language, evt = this.normalizeTranscription(evt, this.vendor, 1, this.language);
this.shortUtterance, this.data.recognizer.punctuation);
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;
@@ -618,27 +586,15 @@ class TaskGather extends SttTask {
const bufferedWords = this._sonioxTranscripts.length + const bufferedWords = this._sonioxTranscripts.length +
this._bufferedTranscripts.reduce((count, e) => count + e.alternatives[0]?.transcript.split(' ').length, 0); this._bufferedTranscripts.reduce((count, e) => count + e.alternatives[0]?.transcript.split(' ').length, 0);
let emptyTranscript = false;
if (evt.is_final) { if (evt.is_final) {
if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) { if (evt.alternatives[0].transcript === '' && !this.callSession.callGone && !this.killed) {
emptyTranscript = true;
if (finished === 'true' && ['microsoft', 'deepgram'].includes(this.vendor)) { if (finished === 'true' && ['microsoft', 'deepgram'].includes(this.vendor)) {
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;
} }
else if (this.vendor !== 'deepgram') { else {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening'); this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
return;
}
else if (this.isContinuousAsr) {
this.logger.info({evt},
'TaskGather:_onTranscription - got empty deepgram transcript during continous asr, continue listening');
return;
}
else if (this.vendor === 'deepgram' && this._bufferedTranscripts.length > 0) {
this.logger.info({evt},
'TaskGather:_onTranscription - got empty transcript from deepgram, return the buffered transcripts');
} }
return;
} }
if (this.isContinuousAsr) { if (this.isContinuousAsr) {
@@ -650,14 +606,14 @@ class TaskGather extends SttTask {
this.logger.debug('TaskGather:_onTranscription - removing trailing punctuation'); this.logger.debug('TaskGather:_onTranscription - removing trailing punctuation');
evt.alternatives[0].transcript = t.slice(0, -1); evt.alternatives[0].transcript = t.slice(0, -1);
} }
else this.logger.debug({t}, 'TaskGather:_onTranscription - no trailing punctuation');
} }
this.logger.info({evt}, 'TaskGather:_onTranscription - got transcript during continous asr'); this.logger.info({evt}, 'TaskGather:_onTranscription - got transcript during continous asr');
this._bufferedTranscripts.push(evt); this._bufferedTranscripts.push(evt);
this._clearTimer(); this._clearTimer();
if (this._finalAsrTimer) { if (this._finalAsrTimer) {
this._clearFinalAsrTimer(); this._clearFinalAsrTimer();
const evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language); return this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout');
return this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout', evt);
} }
this._startAsrTimer(); this._startAsrTimer();
@@ -679,25 +635,16 @@ class TaskGather extends SttTask {
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') {
/* compile transcripts into one */
if (!emptyTranscript) this._bufferedTranscripts.push(evt);
if (this.data.recognizer?.deepgramOptions?.utteranceEndMs) {
this.logger.debug('TaskGather:_onTranscription - got speech_final waiting for UtteranceEnd event');
return;
}
this.logger.debug({evt}, 'TaskGather:_onTranscription - compiling deepgram transcripts');
evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language);
this._bufferedTranscripts = [];
this.logger.debug({evt}, 'TaskGather:_onTranscription - compiled deepgram transcripts');
}
/* here is where we return a final transcript */
this._resolve('speech', evt); this._resolve('speech', evt);
} }
} }
} }
else { else {
/* google has a measure of stability:
https://cloud.google.com/speech-to-text/docs/basics#streaming_responses
others do not.
*/
//const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD;
this._clearTimer(); this._clearTimer();
this._startTimer(); this._startTimer();
if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) { if (this.bargein && (words + bufferedWords) >= this.minBargeinWordCount) {
@@ -707,9 +654,6 @@ class TaskGather extends SttTask {
} }
this._killAudio(cs); this._killAudio(cs);
} }
if (this.fastRecognitionTimeout) {
this._startFastRecognitionTimer(evt);
}
if (this.partialResultHook) { if (this.partialResultHook) {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
@@ -723,14 +667,6 @@ class TaskGather extends SttTask {
this._sonioxTranscripts.push(evt.vendor.finalWords); this._sonioxTranscripts.push(evt.vendor.finalWords);
} }
} }
/* deepgram can send a non-final transcript but with words that are final, so we need to buffer */
if (this.vendor === 'deepgram') {
const originalEvent = evt.vendor.evt;
if (originalEvent.is_final && evt.alternatives[0].transcript !== '') {
this.logger.debug({evt}, 'Gather:_onTranscription - buffering a completed (partial) deepgram transcript');
this._bufferedTranscripts.push(evt);
}
}
} }
} }
_onEndOfUtterance(cs, ep) { _onEndOfUtterance(cs, ep) {
@@ -745,7 +681,7 @@ class TaskGather extends SttTask {
* getting a transcription. This can happen if someone coughs or mumbles. * getting a transcription. This can happen if someone coughs or mumbles.
* For that reason don't ask for a single utterance and we'll terminate the transcribe operation * For that reason don't ask for a single utterance and we'll terminate the transcribe operation
* once we get a final transcript. * once we get a final transcript.
* However, if the user has specified a singleUtterance, then we need to restart here * However, if the usr has specified a singleUtterance, then we need to restart here
* 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) {
@@ -762,25 +698,14 @@ 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) { _onDeepgramConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onDeepgramConnect');
}
_onJambonzConnect(_cs, _ep) {
this.logger.debug('TaskGather:_onJambonzConnect');
}
_onJambonzError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskGather:_onJambonzError'); this.logger.info({evt}, 'TaskGather:_onJambonzError');
if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
ep.stopTranscription({
vendor: this.vendor,
bugname: this.bugname
})
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
try {
await this._fallback();
await this._initSpeech(cs, ep);
this._startTranscribing(ep);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
return;
} catch (error) {
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
}
}
const {writeAlerts, AlertType} = cs.srf.locals; const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') { if (this.vendor === 'nuance') {
@@ -798,16 +723,54 @@ class TaskGather extends SttTask {
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`}); this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
} }
_onVendorConnectFailure(cs, _ep, evt) { _onDeepGramConnectFailure(cs, _ep, evt) {
super._onVendorConnectFailure(cs, _ep, evt); const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onDeepgramConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to Deepgram speech recognizer: ${reason}`,
vendor: 'deepgram',
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor deepgram: ${reason}`});
this.notifyTaskDone();
}
_onJambonzConnectFailure(cs, _ep, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onJambonzConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${reason}`});
this.notifyTaskDone(); this.notifyTaskDone();
} }
_onVendorError(cs, _ep, evt) { _onIbmConnect(_cs, _ep) {
super._onVendorError(cs, _ep, evt); this.logger.debug('TaskGather:_onIbmConnect');
this._resolve('stt-error', evt);
} }
_onIbmConnectFailure(cs, _ep, evt) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskGather:_onIbmConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to IBM watson speech recognizer: ${reason}`,
vendor: 'ibm',
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor IBM: ${reason}`});
this.notifyTaskDone();
}
_onIbmError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskGather:_onIbmError'); }
_onVadDetected(cs, ep) { _onVadDetected(cs, ep) {
if (this.bargein && this.minBargeinWordCount === 0) { if (this.bargein && this.minBargeinWordCount === 0) {
this.logger.debug('TaskGather:_onVadDetected'); this.logger.debug('TaskGather:_onVadDetected');
@@ -841,7 +804,18 @@ class TaskGather extends SttTask {
} }
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearTimer(); this._clearTimer();
this._clearFastRecognitionTimer();
if (this.isContinuousAsr && reason.startsWith('speech')) {
evt = {
is_final: true,
transcripts: this._bufferedTranscripts
};
this.logger.debug({evt}, 'TaskGather:resolve continuous asr');
}
else if (!this.isContinuousAsr && reason.startsWith('speech') && this._bufferedTranscripts.length) {
compileTranscripts(this.logger, evt, this._bufferedTranscripts);
this.logger.debug({evt}, 'TaskGather:resolve buffered results');
}
this.span.setAttributes({ this.span.setAttributes({
channel: 1, channel: 1,
@@ -849,10 +823,7 @@ class TaskGather extends SttTask {
'stt.result': JSON.stringify(evt) 'stt.result': JSON.stringify(evt)
}); });
if (this.needsStt && this.ep && this.ep.connected) { if (this.needsStt && this.ep && this.ep.connected) {
this.ep.stopTranscription({ this.ep.stopTranscription({vendor: this.vendor})
vendor: this.vendor,
bugname: this.bugname
})
.catch((err) => this.logger.error({err}, 'Error stopping transcription')); .catch((err) => this.logger.error({err}, 'Error stopping transcription'));
} }
@@ -884,13 +855,6 @@ class TaskGather extends SttTask {
await this.performAction({reason: 'timeout'}); await this.performAction({reason: 'timeout'});
} }
} }
else if (reason.startsWith('stt-error')) {
if (this.parentTask) this.parentTask.emit('stt-error', evt);
else {
this.emit('stt-error', evt);
await this.performAction({reason: 'error', details: evt.error});
}
}
} catch (err) { /*already logged error*/ } } catch (err) { /*already logged error*/ }
this.notifyTaskDone(); this.notifyTaskDone();
} }

View File

@@ -25,13 +25,6 @@ class Lex extends Task {
this.vendor = this.data.tts.vendor || 'default'; this.vendor = this.data.tts.vendor || 'default';
this.language = this.data.tts.language || 'default'; this.language = this.data.tts.language || 'default';
this.voice = this.data.tts.voice || 'default'; this.voice = this.data.tts.voice || 'default';
this.speechCredentialLabel = this.data.tts.label || 'default';
// fallback tts
this.fallbackVendor = this.data.tts.fallbackVendor || 'default';
this.fallbackLanguage = this.data.tts.fallbackLanguage || 'default';
this.fallbackVoice = this.data.tts.fallbackLanguage || 'default';
this.fallbackLabel = this.data.tts.fallbackLabel || 'default';
} }
this.botName = `${this.bot}:${this.alias}:${this.region}`; this.botName = `${this.bot}:${this.alias}:${this.region}`;
@@ -109,16 +102,8 @@ class Lex extends Task {
this.vendor = cs.speechSynthesisVendor; this.vendor = cs.speechSynthesisVendor;
this.language = cs.speechSynthesisLanguage; this.language = cs.speechSynthesisLanguage;
this.voice = cs.speechSynthesisVoice; this.voice = cs.speechSynthesisVoice;
this.speechCredentialLabel = cs.speechSynthesisLabel;
} }
if (this.fallbackVendor === 'default') { this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
this.fallbackVendor = cs.fallbackSpeechSynthesisVendor;
this.fallbackLanguage = cs.fallbackSpeechSynthesisLanguage;
this.fallbackVoice = cs.fallbackSpeechSynthesisVoice;
this.fallbackLabel = cs.fallbackSpeechSynthesisLabel;
}
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts', this.speechCredentialLabel);
this.ep.addCustomEventListener('lex::intent', this._onIntent.bind(this, ep, cs)); this.ep.addCustomEventListener('lex::intent', this._onIntent.bind(this, ep, cs));
this.ep.addCustomEventListener('lex::transcription', this._onTranscription.bind(this, ep, cs)); this.ep.addCustomEventListener('lex::transcription', this._onTranscription.bind(this, ep, cs));
@@ -183,41 +168,6 @@ class Lex extends Task {
} }
} }
async _fallbackSynthAudio(cs, msg, stats, synthAudio) {
try {
const {filePath} = await synthAudio(stats, {
account_sid: cs.accountSid,
text: msg,
vendor: this.vendor,
language: this.language,
voice: this.voice,
salt: cs.callSid,
credentials: this.ttsCredentials
});
return filePath;
} catch (error) {
this.logger.info({error}, 'failed to synth audio from primary vendor');
if (this.fallbackVendor) {
try {
const credential = cs.getSpeechCredentials(this.fallbackVendor, 'tts', this.fallbackLabel);
const {filePath} = await synthAudio(stats, {
account_sid: cs.accountSid,
text: msg,
vendor: this.fallbackVendor,
language: this.fallbackLanguage,
voice: this.fallbackVoice,
salt: cs.callSid,
credentials: credential
});
return filePath;
} catch (err) {
this.logger.info({err}, 'failed to synth audio from fallback vendor');
}
}
}
}
/** /**
* @param {*} evt - event data * @param {*} evt - event data
*/ */
@@ -237,7 +187,16 @@ class Lex extends Task {
try { try {
this.logger.debug(`tts with ${this.vendor} ${this.voice}`); this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
const filePath = await this._fallbackSynthAudio(cs, msg, stats, synthAudio); // eslint-disable-next-line no-unused-vars
const {filePath, servedFromCache} = await synthAudio(stats, {
account_sid: cs.accountSid,
text: msg,
vendor: this.vendor,
language: this.language,
voice: this.voice,
salt: cs.callSid,
credentials: this.ttsCredentials
});
if (filePath) cs.trackTmpFile(filePath); if (filePath) cs.trackTmpFile(filePath);
if (this.events.includes('start-play')) { if (this.events.includes('start-play')) {

View File

@@ -8,7 +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);
this.disableBidirectionalAudio = opts.disableBidirectionalAudio;
this.preconditions = TaskPreconditions.Endpoint; this.preconditions = TaskPreconditions.Endpoint;
[ [
@@ -155,7 +154,7 @@ class TaskListen extends Task {
} }
/* support bi-directional audio */ /* support bi-directional audio */
if (!this.disableBidirectionalAudio) { 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

@@ -16,7 +16,6 @@ class TaskRestDial extends Task {
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.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));
@@ -64,8 +63,7 @@ class TaskRestDial extends Task {
this.canCancel = false; this.canCancel = false;
const cs = this.callSession; const cs = this.callSession;
cs.setDialog(dlg); cs.setDialog(dlg);
this.logger.debug('TaskRestDial:_onConnect - call connected');
if (this.sipRequestWithinDialogHook) this._initSipRequestWithinDialogHandler(cs, dlg);
try { try {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
@@ -92,10 +90,8 @@ class TaskRestDial extends Task {
} }
let tasks; let tasks;
if (this.app_json) { if (this.app_json) {
this.logger.debug('TaskRestDial: using app_json from task data');
tasks = JSON.parse(this.app_json); tasks = JSON.parse(this.app_json);
} else { } else {
this.logger.debug({call_hook: this.call_hook}, 'TaskRestDial: retrieving application');
tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders); tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
} }
if (tasks && Array.isArray(tasks)) { if (tasks && Array.isArray(tasks)) {
@@ -129,10 +125,7 @@ class TaskRestDial extends Task {
_onCallTimeout() { _onCallTimeout() {
this.logger.debug('TaskRestDial: timeout expired without answer, killing task'); this.logger.debug('TaskRestDial: timeout expired without answer, killing task');
this.timer = null; this.timer = null;
if (this.canCancel) { this.kill(this.cs);
this.canCancel = false;
this.cs?.req?.cancel();
}
} }
_onAmdEvent(cs, evt) { _onAmdEvent(cs, evt) {
@@ -143,16 +136,6 @@ class TaskRestDial extends Task {
this.logger.error({err}, 'Rest:dial:_onAmdEvent - error calling actionHook'); this.logger.error({err}, 'Rest:dial:_onAmdEvent - error calling actionHook');
}); });
} }
_initSipRequestWithinDialogHandler(cs, dlg) {
cs.sipRequestWithinDialogHook = this.sipRequestWithinDialogHook;
dlg.on('info', this._onRequestWithinDialog.bind(this, cs));
dlg.on('message', this._onRequestWithinDialog.bind(this, cs));
}
async _onRequestWithinDialog(cs, req, res) {
cs._onRequestWithinDialog(req, res);
}
} }
module.exports = TaskRestDial; module.exports = TaskRestDial;

View File

@@ -37,7 +37,6 @@ class TaskSay extends Task {
this.synthesizer = this.data.synthesizer || {}; this.synthesizer = this.data.synthesizer || {};
this.disableTtsCache = this.data.disableTtsCache; this.disableTtsCache = this.data.disableTtsCache;
this.options = this.synthesizer.options || {}; this.options = this.synthesizer.options || {};
this.isHandledByPrimaryProvider = true;
} }
get name() { return TaskName.Say; } get name() { return TaskName.Say; }
@@ -50,24 +49,26 @@ class TaskSay extends Task {
return `${this.name}{${this.text[0]}}`; return `${this.name}{${this.text[0]}}`;
} }
_validateURL(urlString) { async exec(cs, {ep}) {
try { await super.exec(cs);
new URL(urlString);
return true;
} catch (e) {
return false;
}
}
async _synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label, preCache = false}) {
const {srf} = cs; const {srf} = cs;
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
const {writeAlerts, AlertType, stats} = srf.locals; const {writeAlerts, AlertType, stats} = srf.locals;
const {synthAudio} = srf.locals.dbHelpers; const {synthAudio} = srf.locals.dbHelpers;
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 ;
let voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
this.synthesizer.voice :
cs.speechSynthesisVoice;
const engine = this.synthesizer.engine || 'standard'; const engine = this.synthesizer.engine || 'standard';
const salt = cs.callSid; const salt = cs.callSid;
let credentials = cs.getSpeechCredentials(vendor, 'tts');
let credentials = cs.getSpeechCredentials(vendor, 'tts', label);
/* parse Nuance voices into name and model */ /* parse Nuance voices into name and model */
let model; let model;
if (vendor === 'nuance' && voice) { if (vendor === 'nuance' && voice) {
@@ -76,8 +77,6 @@ class TaskSay extends Task {
voice = arr[1]; voice = arr[1];
model = arr[2]; model = arr[2];
} }
} else if (vendor === 'deepgram') {
model = voice;
} }
/* allow for microsoft custom region voice and api_key to be specified as an override */ /* allow for microsoft custom region voice and api_key to be specified as an override */
@@ -88,16 +87,10 @@ class TaskSay extends Task {
credentials.api_key = this.options.apiKey || credentials.apiKey; credentials.api_key = this.options.apiKey || credentials.apiKey;
credentials.region = this.options.region || credentials.region; credentials.region = this.options.region || credentials.region;
voice = this.options.voice || voice; 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;
} }
if (!preCache) this.logger.info({vendor, language, voice, model}, 'TaskSay:exec'); this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
this.ep = ep;
try { try {
if (!credentials) { if (!credentials) {
writeAlerts({ writeAlerts({
@@ -120,15 +113,11 @@ class TaskSay extends Task {
if (text.startsWith('silence_stream://')) return text; if (text.startsWith('silence_stream://')) return text;
/* otel: trace time for tts */ /* otel: trace time for tts */
let otelSpan; const {span} = this.startChildSpan('tts-generation', {
if (!preCache) { 'tts.vendor': vendor,
const {span} = this.startChildSpan('tts-generation', { 'tts.language': language,
'tts.vendor': vendor, 'tts.voice': voice
'tts.language': language, });
'tts.voice': voice
});
otelSpan = span;
}
try { try {
const {filePath, servedFromCache, rtt} = await synthAudio(stats, { const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
account_sid: cs.accountSid, account_sid: cs.accountSid,
@@ -140,7 +129,6 @@ class TaskSay extends Task {
model, model,
salt, salt,
credentials, credentials,
options: this.options,
disableTtsCache : this.disableTtsCache disableTtsCache : this.disableTtsCache
}); });
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`); this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`);
@@ -150,9 +138,9 @@ class TaskSay extends Task {
updateSpeechCredentialLastUsed(credentials.speech_credential_sid) updateSpeechCredentialLastUsed(credentials.speech_credential_sid)
.catch(() => {/*already logged error */}); .catch(() => {/*already logged error */});
} }
if (otelSpan) otelSpan.setAttributes({'tts.cached': servedFromCache}); span.setAttributes({'tts.cached': servedFromCache});
if (otelSpan) otelSpan.end(); span.end();
if (!servedFromCache && rtt && !preCache) { if (!servedFromCache && rtt) {
this.notifyStatus({ this.notifyStatus({
event: 'synthesized-audio', event: 'synthesized-audio',
vendor, vendor,
@@ -164,7 +152,7 @@ class TaskSay extends Task {
return filePath; return filePath;
} catch (err) { } catch (err) {
this.logger.info({err}, 'Error synthesizing tts'); this.logger.info({err}, 'Error synthesizing tts');
if (otelSpan) otelSpan.end(); span.end();
writeAlerts({ writeAlerts({
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.TTS_FAILURE, alert_type: AlertType.TTS_FAILURE,
@@ -172,83 +160,31 @@ class TaskSay extends Task {
detail: err.message detail: err.message
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
this.notifyError({msg: 'TTS error', details: err.message || err}); this.notifyError({msg: 'TTS error', details: err.message || err});
throw err; return;
} }
}; };
const arr = this.text.map((t) => (this._validateURL(t) ? t : generateAudio(t))); const arr = this.text.map((t) => generateAudio(t));
return (await Promise.all(arr)).filter((fp) => fp && fp.length); const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
this.notifyStatus({event: 'start-playback'});
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
let segment = 0;
while (!this.killed && segment < filepath.length) {
if (cs.isInConference) {
const {memberId, confName, confUuid} = cs;
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
}
else {
this.logger.debug(`Say:exec sending command to play file ${filepath[segment]}`);
await ep.play(filepath[segment]);
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
}
segment++;
}
}
} catch (err) { } catch (err) {
this.logger.info(err, 'TaskSay:exec error'); this.logger.info(err, 'TaskSay:exec error');
throw err;
}
}
async exec(cs, {ep}) {
await super.exec(cs);
this.ep = ep;
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 fallbackVendor = this.synthesizer.fallbackVendor && this.synthesizer.fallbackVendor !== 'default' ?
this.synthesizer.fallbackVendor :
cs.fallbackSpeechSynthesisVendor;
const fallbackLanguage = this.synthesizer.fallbackLanguage && this.synthesizer.fallbackLanguage !== 'default' ?
this.synthesizer.fallbackLanguage :
cs.fallbackSpeechSynthesisLanguage ;
const fallbackVoice = this.synthesizer.fallbackVoice && this.synthesizer.fallbackVoice !== 'default' ?
this.synthesizer.fallbackVoice :
cs.fallbackSpeechSynthesisVoice;
const fallbackLabel = this.synthesizer.fallbackLabel && this.synthesizer.fallbackLabel !== 'default' ?
this.synthesizer.fallbackLabel :
cs.fallbackSpeechSynthesisLabel;
let filepath;
try {
filepath = await this._synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label});
} catch (error) {
if (fallbackVendor && this.isHandledByPrimaryProvider) {
this.isHandledByPrimaryProvider = false;
this.logger.info(`Synthesize error, fallback to ${fallbackVendor}`);
filepath = await this._synthesizeWithSpecificVendor(cs, ep,
{
vendor: fallbackVendor,
language: fallbackLanguage,
voice: fallbackVoice,
label: fallbackLabel
});
} else {
throw error;
}
}
this.notifyStatus({event: 'start-playback'});
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
let segment = 0;
while (!this.killed && segment < filepath.length) {
if (cs.isInConference) {
const {memberId, confName, confUuid} = cs;
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
}
else {
this.logger.debug(`Say:exec sending command to play file ${filepath[segment]}`);
await ep.play(filepath[segment]);
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
}
segment++;
}
} }
this.emit('playDone'); this.emit('playDone');
} }

View File

@@ -1,278 +0,0 @@
const Task = require('./task');
const assert = require('assert');
const crypto = require('crypto');
const { TaskPreconditions, CobaltTranscriptionEvents } = require('../utils/constants');
class SttTask extends Task {
constructor(logger, data, parentTask) {
super(logger, data);
this.parentTask = parentTask;
this.preconditions = TaskPreconditions.Endpoint;
const {
setChannelVarsForStt,
normalizeTranscription,
setSpeechCredentialsAtRuntime,
compileSonioxTranscripts,
consolidateTranscripts
} = require('../utils/transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt;
this.normalizeTranscription = normalizeTranscription;
this.compileSonioxTranscripts = compileSonioxTranscripts;
this.consolidateTranscripts = consolidateTranscripts;
this.eventHandlers = [];
this.isHandledByPrimaryProvider = true;
if (this.data.recognizer) {
const recognizer = this.data.recognizer;
this.vendor = recognizer.vendor;
this.language = recognizer.language;
this.label = recognizer.label;
//fallback
this.fallbackVendor = recognizer.fallbackVendor || 'default';
this.fallbackLanguage = recognizer.fallbackLanguage || 'default';
this.fallbackLabel = recognizer.fallbackLabel || 'default';
/* let credentials be supplied in the recognizer object at runtime */
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
if (!Array.isArray(this.data.recognizer.altLanguages)) {
this.data.recognizer.altLanguages = [];
}
} else {
this.data.recognizer = {hints: [], altLanguages: []};
}
/* buffer for soniox transcripts */
this._sonioxTranscripts = [];
/*bug name prefix */
this.bugname_prefix = '';
}
async exec(cs, {ep, ep2}) {
super.exec(cs);
this.ep = ep;
this.ep2 = ep2;
// copy all value from config verb to this object.
if (cs.recognizer) {
for (const k in cs.recognizer) {
if (Array.isArray(this.data.recognizer[k]) ||
Array.isArray(cs.recognizer[k])) {
this.data.recognizer[k] = [
...this.data.recognizer[k],
...cs.recognizer[k]
];
} else if (typeof this.data.recognizer[k] === 'object' ||
typeof cs.recognizer[k] === 'object'
) {
this.data.recognizer[k] = {
...this.data.recognizer[k],
...cs.recognizer[k]
};
} else {
this.data.recognizer[k] = cs.recognizer[k] || this.data.recognizer[k];
}
}
}
if ('default' === this.vendor || !this.vendor) {
this.vendor = cs.speechRecognizerVendor;
if (this.data.recognizer) this.data.recognizer.vendor = this.vendor;
}
if ('default' === this.language || !this.language) {
this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language;
}
if ('default' === this.label || !this.label) {
this.label = cs.speechRecognizerLabel;
if (this.data.recognizer) this.data.recognizer.label = this.label;
}
// Fallback options
if ('default' === this.fallbackVendor || !this.fallbackVendor) {
this.fallbackVendor = cs.fallbackSpeechRecognizerVendor;
if (this.data.recognizer) this.data.recognizer.fallbackVendor = this.fallbackVendor;
}
if ('default' === this.fallbackLanguage || !this.fallbackLanguage) {
this.fallbackLanguage = cs.fallbackSpeechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.fallbackLanguage = this.fallbackLanguage;
}
if ('default' === this.fallbackLabel || !this.fallbackLabel) {
this.fallbackLabel = cs.fallbackSpeechRecognizerLabel;
if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel;
}
if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor;
}
if (this.vendor === 'cobalt' && !this.data.recognizer.model) {
// By default, application saves cobalt model in language
this.data.recognizer.model = cs.speechRecognizerLanguage;
}
if (!this.sttCredentials) {
try {
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
} catch (error) {
if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
await this._fallback();
} else {
throw error;
}
}
}
/* when using cobalt model is required */
if (this.vendor === 'cobalt' && !this.data.recognizer.model) {
this.notifyError({ msg: 'ASR error', details:'Cobalt requires a model to be specified'});
throw new Error('Cobalt requires a model to be specified');
}
if (cs.hasAltLanguages) {
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages},
'STT:exec - applying altLanguages');
}
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
this.data.recognizer.punctuation = cs.globalSttPunctuation;
}
}
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 _initSpeechCredentials(cs, vendor, label) {
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
let credentials = cs.getSpeechCredentials(vendor, 'stt', label);
if (!credentials) {
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`ERROR stt using ${vendor} requested but creds not supplied`);
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED,
vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
// Notify application that STT vender is wrong.
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) {
/* get nuance access token */
const {client_id, secret} = credentials;
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
this.logger.debug({client_id}, `got nuance access token ${servedFromCache ? 'from cache' : ''}`);
credentials = {...credentials, access_token};
}
else if (vendor == 'ibm' && credentials.stt_api_key) {
/* get ibm access token */
const {stt_api_key, stt_region} = credentials;
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
this.logger.debug({stt_api_key}, `got ibm access token ${servedFromCache ? 'from cache' : ''}`);
credentials = {...credentials, access_token, stt_region};
}
return credentials;
}
async _fallback() {
assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration');
this.isHandledByPrimaryProvider = false;
this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
this.vendor = this.fallbackVendor;
this.language = this.fallbackLanguage;
this.label = this.fallbackLabel;
this.data.recognizer.vendor = this.vendor;
this.data.recognizer.language = this.language;
this.data.recognizer.label = this.label;
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
}
async compileHintsForCobalt(ep, hostport, model, token, hints) {
const {retrieveKey} = this.cs.srf.locals.dbHelpers;
const hash = crypto.createHash('sha1');
hash.update(`${model}:${hints}`);
const key = `cobalt:${hash.digest('hex')}`;
this.context = await retrieveKey(key);
if (this.context) {
this.logger.debug({model, hints}, 'found cached cobalt context for supplied hints');
return this.context;
}
this.logger.debug({model, hints}, 'compiling cobalt context for supplied hints');
return new Promise((resolve, reject) => {
this.cobaltCompileResolver = resolve;
ep.addCustomEventListener(CobaltTranscriptionEvents.CompileContext, this._onCompileContext.bind(this, ep, key));
ep.api('uuid_cobalt_compile_context', [ep.uuid, hostport, model, token, hints], (err, evt) => {
if (err || 0 !== evt.getBody().indexOf('+OK')) {
ep.removeCustomEventListener(CobaltTranscriptionEvents.CompileContext);
return reject(err);
}
});
});
}
_onCompileContext(ep, key, evt) {
const {addKey} = this.cs.srf.locals.dbHelpers;
this.logger.debug({evt}, `received cobalt compile context event, will cache under ${key}`);
this.cobaltCompileResolver(evt.compiled_context);
ep.removeCustomEventListener(CobaltTranscriptionEvents.CompileContext);
this.cobaltCompileResolver = null;
//cache the compiled context
addKey(key, evt.compiled_context, 3600 * 12)
.catch((err) => this.logger.info({err}, `Error caching cobalt context for ${key}`));
}
_doContinuousAsrWithDeepgram(asrTimeout) {
/* deepgram has an utterance_end_ms property that simplifies things */
assert(this.vendor === 'deepgram');
this.logger.debug(`_doContinuousAsrWithDeepgram - setting utterance_end_ms to ${asrTimeout}`);
const dgOptions = this.data.recognizer.deepgramOptions = this.data.recognizer.deepgramOptions || {};
dgOptions.utteranceEndMs = dgOptions.utteranceEndMs || asrTimeout;
}
_onVendorConnect(_cs, _ep) {
this.logger.debug(`TaskGather:_on${this.vendor}Connect`);
}
_onVendorError(cs, _ep, evt) {
this.logger.info({evt}, `${this.name}:_on${this.vendor}Error`);
const {writeAlerts, AlertType} = cs.srf.locals;
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: 'STT failure reported by vendor',
detail: evt.error,
vendor: this.vendor,
}).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) {
const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, `${this.name}:_on${this.vendor}ConnectFailure`);
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
vendor: this.vendor,
}).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}`});
}
}
module.exports = SttTask;

View File

@@ -12,7 +12,7 @@ class TaskTag extends Task {
async exec(cs) { async exec(cs) {
super.exec(cs); super.exec(cs);
cs.callInfo.customerData = this.data; cs.callInfo.customerData = this.data;
this.logger.debug({customerData: cs.callInfo.customerData}, 'TaskTag:exec set customer data in callInfo'); //this.logger.debug({callInfo: cs.callInfo.toJSON()}, 'TaskTag:exec set customer data in callInfo');
} }
} }

View File

@@ -1,62 +1,69 @@
const assert = require('assert'); const Task = require('./task');
const { const {
TaskName, TaskName,
TaskPreconditions,
GoogleTranscriptionEvents, GoogleTranscriptionEvents,
NuanceTranscriptionEvents, NuanceTranscriptionEvents,
AwsTranscriptionEvents, AwsTranscriptionEvents,
AzureTranscriptionEvents, AzureTranscriptionEvents,
DeepgramTranscriptionEvents, DeepgramTranscriptionEvents,
SonioxTranscriptionEvents, SonioxTranscriptionEvents,
CobaltTranscriptionEvents,
IbmTranscriptionEvents, IbmTranscriptionEvents,
NvidiaTranscriptionEvents, NvidiaTranscriptionEvents,
JambonzTranscriptionEvents, JambonzTranscriptionEvents
TranscribeStatus, } = require('../utils/constants');
AssemblyAiTranscriptionEvents
} = require('../utils/constants.json');
const { normalizeJambones } = require('@jambonz/verb-specifications'); const { normalizeJambones } = require('@jambonz/verb-specifications');
const SttTask = require('./stt-task');
const STT_LISTEN_SPAN_NAME = 'stt-listen'; const STT_LISTEN_SPAN_NAME = 'stt-listen';
class TaskTranscribe extends SttTask { class TaskTranscribe extends Task {
constructor(logger, opts, parentTask) { constructor(logger, opts, parentTask) {
super(logger, opts, parentTask); super(logger, opts);
this.preconditions = TaskPreconditions.Endpoint;
this.parentTask = parentTask;
const {
setChannelVarsForStt,
normalizeTranscription,
removeSpeechListeners,
setSpeechCredentialsAtRuntime,
compileSonioxTranscripts
} = require('../utils/transcription-utils')(logger);
this.setChannelVarsForStt = setChannelVarsForStt;
this.normalizeTranscription = normalizeTranscription;
this.removeSpeechListeners = removeSpeechListeners;
this.compileSonioxTranscripts = compileSonioxTranscripts;
this.transcriptionHook = this.data.transcriptionHook; this.transcriptionHook = this.data.transcriptionHook;
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia); this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
if (this.data.recognizer) { if (this.data.recognizer) {
this.interim = !!this.data.recognizer.interim; const recognizer = this.data.recognizer;
this.separateRecognitionPerChannel = this.data.recognizer.separateRecognitionPerChannel; this.vendor = recognizer.vendor;
this.language = recognizer.language;
/* let credentials be supplied in the recognizer object at runtime */
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
this.interim = !!recognizer.interim;
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
this.data.recognizer.hints = this.data.recognizer.hints || [];
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || [];
} }
else this.data.recognizer = {hints: [], altLanguages: []};
/* buffer for soniox transcripts */
this._sonioxTranscripts = [];
this.childSpan = [null, null]; this.childSpan = [null, null];
// Continuous asr timeout
this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ? this.data.recognizer.asrTimeout * 1000 : 0;
if (this.asrTimeout > 0) {
this.isContinuousAsr = true;
}
/* buffer speech for continuous asr */
this._bufferedTranscripts = [];
this.bugname_prefix = 'transcribe_';
} }
get name() { return TaskName.Transcribe; } get name() { return TaskName.Transcribe; }
async exec(cs, {ep, ep2}) { async exec(cs, {ep, ep2}) {
await super.exec(cs, {ep, ep2}); super.exec(cs);
if (this.data.recognizer.vendor === 'nuance') {
this.data.recognizer.nuanceOptions = {
// by default, nuance STT will recognize only 1st utterance.
// enable multiple allow nuance detact all utterances
utteranceDetectionMode: 'multiple',
...this.data.recognizer.nuanceOptions
};
}
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
if (cs.hasGlobalSttHints) { if (cs.hasGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints; const {hints, hintsBoost} = cs.globalSttHints;
@@ -65,8 +72,57 @@ class TaskTranscribe extends SttTask {
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost}, this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
'Transcribe:exec - applying global sttHints'); 'Transcribe:exec - applying global sttHints');
} }
if (cs.hasAltLanguages) {
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
this.logger.debug({altLanguages: this.altLanguages},
'Transcribe:exec - applying altLanguages');
}
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
this.data.recognizer.punctuation = cs.globalSttPunctuation;
}
this.ep = ep;
this.ep2 = ep2;
if ('default' === this.vendor || !this.vendor) {
this.vendor = cs.speechRecognizerVendor;
if (this.data.recognizer) this.data.recognizer.vendor = this.vendor;
}
if ('default' === this.language || !this.language) {
this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language;
}
if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor;
}
if (!this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
try { try {
if (!this.sttCredentials) {
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info(`TaskTranscribe:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED,
vendor: this.vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
throw new Error('no provisioned speech credentials for TTS');
}
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
/* get nuance access token */
const {client_id, secret} = this.sttCredentials;
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
this.logger.debug({client_id},
`Transcribe:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token};
}
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
/* get ibm access token */
const {stt_api_key, stt_region} = this.sttCredentials;
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
}
await this._startTranscribing(cs, ep, 1); await this._startTranscribing(cs, ep, 1);
if (this.separateRecognitionPerChannel && ep2) { if (this.separateRecognitionPerChannel && ep2) {
await this._startTranscribing(cs, ep2, 2); await this._startTranscribing(cs, ep2, 2);
@@ -80,17 +136,15 @@ class TaskTranscribe extends SttTask {
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(); this.removeSpeechListeners(ep);
} }
async _stopTranscription() { async kill(cs) {
super.kill(cs);
let stopTranscription = false; let stopTranscription = false;
if (this.ep?.connected) { if (this.ep?.connected) {
stopTranscription = true; stopTranscription = true;
this.ep.stopTranscription({ this.ep.stopTranscription({vendor: this.vendor})
vendor: this.vendor,
bugname: this.bugname
})
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill')); .catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
} }
if (this.separateRecognitionPerChannel && this.ep2 && this.ep2.connected) { if (this.separateRecognitionPerChannel && this.ep2 && this.ep2.connected) {
@@ -98,13 +152,6 @@ class TaskTranscribe extends SttTask {
this.ep2.stopTranscription({vendor: this.vendor}) this.ep2.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill')); .catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
} }
return stopTranscription;
}
async kill(cs) {
super.kill(cs);
const stopTranscription = this._stopTranscription();
// hangup after 1 sec if we don't get a final transcription // hangup after 1 sec if we don't get a final transcription
if (stopTranscription) this._timer = setTimeout(() => this.notifyTaskDone(), 1500); if (stopTranscription) this._timer = setTimeout(() => this.notifyTaskDone(), 1500);
else this.notifyTaskDone(); else this.notifyTaskDone();
@@ -112,166 +159,89 @@ class TaskTranscribe extends SttTask {
await this.awaitTaskDone(); await this.awaitTaskDone();
} }
async updateTranscribe(status) { async _startTranscribing(cs, ep, channel) {
if (!this.killed && this.ep && this.ep.connected) { const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
this.logger.info(`TaskTranscribe:updateTranscribe status ${status}`);
switch (status) {
case TranscribeStatus.Pause:
await this._stopTranscription();
break;
case TranscribeStatus.Resume:
await this._startTranscribing(this.cs, this.ep, 1);
if (this.separateRecognitionPerChannel && this.ep2) {
await this._startTranscribing(this.cs, this.ep2, 2);
}
break;
}
}
}
async _setSpeechHandlers(cs, ep, channel) {
if (this[`_speechHandlersSet_${channel}`]) return;
this[`_speechHandlersSet_${channel}`] = true;
/* some special deepgram logic */
if (this.vendor === 'deepgram') {
if (this.isContinuousAsr) this._doContinuousAsrWithDeepgram(this.asrTimeout);
}
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.language, this.data.recognizer);
switch (this.vendor) { switch (this.vendor) {
case 'google': case 'google':
this.bugname = `${this.bugname_prefix}google_transcribe`; this.bugname = 'google_transcribe';
this.addCustomEventListener(ep, GoogleTranscriptionEvents.Transcription, ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, GoogleTranscriptionEvents.NoAudioDetected, ep.addCustomEventListener(GoogleTranscriptionEvents.NoAudioDetected,
this._onNoAudio.bind(this, cs, ep, channel)); this._onNoAudio.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, GoogleTranscriptionEvents.MaxDurationExceeded, ep.addCustomEventListener(GoogleTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel)); this._onMaxDurationExceeded.bind(this, cs, ep, channel));
break; break;
case 'aws': case 'aws':
case 'polly': case 'polly':
this.bugname = `${this.bugname_prefix}aws_transcribe`; this.bugname = 'aws_transcribe';
this.addCustomEventListener(ep, AwsTranscriptionEvents.Transcription, ep.addCustomEventListener(AwsTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, AwsTranscriptionEvents.NoAudioDetected, ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected,
this._onNoAudio.bind(this, cs, ep, channel)); this._onNoAudio.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, AwsTranscriptionEvents.MaxDurationExceeded, ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
this._onMaxDurationExceeded.bind(this, cs, ep, channel)); this._onMaxDurationExceeded.bind(this, cs, ep, channel));
break; break;
case 'microsoft': case 'microsoft':
this.bugname = `${this.bugname_prefix}azure_transcribe`; this.bugname = 'azure_transcribe';
this.addCustomEventListener(ep, AzureTranscriptionEvents.Transcription, ep.addCustomEventListener(AzureTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected, ep.addCustomEventListener(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 = 'nuance_transcribe';
this.addCustomEventListener(ep, NuanceTranscriptionEvents.Transcription, ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
ep.addCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep, channel));
ep.addCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep, channel));
break; break;
case 'deepgram': case 'deepgram':
this.bugname = `${this.bugname_prefix}deepgram_transcribe`; this.bugname = 'deepgram_transcribe';
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Transcription, ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Connect, ep.addCustomEventListener(DeepgramTranscriptionEvents.Connect,
this._onVendorConnect.bind(this, cs, ep)); this._onDeepgramConnect.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.ConnectFailure, ep.addCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure,
this._onVendorConnectFailure.bind(this, cs, ep, channel)); this._onDeepGramConnectFailure.bind(this, cs, ep, channel));
/* if app sets deepgramOptions.utteranceEndMs they essentially want continuous asr */
if (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true;
break; break;
case 'soniox': case 'soniox':
this.bugname = `${this.bugname_prefix}soniox_transcribe`; this.bugname = 'soniox_transcribe';
this.addCustomEventListener(ep, SonioxTranscriptionEvents.Transcription, ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
break; break;
case 'cobalt':
this.bugname = `${this.bugname_prefix}cobalt_transcribe`;
this.addCustomEventListener(ep, CobaltTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
/* cobalt doesnt have language, it has model, which is required */
if (!this.data.recognizer.model) {
throw new Error('Cobalt requires a model to be specified');
}
this.language = this.data.recognizer.model;
/* special case: if using hints with cobalt we need to compile them */
this.hostport = opts.COBALT_SERVER_URI;
if (this.vendor === 'cobalt' && opts.COBALT_SPEECH_HINTS) {
try {
const context = await this.compileHintsForCobalt(
ep,
opts.COBALT_SERVER_URI,
this.data.recognizer.model,
opts.COBALT_CONTEXT_TOKEN,
opts.COBALT_SPEECH_HINTS
);
if (context) opts.COBALT_COMPILED_CONTEXT_DATA = context;
delete opts.COBALT_SPEECH_HINTS;
} catch (err) {
this.logger.error({err}, 'Error compiling hints for cobalt');
}
}
break;
case 'ibm': case 'ibm':
this.bugname = `${this.bugname_prefix}ibm_transcribe`; this.bugname = 'ibm_transcribe';
this.addCustomEventListener(ep, IbmTranscriptionEvents.Transcription, ep.addCustomEventListener(IbmTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, IbmTranscriptionEvents.Connect, ep.addCustomEventListener(IbmTranscriptionEvents.Connect,
this._onVendorConnect.bind(this, cs, ep)); this._onIbmConnect.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, IbmTranscriptionEvents.ConnectFailure, ep.addCustomEventListener(IbmTranscriptionEvents.ConnectFailure,
this._onVendorConnectFailure.bind(this, cs, ep, channel)); this._onIbmConnectFailure.bind(this, cs, ep, channel));
break; break;
case 'nvidia': case 'nvidia':
this.bugname = `${this.bugname_prefix}nvidia_transcribe`; this.bugname = 'nvidia_transcribe';
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.Transcription, ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech,
this._onStartOfSpeech.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep));
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
break; break;
case 'assemblyai':
this.bugname = `${this.bugname_prefix}assemblyai_transcribe`;
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
this.addCustomEventListener(ep,
AssemblyAiTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
this.addCustomEventListener(ep, AssemblyAiTranscriptionEvents.ConnectFailure,
this._onVendorConnectFailure.bind(this, cs, ep, channel));
break;
default: default:
if (this.vendor.startsWith('custom:')) { throw new Error(`Invalid vendor ${this.vendor}`);
this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`;
this.addCustomEventListener(ep, JambonzTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, JambonzTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
this.addCustomEventListener(ep, JambonzTranscriptionEvents.ConnectFailure,
this._onVendorConnectFailure.bind(this, cs, ep));
break;
}
else {
this.notifyError({ msg: 'ASR error', details:`Invalid vendor ${this.vendor}`});
this.notifyTaskDone();
throw new Error(`Invalid vendor ${this.vendor}`);
}
} }
/* common handler for all stt engine errors */ /* common handler for all stt engine errors */
this.addCustomEventListener(ep, JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep)); ep.addCustomEventListener(JambonzTranscriptionEvents.Error, this._onJambonzError.bind(this, cs, ep));
await ep.set(opts) await ep.set(opts)
.catch((err) => this.logger.info(err, 'Error setting channel variables')); .catch((err) => this.logger.info(err, 'Error setting channel variables'));
}
async _startTranscribing(cs, ep, channel) {
await this._setSpeechHandlers(cs, ep, channel);
await this._transcribe(ep); await this._transcribe(ep);
/* start child span for this channel */ /* start child span for this channel */
@@ -280,15 +250,12 @@ class TaskTranscribe extends SttTask {
} }
async _transcribe(ep) { async _transcribe(ep) {
this.logger.debug(
`TaskTranscribe:_transcribe - starting transcription vendor ${this.vendor} bugname ${this.bugname}`);
await ep.startTranscription({ await ep.startTranscription({
vendor: this.vendor, vendor: this.vendor,
interim: this.interim ? true : false, interim: this.interim ? true : false,
locale: this.language, locale: this.language,
channels: /*this.separateRecognitionPerChannel ? 2 : */ 1, channels: /*this.separateRecognitionPerChannel ? 2 : */ 1,
bugname: this.bugname, bugname: this.bugname
hostport: this.hostport
}); });
} }
@@ -297,25 +264,12 @@ class TaskTranscribe extends SttTask {
const bugname = fsEvent.getHeader('media-bugname'); const bugname = fsEvent.getHeader('media-bugname');
if (bugname && this.bugname !== bugname) return; if (bugname && this.bugname !== bugname) return;
if (this.vendor === 'ibm' && evt?.state === 'listening') return; if (this.vendor === 'ibm') {
if (evt?.state === 'listening') return;
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
/* we will only get this when we have set utterance_end_ms */
if (this._bufferedTranscripts.length === 0) {
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram but no buffered transcripts');
}
else {
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram, return buffered transcript');
evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language);
this._bufferedTranscripts = [];
this._resolve('speech', evt);
}
return;
} }
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization'); this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - before normalization');
evt = this.normalizeTranscription(evt, this.vendor, channel, this.language, undefined, evt = this.normalizeTranscription(evt, this.vendor, channel, this.language);
this.data.recognizer.punctuation);
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription'); this.logger.debug({evt}, 'TaskTranscribe:_onTranscription');
if (evt.alternatives.length === 0) { if (evt.alternatives.length === 0) {
this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, continue listening'); this.logger.info({evt}, 'TaskTranscribe:_onTranscription - got empty transcript, continue listening');
@@ -342,15 +296,6 @@ class TaskTranscribe extends SttTask {
} }
} }
if (this.isContinuousAsr && evt.is_final) {
this._bufferedTranscripts.push(evt);
this._startAsrTimer(channel);
} else {
await this._resolve(channel, evt);
}
}
async _resolve(channel, evt) {
/* we've got a transcript, so end the otel child span for this channel */ /* we've got a 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({
@@ -437,49 +382,22 @@ class TaskTranscribe extends SttTask {
this._timer = null; this._timer = null;
} }
} }
_onDeepgramConnect(_cs, _ep) {
async _onJambonzError(cs, _ep, evt) { this.logger.debug('TaskTranscribe:_onDeepgramConnect');
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
_ep.stopTranscription({
vendor: this.vendor,
bugname: this.bugname
})
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
try {
await this._fallback();
let channel = 1;
if (this.ep !== _ep) {
channel = 2;
}
this._startTranscribing(cs, _ep, channel);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
return;
} catch (error) {
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
}
} else {
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') {
const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout');
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
}
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,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
}
} }
_onVendorConnectFailure(cs, _ep, channel, evt) { _onDeepGramConnectFailure(cs, _ep, channel, evt) {
super._onVendorConnectFailure(cs, _ep, evt); const {reason} = evt;
const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskTranscribe:_onDeepgramConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to Deepgram speech recognizer: ${reason}`,
vendor: 'deepgram',
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
this.notifyError(`Failed connecting to speech vendor deepgram: ${reason}`);
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,
@@ -490,23 +408,54 @@ class TaskTranscribe extends SttTask {
this.notifyTaskDone(); this.notifyTaskDone();
} }
_startAsrTimer(channel) { _onIbmConnect(_cs, _ep) {
if (this.vendor === 'deepgram') return; // no need this.logger.debug('TaskTranscribe:_onIbmConnect');
assert(this.isContinuousAsr);
this._clearAsrTimer(channel);
this._asrTimer = setTimeout(() => {
this.logger.debug(`TaskTranscribe:_startAsrTimer - asr timer went off for channel: ${channel}`);
const evt = this.consolidateTranscripts(this._bufferedTranscripts, channel, this.language);
this._bufferedTranscripts = [];
this._resolve(channel, evt);
}, this.asrTimeout);
this.logger.debug(`TaskTranscribe:_startAsrTimer: set for ${this.asrTimeout}ms for channel ${channel}`);
} }
_clearAsrTimer(channel) { _onIbmConnectFailure(cs, _ep, channel, evt) {
if (this._asrTimer) clearTimeout(this._asrTimer); const {reason} = evt;
this._asrTimer = null; const {writeAlerts, AlertType} = cs.srf.locals;
this.logger.info({evt}, 'TaskTranscribe:_onIbmConnectFailure');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to IBM watson speech recognizer: ${reason}`,
vendor: 'ibm',
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
this.notifyError(`Failed connecting to speech vendor IBM: ${reason}`);
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({
channel,
'stt.resolve': 'connection failure'
});
this.childSpan[channel - 1].span.end();
}
this.notifyTaskDone();
} }
_onIbmError(cs, _ep, _channel, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onIbmError');
}
_onJambonzError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') {
const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout');
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
}
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,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
}
} }
module.exports = TaskTranscribe; module.exports = TaskTranscribe;

View File

@@ -9,7 +9,6 @@ const {
NvidiaTranscriptionEvents, NvidiaTranscriptionEvents,
IbmTranscriptionEvents, IbmTranscriptionEvents,
SonioxTranscriptionEvents, SonioxTranscriptionEvents,
CobaltTranscriptionEvents,
DeepgramTranscriptionEvents, DeepgramTranscriptionEvents,
JambonzTranscriptionEvents, JambonzTranscriptionEvents,
AmdEvents, AmdEvents,
@@ -55,8 +54,7 @@ class Amd extends Emitter {
this.language = opts.recognizer?.language || cs.speechRecognizerLanguage; this.language = opts.recognizer?.language || cs.speechRecognizerLanguage;
if ('default' === this.language) this.language = cs.speechRecognizerLanguage; if ('default' === this.language) this.language = cs.speechRecognizerLanguage;
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt', this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
opts.recognizer?.label || cs.speechRecognizerLabel);
if (!this.sttCredentials) throw new Error(`No speech credentials found for vendor ${this.vendor}`); if (!this.sttCredentials) throw new Error(`No speech credentials found for vendor ${this.vendor}`);
@@ -266,7 +264,7 @@ module.exports = (logger) => {
/* set stt options */ /* set stt options */
logger.info(`starting amd for vendor ${vendor} and language ${language}`); logger.info(`starting amd for vendor ${vendor} and language ${language}`);
const sttOpts = amd.setChannelVarsForStt({name: TaskName.Gather}, sttCredentials, language, { const sttOpts = amd.setChannelVarsForStt({name: TaskName.Gather}, sttCredentials, {
vendor, vendor,
hints, hints,
enhancedModel: true, enhancedModel: true,
@@ -315,10 +313,6 @@ module.exports = (logger) => {
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription, amd.transcriptionHandler); ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription, amd.transcriptionHandler);
break; break;
case 'cobalt':
ep.addCustomEventListener(CobaltTranscriptionEvents.Transcription, amd.transcriptionHandler);
break;
default: default:
if (vendor.startsWith('custom:')) { if (vendor.startsWith('custom:')) {
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, amd.transcriptionHandler); ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, amd.transcriptionHandler);

View File

@@ -1,197 +0,0 @@
const { normalizeJambones } = require('@jambonz/verb-specifications');
const makeTask = require('../tasks/make_task');
const { JAMBONZ_RECORD_WS_BASE_URL, JAMBONZ_RECORD_WS_USERNAME, JAMBONZ_RECORD_WS_PASSWORD } = require('../config');
const Emitter = require('events');
class BackgroundTaskManager extends Emitter {
constructor({cs, logger, rootSpan}) {
super();
this.tasks = new Map();
this.cs = cs;
this.logger = logger;
this.rootSpan = rootSpan;
}
isTaskRunning(type) {
return this.tasks.has(type);
}
getTask(type) {
if (this.tasks.has(type)) {
return this.tasks.get(type);
}
}
count() {
return this.tasks.size;
}
async newTask(type, taskOpts) {
this.logger.info({taskOpts}, `initiating Background task ${type}`);
if (this.tasks.has(type)) {
this.logger.info(`Background task ${type} is running, skiped`);
return;
}
let task;
switch (type) {
case 'listen':
task = await this._initListen(taskOpts);
break;
case 'bargeIn':
task = await this._initBargeIn(taskOpts);
break;
case 'record':
task = await this._initRecord();
break;
case 'transcribe':
task = await this._initTranscribe(taskOpts);
break;
default:
break;
}
if (task) {
this.tasks.set(type, task);
}
return task;
}
stop(type) {
const task = this.getTask(type);
if (task) {
this.logger.info(`stopping background task: ${type}`);
task.removeAllListeners();
task.span.end();
task.kill();
// Remove task from managed List
this.tasks.delete(type);
} else {
this.logger.info(`stopping background task, ${type} is not running, skipped`);
}
}
stopAll() {
this.logger.info('BackgroundTaskManager:stopAll');
for (const key of this.tasks.keys()) {
this.stop(key);
}
}
// Initiate Task
// Initiate Listen
async _initListen(opts, bugname = 'jambonz-background-listen', ignoreCustomerData = false, type = 'listen') {
let task;
try {
const t = normalizeJambones(this.logger, [opts]);
task = makeTask(this.logger, t[0]);
task.bugname = bugname;
task.ignoreCustomerData = ignoreCustomerData;
const resources = await this.cs._evaluatePreconditions(task);
const {span, ctx} = this.rootSpan.startChildSpan(`background-${type}:${task.summary}`);
task.span = span;
task.ctx = ctx;
task.exec(this.cs, resources)
.then(this._taskCompleted.bind(this, type, task))
.catch(this._taskError.bind(this, type, task));
} catch (err) {
this.logger.info({err, opts}, `BackgroundTaskManager:_initListen - Error creating ${bugname} task`);
}
return task;
}
// Initiate Gather
async _initBargeIn(opts) {
let task;
try {
const t = normalizeJambones(this.logger, [opts]);
task = makeTask(this.logger, t[0]);
task
.once('dtmf', this._bargeInTaskCompleted.bind(this))
.once('vad', this._bargeInTaskCompleted.bind(this))
.once('transcription', this._bargeInTaskCompleted.bind(this))
.once('timeout', this._bargeInTaskCompleted.bind(this));
const resources = await this.cs._evaluatePreconditions(task);
const {span, ctx} = this.rootSpan.startChildSpan(`background-bargeIn:${task.summary}`);
task.span = span;
task.ctx = ctx;
task.bugname_prefix = 'background_bargeIn_';
task.exec(this.cs, resources)
.then(() => {
this._taskCompleted('bargeIn', task);
if (task.sticky && !this.cs.callGone && !this.cs._stopping) {
this.logger.info('BackgroundTaskManager:_initBargeIn: restarting background bargeIn');
this.newTask('bargeIn', opts);
}
return;
})
.catch(this._taskError.bind(this, 'bargeIn', task));
} catch (err) {
this.logger.info(err, 'BackgroundTaskManager:_initGather - Error creating bargeIn task');
}
return task;
}
// Initiate Record
async _initRecord() {
if (this.cs.accountInfo.account.record_all_calls || this.cs.application.record_all_calls) {
if (!JAMBONZ_RECORD_WS_BASE_URL || !this.cs.accountInfo.account.bucket_credential) {
this.logger.error(`_initRecord: invalid configuration,
missing JAMBONZ_RECORD_WS_BASE_URL or bucket configuration`);
return undefined;
}
const listenOpts = {
url: `${JAMBONZ_RECORD_WS_BASE_URL}/record/${this.cs.accountInfo.account.bucket_credential.vendor}`,
disableBidirectionalAudio: true,
mixType : 'stereo',
passDtmf: true
};
if (JAMBONZ_RECORD_WS_USERNAME && JAMBONZ_RECORD_WS_PASSWORD) {
listenOpts.wsAuth = {
username: JAMBONZ_RECORD_WS_USERNAME,
password: JAMBONZ_RECORD_WS_PASSWORD
};
}
this.logger.debug({listenOpts}, '_initRecord: enabling listen');
return await this._initListen({verb: 'listen', ...listenOpts}, 'jambonz-session-record', true, 'record');
}
}
// Initiate Transcribe
async _initTranscribe(opts) {
let task;
try {
const t = normalizeJambones(this.logger, [opts]);
task = makeTask(this.logger, t[0]);
const resources = await this.cs._evaluatePreconditions(task);
const {span, ctx} = this.rootSpan.startChildSpan(`background-transcribe:${task.summary}`);
task.span = span;
task.ctx = ctx;
task.bugname_prefix = 'background_transcribe_';
task.exec(this.cs, resources)
.then(this._taskCompleted.bind(this, 'transcribe', task))
.catch(this._taskError.bind(this, 'transcribe', task));
} catch (err) {
this.logger.info(err, 'BackgroundTaskManager:_initTranscribe - Error creating transcribe task');
}
return task;
}
_taskCompleted(type, task) {
this.logger.info({type, task}, 'BackgroundTaskManager:_taskCompleted: task completed');
task.removeAllListeners();
task.span.end();
this.tasks.delete(type);
}
_taskError(type, task, error) {
this.logger.info({type, task, error}, 'BackgroundTaskManager:_taskError: task Error');
task.removeAllListeners();
task.span.end();
this.tasks.delete(type);
}
_bargeInTaskCompleted(evt) {
this.logger.info({evt}, 'BackgroundTaskManager:_bargeInTaskCompleted on event from background bargeIn');
this.emit('bargeIn-done', evt);
}
}
module.exports = BackgroundTaskManager;

View File

@@ -2,24 +2,17 @@ const {context, trace} = require('@opentelemetry/api');
const {Dialog} = require('drachtio-srf'); const {Dialog} = require('drachtio-srf');
class RootSpan { class RootSpan {
constructor(callType, req) { constructor(callType, req) {
const {srf} = require('../../'); let tracer, callSid, linkedSpanId;
const tracer = srf.locals.otel.tracer;
let callSid, accountSid, applicationSid, linkedSpanId;
if (req instanceof Dialog) { if (req instanceof Dialog) {
const dlg = req; const dlg = req;
tracer = dlg.srf.locals.otel.tracer;
callSid = dlg.callSid; callSid = dlg.callSid;
linkedSpanId = dlg.linkedSpanId; linkedSpanId = dlg.linkedSpanId;
} }
else if (req.srf) {
callSid = req.locals.callSid;
accountSid = req.get('X-Account-Sid'),
applicationSid = req.locals.application_sid;
}
else { else {
callSid = req.callSid; tracer = req.srf.locals.otel.tracer;
accountSid = req.accountSid; callSid = req.locals.callSid;
applicationSid = req.applicationSid;
} }
this._span = tracer.startSpan(callType || 'incoming-call'); this._span = tracer.startSpan(callType || 'incoming-call');
if (req instanceof Dialog) { if (req instanceof Dialog) {
@@ -29,20 +22,13 @@ class RootSpan {
callId: dlg.sip.callId callId: dlg.sip.callId
}); });
} }
else if (req.srf) {
this._span.setAttributes({
callSid,
accountSid,
applicationSid,
callId: req.get('Call-ID'),
externalCallId: req.get('X-CID')
});
}
else { else {
this._span.setAttributes({ this._span.setAttributes({
callSid, callSid,
accountSid, accountSid: req.get('X-Account-Sid'),
applicationSid applicationSid: req.locals.application_sid,
callId: req.get('Call-ID'),
externalCallId: req.get('X-CID')
}); });
} }

View File

@@ -29,7 +29,7 @@
"Tag": "tag", "Tag": "tag",
"Transcribe": "transcribe" "Transcribe": "transcribe"
}, },
"AllowedSipRecVerbs": ["config", "gather", "transcribe", "listen", "tag"], "AllowedSipRecVerbs": ["config", "gather", "transcribe", "listen"],
"CallStatus": { "CallStatus": {
"Trying": "trying", "Trying": "trying",
"Ringing": "ringing", "Ringing": "ringing",
@@ -51,11 +51,6 @@
"Silence": "silence", "Silence": "silence",
"Resume": "resume" "Resume": "resume"
}, },
"TranscribeStatus": {
"Pause": "pause",
"Silence": "silence",
"Resume": "resume"
},
"TaskPreconditions": { "TaskPreconditions": {
"None": "none", "None": "none",
"Endpoint": "endpoint", "Endpoint": "endpoint",
@@ -95,11 +90,6 @@
"Transcription": "soniox_transcribe::transcription", "Transcription": "soniox_transcribe::transcription",
"Error": "soniox_transcribe::error" "Error": "soniox_transcribe::error"
}, },
"CobaltTranscriptionEvents": {
"Transcription": "cobalt_speech::transcription",
"CompileContext": "cobalt_speech::compile_context_response",
"Error": "cobalt_speech::error"
},
"IbmTranscriptionEvents": { "IbmTranscriptionEvents": {
"Transcription": "ibm_transcribe::transcription", "Transcription": "ibm_transcribe::transcription",
"ConnectFailure": "ibm_transcribe::connect_failed", "ConnectFailure": "ibm_transcribe::connect_failed",
@@ -126,12 +116,6 @@
"Connect": "jambonz_transcribe::connect", "Connect": "jambonz_transcribe::connect",
"Error": "jambonz_transcribe::error" "Error": "jambonz_transcribe::error"
}, },
"AssemblyAiTranscriptionEvents": {
"Transcription": "assemblyai_transcribe::transcription",
"Error": "assemblyai_transcribe::error",
"ConnectFailure": "assemblyai_transcribe::connect_failed",
"Connect": "assemblyai_transcribe::connect"
},
"ListenEvents": { "ListenEvents": {
"Connect": "mod_audio_fork::connect", "Connect": "mod_audio_fork::connect",
"ConnectFailure": "mod_audio_fork::connect_failed", "ConnectFailure": "mod_audio_fork::connect_failed",

View File

@@ -3,10 +3,13 @@ const {decrypt} = require('./encrypt-decrypt');
const sqlAccountDetails = `SELECT * const sqlAccountDetails = `SELECT *
FROM accounts account FROM accounts account
WHERE account.account_sid = ?`; WHERE account.account_sid = ?`;
const sqlSpeechCredentialsForAccount = `SELECT * const sqlSpeechCredentials = `SELECT *
FROM speech_credentials FROM speech_credentials
WHERE account_sid = ? OR (account_sid is NULL AND service_provider_sid = WHERE account_sid = ? `;
(SELECT service_provider_sid from accounts where account_sid = ?))`; const sqlSpeechCredentialsForSP = `SELECT *
FROM speech_credentials
WHERE service_provider_sid =
(SELECT service_provider_sid from accounts where account_sid = ?)`;
const sqlQueryAccountCarrierByName = `SELECT voip_carrier_sid const sqlQueryAccountCarrierByName = `SELECT voip_carrier_sid
FROM voip_carriers vc FROM voip_carriers vc
WHERE vc.account_sid = ? WHERE vc.account_sid = ?
@@ -27,9 +30,6 @@ WHERE pn.account_sid IS NULL
AND pn.service_provider_sid = AND pn.service_provider_sid =
(SELECT service_provider_sid from accounts where account_sid = ?) (SELECT service_provider_sid from accounts where account_sid = ?)
AND pn.number = ?`; AND pn.number = ?`;
const sqlQueryGoogleCustomVoices = `SELECT *
FROM google_custom_voices
WHERE google_custom_voice_sid = ?`;
const speechMapper = (cred) => { const speechMapper = (cred) => {
const {credential, ...obj} = cred; const {credential, ...obj} = cred;
@@ -49,10 +49,8 @@ const speechMapper = (cred) => {
obj.region = o.region; obj.region = o.region;
obj.use_custom_stt = o.use_custom_stt; obj.use_custom_stt = o.use_custom_stt;
obj.custom_stt_endpoint = o.custom_stt_endpoint; obj.custom_stt_endpoint = o.custom_stt_endpoint;
obj.custom_stt_endpoint_url = o.custom_stt_endpoint_url;
obj.use_custom_tts = o.use_custom_tts; obj.use_custom_tts = o.use_custom_tts;
obj.custom_tts_endpoint = o.custom_tts_endpoint; obj.custom_tts_endpoint = o.custom_tts_endpoint;
obj.custom_tts_endpoint_url = o.custom_tts_endpoint_url;
} }
else if ('wellsaid' === obj.vendor) { else if ('wellsaid' === obj.vendor) {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
@@ -84,22 +82,7 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.riva_server_uri = o.riva_server_uri; obj.riva_server_uri = o.riva_server_uri;
} }
else if ('cobalt' === obj.vendor) { else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential));
obj.cobalt_server_uri = o.cobalt_server_uri;
} else if ('elevenlabs' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
obj.model_id = o.model_id;
obj.options = o.options;
} 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 (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;
@@ -125,9 +108,16 @@ module.exports = (logger, srf) => {
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, [account_sid]); const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, [account_sid]);
if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`); if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`);
const [r2] = await pp.query(sqlSpeechCredentialsForAccount, [account_sid, account_sid]); const [r2] = await pp.query(sqlSpeechCredentials, [account_sid]);
const speech = r2.map(speechMapper); const speech = r2.map(speechMapper);
/* add service provider creds unless we have that vendor at the account level */
const [r3] = await pp.query(sqlSpeechCredentialsForSP, [account_sid]);
r3.forEach((s) => {
if (!speech.find((s2) => s2.vendor === s.vendor)) {
speech.push(speechMapper(s));
}
});
const account = r[0]; const account = r[0];
bucketCredentialDecrypt(account); bucketCredentialDecrypt(account);
@@ -172,22 +162,10 @@ module.exports = (logger, srf) => {
} }
}; };
const lookupGoogleCustomVoice = async(google_custom_voice_sid) => {
const pp = pool.promise();
try {
const [r] = await pp.query(sqlQueryGoogleCustomVoices, [google_custom_voice_sid]);
return r;
} catch (err) {
logger.error({err}, `lookupGoogleCustomVoices: Error ${google_custom_voice_sid}`);
}
};
return { return {
lookupAccountDetails, lookupAccountDetails,
updateSpeechCredentialLastUsed, updateSpeechCredentialLastUsed,
lookupCarrier, lookupCarrier,
lookupCarrierByPhoneNumber, lookupCarrierByPhoneNumber
lookupGoogleCustomVoice
}; };
}; };

View File

@@ -6,8 +6,7 @@ const {PORT, HTTP_PORT_MAX} = require('../config');
const doListen = (logger, app, port, resolve) => { const doListen = (logger, app, port, resolve) => {
const server = app.listen(port, () => { const server = app.listen(port, () => {
const {srf} = app.locals; const {srf} = app.locals;
srf.locals.serviceUrl = `http://${srf.locals.ipv4}:${port}`; logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
logger.info(`listening for HTTP requests on port ${port}, serviceUrl is ${srf.locals.serviceUrl}`);
resolve({server, app}); resolve({server, app});
}); });
return server; return server;

View File

@@ -1,4 +1,4 @@
const {request, getGlobalDispatcher, setGlobalDispatcher, Dispatcher, ProxyAgent, Client, Pool} = require('undici'); const {Client, Pool} = require('undici');
const parseUrl = require('parse-url'); const parseUrl = require('parse-url');
const assert = require('assert'); const assert = require('assert');
const BaseRequestor = require('./base-requestor'); const BaseRequestor = require('./base-requestor');
@@ -10,11 +10,6 @@ const {
HTTP_POOLSIZE, HTTP_POOLSIZE,
HTTP_PIPELINING, HTTP_PIPELINING,
HTTP_TIMEOUT, HTTP_TIMEOUT,
HTTP_PROXY_IP,
HTTP_PROXY_PORT,
HTTP_PROXY_PROTOCOL,
NODE_ENV,
HTTP_USER_AGENT_HEADER,
} = require('../config'); } = require('../config');
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64'); const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
@@ -26,15 +21,6 @@ function basicAuth(username, password) {
return {Authorization: header}; return {Authorization: header};
} }
const defaultDispatcher = HTTP_PROXY_IP ?
new ProxyAgent(`${HTTP_PROXY_PROTOCOL}://${HTTP_PROXY_IP}${HTTP_PROXY_PORT ? `:${HTTP_PROXY_PORT}` : ''}`) :
getGlobalDispatcher();
setGlobalDispatcher(new class extends Dispatcher {
dispatch(options, handler) {
return defaultDispatcher.dispatch(options, handler);
}
}());
class HttpRequestor extends BaseRequestor { class HttpRequestor extends BaseRequestor {
constructor(logger, account_sid, hook, secret) { constructor(logger, account_sid, hook, secret) {
@@ -74,18 +60,6 @@ class HttpRequestor extends BaseRequestor {
if (u.port) this.client = new Client(`${u.protocol}://${u.resource}:${u.port}`); if (u.port) this.client = new Client(`${u.protocol}://${u.resource}:${u.port}`);
else this.client = new Client(`${u.protocol}://${u.resource}`); else this.client = new Client(`${u.protocol}://${u.resource}`);
} }
if (NODE_ENV == 'test' && process.env.JAMBONES_HTTP_PROXY_IP) {
const defDispatcher =
new ProxyAgent(`${process.env.JAMBONES_HTTP_PROXY_PROTOCOL}://${process.env.JAMBONES_HTTP_PROXY_IP}${
process.env.JAMBONES_HTTP_PROXY_PORT ? `:${process.env.JAMBONES_HTTP_PROXY_PORT}` : ''}`);
setGlobalDispatcher(new class extends Dispatcher {
dispatch(options, handler) {
return defDispatcher.dispatch(options, handler);
}
}());
}
} }
get baseUrl() { get baseUrl() {
@@ -117,10 +91,6 @@ class HttpRequestor extends BaseRequestor {
const url = hook.url || hook; const url = hook.url || hook;
const method = hook.method || 'POST'; const method = hook.method || 'POST';
let buf = ''; let buf = '';
httpHeaders = {
...httpHeaders,
...(HTTP_USER_AGENT_HEADER && {'user-agent' : HTTP_USER_AGENT_HEADER})
};
assert.ok(url, 'HttpRequestor:request url was not provided'); assert.ok(url, 'HttpRequestor:request url was not provided');
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`); assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
@@ -169,18 +139,7 @@ class HttpRequestor extends BaseRequestor {
}; };
const absUrl = this._isRelativeUrl(url) ? `${this.baseUrl}${url}` : url; const absUrl = this._isRelativeUrl(url) ? `${this.baseUrl}${url}` : url;
this.logger.debug({url, absUrl, hdrs}, 'send webhook'); this.logger.debug({url, absUrl, hdrs}, 'send webhook');
const {statusCode, headers, body} = HTTP_PROXY_IP ? await request( const {statusCode, headers, body} = await client.request({
this.baseUrl,
{
path,
query,
method,
headers: hdrs,
...('POST' === method && {body: JSON.stringify(payload)}),
timeout: HTTP_TIMEOUT,
followRedirects: false
}
) : await client.request({
path, path,
query, query,
method, method,

View File

@@ -8,13 +8,15 @@ const {
JAMBONES_MYSQL_CONNECTION_LIMIT, JAMBONES_MYSQL_CONNECTION_LIMIT,
JAMBONES_MYSQL_PORT, JAMBONES_MYSQL_PORT,
JAMBONES_FREESWITCH, JAMBONES_FREESWITCH,
JAMBONES_REDIS_HOST,
JAMBONES_REDIS_PORT,
JAMBONES_REDIS_SENTINELS,
SMPP_URL, SMPP_URL,
JAMBONES_TIME_SERIES_HOST, JAMBONES_TIME_SERIES_HOST,
JAMBONES_ESL_LISTEN_ADDRESS, JAMBONES_ESL_LISTEN_ADDRESS,
PORT, PORT,
NODE_ENV, NODE_ENV,
} = require('../config'); } = require('../config');
const Registrar = require('@jambonz/mw-registrar');
const assert = require('assert'); const assert = require('assert');
function initMS(logger, wrapper, ms) { function initMS(logger, wrapper, ms) {
@@ -137,8 +139,7 @@ function installSrfLocals(srf, logger) {
lookupTeamsByAccount, lookupTeamsByAccount,
lookupAccountBySid, lookupAccountBySid,
lookupAccountCapacitiesBySid, lookupAccountCapacitiesBySid,
lookupSmppGateways, lookupSmppGateways
lookupClientByAccountAndUsername
} = require('@jambonz/db-helpers')({ } = require('@jambonz/db-helpers')({
host: JAMBONES_MYSQL_HOST, host: JAMBONES_MYSQL_HOST,
user: JAMBONES_MYSQL_USER, user: JAMBONES_MYSQL_USER,
@@ -172,13 +173,18 @@ function installSrfLocals(srf, logger) {
retrieveByPatternSortedSet, retrieveByPatternSortedSet,
sortedSetLength, sortedSetLength,
sortedSetPositionByPattern sortedSetPositionByPattern
} = require('@jambonz/realtimedb-helpers')({}, logger, tracer); } = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
const registrar = new Registrar(logger, client); host: JAMBONES_REDIS_HOST,
port: JAMBONES_REDIS_PORT || 6379
}, logger, tracer);
const { const {
synthAudio, synthAudio,
getNuanceAccessToken, getNuanceAccessToken,
getIbmAccessToken, getIbmAccessToken,
} = require('@jambonz/speech-utils')({}, logger); } = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
host: JAMBONES_REDIS_HOST,
port: JAMBONES_REDIS_PORT || 6379
}, logger, tracer);
const { const {
writeAlerts, writeAlerts,
AlertType AlertType
@@ -198,7 +204,6 @@ function installSrfLocals(srf, logger) {
srf.locals = {...srf.locals, srf.locals = {...srf.locals,
dbHelpers: { dbHelpers: {
client, client,
registrar,
pool, pool,
lookupAppByPhoneNumber, lookupAppByPhoneNumber,
lookupAppByRegex, lookupAppByRegex,
@@ -209,7 +214,6 @@ function installSrfLocals(srf, logger) {
lookupAccountBySid, lookupAccountBySid,
lookupAccountCapacitiesBySid, lookupAccountCapacitiesBySid,
lookupSmppGateways, lookupSmppGateways,
lookupClientByAccountAndUsername,
updateCallStatus, updateCallStatus,
retrieveCall, retrieveCall,
listCalls, listCalls,

View File

@@ -13,12 +13,9 @@ const moment = require('moment');
const stripCodecs = require('./strip-ancillary-codecs'); const stripCodecs = require('./strip-ancillary-codecs');
const RootSpan = require('./call-tracer'); const RootSpan = require('./call-tracer');
const uuidv4 = require('uuid-random'); const uuidv4 = require('uuid-random');
const HttpRequestor = require('./http-requestor');
const WsRequestor = require('./ws-requestor');
const {makeOpusFirst} = require('./sdp-utils');
class SingleDialer extends Emitter { class SingleDialer extends Emitter {
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask}) { constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) {
super(); super();
assert(target.type); assert(target.type);
@@ -40,7 +37,6 @@ class SingleDialer extends Emitter {
this.callGone = false; this.callGone = false;
this.callSid = uuidv4(); this.callSid = uuidv4();
this.dialTask = dialTask;
this.on('callStatusChange', this._notifyCallStatusChange.bind(this)); this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
} }
@@ -79,8 +75,7 @@ class SingleDialer extends Emitter {
...(this.from.host && {'X-Preferred-From-Host': this.from.host}), ...(this.from.host && {'X-Preferred-From-Host': this.from.host}),
'X-Jambonz-Routing': this.target.type, 'X-Jambonz-Routing': this.target.type,
'X-Call-Sid': this.callSid, 'X-Call-Sid': this.callSid,
...(this.applicationSid && {'X-Application-Sid': this.applicationSid}), ...(this.applicationSid && {'X-Application-Sid': this.applicationSid})
...(this.target.proxy && {'X-SIP-Proxy': this.target.proxy})
}; };
if (srf.locals.fsUUID) { if (srf.locals.fsUUID) {
opts.headers = { opts.headers = {
@@ -152,7 +147,7 @@ class SingleDialer extends Emitter {
Object.assign(opts, { Object.assign(opts, {
proxy: `sip:${this.sbcAddress}`, proxy: `sip:${this.sbcAddress}`,
localSdp: opts.opusFirst ? makeOpusFirst(this.ep.local.sdp) : this.ep.local.sdp localSdp: this.ep.local.sdp
}); });
if (this.target.auth) opts.auth = this.target.auth; if (this.target.auth) opts.auth = this.target.auth;
inviteSpan = this.startSpan('invite', { inviteSpan = this.startSpan('invite', {
@@ -180,7 +175,6 @@ class SingleDialer extends Emitter {
* (a) create a logger for this call * (a) create a logger for this call
*/ */
req.srf = srf; req.srf = srf;
this.req = req;
this.callInfo = new CallInfo({ this.callInfo = new CallInfo({
direction: CallDirection.Outbound, direction: CallDirection.Outbound,
parentCallInfo: this.parentCallInfo, parentCallInfo: this.parentCallInfo,
@@ -253,14 +247,9 @@ class SingleDialer extends Emitter {
.on('modify', async(req, res) => { .on('modify', async(req, res) => {
try { try {
if (this.ep) { if (this.ep) {
if (this.dialTask && this.dialTask.isOnHold) { const newSdp = await this.ep.modify(req.body);
this.logger.info('dial is onhold, emit event'); res.send(200, {body: newSdp});
this.emit('reinvite', req, res); this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
} else {
const newSdp = await this.ep.modify(req.body);
res.send(200, {body: newSdp});
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
}
} }
else { else {
this.logger.info('SingleDialer:exec: handling reINVITE with released media, emit event'); this.logger.info('SingleDialer:exec: handling reINVITE with released media, emit event');
@@ -331,15 +320,10 @@ class SingleDialer extends Emitter {
try { try {
// retrieve set of tasks // retrieve set of tasks
const json = await this.requestor.request('dial:confirm', confirmHook, this.callInfo.toJSON()); const json = await this.requestor.request('dial:confirm', confirmHook, this.callInfo.toJSON());
if (!json || (Array.isArray(json) && json.length === 0)) {
this.logger.info('SingleDialer:_executeApp: no tasks returned from confirm hook');
return;
}
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata)); const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
// verify it contains only allowed verbs // verify it contains only allowed verbs
const allowedTasks = tasks.filter((task) => { const allowedTasks = tasks.filter((task) => {
return [ return [
TaskPreconditions.None,
TaskPreconditions.StableCall, TaskPreconditions.StableCall,
TaskPreconditions.Endpoint TaskPreconditions.Endpoint
].includes(task.preconditions); ].includes(task.preconditions);
@@ -387,35 +371,15 @@ class SingleDialer extends Emitter {
this.dlg.linkedSpanId = this.rootSpan.traceId; this.dlg.linkedSpanId = this.rootSpan.traceId;
const rootSpan = new RootSpan('outbound-call', this.dlg); const rootSpan = new RootSpan('outbound-call', this.dlg);
const newLogger = logger.child({traceId: rootSpan.traceId}); const newLogger = logger.child({traceId: rootSpan.traceId});
//clone application from parent call with new requestor
//parrent application will be closed in case the parent hangup
const app = {...application};
if ('WS' === app.call_hook?.method ||
app.call_hook?.url.startsWith('ws://') || app.call_hook?.url.startsWith('wss://')) {
const requestor = new WsRequestor(logger, this.accountInfo.account.account_sid,
app.call_hook, this.accountInfo.account.webhook_secret);
app.requestor = requestor;
app.notifier = requestor;
app.call_hook.method = 'WS';
}
else {
app.requestor = new HttpRequestor(logger, this.accountInfo.account.account_sid,
app.call_hook, this.accountInfo.account.webhook_secret);
if (app.call_status_hook) app.notifier = new HttpRequestor(logger,
this.accountInfo.account.account_sid, app.call_status_hook,
this.accountInfo.account.webhook_secret);
else app.notifier = {request: () => {}, close: () => {}};
}
const cs = new AdultingCallSession({ const cs = new AdultingCallSession({
logger: newLogger, logger: newLogger,
singleDialer: this, singleDialer: this,
application: app, application,
callInfo: this.callInfo, callInfo: this.callInfo,
accountInfo: this.accountInfo, accountInfo: this.accountInfo,
tasks, tasks,
rootSpan rootSpan
}); });
cs.req = this.req;
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;
} }
@@ -466,11 +430,11 @@ class SingleDialer extends Emitter {
} }
function placeOutdial({ function placeOutdial({
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan
}) { }) {
const myOpts = deepcopy(opts); const myOpts = deepcopy(opts);
const sd = new SingleDialer({ const sd = new SingleDialer({
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan
}); });
sd.exec(srf, ms, myOpts); sd.exec(srf, ms, myOpts);
return sd; return sd;

View File

@@ -1,58 +0,0 @@
const sdpTransform = require('sdp-transform');
const isOnhold = (sdp) => {
return sdp && (sdp.includes('a=sendonly') || sdp.includes('a=inactive'));
};
const mergeSdpMedia = (sdp1, sdp2) => {
const parsedSdp1 = sdpTransform.parse(sdp1);
const parsedSdp2 = sdpTransform.parse(sdp2);
parsedSdp1.media.push(...parsedSdp2.media);
return sdpTransform.write(parsedSdp1);
};
const getCodecPlacement = (parsedSdp, codec) => parsedSdp?.media[0]?.rtp?.findIndex((e) => e.codec === codec);
const isOpusFirst = (sdp) => {
return getCodecPlacement(sdpTransform.parse(sdp), 'opus') === 0;
};
const makeOpusFirst = (sdp) => {
const parsedSdp = sdpTransform.parse(sdp);
// Find the index of the OPUS codec
const opusIndex = getCodecPlacement(parsedSdp, 'opus');
// Move OPUS codec to the beginning
if (opusIndex > 0) {
const opusEntry = parsedSdp.media[0].rtp.splice(opusIndex, 1)[0];
parsedSdp.media[0].rtp.unshift(opusEntry);
// Also move the corresponding payload type in the "m" line
const opusPayloadType = parsedSdp.media[0].payloads.split(' ')[opusIndex];
const otherPayloadTypes = parsedSdp.media[0].payloads.split(' ').filter((pt) => pt != opusPayloadType);
parsedSdp.media[0].payloads = [opusPayloadType, ...otherPayloadTypes].join(' ');
}
return sdpTransform.write(parsedSdp);
};
const extractSdpMedia = (sdp) => {
const parsedSdp1 = sdpTransform.parse(sdp);
if (parsedSdp1.media.length > 1) {
parsedSdp1.media = [parsedSdp1.media[0]];
const parsedSdp2 = sdpTransform.parse(sdp);
parsedSdp2.media = [parsedSdp2.media[1]];
return [sdpTransform.write(parsedSdp1), sdpTransform.write(parsedSdp2)];
} else {
return [sdp, sdp];
}
};
module.exports = {
isOnhold,
mergeSdpMedia,
extractSdpMedia,
isOpusFirst,
makeOpusFirst
};

View File

@@ -97,12 +97,8 @@ const parseSiprecPayload = (req, logger) => {
obj[`${prefix}participantstreamassoc`].forEach((ps) => { obj[`${prefix}participantstreamassoc`].forEach((ps) => {
const part = participants[ps.$.participant_id]; const part = participants[ps.$.participant_id];
if (part) { if (part) {
if (ps.hasOwnProperty(`${prefix}send`)) { part.send = ps[`${prefix}send`][0];
part.send = ps[`${prefix}send`][0]; part.recv = ps[`${prefix}recv`][0];
}
if (ps.hasOwnProperty(`${prefix}recv`)) {
part.recv = ps[`${prefix}recv`][0];
}
} }
}); });
} }
@@ -113,9 +109,9 @@ const parseSiprecPayload = (req, logger) => {
obj[`${prefix}stream`].forEach((s) => { obj[`${prefix}stream`].forEach((s) => {
const streamId = s.$.stream_id; const streamId = s.$.stream_id;
let sender; let sender;
for (const v of Object.values(participants)) { for (const [k, v] of Object.entries(participants)) {
if (v.send === streamId) { if (v.send === streamId) {
sender = v; sender = k;
break; break;
} }
} }
@@ -125,15 +121,9 @@ const parseSiprecPayload = (req, logger) => {
sender.label = s[`${prefix}label`][0]; sender.label = s[`${prefix}label`][0];
if (-1 !== ['1', 'a_leg', 'inbound', '10'].indexOf(sender.label)) { if (-1 !== ['1', 'a_leg', 'inbound'].indexOf(sender.label)) {
opts.caller.aor = sender.aor; opts.caller.aor = sender.aor ;
if (sender.name) opts.caller.name = sender.name; if (sender.name) opts.caller.name = sender.name;
// Remap the sdp stream base on sender label
if (!opts.sdp1.includes(`a=label:${sender.label}`)) {
const tmp = opts.sdp1;
opts.sdp1 = opts.sdp2;
opts.sdp2 = tmp;
}
} }
else { else {
opts.callee.aor = sender.aor ; opts.callee.aor = sender.aor ;

View File

@@ -1,6 +1,14 @@
const { const {
TaskName, TaskName,
} = require('./constants.json'); AzureTranscriptionEvents,
GoogleTranscriptionEvents,
AwsTranscriptionEvents,
NuanceTranscriptionEvents,
DeepgramTranscriptionEvents,
SonioxTranscriptionEvents,
NvidiaTranscriptionEvents,
JambonzTranscriptionEvents
} = require('./constants');
const stickyVars = { const stickyVars = {
google: [ google: [
@@ -25,14 +33,12 @@ const stickyVars = {
'AZURE_SERVICE_ENDPOINT', 'AZURE_SERVICE_ENDPOINT',
'AZURE_INITIAL_SPEECH_TIMEOUT_MS', 'AZURE_INITIAL_SPEECH_TIMEOUT_MS',
'AZURE_USE_OUTPUT_FORMAT_DETAILED', 'AZURE_USE_OUTPUT_FORMAT_DETAILED',
'AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS'
], ],
deepgram: [ deepgram: [
'DEEPGRAM_SPEECH_KEYWORDS', 'DEEPGRAM_SPEECH_KEYWORDS',
'DEEPGRAM_API_KEY', 'DEEPGRAM_API_KEY',
'DEEPGRAM_SPEECH_TIER', 'DEEPGRAM_SPEECH_TIER',
'DEEPGRAM_SPEECH_MODEL', 'DEEPGRAM_SPEECH_MODEL',
'DEEPGRAM_SPEECH_ENABLE_SMART_FORMAT',
'DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION', 'DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
'DEEPGRAM_SPEECH_PROFANITY_FILTER', 'DEEPGRAM_SPEECH_PROFANITY_FILTER',
'DEEPGRAM_SPEECH_REDACT', 'DEEPGRAM_SPEECH_REDACT',
@@ -43,7 +49,6 @@ const stickyVars = {
'DEEPGRAM_SPEECH_SEARCH', 'DEEPGRAM_SPEECH_SEARCH',
'DEEPGRAM_SPEECH_REPLACE', 'DEEPGRAM_SPEECH_REPLACE',
'DEEPGRAM_SPEECH_ENDPOINTING', 'DEEPGRAM_SPEECH_ENDPOINTING',
'DEEPGRAM_SPEECH_UTTERANCE_END_MS',
'DEEPGRAM_SPEECH_VAD_TURNOFF', 'DEEPGRAM_SPEECH_VAD_TURNOFF',
'DEEPGRAM_SPEECH_TAG' 'DEEPGRAM_SPEECH_TAG'
], ],
@@ -87,112 +92,12 @@ const stickyVars = {
nvidia: [ nvidia: [
'NVIDIA_HINTS' 'NVIDIA_HINTS'
], ],
cobalt: [
'COBALT_SPEECH_HINTS',
'COBALT_COMPILED_CONTEXT_DATA',
'COBALT_METADATA'
],
soniox: [ soniox: [
'SONIOX_PROFANITY_FILTER', 'SONIOX_PROFANITY_FILTER',
'SONIOX_MODEL' 'SONIOX_MODEL'
],
assemblyai: [
'ASSEMBLYAI_API_KEY',
'ASSEMBLYAI_WORD_BOOST'
] ]
}; };
const optimalDeepramModels = {
zh: ['base', 'base'],
'zh-CN':['base', 'base'],
'zh-TW': ['base', 'base'],
da: ['enhanced', 'enhanced'],
en: ['nova-2-conversationalai', 'nova-2'],
'en-US': ['nova-2-conversationalai', 'nova-2'],
'en-AU': ['nova-2-conversationalai', 'nova-2'],
'en-GB': ['nova-2-conversationalai', 'nova-2'],
'en-IN': ['nova-2-conversationalai', 'nova-2'],
'en-NZ': ['nova-2-conversationalai', 'nova-2'],
nl: ['nova-2-conversationalai', 'nova-2'],
fr: ['nova-2-conversationalai', 'nova-2'],
'fr-CA': ['nova-2-conversationalai', 'nova-2'],
de: ['nova-2-conversationalai', 'nova-2'],
hi: ['nova-2-conversationalai', 'nova-2'],
'hi-Latn': ['nova-2-conversationalai', 'nova-2'],
id: ['base', 'base'],
it: ['enhanced', 'enhanced'],
ja: ['enhanced', 'enhanced'],
ko: ['enhanced', 'enhanced'],
no: ['enhanced', 'enhanced'],
pl: ['enhanced', 'enhanced'],
pt: ['nova-2-conversationalai', 'nova-2'],
'pt-BR': ['nova-2-conversationalai', 'nova-2'],
'pt-PT': ['base', 'base'],
ru: ['base', 'base'],
es: ['nova-2-conversationalai', 'nova-2'],
'es-419': ['nova-2-conversationalai', 'nova-2'],
'es-LATAM': ['enhanced', 'enhanced'],
sv: ['enhanced', 'enhanced'],
ta: ['enhanced', 'enhanced'],
taq: ['enhanced', 'enhanced'],
tr: ['base', 'base'],
uk: ['base', 'base']
};
const selectDefaultDeepgramModel = (task, language) => {
if (language in optimalDeepramModels) {
const [gather, transcribe] = optimalDeepramModels[language];
return task.name === TaskName.Gather ? gather : transcribe;
}
};
const consolidateTranscripts = (bufferedTranscripts, channel, language) => {
if (bufferedTranscripts.length === 1) return bufferedTranscripts[0];
let totalConfidence = 0;
const finalTranscript = bufferedTranscripts.reduce((acc, evt) => {
totalConfidence += evt.alternatives[0].confidence;
let newTranscript = evt.alternatives[0].transcript;
// If new transcript consists only of digits, spaces, and a trailing comma or period
if (newTranscript.match(/^[\d\s]+[,.]?$/)) {
newTranscript = newTranscript.replace(/\s/g, ''); // Remove all spaces
if (newTranscript.endsWith(',')) {
newTranscript = newTranscript.slice(0, -1); // Remove the trailing comma
} else if (newTranscript.endsWith('.')) {
newTranscript = newTranscript.slice(0, -1); // Remove the trailing period
}
}
const lastChar = acc.alternatives[0].transcript.slice(-1);
const firstChar = newTranscript.charAt(0);
if (lastChar.match(/\d/) && firstChar.match(/\d/)) {
acc.alternatives[0].transcript += newTranscript;
} else {
acc.alternatives[0].transcript += ` ${newTranscript}`;
}
return acc;
}, {
language_code: language,
channel_tag: channel,
is_final: true,
alternatives: [{
transcript: ''
}]
});
finalTranscript.alternatives[0].confidence = bufferedTranscripts.length === 1 ?
bufferedTranscripts[0].alternatives[0].confidence :
totalConfidence / bufferedTranscripts.length;
finalTranscript.alternatives[0].transcript = finalTranscript.alternatives[0].transcript.trim();
finalTranscript.vendor = {
name: 'deepgram',
evt: bufferedTranscripts
};
return finalTranscript;
};
const compileSonioxTranscripts = (finalWordChunks, channel, language) => { const compileSonioxTranscripts = (finalWordChunks, channel, language) => {
const words = finalWordChunks.flat(); const words = finalWordChunks.flat();
const transcript = words.reduce((acc, word) => { const transcript = words.reduce((acc, word) => {
@@ -250,7 +155,7 @@ const normalizeSoniox = (evt, channel, language) => {
}; };
}; };
const normalizeDeepgram = (evt, channel, language, shortUtterance) => { const normalizeDeepgram = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt)); const copy = JSON.parse(JSON.stringify(evt));
const alternatives = (evt.channel?.alternatives || []) const alternatives = (evt.channel?.alternatives || [])
.map((alt) => ({ .map((alt) => ({
@@ -258,14 +163,10 @@ const normalizeDeepgram = (evt, channel, language, shortUtterance) => {
transcript: alt.transcript, transcript: alt.transcript,
})); }));
/**
* note difference between is_final and speech_final in Deepgram:
* https://developers.deepgram.com/docs/understand-endpointing-interim-results
*/
return { return {
language_code: language, language_code: language,
channel_tag: channel, channel_tag: channel,
is_final: shortUtterance ? evt.is_final : evt.speech_final, is_final: evt.is_final,
alternatives: [alternatives[0]], alternatives: [alternatives[0]],
vendor: { vendor: {
name: 'deepgram', name: 'deepgram',
@@ -324,37 +225,12 @@ const normalizeGoogle = (evt, channel, language) => {
}; };
}; };
const normalizeCobalt = (evt, channel, language) => { const normalizeCustom = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
const alternatives = (evt.alternatives || [])
.map((alt) => ({
confidence: alt.confidence,
transcript: alt.transcript_formatted,
}));
return { return {
language_code: language, language_code: language,
channel_tag: channel, channel_tag: channel,
is_final: evt.is_final, is_final: evt.is_final,
alternatives, alternatives: [evt.alternatives[0]]
vendor: {
name: 'cobalt',
evt: copy
}
};
};
const normalizeCustom = (evt, channel, language, vendor) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: [evt.alternatives[0]],
vendor: {
name: vendor,
evt: copy
}
}; };
}; };
@@ -372,20 +248,19 @@ const normalizeNuance = (evt, channel, language) => {
}; };
}; };
const normalizeMicrosoft = (evt, channel, language, punctuation = true) => { const normalizeMicrosoft = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt)); const copy = JSON.parse(JSON.stringify(evt));
const nbest = evt.NBest; const nbest = evt.NBest;
const language_code = evt.PrimaryLanguage?.Language || language; const language_code = evt.PrimaryLanguage?.Language || language;
const alternatives = nbest ? nbest.map((n) => { const alternatives = nbest ? nbest.map((n) => {
return { return {
confidence: n.Confidence, confidence: n.Confidence,
// remove all puntuation if needed transcript: n.Display
transcript: punctuation ? n.Display : n.Display.replace(/\p{P}/gu, '')
}; };
}) : }) :
[ [
{ {
transcript: punctuation ? evt.DisplayText || evt.Text : (evt.DisplayText || evt.Text).replace(/\p{P}/gu, '') transcript: evt.DisplayText || evt.Text
} }
]; ];
@@ -415,34 +290,16 @@ const normalizeAws = (evt, channel, language) => {
}; };
}; };
const normalizeAssemblyAi = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt.message_type === 'FinalTranscript',
alternatives: [
{
confidence: evt.confidence,
transcript: evt.text,
}
],
vendor: {
name: 'ASSEMBLYAI',
evt: copy
}
};
};
module.exports = (logger) => { module.exports = (logger) => {
const normalizeTranscription = (evt, vendor, channel, language, shortUtterance, punctuation) => { const normalizeTranscription = (evt, vendor, channel, language) => {
//logger.debug({ evt, vendor, channel, language }, 'normalizeTranscription'); //logger.debug({ evt, vendor, channel, language }, 'normalizeTranscription');
switch (vendor) { switch (vendor) {
case 'deepgram': case 'deepgram':
return normalizeDeepgram(evt, channel, language, shortUtterance); return normalizeDeepgram(evt, channel, language);
case 'microsoft': case 'microsoft':
return normalizeMicrosoft(evt, channel, language, punctuation); return normalizeMicrosoft(evt, channel, language);
case 'google': case 'google':
return normalizeGoogle(evt, channel, language); return normalizeGoogle(evt, channel, language);
case 'aws': case 'aws':
@@ -455,20 +312,16 @@ module.exports = (logger) => {
return normalizeNvidia(evt, channel, language); return normalizeNvidia(evt, channel, language);
case 'soniox': case 'soniox':
return normalizeSoniox(evt, channel, language); return normalizeSoniox(evt, channel, language);
case 'cobalt':
return normalizeCobalt(evt, channel, language);
case 'assemblyai':
return normalizeAssemblyAi(evt, channel, language, shortUtterance);
default: default:
if (vendor.startsWith('custom:')) { if (vendor.startsWith('custom:')) {
return normalizeCustom(evt, channel, language, vendor); return normalizeCustom(evt, channel, language);
} }
logger.error(`Unknown vendor ${vendor}`); logger.error(`Unknown vendor ${vendor}`);
return evt; return evt;
} }
}; };
const setChannelVarsForStt = (task, sttCredentials, language, rOpts = {}) => { const setChannelVarsForStt = (task, sttCredentials, rOpts = {}) => {
let opts = {}; let opts = {};
const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {}; const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {};
const vad = {enable, voiceMs, mode}; const vad = {enable, voiceMs, mode};
@@ -503,13 +356,12 @@ module.exports = (logger) => {
...(rOpts.punctuation === false && {GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 0}), ...(rOpts.punctuation === false && {GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 0}),
...(rOpts.words == false && {GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 0}), ...(rOpts.words == false && {GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 0}),
...(rOpts.diarization === false && {GOOGLE_SPEECH_SPEAKER_DIARIZATION: 0}), ...(rOpts.diarization === false && {GOOGLE_SPEECH_SPEAKER_DIARIZATION: 0}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{GOOGLE_SPEECH_HINTS: rOpts.hints.join(',')}), {GOOGLE_SPEECH_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{GOOGLE_SPEECH_HINTS: JSON.stringify(rOpts.hints)}), {GOOGLE_SPEECH_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' && {GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}), ...(typeof rOpts.hintsBoost === 'number' && {GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}),
// When altLanguages is emptylist, we have to send value to freeswitch to clear the previous settings ...(rOpts.altLanguages.length > 0 &&
...(rOpts.altLanguages &&
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}), {GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
...(rOpts.interactionType && ...(rOpts.interactionType &&
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}), {GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
@@ -532,34 +384,28 @@ module.exports = (logger) => {
}; };
} }
else if ('microsoft' === vendor) { else if ('microsoft' === vendor) {
const {azureOptions = {}} = rOpts;
opts = { opts = {
...opts, ...opts,
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.trim()).join(',')}), {AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.trim()).join(',')}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.phrase).join(',')}), {AZURE_SPEECH_HINTS: rOpts.hints.map((h) => h.phrase).join(',')}),
// When altLanguages is emptylist, we have to send value to freeswitch to clear the previous settings ...(rOpts.altLanguages && rOpts.altLanguages.length > 0 &&
...(rOpts.altLanguages &&
{AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}), {AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}), ...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
...(rOpts.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}), ...(rOpts.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}),
...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint_url &&
{AZURE_SERVICE_ENDPOINT: sttCredentials.custom_stt_endpoint_url}),
...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}), ...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}),
...(rOpts.initialSpeechTimeoutMs > 0 && ...(rOpts.initialSpeechTimeoutMs > 0 &&
{AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}), {AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}),
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}), ...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}), ...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1}, ...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
...(azureOptions.speechSegmentationSilenceTimeoutMs &&
{AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS: azureOptions.speechSegmentationSilenceTimeoutMs}),
...(sttCredentials && { ...(sttCredentials && {
...(sttCredentials.api_key && {AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key}), AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key,
...(sttCredentials.region && {AZURE_REGION: sttCredentials.region}), AZURE_REGION: sttCredentials.region,
}), }),
...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint && ...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint &&
{AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint}), {AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint})
}; };
} }
else if ('nuance' === vendor) { else if ('nuance' === vendor) {
@@ -612,9 +458,6 @@ module.exports = (logger) => {
} }
else if ('deepgram' === vendor) { else if ('deepgram' === vendor) {
const {deepgramOptions = {}} = rOpts; const {deepgramOptions = {}} = rOpts;
if (!deepgramOptions.model) {
deepgramOptions.model = selectDefaultDeepgramModel(task, language);
}
opts = { opts = {
...opts, ...opts,
...(sttCredentials.api_key) && ...(sttCredentials.api_key) &&
@@ -625,12 +468,10 @@ module.exports = (logger) => {
{DEEPGRAM_SPEECH_MODEL: deepgramOptions.model}, {DEEPGRAM_SPEECH_MODEL: deepgramOptions.model},
...(deepgramOptions.punctuate) && ...(deepgramOptions.punctuate) &&
{DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1}, {DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1},
...(deepgramOptions.smartFormatting) &&
{DEEPGRAM_SPEECH_ENABLE_SMART_FORMAT: 1},
...(deepgramOptions.profanityFilter) && ...(deepgramOptions.profanityFilter) &&
{DEEPGRAM_SPEECH_PROFANITY_FILTER: 1}, {DEEPGRAM_SPEECH_PROFANITY_FILTER: 1},
...(deepgramOptions.redact) && ...(deepgramOptions.redact) &&
{DEEPGRAM_SPEECH_REDACT: deepgramOptions.redact}, {DEEPGRAM_SPEECH_REDACT: 1},
...(deepgramOptions.diarize) && ...(deepgramOptions.diarize) &&
{DEEPGRAM_SPEECH_DIARIZE: 1}, {DEEPGRAM_SPEECH_DIARIZE: 1},
...(deepgramOptions.diarizeVersion) && ...(deepgramOptions.diarizeVersion) &&
@@ -645,16 +486,14 @@ module.exports = (logger) => {
{DEEPGRAM_SPEECH_SEARCH: deepgramOptions.search.join(',')}, {DEEPGRAM_SPEECH_SEARCH: deepgramOptions.search.join(',')},
...(deepgramOptions.replace) && ...(deepgramOptions.replace) &&
{DEEPGRAM_SPEECH_REPLACE: deepgramOptions.replace.join(',')}, {DEEPGRAM_SPEECH_REPLACE: deepgramOptions.replace.join(',')},
...(rOpts.hints && rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.trim()).join(',')}), {DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.trim()).join(',')}),
...(rOpts.hints && rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.phrase).join(',')}), {DEEPGRAM_SPEECH_KEYWORDS: rOpts.hints.map((h) => h.phrase).join(',')}),
...(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},
...(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) &&
@@ -668,9 +507,9 @@ module.exports = (logger) => {
...opts, ...opts,
...(sttCredentials.api_key) && ...(sttCredentials.api_key) &&
{SONIOX_API_KEY: sttCredentials.api_key}, {SONIOX_API_KEY: sttCredentials.api_key},
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{SONIOX_HINTS: rOpts.hints.join(',')}), {SONIOX_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{SONIOX_HINTS: JSON.stringify(rOpts.hints)}), {SONIOX_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' && ...(typeof rOpts.hintsBoost === 'number' &&
{SONIOX_HINTS_BOOST: rOpts.hintsBoost}), {SONIOX_HINTS_BOOST: rOpts.hintsBoost}),
@@ -728,9 +567,9 @@ module.exports = (logger) => {
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 && ...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
{NVIDIA_DIARIZATION_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}), {NVIDIA_DIARIZATION_SPEAKER_COUNT: rOpts.diarizationMaxSpeakers}),
...(rOpts.separateRecognitionPerChannel && {NVIDIA_SEPARATE_RECOGNITION_PER_CHANNEL: 1}), ...(rOpts.separateRecognitionPerChannel && {NVIDIA_SEPARATE_RECOGNITION_PER_CHANNEL: 1}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{NVIDIA_HINTS: rOpts.hints.join(',')}), {NVIDIA_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
{NVIDIA_HINTS: JSON.stringify(rOpts.hints)}), {NVIDIA_HINTS: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' && ...(typeof rOpts.hintsBoost === 'number' &&
{NVIDIA_HINTS_BOOST: rOpts.hintsBoost}), {NVIDIA_HINTS_BOOST: rOpts.hintsBoost}),
@@ -738,49 +577,21 @@ module.exports = (logger) => {
{NVIDIA_CUSTOM_CONFIGURATION: JSON.stringify(nvidiaOptions.customConfiguration)}), {NVIDIA_CUSTOM_CONFIGURATION: JSON.stringify(nvidiaOptions.customConfiguration)}),
}; };
} }
else if ('cobalt' === vendor) {
const {cobaltOptions = {}} = rOpts;
const cobaltUri = cobaltOptions.serverUri || sttCredentials.cobalt_server_uri;
opts = {
...opts,
...(rOpts.words && {COBALT_WORD_TIME_OFFSETS: 1}),
...(!rOpts.words && {COBALT_WORD_TIME_OFFSETS: 0}),
...(rOpts.model && {COBALT_MODEL: rOpts.model}),
...(cobaltUri && {COBALT_SERVER_URI: cobaltUri}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' &&
{COBALT_SPEECH_HINTS: rOpts.hints.join(',')}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
{COBALT_SPEECH_HINTS: JSON.stringify(rOpts.hints)}),
...(rOpts.hints?.length > 0 &&
{COBALT_CONTEXT_TOKEN: cobaltOptions.contextToken || 'unk:default'}),
...(cobaltOptions.metadata && {COBALT_METADATA: cobaltOptions.metadata}),
...(cobaltOptions.enableConfusionNetwork && {COBALT_ENABLE_CONFUSION_NETWORK: 1}),
...(cobaltOptions.compiledContextData && {COBALT_COMPILED_CONTEXT_DATA: cobaltOptions.compiledContextData}),
};
} else if ('assemblyai' === vendor) {
opts = {
...opts,
...(sttCredentials.api_key) &&
{ASSEMBLYAI_API_KEY: sttCredentials.api_key},
...(rOpts.hints?.length > 0 &&
{ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)})
};
}
else if (vendor.startsWith('custom:')) { else if (vendor.startsWith('custom:')) {
let {options = {}} = rOpts; let {options = {}} = rOpts;
const {auth_token, custom_stt_url} = sttCredentials; const {auth_token, custom_stt_url} = sttCredentials;
options = { options = {
...options, ...options,
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' && ...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
{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})
}; };
opts = { opts = {
...opts, ...opts,
...(auth_token && {JAMBONZ_STT_API_KEY: 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)}),
}; };
@@ -792,6 +603,41 @@ module.exports = (logger) => {
return opts; return opts;
}; };
const removeSpeechListeners = (ep) => {
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
ep.removeCustomEventListener(GoogleTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AwsTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
ep.removeCustomEventListener(AzureTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(NuanceTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NuanceTranscriptionEvents.TranscriptionComplete);
ep.removeCustomEventListener(NuanceTranscriptionEvents.StartOfSpeech);
ep.removeCustomEventListener(NuanceTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.Transcription);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.Connect);
ep.removeCustomEventListener(DeepgramTranscriptionEvents.ConnectFailure);
ep.removeCustomEventListener(SonioxTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.Transcription);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech);
ep.removeCustomEventListener(NvidiaTranscriptionEvents.VadDetected);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Transcription);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Connect);
ep.removeCustomEventListener(JambonzTranscriptionEvents.ConnectFailure);
ep.removeCustomEventListener(JambonzTranscriptionEvents.Error);
};
const setSpeechCredentialsAtRuntime = (recognizer) => { const setSpeechCredentialsAtRuntime = (recognizer) => {
if (!recognizer) return; if (!recognizer) return;
if (recognizer.vendor === 'nuance') { if (recognizer.vendor === 'nuance') {
@@ -811,10 +657,6 @@ module.exports = (logger) => {
const {apiKey} = recognizer.sonioxOptions || {}; const {apiKey} = recognizer.sonioxOptions || {};
if (apiKey) return {api_key: apiKey}; if (apiKey) return {api_key: apiKey};
} }
else if (recognizer.vendor === 'cobalt') {
const {serverUri} = recognizer.cobaltOptions || {};
if (serverUri) return {cobalt_server_uri: serverUri};
}
else if (recognizer.vendor === 'ibm') { else if (recognizer.vendor === 'ibm') {
const {ttsApiKey, ttsRegion, sttApiKey, sttRegion, instanceId} = recognizer.ibmOptions || {}; const {ttsApiKey, ttsRegion, sttApiKey, sttRegion, instanceId} = recognizer.ibmOptions || {};
if (ttsApiKey || sttApiKey) return { if (ttsApiKey || sttApiKey) return {
@@ -830,8 +672,8 @@ module.exports = (logger) => {
return { return {
normalizeTranscription, normalizeTranscription,
setChannelVarsForStt, setChannelVarsForStt,
removeSpeechListeners,
setSpeechCredentialsAtRuntime, setSpeechCredentialsAtRuntime,
compileSonioxTranscripts, compileSonioxTranscripts
consolidateTranscripts
}; };
}; };

View File

@@ -9,8 +9,7 @@ const {
JAMBONES_WS_PING_INTERVAL_MS, JAMBONES_WS_PING_INTERVAL_MS,
MAX_RECONNECTS, MAX_RECONNECTS,
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS, JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
JAMBONES_WS_MAX_PAYLOAD, JAMBONES_WS_MAX_PAYLOAD
HTTP_USER_AGENT_HEADER
} = require('../config'); } = require('../config');
class WsRequestor extends BaseRequestor { class WsRequestor extends BaseRequestor {
@@ -44,7 +43,6 @@ 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'].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');
@@ -75,19 +73,11 @@ class WsRequestor extends BaseRequestor {
if (this.connectInProgress) { if (this.connectInProgress) {
this.logger.debug( this.logger.debug(
`WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`); `WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`);
if (wantsAck) { this.queuedMsg.push({type, hook, params, httpHeaders});
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; 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`);
if (this.connections >= MAX_RECONNECTS) { if (this.connections >= MAX_RECONNECTS) {
return Promise.reject(`max attempts connecting to ${this.url}`); return Promise.reject(`max attempts connecting to ${this.url}`);
} }
@@ -126,14 +116,9 @@ class WsRequestor extends BaseRequestor {
const sendQueuedMsgs = () => { const sendQueuedMsgs = () => {
if (this.queuedMsg.length > 0) { if (this.queuedMsg.length > 0) {
for (const {type, hook, params, httpHeaders, promise} of this.queuedMsg) { for (const {type, hook, params, httpHeaders} of this.queuedMsg) {
this.logger.debug(`WsRequestor:request - preparing queued ${type} for sending`); this.logger.debug(`WsRequestor:request - preparing queued ${type} for sending`);
if (promise) { setImmediate(this.request.bind(this, type, hook, params, httpHeaders));
this.request(type, hook, params, httpHeaders)
.then((res) => promise.resolve(res))
.catch((err) => promise.reject(err));
}
else setImmediate(this.request.bind(this, type, hook, params, httpHeaders));
} }
this.queuedMsg.length = 0; this.queuedMsg.length = 0;
} }
@@ -152,7 +137,7 @@ class WsRequestor extends BaseRequestor {
} }
/* simple notifications */ /* simple notifications */
if (!wantsAck || reconnectingWithoutAck) { if (['call:status', 'verb:status', 'jambonz:error'].includes(type) || reconnectingWithoutAck) {
this.ws?.send(JSON.stringify(obj), () => { this.ws?.send(JSON.stringify(obj), () => {
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`); this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
sendQueuedMsgs(); sendQueuedMsgs();
@@ -229,9 +214,6 @@ class WsRequestor extends BaseRequestor {
maxRedirects: 2, maxRedirects: 2,
handshakeTimeout, handshakeTimeout,
maxPayload: JAMBONES_WS_MAX_PAYLOAD ? parseInt(JAMBONES_WS_MAX_PAYLOAD) : 24 * 1024, maxPayload: JAMBONES_WS_MAX_PAYLOAD ? parseInt(JAMBONES_WS_MAX_PAYLOAD) : 24 * 1024,
headers: {
...(HTTP_USER_AGENT_HEADER && {'user-agent' : HTTP_USER_AGENT_HEADER})
}
}; };
if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`}; if (this.username && this.password) opts = {...opts, auth: `${this.username}:${this.password}`};

3448
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.8.5", "version": "0.8.4",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 10.16.0" "node": ">= 10.16.0"
@@ -25,16 +25,13 @@
"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.360.0",
"@aws-sdk/client-sns": "^3.360.0",
"@jambonz/db-helpers": "^0.9.1", "@jambonz/db-helpers": "^0.9.1",
"@jambonz/http-health-check": "^0.0.1", "@jambonz/http-health-check": "^0.0.1",
"@jambonz/mw-registrar": "^0.2.4", "@jambonz/realtimedb-helpers": "^0.8.6",
"@jambonz/realtimedb-helpers": "^0.8.7", "@jambonz/speech-utils": "^0.0.19",
"@jambonz/speech-utils": "^0.0.33", "@jambonz/stats-collector": "^0.1.8",
"@jambonz/stats-collector": "^0.1.9",
"@jambonz/time-series": "^0.2.8", "@jambonz/time-series": "^0.2.8",
"@jambonz/verb-specifications": "^0.0.50", "@jambonz/verb-specifications": "^0.0.26",
"@opentelemetry/api": "^1.4.0", "@opentelemetry/api": "^1.4.0",
"@opentelemetry/exporter-jaeger": "^1.9.0", "@opentelemetry/exporter-jaeger": "^1.9.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.35.0", "@opentelemetry/exporter-trace-otlp-http": "^0.35.0",
@@ -44,13 +41,14 @@
"@opentelemetry/sdk-trace-base": "^1.9.0", "@opentelemetry/sdk-trace-base": "^1.9.0",
"@opentelemetry/sdk-trace-node": "^1.9.0", "@opentelemetry/sdk-trace-node": "^1.9.0",
"@opentelemetry/semantic-conventions": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.9.0",
"@aws-sdk/client-sns": "^3.360.0",
"@aws-sdk/client-auto-scaling": "^3.360.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.33", "drachtio-fsmrf": "^3.0.23",
"drachtio-srf": "^4.5.31", "drachtio-srf": "^4.5.26",
"express": "^4.18.2", "express": "^4.18.2",
"express-validator": "^7.0.1",
"ip": "^1.1.8", "ip": "^1.1.8",
"moment": "^2.29.4", "moment": "^2.29.4",
"parse-url": "^8.1.0", "parse-url": "^8.1.0",
@@ -61,11 +59,11 @@
"short-uuid": "^4.2.2", "short-uuid": "^4.2.2",
"sinon": "^15.0.1", "sinon": "^15.0.1",
"to-snake-case": "^1.0.0", "to-snake-case": "^1.0.0",
"undici": "^5.26.2", "undici": "^5.19.1",
"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.9.0", "ws": "^8.9.0",
"xml2js": "^0.6.2" "xml2js": "^0.5.0"
}, },
"devDependencies": { "devDependencies": {
"clear-module": "^4.1.2", "clear-module": "^4.1.2",

View File

@@ -1,86 +0,0 @@
#
# Recommended minimum configuration:
#
# Example rule allowing access from your local networks.
# Adapt to list your (internal) IP networks from where browsing
# should be allowed
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN)
acl localnet src 172.38.0.0/12 # RFC 1918 local private network (LAN)
acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
acl localnet src fc00::/7 # RFC 4193 local private network range
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
acl SSL_ports port 443
acl Safe_ports port 80 # http
acl Safe_ports port 21 # ftp
acl Safe_ports port 443 # https
acl Safe_ports port 70 # gopher
acl Safe_ports port 210 # wais
acl Safe_ports port 1025-65535 # unregistered ports
acl Safe_ports port 280 # http-mgmt
acl Safe_ports port 488 # gss-http
acl Safe_ports port 591 # filemaker
acl Safe_ports port 777 # multiling http
#
# Recommended minimum Access Permission configuration:
#
# Deny requests to certain unsafe ports
http_access allow !Safe_ports
# Deny CONNECT to other than secure SSL ports
http_access allow CONNECT !SSL_ports
# Only allow cachemgr access from localhost
http_access allow localhost manager
http_access allow manager
# This default configuration only allows localhost requests because a more
# permissive Squid installation could introduce new attack vectors into the
# network by proxying external TCP connections to unprotected services.
http_access allow localhost
# The two deny rules below are unnecessary in this default configuration
# because they are followed by a "deny all" rule. However, they may become
# critically important when you start allowing external requests below them.
# Protect web applications running on the same server as Squid. They often
# assume that only local users can access them at "localhost" ports.
http_access allow to_localhost
# Protect cloud servers that provide local users with sensitive info about
# their server via certain well-known link-local (a.k.a. APIPA) addresses.
# http_access deny to_linklocal
#
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
#
# For example, to allow access from your local networks, you may uncomment the
# following rule (and/or add rules that match your definition of "local"):
# http_access allow localnet
# And finally deny all other access to this proxy
http_access allow all
# Squid normally listens to port 3128
http_port 3128
# Uncomment and adjust the following to add a disk cache directory.
#cache_dir ufs /usr/local/var/cache/squid 100 16 256
# Leave coredumps in the first cache dir
coredump_dir /usr/local/var/cache/squid
#
# Add any of your own refresh_pattern entries above these.
#
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
refresh_pattern . 0 20% 4320

View File

@@ -37,7 +37,7 @@ test('test create-call timeout', async(t) => {
'account_sid':account_sid, 'account_sid':account_sid,
'timeout': 1, 'timeout': 1,
"call_hook": { "call_hook": {
"url": "https://public-apps.jambonz.cloud/hello-world", "url": "https://public-apps.jambonz.us/hello-world",
"method": "POST" "method": "POST"
}, },
"from": "15083718299", "from": "15083718299",
@@ -88,8 +88,8 @@ test('test create-call call-hook basic authentication', async(t) => {
let verbs = [ let verbs = [
{ {
"verb": "pause", "verb": "say",
"length": 1 "text": "hello"
} }
]; ];
await provisionCallHook(from, verbs); await provisionCallHook(from, verbs);
@@ -99,8 +99,6 @@ test('test create-call call-hook basic authentication', async(t) => {
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}`) let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}`)
t.ok(obj.headers.Authorization = 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=', t.ok(obj.headers.Authorization = 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=',
'create-call: call-hook contains basic authentication header'); 'create-call: call-hook contains basic authentication header');
t.ok(obj.headers['user-agent'] = 'jambonz',
'create-call: call-hook contains user-agent header');
disconnect(); disconnect();
} catch (err) { } catch (err) {
console.log(`error received: ${err}`); console.log(`error received: ${err}`);

View File

@@ -330,7 +330,7 @@ CREATE TABLE `applications` (
LOCK TABLES `applications` WRITE; LOCK TABLES `applications` WRITE;
/*!40000 ALTER TABLE `applications` DISABLE KEYS */; /*!40000 ALTER TABLE `applications` DISABLE KEYS */;
INSERT INTO `applications` VALUES ('0dddaabf-0a30-43e3-84e8-426873b1a78b','decline call',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b341','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('0dddaabf-0a30-43e3-84e8-426873b1a78c','app json',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b341','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]','google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('17461c69-56b5-4dab-ad83-1c43a0f93a3d','gather',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','10692465-a511-4277-9807-b7157e4f81e1','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('195d9507-6a42-46a8-825f-f009e729d023','sip info',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c9113e7a-741f-48b9-96c1-f2f78176eeb3','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('24d0f6af-e976-44dd-a2e8-41c7b55abe33','say account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('308b4f41-1a18-4052-b89a-c054e75ce242','say',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('ae026ab5-3029-47b4-9d7c-236e3a4b4ebe','transcribe account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('baf9213b-5556-4c20-870c-586392ed246f','transcribe',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('0dddaabf-0a30-43e3-84e8-426873b1a7ac','http proxy app',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b3ac','293904c1-351b-4bca-8d58-1a29b853c7ac',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'); INSERT INTO `applications` VALUES ('0dddaabf-0a30-43e3-84e8-426873b1a78b','decline call',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b341','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('0dddaabf-0a30-43e3-84e8-426873b1a78c','app json',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c71e79db-24f2-4866-a3ee-febb0f97b341','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,'[{\"verb\": \"play\",\"url\": \"silence_stream://5000\"}]','google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('17461c69-56b5-4dab-ad83-1c43a0f93a3d','gather',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','10692465-a511-4277-9807-b7157e4f81e1','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('195d9507-6a42-46a8-825f-f009e729d023','sip info',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','c9113e7a-741f-48b9-96c1-f2f78176eeb3','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('24d0f6af-e976-44dd-a2e8-41c7b55abe33','say account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('308b4f41-1a18-4052-b89a-c054e75ce242','say',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','54ab0976-a6c0-45d8-89a4-d90d45bf9d96','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('ae026ab5-3029-47b4-9d7c-236e3a4b4ebe','transcribe account 2',NULL,'622f62e4-303a-49f2-bbe0-eb1e1714e37a','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48'),('baf9213b-5556-4c20-870c-586392ed246f','transcribe',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','293904c1-351b-4bca-8d58-1a29b853c7db',NULL,NULL,'google','en-US','en-US-Standard-C','google','en-US','2023-05-31 03:52:48');
/*!40000 ALTER TABLE `applications` ENABLE KEYS */; /*!40000 ALTER TABLE `applications` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -646,7 +646,7 @@ CREATE TABLE `phone_numbers` (
LOCK TABLES `phone_numbers` WRITE; LOCK TABLES `phone_numbers` WRITE;
/*!40000 ALTER TABLE `phone_numbers` DISABLE KEYS */; /*!40000 ALTER TABLE `phone_numbers` DISABLE KEYS */;
INSERT INTO `phone_numbers` VALUES ('05eeed62-b29b-4679-bf38-d7a4e318be44','16174000003','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','17461c69-56b5-4dab-ad83-1c43a0f93a3d',NULL),('4b439355-debc-40c7-9cfa-5be58c2bed6b','16174000000','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','0dddaabf-0a30-43e3-84e8-426873b1a78b',NULL),('964d0581-9627-44cb-be20-8118050406b2','16174000006','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','195d9507-6a42-46a8-825f-f009e729d023',NULL),('964d0581-9627-44cb-be20-8118050406b3','16174000007','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','0dddaabf-0a30-43e3-84e8-426873b1a78c',NULL),('9cc9e7fc-b7b0-4101-8f3c-9fe13ce5df0a','16174000001','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','308b4f41-1a18-4052-b89a-c054e75ce242',NULL),('e686a320-0725-418f-be65-532159bdc3ed','16174000002','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','24d0f6af-e976-44dd-a2e8-41c7b55abe33',NULL),('f3c53863-b629-4cf6-9dcb-c7fb7072314b','16174000004','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','baf9213b-5556-4c20-870c-586392ed246f',NULL),('f6416c17-829a-4f11-9c32-f0d00e4a9ae9','16174000005','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','ae026ab5-3029-47b4-9d7c-236e3a4b4ebe',NULL),('4b439355-debc-40c7-9cfa-5be58c2bedac','16174000015','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','0dddaabf-0a30-43e3-84e8-426873b1a7ac',NULL); INSERT INTO `phone_numbers` VALUES ('05eeed62-b29b-4679-bf38-d7a4e318be44','16174000003','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','17461c69-56b5-4dab-ad83-1c43a0f93a3d',NULL),('4b439355-debc-40c7-9cfa-5be58c2bed6b','16174000000','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','0dddaabf-0a30-43e3-84e8-426873b1a78b',NULL),('964d0581-9627-44cb-be20-8118050406b2','16174000006','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','195d9507-6a42-46a8-825f-f009e729d023',NULL),('964d0581-9627-44cb-be20-8118050406b3','16174000007','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','0dddaabf-0a30-43e3-84e8-426873b1a78c',NULL),('9cc9e7fc-b7b0-4101-8f3c-9fe13ce5df0a','16174000001','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','308b4f41-1a18-4052-b89a-c054e75ce242',NULL),('e686a320-0725-418f-be65-532159bdc3ed','16174000002','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','24d0f6af-e976-44dd-a2e8-41c7b55abe33',NULL),('f3c53863-b629-4cf6-9dcb-c7fb7072314b','16174000004','5145b436-2f38-4029-8d4c-fd8c67831c7a','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','baf9213b-5556-4c20-870c-586392ed246f',NULL),('f6416c17-829a-4f11-9c32-f0d00e4a9ae9','16174000005','5145b436-2f38-4029-8d4c-fd8c67831c7a','622f62e4-303a-49f2-bbe0-eb1e1714e37a','ae026ab5-3029-47b4-9d7c-236e3a4b4ebe',NULL);
/*!40000 ALTER TABLE `phone_numbers` ENABLE KEYS */; /*!40000 ALTER TABLE `phone_numbers` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -896,7 +896,7 @@ CREATE TABLE `service_providers` (
LOCK TABLES `service_providers` WRITE; LOCK TABLES `service_providers` WRITE;
/*!40000 ALTER TABLE `service_providers` DISABLE KEYS */; /*!40000 ALTER TABLE `service_providers` DISABLE KEYS */;
INSERT INTO `service_providers` VALUES ('2708b1b3-2736-40ea-b502-c53d8396247f','jambonz.cloud','jambonz.cloud service provider','yakeeda.com',NULL,NULL); INSERT INTO `service_providers` VALUES ('2708b1b3-2736-40ea-b502-c53d8396247f','jambonz.us','jambonz.us service provider','yakeeda.com',NULL,NULL);
/*!40000 ALTER TABLE `service_providers` ENABLE KEYS */; /*!40000 ALTER TABLE `service_providers` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -1045,7 +1045,6 @@ CREATE TABLE `speech_credentials` (
`tts_tested_ok` tinyint(1) DEFAULT NULL, `tts_tested_ok` tinyint(1) DEFAULT NULL,
`stt_tested_ok` tinyint(1) DEFAULT NULL, `stt_tested_ok` tinyint(1) DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`label` VARCHAR(64),
PRIMARY KEY (`speech_credential_sid`), PRIMARY KEY (`speech_credential_sid`),
UNIQUE KEY `speech_credential_sid` (`speech_credential_sid`), UNIQUE KEY `speech_credential_sid` (`speech_credential_sid`),
UNIQUE KEY `speech_credentials_idx_1` (`vendor`,`account_sid`), UNIQUE KEY `speech_credentials_idx_1` (`vendor`,`account_sid`),
@@ -1064,7 +1063,7 @@ CREATE TABLE `speech_credentials` (
LOCK TABLES `speech_credentials` WRITE; LOCK TABLES `speech_credentials` WRITE;
/*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */; /*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */;
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1,'2023-05-31 03:44:21', NULL),('2add347f-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','microsoft','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1,'2023-05-31 03:44:21', NULL),('84154212-5c99-4c94-8993-bc2a46288daa',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',1,1,NULL,NULL,1,1,'2023-05-31 03:44:21', NULL); INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1,'2023-05-31 03:44:21'),('2add347f-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','microsoft','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1,'2023-05-31 03:44:21'),('84154212-5c99-4c94-8993-bc2a46288daa',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',1,1,NULL,NULL,NULL,NULL,'2023-05-31 03:44:21');
/*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */; /*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
@@ -1254,7 +1253,7 @@ CREATE TABLE `webhooks` (
LOCK TABLES `webhooks` WRITE; LOCK TABLES `webhooks` WRITE;
/*!40000 ALTER TABLE `webhooks` DISABLE KEYS */; /*!40000 ALTER TABLE `webhooks` DISABLE KEYS */;
INSERT INTO `webhooks` VALUES ('10692465-a511-4277-9807-b7157e4f81e1','http://127.0.0.1:3102/','POST',NULL,NULL),('293904c1-351b-4bca-8d58-1a29b853c7db','http://127.0.0.1:3100/callStatus','POST',NULL,NULL),('54ab0976-a6c0-45d8-89a4-d90d45bf9d96','http://127.0.0.1:3101/','POST',NULL,NULL),('6ac36aeb-6bd0-428a-80a1-aed95640a296','https://flows.jambonz.cloud/callStatus','POST',NULL,NULL),('c71e79db-24f2-4866-a3ee-febb0f97b341','http://127.0.0.1:3100/','POST',NULL,NULL),('c9113e7a-741f-48b9-96c1-f2f78176eeb3','http://127.0.0.1:3104/','POST',NULL,NULL),('d9c205c6-a129-443e-a9c0-d1bb437d4bb7','https://flows.jambonz.cloud/testCall','POST',NULL,NULL),('ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','http://127.0.0.1:3103/','POST',NULL,NULL),('293904c1-351b-4bca-8d58-1a29b853c7ac','http://172.38.0.60:3000/callStatus','POST',NULL,NULL),('c71e79db-24f2-4866-a3ee-febb0f97b3ac','http://172.38.0.60:3000/','POST',NULL,NULL); INSERT INTO `webhooks` VALUES ('10692465-a511-4277-9807-b7157e4f81e1','http://127.0.0.1:3102/','POST',NULL,NULL),('293904c1-351b-4bca-8d58-1a29b853c7db','http://127.0.0.1:3100/callStatus','POST',NULL,NULL),('54ab0976-a6c0-45d8-89a4-d90d45bf9d96','http://127.0.0.1:3101/','POST',NULL,NULL),('6ac36aeb-6bd0-428a-80a1-aed95640a296','https://flows.jambonz.us/callStatus','POST',NULL,NULL),('c71e79db-24f2-4866-a3ee-febb0f97b341','http://127.0.0.1:3100/','POST',NULL,NULL),('c9113e7a-741f-48b9-96c1-f2f78176eeb3','http://127.0.0.1:3104/','POST',NULL,NULL),('d9c205c6-a129-443e-a9c0-d1bb437d4bb7','https://flows.jambonz.us/testCall','POST',NULL,NULL),('ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','http://127.0.0.1:3103/','POST',NULL,NULL);
/*!40000 ALTER TABLE `webhooks` ENABLE KEYS */; /*!40000 ALTER TABLE `webhooks` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

View File

@@ -13,8 +13,6 @@ DROP TABLE IF EXISTS beta_invite_codes;
DROP TABLE IF EXISTS call_routes; DROP TABLE IF EXISTS call_routes;
DROP TABLE IF EXISTS clients;
DROP TABLE IF EXISTS dns_records; DROP TABLE IF EXISTS dns_records;
DROP TABLE IF EXISTS lcr; DROP TABLE IF EXISTS lcr;
@@ -129,16 +127,6 @@ application_sid CHAR(36) NOT NULL,
PRIMARY KEY (call_route_sid) PRIMARY KEY (call_route_sid)
) COMMENT='a regex-based pattern match for call routing'; ) COMMENT='a regex-based pattern match for call routing';
CREATE TABLE clients
(
client_sid CHAR(36) NOT NULL UNIQUE ,
account_sid CHAR(36) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
username VARCHAR(64),
password VARCHAR(1024),
PRIMARY KEY (client_sid)
);
CREATE TABLE dns_records CREATE TABLE dns_records
( (
dns_record_sid CHAR(36) NOT NULL UNIQUE , dns_record_sid CHAR(36) NOT NULL UNIQUE ,
@@ -334,7 +322,6 @@ last_tested DATETIME,
tts_tested_ok BOOLEAN, tts_tested_ok BOOLEAN,
stt_tested_ok BOOLEAN, stt_tested_ok BOOLEAN,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
label VARCHAR(64),
PRIMARY KEY (speech_credential_sid) PRIMARY KEY (speech_credential_sid)
); );
@@ -424,7 +411,7 @@ PRIMARY KEY (smpp_gateway_sid)
CREATE TABLE phone_numbers CREATE TABLE phone_numbers
( (
phone_number_sid CHAR(36) UNIQUE , phone_number_sid CHAR(36) UNIQUE ,
number VARCHAR(132) NOT NULL, number VARCHAR(132) NOT NULL UNIQUE ,
voip_carrier_sid CHAR(36), voip_carrier_sid CHAR(36),
account_sid CHAR(36), account_sid CHAR(36),
application_sid CHAR(36), application_sid CHAR(36),
@@ -479,12 +466,9 @@ 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(64), speech_synthesis_voice 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),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
record_all_calls BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (application_sid) PRIMARY KEY (application_sid)
) COMMENT='A defined set of behaviors to be applied to phone calls '; ) COMMENT='A defined set of behaviors to be applied to phone calls ';
@@ -522,9 +506,6 @@ subspace_client_secret VARCHAR(255),
subspace_sip_teleport_id VARCHAR(255), subspace_sip_teleport_id VARCHAR(255),
subspace_sip_teleport_destinations VARCHAR(255), subspace_sip_teleport_destinations VARCHAR(255),
siprec_hook_sid CHAR(36), siprec_hook_sid CHAR(36),
record_all_calls BOOLEAN NOT NULL DEFAULT false,
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
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';
@@ -545,9 +526,6 @@ ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERE
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid); ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
CREATE INDEX client_sid_idx ON clients (client_sid);
ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid);
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid); CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
@@ -612,6 +590,8 @@ CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid); CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid); CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid); CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
@@ -648,8 +628,6 @@ CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid); CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
CREATE UNIQUE INDEX phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid);
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid); CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
CREATE INDEX number_idx ON phone_numbers (number); CREATE INDEX number_idx ON phone_numbers (number);
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid); CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
@@ -704,4 +682,5 @@ 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

@@ -42,7 +42,7 @@ services:
ipv4_address: 172.38.0.7 ipv4_address: 172.38.0.7
drachtio: drachtio:
image: drachtio/drachtio-server:0.8.24 image: drachtio/drachtio-server:0.8.22
restart: always restart: always
command: drachtio --contact "sip:*;transport=udp" --mtu 4096 --address 0.0.0.0 --port 9022 command: drachtio --contact "sip:*;transport=udp" --mtu 4096 --address 0.0.0.0 --port 9022
ports: ports:
@@ -57,7 +57,7 @@ services:
condition: service_healthy condition: service_healthy
freeswitch: freeswitch:
image: drachtio/drachtio-freeswitch-mrf:0.6.1 image: drachtio/drachtio-freeswitch-mrf:0.4.33
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:
@@ -92,13 +92,3 @@ services:
networks: networks:
fs: fs:
ipv4_address: 172.38.0.90 ipv4_address: 172.38.0.90
squid:
image: ubuntu/squid:edge
ports:
- "3128:3128"
volumes:
- ./configuration/squid.conf:/etc/squid/squid.conf
networks:
fs:
ipv4_address: 172.38.0.91

View File

@@ -1,53 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
const {provisionCallHook, provisionCustomHook} = require('./utils')
const bent = require('bent');
const getJSON = bent('json')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'hangup\' custom headers', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'play',
url: 'https://example.com/example.mp3'
},
{
"verb": "hangup",
"headers": {
"X-Reason" : "maximum call duration exceeded"
}
}
];
const from = 'hangup_custom_headers';
await provisionCallHook(from, verbs)
// THEN
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
t.pass('play: succeeds when using single link');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -1,72 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
const {provisionCallHook, provisionCustomHook} = require('./utils')
const bent = require('bent');
const getJSON = bent('json')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'HTTP proxy\' test Info', async(t) => {
clearModule.all();
process.env.JAMBONES_HTTP_PROXY_IP = "127.0.0.1";
process.env.JAMBONES_HTTP_PROXY_PROTOCOL = "http";
process.env.JAMBONES_HTTP_PROXY_PORT = 3128;
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'config',
sipRequestWithinDialogHook: '/customHook'
},
{
verb: 'play',
url: 'silence_stream://5000',
}
];
const waitHookVerbs = [
{
verb: 'hangup'
}
];
const from = 'http_proxy_info';
await provisionCustomHook(from, waitHookVerbs)
await provisionCallHook(from, verbs);
// THEN
await sippUac('uac-success-info-received-bye.xml', '172.38.0.10', from, "16174000015");
t.pass('sip Info: success send Info');
// Make sure that sipRequestWithinDialogHook is called and success
const json = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
t.pass(json.body.sip_method === 'INFO', 'sipRequestWithinDialogHook contains sip_method')
t.pass(json.body.sip_body === 'hello jambonz\r\n', 'sipRequestWithinDialogHook contains sip_method')
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
} finally {
process.env.JAMBONES_HTTP_PROXY_IP = null;
process.env.JAMBONES_HTTP_PROXY_PROTOCOL = null;
process.env.JAMBONES_HTTP_PROXY_PORT = null;
}
});

View File

@@ -1,65 +0,0 @@
const test = require('tape');
const { sippUac } = require('./sipp')('test_fs');
const clearModule = require('clear-module');
const {provisionCallHook, provisionCustomHook} = require('./utils')
const bent = require('bent');
const getJSON = bent('json')
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
function connect(connectable) {
return new Promise((resolve, reject) => {
connectable.on('connect', () => {
return resolve();
});
});
}
test('\'sip Indialog\' test Info', async(t) => {
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
const verbs = [
{
verb: 'config',
sipRequestWithinDialogHook: '/customHook'
},
{
verb: 'play',
url: 'silence_stream://5000',
}
];
const waitHookVerbs = [
{
verb: 'hangup'
}
];
const from = 'sip_indialog_info';
await provisionCustomHook(from, waitHookVerbs)
await provisionCallHook(from, verbs);
// THEN
await sippUac('uac-success-info-received-bye.xml', '172.38.0.10', from);
t.pass('sip Info: success send Info');
// Make sure that sipRequestWithinDialogHook is called and success
const json = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_customHook`)
t.pass(json.body.sip_method === 'INFO', 'sipRequestWithinDialogHook contains sip_method')
t.pass(json.body.sip_body === 'hello jambonz\r\n', 'sipRequestWithinDialogHook contains sip_method')
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});

View File

@@ -15,9 +15,5 @@ require('./sip-refer-tests');
require('./listen-tests'); require('./listen-tests');
require('./config-test'); require('./config-test');
require('./queue-test'); require('./queue-test');
require('./in-dialog-test');
require('./hangup-test');
require('./sdp-utils-test');
require('./http-proxy-test');
require('./remove-test-db'); require('./remove-test-db');
require('./docker_stop'); require('./docker_stop');

View File

@@ -188,7 +188,7 @@ test('\'play\' tests with seekOffset and actionHook', async(t) => {
const seconds = parseInt(obj.body.playback_seconds); const seconds = parseInt(obj.body.playback_seconds);
const milliseconds = parseInt(obj.body.playback_milliseconds); const milliseconds = parseInt(obj.body.playback_milliseconds);
const lastOffsetPos = parseInt(obj.body.playback_last_offset_pos); const lastOffsetPos = parseInt(obj.body.playback_last_offset_pos);
console.log({obj}, 'lastRequest'); //console.log({obj}, 'lastRequest');
t.ok(obj.body.reason === "playCompleted", "play: actionHook success received"); t.ok(obj.body.reason === "playCompleted", "play: actionHook success received");
t.ok(seconds === 2, "playback_seconds: actionHook success received"); t.ok(seconds === 2, "playback_seconds: actionHook success received");
t.ok(milliseconds === 2048, "playback_milliseconds: actionHook success received"); t.ok(milliseconds === 2048, "playback_milliseconds: actionHook success received");

View File

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

View File

@@ -1,114 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="Basic Sipstone UAC">
<!-- In client mode (sipp placing calls), the Call-ID MUST be -->
<!-- generated by sipp. To do so, use [call_id] keyword. -->
<send retrans="500">
<![CDATA[
INVITE sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: <sip:[to]@[remote_ip]:[remote_port]>
Call-ID: [call_id]
CSeq: 1 INVITE
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
X-Account-Sid: bb845d4b-83a9-4cde-a6e9-50f3743bab3f
Subject: uac-say
Content-Type: application/sdp
Content-Length: [len]
v=0
o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
s=-
c=IN IP[media_ip_type] [media_ip]
t=0 0
m=audio [media_port] RTP/AVP 0
a=rtpmap:0 PCMU/8000
]]>
</send>
<recv response="100"
optional="true">
</recv>
<recv response="180" optional="true">
</recv>
<recv response="183" optional="true">
</recv>
<!-- By adding rrs="true" (Record Route Sets), the route sets -->
<!-- are saved and used for following messages sent. Useful to test -->
<!-- against stateful SIP proxies/B2BUAs. -->
<recv response="200" rtd="true">
</recv>
<!-- Packet lost can be simulated in any send/recv message by -->
<!-- by adding the 'lost = "10"'. Value can be [1-100] percent. -->
<send>
<![CDATA[
ACK sip:[to]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 1 ACK
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: uac-say
Content-Length: 0
]]>
</send>
<pause milliseconds="2000"/>
<!-- Send an INFO message -->
<send>
<![CDATA[
INFO sip:[service]@[remote_ip]:[remote_port] SIP/2.0
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
From: [from] <sip:[from]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
To: [to] <sip:[to]@[remote_ip]:[remote_port]>[peer_tag_param]
Call-ID: [call_id]
CSeq: 2 INFO
Contact: sip:[from]@[local_ip]:[local_port]
Max-Forwards: 70
Subject: Performance Test
Content-Type: text/plain
Content-Length: [len]
hello jambonz
]]>
</send>
<!-- Receive 200 OK -->
<recv response="200">
</recv>
<recv request="BYE">
</recv>
<send>
<![CDATA[
SIP/2.0 200 OK
[last_Via:]
[last_From:]
[last_To:]
[last_Call-ID:]
[last_CSeq:]
Contact: <sip:[local_ip]:[local_port];transport=[transport]>
Content-Length: 0
]]>
</send>
</scenario>

View File

@@ -1,26 +0,0 @@
const test = require('tape');
const {makeOpusFirst, isOpusFirst} = require('../lib/utils/sdp-utils');
const sdpTransform = require('sdp-transform');
test('test opus first', (t) => {
const sdp = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:111 opus/48000/2\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n';
const opusSdp = makeOpusFirst(sdp);
const parsedSdp = sdpTransform.parse(opusSdp);
const opusIndex = parsedSdp.media[0].rtp.findIndex((entry) => entry.codec === 'opus');
t.ok(opusIndex === 0, 'succesffuly move opus to be first offer')
t.end();
});
test('test is opus first', (t) => {
const sdp = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n';
t.ok(isOpusFirst(sdp), "opus is first offer");
const sdp2 = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:111 opus/48000/2\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n';
t.ok(!isOpusFirst(sdp2), "opus is not first offer")
const sdp3 = 'v=0\r\no=xhoaluu2 1314 1504 IN IP4 192.168.1.4\r\ns=Talk\r\nc=IN IP4 192.168.1.4\r\nt=0 0\r\na=ice-pwd:397d063ea23fdc05164e3ee4\r\na=ice-ufrag:16c449a3\r\na=rtcp-xr:rcvr-rtt=all:10000 stat-summary=loss,dup,jitt,TTL voip-metrics\r\na=group:BUNDLE as\r\na=record:off\r\nm=audio 56542 RTP/AVPF 0 8\r\nc=IN IP4 14.226.233.151\r\na=rtcp-mux\r\na=mid:as\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=rtcp:63076 IN IP4 192.168.1.4\r\na=candidate:1 1 UDP 2130706303 192.168.1.4 56542 typ host\r\na=candidate:1 2 UDP 2130706302 192.168.1.4 63076 typ host\r\na=candidate:2 1 UDP 2130706431 2001:ee0:d744:dcf0:c1d3:d73f:7a93:dc9f 56542 typ host\r\na=candidate:2 2 UDP 2130706430 2001:ee0:d744:dcf0:c1d3:d73f:7a93:dc9f 63076 typ host\r\na=candidate:3 1 UDP 2130706431 2001:ee0:d744:dcf0:15:6be3:8e6b:b736 56542 typ host\r\na=candidate:3 2 UDP 2130706430 2001:ee0:d744:dcf0:15:6be3:8e6b:b736 63076 typ host\r\na=candidate:4 1 UDP 1694498687 14.226.233.151 56542 typ srflx raddr 192.168.1.4 rport 56542\r\na=rtcp-fb:* trr-int 1000\r\na=rtcp-fb:* ccm tmmbr';
t.ok(!isOpusFirst(sdp2), "opus is not first offer")
t.end();
});

View File

@@ -52,7 +52,6 @@ test('\'transcribe\' test - google', async(t) => {
// THEN // THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from); await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`); let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'), t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using google credentials'); 'transcribe: succeeds when using google credentials');
@@ -90,7 +89,6 @@ test('\'transcribe\' test - microsoft', async(t) => {
// THEN // THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from); await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`); let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'), t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using microsoft credentials'); 'transcribe: succeeds when using microsoft credentials');
@@ -128,7 +126,6 @@ test('\'transcribe\' test - aws', async(t) => {
// THEN // THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from); await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`); let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'), t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using aws credentials'); 'transcribe: succeeds when using aws credentials');
@@ -140,71 +137,6 @@ test('\'transcribe\' test - aws', async(t) => {
} }
}); });
test('\'transcribe\' test - deepgram config options', async(t) => {
if (!DEEPGRAM_API_KEY ) {
t.pass('skipping deepgram tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "config",
"recognizer": {
"vendor": "deepgram",
"language": "en-US",
"altLanguages": [
"en-US"
],
"deepgramOptions": {
"model": "2-ea",
"tier": "nova",
"numerals": true,
"ner": true,
"vadTurnoff": 10,
"keywords": [
"CPT"
]
}
}
},
{
"verb": "transcribe",
"transcriptionHook": "/transcriptionHook",
"recognizer": {
"vendor": "deepgram",
"altLanguages": [
"en-AU"
],
"hints": ["customer support", "sales", "human resources", "HR"],
"deepgramOptions": {
"apiKey": DEEPGRAM_API_KEY,
}
}
}
];
let from = "gather_success";
await provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().includes('like to speak to customer support'),
'transcribe: succeeds when using deepgram credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'transcribe\' test - deepgram', async(t) => { test('\'transcribe\' test - deepgram', async(t) => {
if (!DEEPGRAM_API_KEY ) { if (!DEEPGRAM_API_KEY ) {
t.pass('skipping deepgram tests'); t.pass('skipping deepgram tests');
@@ -234,7 +166,6 @@ test('\'transcribe\' test - deepgram', async(t) => {
// THEN // THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from); await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`); let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().includes('like to speak to customer support'), t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().includes('like to speak to customer support'),
'transcribe: succeeds when using deepgram credentials'); 'transcribe: succeeds when using deepgram credentials');
@@ -279,168 +210,6 @@ test('\'transcribe\' test - soniox', async(t) => {
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'), t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using soniox credentials'); 'transcribe: succeeds when using soniox credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'transcribe\' test - google with asrTimeout', async(t) => {
if (!GCP_JSON_KEY) {
t.pass('skipping google tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "transcribe",
"recognizer": {
"vendor": "google",
"hints": ["customer support", "sales", "human resources", "HR"],
"asrTimeout": 4
},
"transcriptionHook": "/transcriptionHook"
}
];
let from = "gather_success";
await provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
'transcribe: succeeds when using google credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'transcribe\' test - deepgram config options altLanguages', async(t) => {
if (!DEEPGRAM_API_KEY ) {
t.pass('skipping deepgram tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "config",
"recognizer": {
"vendor": "deepgram",
"language": "en-US",
"altLanguages": [
"en-US"
],
"deepgramOptions": {
"model": "2-ea",
"tier": "nova",
"numerals": true,
"ner": true,
"vadTurnoff": 10,
"keywords": [
"CPT"
]
}
}
},
{
"verb": "transcribe",
"transcriptionHook": "/transcriptionHook",
"recognizer": {
"vendor": "deepgram",
"hints": ["customer support", "sales", "human resources", "HR"],
"deepgramOptions": {
"apiKey": DEEPGRAM_API_KEY,
}
}
}
];
let from = "gather_success_no_altLanguages";
await provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().includes('like to speak to customer support'),
'transcribe: succeeds when using deepgram credentials');
disconnect();
} catch (err) {
console.log(`error received: ${err}`);
disconnect();
t.error(err);
}
});
test('\'transcribe\' test - deepgram config options altLanguages', async(t) => {
if (!DEEPGRAM_API_KEY ) {
t.pass('skipping deepgram tests');
return t.end();
}
clearModule.all();
const {srf, disconnect} = require('../app');
try {
await connect(srf);
// GIVEN
let verbs = [
{
"verb": "config",
"recognizer": {
"vendor": "deepgram",
"language": "en-US",
"altLanguages": [
"en-US"
],
"deepgramOptions": {
"model": "2-ea",
"tier": "nova",
"numerals": true,
"ner": true,
"vadTurnoff": 10,
"keywords": [
"CPT"
]
}
}
},
{
"verb": "transcribe",
"transcriptionHook": "/transcriptionHook",
"recognizer": {
"vendor": "deepgram",
"hints": ["customer support", "sales", "human resources", "HR"],
"altLanguages": [],
"deepgramOptions": {
"apiKey": DEEPGRAM_API_KEY,
}
}
}
];
let from = "gather_success_has_altLanguages";
await provisionCallHook(from, verbs);
// THEN
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
//console.log(JSON.stringify(obj));
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().includes('like to speak to customer support'),
'transcribe: succeeds when using deepgram credentials');
disconnect(); disconnect();
} catch (err) { } catch (err) {
console.log(`error received: ${err}`); console.log(`error received: ${err}`);