mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-15 10:49:07 +00:00
Compare commits
79 Commits
v0.8.3-rc3
...
v0.8.5-rc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a8a2aa955 | ||
|
|
6587b1f758 | ||
|
|
c29def92e8 | ||
|
|
a1793ac359 | ||
|
|
d220733dea | ||
|
|
a09605fc51 | ||
|
|
7f59bba634 | ||
|
|
1477605e66 | ||
|
|
4f0ab83f5f | ||
|
|
2935574440 | ||
|
|
c10c561ba1 | ||
|
|
2ccd33e212 | ||
|
|
a03baa8461 | ||
|
|
90df33a15c | ||
|
|
a15479e6dc | ||
|
|
dd74cb2cc6 | ||
|
|
7a02c36bad | ||
|
|
78fd4549af | ||
|
|
b1ecf069bf | ||
|
|
6f0dbef433 | ||
|
|
32dcb2adfa | ||
|
|
ee514f7459 | ||
|
|
4cfea0707a | ||
|
|
f8c5abe9e9 | ||
|
|
ad722a55ee | ||
|
|
82939214a2 | ||
|
|
043a171f41 | ||
|
|
c8e9b34b53 | ||
|
|
d7dcdb1d0c | ||
|
|
fbd0782258 | ||
|
|
38f9329b12 | ||
|
|
d4bfdf0916 | ||
|
|
9203deef0f | ||
|
|
48b182c891 | ||
|
|
e8e987cb9d | ||
|
|
38ea9e7411 | ||
|
|
7b11a56a53 | ||
|
|
66305b5aea | ||
|
|
6793bbf330 | ||
|
|
d8543f73f2 | ||
|
|
e1dad569dc | ||
|
|
643bee48c5 | ||
|
|
487bfd90d9 | ||
|
|
810f6eb695 | ||
|
|
62bc6b4bac | ||
|
|
91fe3ceb06 | ||
|
|
a7d07ce7ae | ||
|
|
7cd6c27f90 | ||
|
|
aad24744f3 | ||
|
|
ab0452879e | ||
|
|
ffdb7a0bb5 | ||
|
|
354818b974 | ||
|
|
30beb9c093 | ||
|
|
b978b3bc2f | ||
|
|
a1c38f8a2e | ||
|
|
37f3668016 | ||
|
|
55935e3f35 | ||
|
|
b7070121ee | ||
|
|
01260ad054 | ||
|
|
bd911c88f9 | ||
|
|
d96712a8d6 | ||
|
|
fdd8f7e743 | ||
|
|
bb852600c0 | ||
|
|
210bbcbdbf | ||
|
|
5910dbf0d3 | ||
|
|
90468ffe48 | ||
|
|
863c4dfa34 | ||
|
|
484be8442c | ||
|
|
7393e3bcb7 | ||
|
|
32a84b7b19 | ||
|
|
6933e82d46 | ||
|
|
fb1801ce11 | ||
|
|
09abb23968 | ||
|
|
eb1e0d3bf5 | ||
|
|
3b6c103618 | ||
|
|
feccc0fca7 | ||
|
|
51bcb5a2d2 | ||
|
|
7a184a8bbc | ||
|
|
5043edfd4e |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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: 16
|
node-version: 18
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run jslint
|
- run: npm run jslint
|
||||||
- run: docker pull drachtio/sipp
|
- run: docker pull drachtio/sipp
|
||||||
|
|||||||
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -2,6 +2,8 @@ name: Docker
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ jobs:
|
|||||||
- name: prepare tag
|
- name: prepare tag
|
||||||
id: prepare_tag
|
id: prepare_tag
|
||||||
run: |
|
run: |
|
||||||
IMAGE_ID=$GITHUB_REPOSITORY
|
IMAGE_ID=jambonz/feature-server
|
||||||
|
|
||||||
# Strip git ref prefix from version
|
# Strip git ref prefix from version
|
||||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||||
|
|||||||
9
app.js
9
app.js
@@ -20,7 +20,9 @@ const tracer = require('./tracer')(JAMBONES_OTEL_SERVICE_NAME);
|
|||||||
const api = require('@opentelemetry/api');
|
const api = require('@opentelemetry/api');
|
||||||
srf.locals = {...srf.locals, otel: {tracer, api}};
|
srf.locals = {...srf.locals, otel: {tracer, api}};
|
||||||
|
|
||||||
const opts = {level: JAMBONES_LOGLEVEL};
|
const opts = {
|
||||||
|
level: JAMBONES_LOGLEVEL
|
||||||
|
};
|
||||||
const pino = require('pino');
|
const pino = require('pino');
|
||||||
const logger = pino(opts, pino.destination({sync: false}));
|
const logger = pino(opts, pino.destination({sync: false}));
|
||||||
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants');
|
const {LifeCycleEvents, FS_UUID_SET_NAME} = require('./lib/utils/constants');
|
||||||
@@ -118,10 +120,15 @@ function handle(signal) {
|
|||||||
srf.locals.disabled = true;
|
srf.locals.disabled = true;
|
||||||
logger.info(`got signal ${signal}`);
|
logger.info(`got signal ${signal}`);
|
||||||
const setName = `${(JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
const setName = `${(JAMBONES_CLUSTER_ID || 'default')}:active-fs`;
|
||||||
|
const fsServiceUrlSetName = `${(JAMBONES_CLUSTER_ID || 'default')}:fs-service-url`;
|
||||||
if (setName && srf.locals.localSipAddress) {
|
if (setName && srf.locals.localSipAddress) {
|
||||||
logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`);
|
logger.info(`got signal ${signal}, removing ${srf.locals.localSipAddress} from set ${setName}`);
|
||||||
removeFromSet(setName, srf.locals.localSipAddress);
|
removeFromSet(setName, srf.locals.localSipAddress);
|
||||||
}
|
}
|
||||||
|
if (fsServiceUrlSetName && srf.locals.serviceUrl) {
|
||||||
|
logger.info(`got signal ${signal}, removing ${srf.locals.serviceUrl} from set ${fsServiceUrlSetName}`);
|
||||||
|
removeFromSet(fsServiceUrlSetName, srf.locals.serviceUrl);
|
||||||
|
}
|
||||||
removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID);
|
removeFromSet(FS_UUID_SET_NAME, srf.locals.fsUUID);
|
||||||
if (K8S) {
|
if (K8S) {
|
||||||
srf.locals.lifecycleEmitter.operationalState = LifeCycleEvents.ScaleIn;
|
srf.locals.lifecycleEmitter.operationalState = LifeCycleEvents.ScaleIn;
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ const checkEnvs = () => {
|
|||||||
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
|
assert.ok(process.env.DRACHTIO_PORT || process.env.DRACHTIO_HOST, 'missing DRACHTIO_PORT env var');
|
||||||
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
|
assert.ok(process.env.DRACHTIO_SECRET, 'missing DRACHTIO_SECRET env var');
|
||||||
assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
|
assert.ok(process.env.JAMBONES_FREESWITCH, 'missing JAMBONES_FREESWITCH env var');
|
||||||
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
if (process.env.JAMBONES_REDIS_SENTINELS) {
|
||||||
|
assert.ok(process.env.JAMBONES_REDIS_SENTINEL_MASTER_NAME,
|
||||||
|
'missing JAMBONES_REDIS_SENTINEL_MASTER_NAME env var, JAMBONES_REDIS_SENTINEL_PASSWORD env var is optional');
|
||||||
|
} else {
|
||||||
|
assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var');
|
||||||
|
}
|
||||||
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var');
|
assert.ok(process.env.JAMBONES_NETWORK_CIDR || process.env.K8S, 'missing JAMBONES_SUBNET env var');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,7 +25,7 @@ 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 = process.env.JAMBONES_MYSQL_REFRESH_TTL;
|
const JAMBONES_MYSQL_REFRESH_TTL = parseInt(process.env.JAMBONES_MYSQL_REFRESH_TTL, 10) || 0;
|
||||||
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 */
|
/* redis */
|
||||||
@@ -51,7 +56,7 @@ const JAMBONES_SBCS = process.env.JAMBONES_SBCS;
|
|||||||
/* websockets */
|
/* websockets */
|
||||||
const JAMBONES_WS_HANDSHAKE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS, 10) || 1500;
|
const JAMBONES_WS_HANDSHAKE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_HANDSHAKE_TIMEOUT_MS, 10) || 1500;
|
||||||
const JAMBONES_WS_MAX_PAYLOAD = parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD, 10) || 24 * 1024;
|
const JAMBONES_WS_MAX_PAYLOAD = parseInt(process.env.JAMBONES_WS_MAX_PAYLOAD, 10) || 24 * 1024;
|
||||||
|
const JAMBONES_WS_PING_INTERVAL_MS = parseInt(process.env.JAMBONES_WS_PING_INTERVAL_MS, 10) || 0;
|
||||||
const MAX_RECONNECTS = 5;
|
const MAX_RECONNECTS = 5;
|
||||||
const RESPONSE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT, 10) || 5000;
|
const RESPONSE_TIMEOUT_MS = parseInt(process.env.JAMBONES_WS_API_MSG_RESPONSE_TIMEOUT, 10) || 5000;
|
||||||
|
|
||||||
@@ -119,6 +124,31 @@ const HTTP_TIMEOUT = 10000;
|
|||||||
|
|
||||||
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 JAMBONES_REDIS_SENTINELS = process.env.JAMBONES_REDIS_SENTINELS ? {
|
||||||
|
sentinels: process.env.JAMBONES_REDIS_SENTINELS.split(',').map((sentinel) => {
|
||||||
|
let host, port = 26379;
|
||||||
|
if (sentinel.includes(':')) {
|
||||||
|
const arr = sentinel.split(':');
|
||||||
|
host = arr[0];
|
||||||
|
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;
|
||||||
|
const JAMBONZ_DISABLE_DIAL_PAI_HEADER = process.env.JAMBONZ_DISABLE_DIAL_PAI_HEADER || false;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
JAMBONES_MYSQL_HOST,
|
JAMBONES_MYSQL_HOST,
|
||||||
JAMBONES_MYSQL_USER,
|
JAMBONES_MYSQL_USER,
|
||||||
@@ -138,6 +168,7 @@ module.exports = {
|
|||||||
JAMBONES_FREESWITCH,
|
JAMBONES_FREESWITCH,
|
||||||
JAMBONES_REDIS_HOST,
|
JAMBONES_REDIS_HOST,
|
||||||
JAMBONES_REDIS_PORT,
|
JAMBONES_REDIS_PORT,
|
||||||
|
JAMBONES_REDIS_SENTINELS,
|
||||||
SMPP_URL,
|
SMPP_URL,
|
||||||
JAMBONES_NETWORK_CIDR,
|
JAMBONES_NETWORK_CIDR,
|
||||||
JAMBONES_API_BASE_URL,
|
JAMBONES_API_BASE_URL,
|
||||||
@@ -186,10 +217,15 @@ module.exports = {
|
|||||||
RESPONSE_TIMEOUT_MS,
|
RESPONSE_TIMEOUT_MS,
|
||||||
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
|
JAMBONES_WS_HANDSHAKE_TIMEOUT_MS,
|
||||||
JAMBONES_WS_MAX_PAYLOAD,
|
JAMBONES_WS_MAX_PAYLOAD,
|
||||||
|
JAMBONES_WS_PING_INTERVAL_MS,
|
||||||
MAX_RECONNECTS,
|
MAX_RECONNECTS,
|
||||||
GCP_JSON_KEY,
|
GCP_JSON_KEY,
|
||||||
MICROSOFT_REGION,
|
MICROSOFT_REGION,
|
||||||
MICROSOFT_API_KEY,
|
MICROSOFT_API_KEY,
|
||||||
SONIOX_API_KEY,
|
SONIOX_API_KEY,
|
||||||
DEEPGRAM_API_KEY
|
DEEPGRAM_API_KEY,
|
||||||
|
JAMBONZ_RECORD_WS_BASE_URL,
|
||||||
|
JAMBONZ_RECORD_WS_USERNAME,
|
||||||
|
JAMBONZ_RECORD_WS_PASSWORD,
|
||||||
|
JAMBONZ_DISABLE_DIAL_PAI_HEADER
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,8 +19,16 @@ router.post('/', async(req, res) => {
|
|||||||
logger.debug({body: req.body}, 'got createCall request');
|
logger.debug({body: req.body}, 'got createCall request');
|
||||||
try {
|
try {
|
||||||
let uri, cs, to;
|
let uri, cs, to;
|
||||||
|
// app_json is creaeted by only api-server.
|
||||||
|
// if it available, take it and delete before creating task
|
||||||
|
const app_json = req.body.app_json;
|
||||||
|
delete req.body.app_json;
|
||||||
const restDial = makeTask(logger, {'rest:dial': req.body});
|
const restDial = makeTask(logger, {'rest:dial': req.body});
|
||||||
const {lookupAccountDetails} = dbUtils(logger, srf);
|
restDial.appJson = app_json;
|
||||||
|
const {lookupAccountDetails, lookupCarrierByPhoneNumber, lookupCarrier} = dbUtils(logger, srf);
|
||||||
|
const {
|
||||||
|
lookupAppBySid
|
||||||
|
} = srf.locals.dbHelpers;
|
||||||
const {getSBC, getFreeswitch} = srf.locals;
|
const {getSBC, getFreeswitch} = srf.locals;
|
||||||
const sbcAddress = getSBC();
|
const sbcAddress = getSBC();
|
||||||
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
|
if (!sbcAddress) throw new Error('no available SBCs for outbound call creation');
|
||||||
@@ -36,6 +44,14 @@ router.post('/', async(req, res) => {
|
|||||||
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 record_all_calls = account.record_all_calls || (application && application.record_all_calls);
|
||||||
|
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,
|
||||||
@@ -43,7 +59,10 @@ router.post('/', async(req, res) => {
|
|||||||
'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,
|
||||||
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost})
|
'X-Trace-ID': rootSpan.traceId,
|
||||||
|
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
|
||||||
|
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
|
||||||
|
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat})
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (target.type) {
|
switch (target.type) {
|
||||||
@@ -77,7 +96,6 @@ router.post('/', async(req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (target.type === 'phone' && target.trunk) {
|
if (target.type === 'phone' && target.trunk) {
|
||||||
const {lookupCarrier} = dbUtils(this.logger, srf);
|
|
||||||
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
const voip_carrier_sid = await lookupCarrier(req.body.account_sid, target.trunk);
|
||||||
logger.info(
|
logger.info(
|
||||||
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
|
`createCall: selected ${voip_carrier_sid} for requested carrier: ${target.trunk || 'unspecified'})`);
|
||||||
@@ -91,10 +109,11 @@ router.post('/', async(req, res) => {
|
|||||||
* 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 {lookupCarrierByPhoneNumber} = dbUtils(this.logger, srf);
|
const str = restDial.from || '';
|
||||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, restDial.from);
|
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||||
|
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber);
|
||||||
logger.info(
|
logger.info(
|
||||||
`createCall: selected ${voip_carrier_sid} for requested phone number: ${restDial.from || '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;
|
||||||
}
|
}
|
||||||
@@ -181,7 +200,6 @@ router.post('/', async(req, res) => {
|
|||||||
/* ok our outbound INVITE is in flight */
|
/* ok our outbound INVITE is in flight */
|
||||||
|
|
||||||
const tasks = [restDial];
|
const tasks = [restDial];
|
||||||
const rootSpan = new RootSpan('rest-call', inviteReq);
|
|
||||||
sipLogger = logger.child({
|
sipLogger = logger.child({
|
||||||
callSid,
|
callSid,
|
||||||
callId: inviteReq.get('Call-ID'),
|
callId: inviteReq.get('Call-ID'),
|
||||||
@@ -245,6 +263,7 @@ router.post('/', async(req, res) => {
|
|||||||
sipStatus: err.status,
|
sipStatus: err.status,
|
||||||
sipReason: err.reason
|
sipReason: err.reason
|
||||||
});
|
});
|
||||||
|
cs.callGone = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (cs) cs.emit('callStatusChange', {
|
if (cs) cs.emit('callStatusChange', {
|
||||||
|
|||||||
@@ -9,25 +9,29 @@ 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('call_status_hook can be updated only when call_hook is also being updated');
|
throw new DbErrorBadRequest(
|
||||||
|
`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');
|
throw new DbErrorUnprocessableRequest(`call session is gone for call_sid ${callSid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
|
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
|
||||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
throw new DbErrorUnprocessableRequest(
|
||||||
|
`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('current call state is incompatible with requested action');
|
throw new DbErrorUnprocessableRequest(
|
||||||
|
`current call state is incompatible with requested action for call_sid ${callSid}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (cs.isInboundCallAnswered) {
|
if (cs.isInboundCallAnswered) {
|
||||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
throw new DbErrorUnprocessableRequest(
|
||||||
|
`current call state is incompatible with requested action for call_sid ${callSid}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ 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
|
||||||
} = require('./config');
|
} = require('./config');
|
||||||
|
|
||||||
module.exports = function(srf, logger) {
|
module.exports = function(srf, logger) {
|
||||||
@@ -299,22 +299,30 @@ 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_synthesis_label && {label: app.speech_synthesis_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;
|
||||||
@@ -322,6 +330,7 @@ module.exports = function(srf, logger) {
|
|||||||
const httpHeaders = b3 && { b3 };
|
const httpHeaders = b3 && { b3 };
|
||||||
json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
|
json = await app.requestor.request('session:new', app.call_hook, params, httpHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
app.tasks = normalizeJambones(logger, json).map((tdata) => makeTask(logger, tdata));
|
||||||
span?.setAttributes({
|
span?.setAttributes({
|
||||||
'http.statusCode': 200,
|
'http.statusCode': 200,
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ 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,
|
||||||
AWS_REGION
|
AWS_REGION,
|
||||||
|
JAMBONZ_RECORD_WS_BASE_URL,
|
||||||
|
JAMBONZ_RECORD_WS_USERNAME,
|
||||||
|
JAMBONZ_RECORD_WS_PASSWORD,
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
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';
|
||||||
@@ -64,6 +67,16 @@ class CallSession extends Emitter {
|
|||||||
this.notifiedComplete = false;
|
this.notifiedComplete = false;
|
||||||
this.rootSpan = rootSpan;
|
this.rootSpan = rootSpan;
|
||||||
|
|
||||||
|
this._origRecognizerSettings = {
|
||||||
|
vendor: this.application?.speech_recognizer_vendor,
|
||||||
|
language: this.application?.speech_recognizer_language,
|
||||||
|
};
|
||||||
|
this._origSynthesizerSettings = {
|
||||||
|
vendor: this.application?.speech_synthesis_vendor,
|
||||||
|
language: this.application?.speech_synthesis_language,
|
||||||
|
voice: this.application?.speech_synthesis_voice,
|
||||||
|
};
|
||||||
|
|
||||||
assert(rootSpan);
|
assert(rootSpan);
|
||||||
|
|
||||||
this._recordState = RecordState.RecordingOff;
|
this._recordState = RecordState.RecordingOff;
|
||||||
@@ -122,6 +135,11 @@ class CallSession extends Emitter {
|
|||||||
return this.callInfo.callStatus;
|
return this.callInfo.callStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isBackGroundListen() {
|
||||||
|
return !(this.backgroundListenTask === null ||
|
||||||
|
this.backgroundListenTask === undefined);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SIP call-id for the call
|
* SIP call-id for the call
|
||||||
*/
|
*/
|
||||||
@@ -161,6 +179,30 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -170,6 +212,13 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -180,6 +229,13 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -189,6 +245,29 @@ 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;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
*/
|
*/
|
||||||
@@ -199,6 +278,13 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -320,10 +406,34 @@ class CallSession extends Emitter {
|
|||||||
return this._globalSttPunctuation;
|
return this._globalSttPunctuation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get onHoldMusic() {
|
||||||
|
return this._onHoldMusic;
|
||||||
|
}
|
||||||
|
|
||||||
|
set onHoldMusic(url) {
|
||||||
|
this._onHoldMusic = url;
|
||||||
|
}
|
||||||
|
|
||||||
hasGlobalSttPunctuation() {
|
hasGlobalSttPunctuation() {
|
||||||
return this._globalSttPunctuation !== undefined;
|
return this._globalSttPunctuation !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetRecognizer() {
|
||||||
|
this._globalSttHints = undefined;
|
||||||
|
this._globalSttPunctuation = undefined;
|
||||||
|
this._globalAltLanguages = undefined;
|
||||||
|
this.isContinuousAsr = false;
|
||||||
|
this.asrDtmfTerminationDigits = undefined;
|
||||||
|
this.speechRecognizerLanguage = this._origRecognizerSettings.language;
|
||||||
|
this.speechRecognizerVendor = this._origRecognizerSettings.vendor;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSynthesizer() {
|
||||||
|
this.speechSynthesisLanguage = this._origSynthesizerSettings.language;
|
||||||
|
this.speechSynthesisVendor = this._origSynthesizerSettings.vendor;
|
||||||
|
this.speechSynthesisVoice = this._origSynthesizerSettings.voice;
|
||||||
|
}
|
||||||
|
|
||||||
async notifyRecordOptions(opts) {
|
async notifyRecordOptions(opts) {
|
||||||
const {action} = opts;
|
const {action} = opts;
|
||||||
this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
|
this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
|
||||||
@@ -389,7 +499,10 @@ class CallSession extends Emitter {
|
|||||||
'X-Call-Sid': this.callSid,
|
'X-Call-Sid': this.callSid,
|
||||||
'X-Account-Sid': this.accountSid,
|
'X-Account-Sid': this.accountSid,
|
||||||
'X-Application-Sid': this.applicationSid,
|
'X-Application-Sid': this.applicationSid,
|
||||||
}
|
...(this.recordOptions.headers && {'Content-Type': 'application/json'})
|
||||||
|
},
|
||||||
|
// Siprect Client is initiated from startCallRecording, so just need to pass custom headers in startRecording
|
||||||
|
...(this.recordOptions.headers && {body: JSON.stringify(this.recordOptions.headers) + '\n'})
|
||||||
});
|
});
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
this._recordState = RecordState.RecordingOn;
|
this._recordState = RecordState.RecordingOn;
|
||||||
@@ -410,7 +523,7 @@ class CallSession extends Emitter {
|
|||||||
const res = await this.dlg.request({
|
const res = await this.dlg.request({
|
||||||
method: 'INFO',
|
method: 'INFO',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Reason': 'stopCallRecording',
|
'X-Reason': 'stopCallRecording'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
@@ -432,7 +545,7 @@ class CallSession extends Emitter {
|
|||||||
const res = await this.dlg.request({
|
const res = await this.dlg.request({
|
||||||
method: 'INFO',
|
method: 'INFO',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Reason': 'pauseCallRecording',
|
'X-Reason': 'pauseCallRecording'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
@@ -454,7 +567,7 @@ class CallSession extends Emitter {
|
|||||||
const res = await this.dlg.request({
|
const res = await this.dlg.request({
|
||||||
method: 'INFO',
|
method: 'INFO',
|
||||||
headers: {
|
headers: {
|
||||||
'X-Reason': 'resumeCallRecording',
|
'X-Reason': 'resumeCallRecording'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
@@ -469,7 +582,7 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startBackgroundListen(opts) {
|
async startBackgroundListen(opts, bugname) {
|
||||||
if (this.isListenEnabled) {
|
if (this.isListenEnabled) {
|
||||||
this.logger.info('CallSession:startBackgroundListen - listen is already enabled, ignoring request');
|
this.logger.info('CallSession:startBackgroundListen - listen is already enabled, ignoring request');
|
||||||
return;
|
return;
|
||||||
@@ -478,8 +591,11 @@ class CallSession extends Emitter {
|
|||||||
this.logger.debug({opts}, 'CallSession:startBackgroundListen');
|
this.logger.debug({opts}, 'CallSession:startBackgroundListen');
|
||||||
const t = normalizeJambones(this.logger, [opts]);
|
const t = normalizeJambones(this.logger, [opts]);
|
||||||
this.backgroundListenTask = makeTask(this.logger, t[0]);
|
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 resources = await this._evaluatePreconditions(this.backgroundListenTask);
|
||||||
const {span, ctx} = this.rootSpan.startChildSpan(`background-gather:${this.backgroundListenTask.summary}`);
|
const {span, ctx} = this.rootSpan.startChildSpan(`background-listen:${this.backgroundListenTask.summary}`);
|
||||||
this.backgroundListenTask.span = span;
|
this.backgroundListenTask.span = span;
|
||||||
this.backgroundListenTask.ctx = ctx;
|
this.backgroundListenTask.ctx = ctx;
|
||||||
this.backgroundListenTask.exec(this, resources)
|
this.backgroundListenTask.exec(this, resources)
|
||||||
@@ -502,6 +618,7 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async stopBackgroundListen() {
|
async stopBackgroundListen() {
|
||||||
|
this.logger.debug('CallSession:stopBackgroundListen');
|
||||||
try {
|
try {
|
||||||
if (this.backgroundListenTask) {
|
if (this.backgroundListenTask) {
|
||||||
this.backgroundListenTask.removeAllListeners();
|
this.backgroundListenTask.removeAllListeners();
|
||||||
@@ -510,7 +627,6 @@ class CallSession extends Emitter {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err}, 'CallSession:stopBackgroundListen - Error stopping listen task');
|
this.logger.info({err}, 'CallSession:stopBackgroundListen - Error stopping listen task');
|
||||||
}
|
}
|
||||||
this.backgroundListenTask = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async enableBotMode(gather, autoEnable) {
|
async enableBotMode(gather, autoEnable) {
|
||||||
@@ -600,15 +716,28 @@ 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) {
|
getSpeechCredentials(vendor, type, label = null) {
|
||||||
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) {
|
||||||
const credential = this.accountInfo.speech.find((s) => s.vendor === vendor);
|
// firstly check if account level has expected credential
|
||||||
|
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 {
|
||||||
@@ -640,8 +769,10 @@ 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) {
|
||||||
@@ -680,6 +811,18 @@ class CallSession extends Emitter {
|
|||||||
stt_region: credential.stt_region
|
stt_region: credential.stt_region
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else if ('nvidia' === vendor) {
|
||||||
|
return {
|
||||||
|
speech_credential_sid: credential.speech_credential_sid,
|
||||||
|
riva_server_uri: credential.riva_server_uri
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('cobalt' === vendor) {
|
||||||
|
return {
|
||||||
|
speech_credential_sid: credential.speech_credential_sid,
|
||||||
|
cobalt_server_uri: credential.cobalt_server_uri
|
||||||
|
};
|
||||||
|
}
|
||||||
else if (vendor.startsWith('custom:')) {
|
else if (vendor.startsWith('custom:')) {
|
||||||
return {
|
return {
|
||||||
speech_credential_sid: credential.speech_credential_sid,
|
speech_credential_sid: credential.speech_credential_sid,
|
||||||
@@ -729,6 +872,7 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
if (!skip) {
|
if (!skip) {
|
||||||
const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`);
|
const {span, ctx} = this.rootSpan.startChildSpan(`verb:${task.summary}`);
|
||||||
|
span.setAttributes({'verb.summary': task.summary});
|
||||||
task.span = span;
|
task.span = span;
|
||||||
task.ctx = ctx;
|
task.ctx = ctx;
|
||||||
await task.exec(this, resources);
|
await task.exec(this, resources);
|
||||||
@@ -749,20 +893,15 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (0 === this.tasks.length && this.requestor instanceof WsRequestor && !this.callGone) {
|
if (0 === this.tasks.length &&
|
||||||
let span;
|
this.requestor instanceof WsRequestor &&
|
||||||
|
!this.requestor.closedGracefully &&
|
||||||
|
!this.callGone
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const {span} = this.rootSpan.startChildSpan('waiting for commands');
|
await this._awaitCommandsOrHangup();
|
||||||
const {reason, queue, command} = await this._awaitCommandsOrHangup();
|
|
||||||
span.setAttributes({
|
|
||||||
'completion.reason': reason,
|
|
||||||
'async.request.queue': queue,
|
|
||||||
'async.request.command': command
|
|
||||||
});
|
|
||||||
span.end();
|
|
||||||
if (this.callGone) break;
|
if (this.callGone) break;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
span.end();
|
|
||||||
this.logger.info(err, 'CallSession:exec - error waiting for new commands');
|
this.logger.info(err, 'CallSession:exec - error waiting for new commands');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -942,6 +1081,24 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -1060,6 +1217,9 @@ class CallSession extends Emitter {
|
|||||||
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');
|
||||||
}
|
}
|
||||||
@@ -1236,6 +1396,10 @@ class CallSession extends Emitter {
|
|||||||
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;
|
||||||
@@ -1255,15 +1419,17 @@ class CallSession extends Emitter {
|
|||||||
this.wakeupResolver(resolution);
|
this.wakeupResolver(resolution);
|
||||||
this.wakeupResolver = null;
|
this.wakeupResolver = null;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
else {
|
else {
|
||||||
const {span} = this.rootSpan.startChildSpan('async command');
|
|
||||||
const {queue, command} = resolution;
|
const {queue, command} = resolution;
|
||||||
|
const {span} = this.rootSpan.startChildSpan(`recv cmd: ${command}`);
|
||||||
span.setAttributes({
|
span.setAttributes({
|
||||||
'async.request.queue': queue,
|
'async.request.queue': queue,
|
||||||
'async.request.command': command
|
'async.request.command': command
|
||||||
});
|
});
|
||||||
span.end();
|
span.end();
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWsConnectionDropped() {
|
_onWsConnectionDropped() {
|
||||||
@@ -1303,7 +1469,10 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we are going from an early media connection to answer
|
// we are going from an early media connection to answer
|
||||||
await this.propagateAnswer();
|
if (this.direction === CallDirection.Inbound) {
|
||||||
|
// only do this for inbound call.
|
||||||
|
await this.propagateAnswer();
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...resources,
|
...resources,
|
||||||
...(this.isSipRecCallSession && {ep2: this.ep2})
|
...(this.isSipRecCallSession && {ep2: this.ep2})
|
||||||
@@ -1323,6 +1492,8 @@ class CallSession extends Emitter {
|
|||||||
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}`);
|
||||||
});
|
});
|
||||||
@@ -1393,6 +1564,7 @@ class CallSession extends Emitter {
|
|||||||
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');
|
||||||
@@ -1474,7 +1646,6 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
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));
|
||||||
|
|
||||||
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
|
this.logger.debug(`CallSession:propagateAnswer - answered callSid ${this.callSid}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1487,9 +1658,14 @@ class CallSession extends Emitter {
|
|||||||
res.send(200, {body: this.ep.local.sdp});
|
res.send(200, {body: this.ep.local.sdp});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const newSdp = await this.ep.modify(req.body);
|
if (this.currentTask.name === TaskName.Dial && this.currentTask.isOnHold) {
|
||||||
res.send(200, {body: newSdp});
|
this.logger.info('onholdMusic reINVITE after media has been released');
|
||||||
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
|
await this.currentTask.handleReinviteAfterMediaReleased(req, res);
|
||||||
|
} 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) {
|
||||||
@@ -1536,6 +1712,7 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
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};
|
||||||
}
|
}
|
||||||
@@ -1549,7 +1726,7 @@ class CallSession extends Emitter {
|
|||||||
const pp = this._pool.promise();
|
const pp = this._pool.promise();
|
||||||
try {
|
try {
|
||||||
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: looking up account');
|
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: looking up account');
|
||||||
const [r] = await pp.query(sqlRetrieveQueueEventHook, this.accountSid);
|
const [r] = await pp.query(sqlRetrieveQueueEventHook, [this.accountSid]);
|
||||||
if (0 === r.length) {
|
if (0 === r.length) {
|
||||||
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: no webhook provisioned');
|
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: no webhook provisioned');
|
||||||
this.queueEventHookRequestor = null;
|
this.queueEventHookRequestor = null;
|
||||||
@@ -1690,6 +1867,7 @@ class CallSession extends Emitter {
|
|||||||
'X-Reason': 'anchor-media'
|
'X-Reason': 'anchor-media'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this._configMsEndpoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleReinviteAfterMediaReleased(req, res) {
|
async handleReinviteAfterMediaReleased(req, res) {
|
||||||
@@ -1711,6 +1889,14 @@ class CallSession extends Emitter {
|
|||||||
async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
async _notifyCallStatusChange({callStatus, sipStatus, sipReason, duration}) {
|
||||||
if (this.callMoved) return;
|
if (this.callMoved) return;
|
||||||
|
|
||||||
|
if (callStatus === CallStatus.InProgress) {
|
||||||
|
// nice, call is in progress, good time to enable record
|
||||||
|
await this.enableRecordAllCall();
|
||||||
|
} else if (callStatus == CallStatus.Completed && this.isBackGroundListen) {
|
||||||
|
this.stopBackgroundListen().catch((err) => this.logger.error(
|
||||||
|
{err}, 'CallSession:_notifyCallStatusChange - error stopping background listen'));
|
||||||
|
}
|
||||||
|
|
||||||
/* race condition: we hang up at the same time as the caller */
|
/* race condition: we hang up at the same time as the caller */
|
||||||
if (callStatus === CallStatus.Completed) {
|
if (callStatus === CallStatus.Completed) {
|
||||||
if (this.notifiedComplete) return;
|
if (this.notifiedComplete) return;
|
||||||
@@ -1723,6 +1909,15 @@ class CallSession extends Emitter {
|
|||||||
|
|
||||||
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
|
this.callInfo.updateCallStatus(callStatus, sipStatus, sipReason);
|
||||||
if (typeof duration === 'number') this.callInfo.duration = duration;
|
if (typeof duration === 'number') this.callInfo.duration = duration;
|
||||||
|
this.executeStatusCallback(callStatus, sipStatus);
|
||||||
|
|
||||||
|
// update calls db
|
||||||
|
//this.logger.debug(`updating redis with ${JSON.stringify(this.callInfo)}`);
|
||||||
|
this.updateCallStatus(Object.assign({}, this.callInfo.toJSON()), this.serviceUrl)
|
||||||
|
.catch((err) => this.logger.error(err, 'redis error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeStatusCallback(callStatus, sipStatus) {
|
||||||
const {span} = this.rootSpan.startChildSpan(`call-status:${this.callInfo.callStatus}`);
|
const {span} = this.rootSpan.startChildSpan(`call-status:${this.callInfo.callStatus}`);
|
||||||
span.setAttributes(this.callInfo.toJSON());
|
span.setAttributes(this.callInfo.toJSON());
|
||||||
try {
|
try {
|
||||||
@@ -1734,11 +1929,30 @@ class CallSession extends Emitter {
|
|||||||
span.end();
|
span.end();
|
||||||
this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
this.logger.info(err, `CallSession:_notifyCallStatusChange error sending ${callStatus} ${sipStatus}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// update calls db
|
async enableRecordAllCall() {
|
||||||
//this.logger.debug(`updating redis with ${JSON.stringify(this.callInfo)}`);
|
if (this.accountInfo.account.record_all_calls || this.application.record_all_calls) {
|
||||||
this.updateCallStatus(Object.assign({}, this.callInfo.toJSON()), this.serviceUrl)
|
const listenOpts = {
|
||||||
.catch((err) => this.logger.error(err, 'redis error'));
|
url: `${JAMBONZ_RECORD_WS_BASE_URL}/record/${this.accountInfo.account.bucket_credential.vendor}`,
|
||||||
|
wsAuth: {
|
||||||
|
username: JAMBONZ_RECORD_WS_USERNAME,
|
||||||
|
password: JAMBONZ_RECORD_WS_PASSWORD
|
||||||
|
},
|
||||||
|
disableBidirectionalAudio: true,
|
||||||
|
mixType : 'stereo',
|
||||||
|
passDtmf: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.debug({listenOpts}, 'Record all calls: enabling listen');
|
||||||
|
await this.startBackgroundListen({verb: 'listen', ...listenOpts}, 'jambonz-session-record');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_configMsEndpoint() {
|
||||||
|
if (this.onHoldMusic) {
|
||||||
|
this.ep.set({hold_music: `shout://${this.onHoldMusic.replace(/^https?:\/\//, '')}`});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ class RestCallSession extends CallSession {
|
|||||||
});
|
});
|
||||||
this.req = req;
|
this.req = req;
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
|
// keep restDialTask reference for closing AMD
|
||||||
|
if (tasks.length) {
|
||||||
|
this.restDialTask = tasks[0];
|
||||||
|
}
|
||||||
|
|
||||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||||
this._notifyCallStatusChange({
|
this._notifyCallStatusChange({
|
||||||
@@ -44,6 +48,9 @@ class RestCallSession extends CallSession {
|
|||||||
* This is invoked when the called party hangs up, in order to calculate the call duration.
|
* This is invoked when the called party hangs up, in order to calculate the call duration.
|
||||||
*/
|
*/
|
||||||
_callerHungup() {
|
_callerHungup() {
|
||||||
|
if (this.restDialTask) {
|
||||||
|
this.restDialTask.turnOffAmd();
|
||||||
|
}
|
||||||
this.callInfo.callTerminationBy = 'caller';
|
this.callInfo.callTerminationBy = 'caller';
|
||||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||||
|
|||||||
@@ -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'
|
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook', 'endConferenceDuration'
|
||||||
].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 = [];
|
||||||
@@ -114,7 +114,12 @@ class Conference extends Task {
|
|||||||
}
|
}
|
||||||
this.emitter.emit('kill');
|
this.emitter.emit('kill');
|
||||||
await this._doFinalMemberCheck(cs);
|
await this._doFinalMemberCheck(cs);
|
||||||
if (this.ep && this.ep.connected) this.ep.conn.removeAllListeners('esl::event::CUSTOM::*') ;
|
if (this.ep && this.ep.connected) {
|
||||||
|
this.ep.conn.removeAllListeners('esl::event::CUSTOM::*');
|
||||||
|
this.ep.api(`conference ${this.confName} kick ${this.memberId}`)
|
||||||
|
.catch((err) => this.logger.info({err}, 'Error kicking participant'));
|
||||||
|
}
|
||||||
|
cs.clearConferenceDetails();
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,9 +344,13 @@ class Conference extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const opts = {};
|
const opts = {};
|
||||||
if (this.endConferenceOnExit) Object.assign(opts, {flags: {endconf: true}});
|
if (this.endConferenceOnExit || this.startConferenceOnEnter || this.joinMuted) {
|
||||||
if (this.startConferenceOnEnter) Object.assign(opts, {flags: {moderator: true}});
|
Object.assign(opts, {flags: {
|
||||||
if (this.joinMuted) Object.assign(opts, {flags: {mute: true}});
|
...(this.endConferenceOnExit && {endconf: 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);
|
||||||
@@ -384,6 +393,11 @@ 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}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,10 +30,18 @@ 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.data.reset) {
|
||||||
|
if (typeof this.data.reset === 'string') this.data.reset = [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.record?.action || this.listen?.url || this.data.amd) ?
|
this.preconditions = (this.bargeIn.enable || this.record?.action || this.listen?.url || this.data.amd) ?
|
||||||
TaskPreconditions.Endpoint :
|
TaskPreconditions.Endpoint :
|
||||||
TaskPreconditions.None;
|
TaskPreconditions.None;
|
||||||
|
|
||||||
|
this.onHoldMusic = this.data.onHoldMusic;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Config; }
|
get name() { return TaskName.Config; }
|
||||||
@@ -45,6 +53,10 @@ class TaskConfig extends Task {
|
|||||||
|
|
||||||
get summary() {
|
get summary() {
|
||||||
const phrase = [];
|
const phrase = [];
|
||||||
|
|
||||||
|
/* reset recognizer and/or synthesizer to default values? */
|
||||||
|
if (this.data.reset.length) phrase.push(`reset ${this.data.reset.join(',')}`);
|
||||||
|
|
||||||
if (this.bargeIn.enable) phrase.push('enable barge-in');
|
if (this.bargeIn.enable) phrase.push('enable barge-in');
|
||||||
if (this.hasSynthesizer) {
|
if (this.hasSynthesizer) {
|
||||||
const {vendor:v, language:l, voice} = this.synthesizer;
|
const {vendor:v, language:l, voice} = this.synthesizer;
|
||||||
@@ -62,7 +74,8 @@ class TaskConfig extends Task {
|
|||||||
}
|
}
|
||||||
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'}`);
|
||||||
return `${this.name}{${phrase.join(',')}`;
|
if (this.onHoldMusic) phrase.push(`onHoldMusic: ${this.onHoldMusic}`);
|
||||||
|
return `${this.name}{${phrase.join(',')}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async exec(cs, {ep} = {}) {
|
async exec(cs, {ep} = {}) {
|
||||||
@@ -73,6 +86,10 @@ 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;
|
||||||
@@ -86,25 +103,62 @@ class TaskConfig extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.data.reset.forEach((k) => {
|
||||||
|
if (k === 'synthesizer') cs.resetSynthesizer();
|
||||||
|
else if (k === 'recognizer') cs.resetRecognizer();
|
||||||
|
});
|
||||||
|
|
||||||
if (this.hasSynthesizer) {
|
if (this.hasSynthesizer) {
|
||||||
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.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;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class TaskDequeue extends Task {
|
|||||||
this.queueName = this.data.name;
|
this.queueName = this.data.name;
|
||||||
this.timeout = this.data.timeout || 0;
|
this.timeout = this.data.timeout || 0;
|
||||||
this.beep = this.data.beep === true;
|
this.beep = this.data.beep === true;
|
||||||
|
this.callSid = this.data.callSid;
|
||||||
|
|
||||||
this.emitter = new Emitter();
|
this.emitter = new Emitter();
|
||||||
this.state = DequeueResults.Timeout;
|
this.state = DequeueResults.Timeout;
|
||||||
@@ -53,7 +54,7 @@ class TaskDequeue extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getMemberFromQueue(cs) {
|
_getMemberFromQueue(cs) {
|
||||||
const {popFront} = cs.srf.locals.dbHelpers;
|
const {retrieveFromSortedSet, retrieveByPatternSortedSet} = cs.srf.locals.dbHelpers;
|
||||||
|
|
||||||
return new Promise(async(resolve) => {
|
return new Promise(async(resolve) => {
|
||||||
let timer;
|
let timer;
|
||||||
@@ -70,7 +71,12 @@ class TaskDequeue extends Task {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
try {
|
try {
|
||||||
const url = await popFront(this.queueName);
|
let url;
|
||||||
|
if (this.callSid) {
|
||||||
|
url = await retrieveByPatternSortedSet(this.queueName, `*${this.callSid}`);
|
||||||
|
} else {
|
||||||
|
url = await retrieveFromSortedSet(this.queueName);
|
||||||
|
}
|
||||||
if (url) {
|
if (url) {
|
||||||
found = true;
|
found = true;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
@@ -78,7 +84,7 @@ class TaskDequeue extends Task {
|
|||||||
resolve(url);
|
resolve(url);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.debug({err}, 'TaskDequeue:_getMemberFromQueue error popFront');
|
this.logger.debug({err}, 'TaskDequeue:_getMemberFromQueue error Sorted Set');
|
||||||
}
|
}
|
||||||
await sleepFor(5000);
|
await sleepFor(5000);
|
||||||
} while (!this.killed && !timedout && !found);
|
} while (!this.killed && !timedout && !found);
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ 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} = require('../config');
|
const {ANCHOR_MEDIA_ALWAYS, JAMBONZ_DISABLE_DIAL_PAI_HEADER} = require('../config');
|
||||||
|
const { isOnhold } = require('../utils/sdp-utils');
|
||||||
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
|
|
||||||
function parseDtmfOptions(logger, dtmfCapture) {
|
function parseDtmfOptions(logger, dtmfCapture) {
|
||||||
let parentDtmfCollector, childDtmfCollector;
|
let parentDtmfCollector, childDtmfCollector;
|
||||||
@@ -135,8 +138,13 @@ 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 ||
|
||||||
ANCHOR_MEDIA_ALWAYS ||
|
ANCHOR_MEDIA_ALWAYS ||
|
||||||
this.listenTask ||
|
this.listenTask ||
|
||||||
this.transcribeTask ||
|
this.transcribeTask ||
|
||||||
@@ -166,6 +174,16 @@ class TaskDial extends Task {
|
|||||||
async exec(cs) {
|
async exec(cs) {
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
try {
|
try {
|
||||||
|
if (this.listenTask) {
|
||||||
|
const {span, ctx} = this.startChildSpan(`nested:${this.listenTask.summary}`);
|
||||||
|
this.listenTask.span = span;
|
||||||
|
this.listenTask.ctx = ctx;
|
||||||
|
}
|
||||||
|
if (this.transcribeTask) {
|
||||||
|
const {span, ctx} = this.startChildSpan(`nested:${this.transcribeTask.summary}`);
|
||||||
|
this.transcribeTask.span = span;
|
||||||
|
this.transcribeTask.ctx = ctx;
|
||||||
|
}
|
||||||
if (this.data.amd) {
|
if (this.data.amd) {
|
||||||
this.startAmd = cs.startAmd;
|
this.startAmd = cs.startAmd;
|
||||||
this.stopAmd = cs.stopAmd;
|
this.stopAmd = cs.stopAmd;
|
||||||
@@ -214,7 +232,7 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
this._removeDtmfDetection(cs.dlg);
|
this._removeDtmfDetection(cs.dlg);
|
||||||
this._removeDtmfDetection(this.dlg);
|
this._removeDtmfDetection(this.dlg);
|
||||||
this._killOutdials();
|
await this._killOutdials();
|
||||||
if (this.sd) {
|
if (this.sd) {
|
||||||
this.sd.kill();
|
this.sd.kill();
|
||||||
this.sd.removeAllListeners();
|
this.sd.removeAllListeners();
|
||||||
@@ -223,10 +241,12 @@ class TaskDial extends Task {
|
|||||||
if (this.callSid) sessionTracker.remove(this.callSid);
|
if (this.callSid) sessionTracker.remove(this.callSid);
|
||||||
if (this.listenTask) {
|
if (this.listenTask) {
|
||||||
await this.listenTask.kill(cs);
|
await this.listenTask.kill(cs);
|
||||||
|
this.listenTask.span.end();
|
||||||
this.listenTask = null;
|
this.listenTask = null;
|
||||||
}
|
}
|
||||||
if (this.transcribeTask) {
|
if (this.transcribeTask) {
|
||||||
await this.transcribeTask.kill(cs);
|
await this.transcribeTask.kill(cs);
|
||||||
|
this.transcribeTask.span.end();
|
||||||
this.transcribeTask = null;
|
this.transcribeTask = null;
|
||||||
}
|
}
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
@@ -333,11 +353,16 @@ class TaskDial extends Task {
|
|||||||
sd.removeAllListeners('callCreateFail');
|
sd.removeAllListeners('callCreateFail');
|
||||||
}
|
}
|
||||||
|
|
||||||
_killOutdials() {
|
async _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}`);
|
||||||
sd.kill().catch((err) => this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`));
|
try {
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
@@ -408,7 +433,9 @@ 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') && {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
|
...(req && req.has('P-Asserted-Identity') && !JAMBONZ_DISABLE_DIAL_PAI_HEADER &&
|
||||||
|
{'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')}),
|
||||||
// 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
|
||||||
};
|
};
|
||||||
@@ -428,10 +455,14 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ms = await cs.getMS();
|
const ms = await cs.getMS();
|
||||||
this.timerRing = setTimeout(() => {
|
this.timerRing = setTimeout(async() => {
|
||||||
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;
|
||||||
this._killOutdials();
|
try {
|
||||||
|
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
|
||||||
@@ -459,7 +490,7 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
if (t.type === 'phone' && t.trunk) {
|
if (t.type === 'phone' && t.trunk) {
|
||||||
const voip_carrier_sid = await lookupCarrier(cs.accountSid, t.trunk);
|
const voip_carrier_sid = await lookupCarrier(cs.accountSid, t.trunk);
|
||||||
this.logger.info(`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested carrier: ${t.trunk})`);
|
this.logger.info(`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested carrier: ${t.trunk}`);
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -470,9 +501,11 @@ class TaskDial extends Task {
|
|||||||
* check if number matches any existing numbers
|
* check if number matches any existing numbers
|
||||||
* */
|
* */
|
||||||
if (t.type === 'phone' && !t.trunk) {
|
if (t.type === 'phone' && !t.trunk) {
|
||||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, t.number);
|
const str = this.callerId || req.callingNumber || '';
|
||||||
|
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||||
|
const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested phone number: ${t.number})`);
|
`Dial:_attemptCalls: selected ${voip_carrier_sid} for requested phone number: ${callingNumber}`);
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -491,7 +524,8 @@ 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);
|
||||||
|
|
||||||
@@ -507,7 +541,8 @@ 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,
|
||||||
@@ -560,11 +595,7 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('reinvite', (req, res) => {
|
.on('reinvite', (req, res) => {
|
||||||
try {
|
this._onReinvite(req, res);
|
||||||
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) => {
|
||||||
|
|
||||||
@@ -600,6 +631,35 @@ 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();
|
||||||
@@ -653,7 +713,7 @@ 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 (this.transcribeTask) this.transcribeTask.exec(cs, {ep2: this.epOther, ep: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});
|
||||||
if (this.startAmd) {
|
if (this.startAmd) {
|
||||||
try {
|
try {
|
||||||
@@ -704,9 +764,29 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleReinviteAfterMediaReleased(req, res) {
|
async handleReinviteAfterMediaReleased(req, res) {
|
||||||
const sdp = await this.dlg.modify(req.body);
|
let isHandled = false;
|
||||||
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
|
if (isOnhold(req.body) && !this.epOther && !this.ep) {
|
||||||
res.send(200, {body: sdp});
|
const sdp = await this.dlg.modify(req.body);
|
||||||
|
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) {
|
||||||
@@ -717,6 +797,48 @@ 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;
|
||||||
|
|||||||
@@ -58,6 +58,13 @@ 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 || '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.bargein = this.data.bargein;
|
this.bargein = this.data.bargein;
|
||||||
}
|
}
|
||||||
@@ -118,8 +125,15 @@ 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;
|
||||||
}
|
}
|
||||||
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
|
if (this.fallbackVendor === 'default') {
|
||||||
|
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));
|
||||||
@@ -221,16 +235,7 @@ class Dialogflow extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const obj = {
|
const {filePath, servedFromCache} = await this._fallbackSynthAudio(cs, intent, stats, synthAudio);
|
||||||
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.ttsCredentials && !servedFromCache) cs.billForTts(intent.fulfillmentText.length);
|
||||||
|
|
||||||
@@ -276,6 +281,46 @@ 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.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class TaskEnqueue extends Task {
|
|||||||
this.preconditions = TaskPreconditions.Endpoint;
|
this.preconditions = TaskPreconditions.Endpoint;
|
||||||
|
|
||||||
this.queueName = this.data.name;
|
this.queueName = this.data.name;
|
||||||
|
this.priority = this.data.priority;
|
||||||
this.waitHook = this.data.waitHook;
|
this.waitHook = this.data.waitHook;
|
||||||
|
|
||||||
this.emitter = new Emitter();
|
this.emitter = new Emitter();
|
||||||
@@ -70,12 +71,22 @@ class TaskEnqueue extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _addToQueue(cs, dlg) {
|
async _addToQueue(cs, dlg) {
|
||||||
const {pushBack} = cs.srf.locals.dbHelpers;
|
const {addToSortedSet, sortedSetLength} = cs.srf.locals.dbHelpers;
|
||||||
const url = getUrl(cs);
|
const url = getUrl(cs);
|
||||||
this.waitStartTime = Date.now();
|
this.waitStartTime = Date.now();
|
||||||
this.logger.debug({queue: this.queueName, url}, 'pushing url onto queue');
|
this.logger.debug({queue: this.queueName, url}, 'pushing url onto queue');
|
||||||
const members = await pushBack(this.queueName, url);
|
if (this.priority < 0) {
|
||||||
this.logger.info(`TaskEnqueue:_addToQueue: added to queue, length now ${members}`);
|
this.logger.warn(`priority ${this.priority} is invalid, need to be non-negative integer,
|
||||||
|
999 will be used for priority`);
|
||||||
|
}
|
||||||
|
let members = await addToSortedSet(this.queueName, url, this.priority);
|
||||||
|
if (members === 1) {
|
||||||
|
this.logger.info('TaskEnqueue:_addToQueue: added to queue');
|
||||||
|
} else {
|
||||||
|
this.logger.info('TaskEnqueue:_addToQueue: failed to add to queue');
|
||||||
|
}
|
||||||
|
members = await sortedSetLength(this.queueName);
|
||||||
|
|
||||||
this.notifyUrl = url;
|
this.notifyUrl = url;
|
||||||
|
|
||||||
/* invoke account-level webhook for queue event notifications */
|
/* invoke account-level webhook for queue event notifications */
|
||||||
@@ -90,9 +101,9 @@ class TaskEnqueue extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _removeFromQueue(cs) {
|
async _removeFromQueue(cs) {
|
||||||
const {removeFromList, lengthOfList} = cs.srf.locals.dbHelpers;
|
const {retrieveByPatternSortedSet, sortedSetLength} = cs.srf.locals.dbHelpers;
|
||||||
await removeFromList(this.queueName, getUrl(cs));
|
await retrieveByPatternSortedSet(this.queueName, `*${getUrl(cs)}`);
|
||||||
return await lengthOfList(this.queueName);
|
return await sortedSetLength(this.queueName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async performAction() {
|
async performAction() {
|
||||||
@@ -279,13 +290,13 @@ class TaskEnqueue extends Task {
|
|||||||
this.emitter.emit('dequeue', opts);
|
this.emitter.emit('dequeue', opts);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {lengthOfList} = cs.srf.locals.dbHelpers;
|
const {sortedSetLength} = cs.srf.locals.dbHelpers;
|
||||||
const members = await lengthOfList(this.queueName);
|
const members = await sortedSetLength(this.queueName);
|
||||||
this.dequeued = true;
|
this.dequeued = true;
|
||||||
cs.performQueueWebhook({
|
cs.performQueueWebhook({
|
||||||
event: 'leave',
|
event: 'leave',
|
||||||
queue: this.data.name,
|
queue: this.data.name,
|
||||||
length: Math.max(members - 1, 0),
|
length: Math.max(members, 0),
|
||||||
leaveReason: 'dequeued',
|
leaveReason: 'dequeued',
|
||||||
leaveTime: Date.now(),
|
leaveTime: Date.now(),
|
||||||
dequeuer: opts.dequeuer
|
dequeuer: opts.dequeuer
|
||||||
@@ -301,7 +312,7 @@ class TaskEnqueue extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave]) {
|
async _playHook(cs, dlg, hook, allowed = [TaskName.Play, TaskName.Say, TaskName.Pause, TaskName.Leave]) {
|
||||||
const {lengthOfList, getListPosition} = 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};
|
||||||
|
|
||||||
@@ -313,9 +324,14 @@ class TaskEnqueue extends Task {
|
|||||||
queueTime: getElapsedTime(this.waitStartTime)
|
queueTime: getElapsedTime(this.waitStartTime)
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const queueSize = await lengthOfList(this.queueName);
|
const queueSize = await sortedSetLength(this.queueName);
|
||||||
const queuePosition = await getListPosition(this.queueName, this.notifyUrl);
|
const queuePosition = await sortedSetPositionByPattern(this.queueName, `*${this.notifyUrl}`);
|
||||||
Object.assign(params, {queueSize, queuePosition});
|
Object.assign(params, {
|
||||||
|
queueSize,
|
||||||
|
queuePosition: queuePosition.length ? queuePosition[0] : 0,
|
||||||
|
callSid: this.cs.callSid,
|
||||||
|
callId: this.cs.callId,
|
||||||
|
});
|
||||||
} 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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
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
|
||||||
@@ -19,6 +18,7 @@ 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');
|
||||||
|
|
||||||
const compileTranscripts = (logger, evt, arr) => {
|
const compileTranscripts = (logger, evt, arr) => {
|
||||||
if (!Array.isArray(arr) || arr.length === 0) return;
|
if (!Array.isArray(arr) || arr.length === 0) return;
|
||||||
@@ -30,23 +30,9 @@ const compileTranscripts = (logger, evt, arr) => {
|
|||||||
evt.alternatives[0].transcript = t.trim();
|
evt.alternatives[0].transcript = t.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
class TaskGather extends Task {
|
class TaskGather extends SttTask {
|
||||||
constructor(logger, opts, parentTask) {
|
constructor(logger, opts, parentTask) {
|
||||||
super(logger, opts);
|
super(logger, opts, parentTask);
|
||||||
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',
|
||||||
@@ -62,16 +48,10 @@ class TaskGather extends Task {
|
|||||||
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 recognizer.asrTimeout === 'number' ? recognizer.asrTimeout * 1000 : 0;
|
this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ?
|
||||||
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit;
|
this.data.recognizer.asrTimeout * 1000 : 0;
|
||||||
|
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = this.data.recognizer.asrDtmfTerminationDigit;
|
||||||
this.isContinuousAsr = this.asrTimeout > 0;
|
this.isContinuousAsr = this.asrTimeout > 0;
|
||||||
|
|
||||||
if (Array.isArray(this.data.recognizer.hints) &&
|
if (Array.isArray(this.data.recognizer.hints) &&
|
||||||
@@ -79,10 +59,10 @@ class TaskGather extends Task {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
this.data.recognizer.hints = this.data.recognizer.hints || [];
|
// fast Recognition, fire event after a specified time after the last hypothesis.
|
||||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || [];
|
this.fastRecognitionTimeout = typeof this.data.recognizer.fastRecognitionTimeout === 'number' ?
|
||||||
|
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;
|
||||||
@@ -97,11 +77,6 @@ class TaskGather extends Task {
|
|||||||
|
|
||||||
/* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +112,6 @@ class TaskGather extends Task {
|
|||||||
this.logger.debug({options: this.data}, 'Gather:exec');
|
this.logger.debug({options: this.data}, 'Gather:exec');
|
||||||
await super.exec(cs);
|
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;
|
||||||
@@ -151,7 +125,7 @@ class TaskGather extends Task {
|
|||||||
}
|
}
|
||||||
if (cs.hasAltLanguages) {
|
if (cs.hasAltLanguages) {
|
||||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
|
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
|
||||||
this.logger.debug({altLanguages: this.altLanguages},
|
this.logger.debug({altLanguages: this.data.recognizer?.altLanguages},
|
||||||
'Gather:exec - applying altLanguages');
|
'Gather:exec - applying altLanguages');
|
||||||
}
|
}
|
||||||
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
||||||
@@ -184,57 +158,65 @@ class TaskGather extends Task {
|
|||||||
this.language = cs.speechRecognizerLanguage;
|
this.language = cs.speechRecognizerLanguage;
|
||||||
if (this.data.recognizer) this.data.recognizer.language = this.language;
|
if (this.data.recognizer) this.data.recognizer.language = this.language;
|
||||||
}
|
}
|
||||||
|
if ('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) {
|
if (!this.data.recognizer.vendor) {
|
||||||
this.data.recognizer.vendor = this.vendor;
|
this.data.recognizer.vendor = this.vendor;
|
||||||
}
|
}
|
||||||
if (this.needsStt && !this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
|
||||||
if (this.needsStt && !this.sttCredentials) {
|
if (this.needsStt && !this.sttCredentials) {
|
||||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
try {
|
||||||
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
|
this.sttCredentials = await this._initSpeechCredentials(cs, this.vendor, this.label);
|
||||||
writeAlerts({
|
} catch (error) {
|
||||||
account_sid: cs.accountSid,
|
if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
|
||||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
await this._fallback();
|
||||||
vendor: this.vendor
|
} else {
|
||||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
throw error;
|
||||||
// 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) {
|
/* when using cobalt model is required */
|
||||||
/* get nuance access token */
|
if (this.vendor === 'cobalt' && !this.data.recognizer.model) {
|
||||||
const {client_id, secret} = this.sttCredentials;
|
this.notifyError({ msg: 'ASR error', details:'Cobalt requires a model to be specified'});
|
||||||
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
|
throw new Error('Cobalt requires a model to be specified');
|
||||||
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 startListening = async(cs, ep) => {
|
||||||
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) {
|
||||||
this._initSpeech(cs, ep)
|
try {
|
||||||
.then(() => {
|
await this._setSpeechHandlers(cs, ep);
|
||||||
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) {
|
||||||
.catch((err) => {
|
if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
|
||||||
this.logger.error({err}, 'error in initSpeech');
|
await this._fallback();
|
||||||
});
|
startListening(cs, ep);
|
||||||
|
} else {
|
||||||
|
this.logger.error({error: e}, 'error in initSpeech');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,7 +270,7 @@ class TaskGather extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.input.includes('speech') && this.listenDuringPrompt) {
|
if (this.input.includes('speech') && this.listenDuringPrompt) {
|
||||||
await this._initSpeech(cs, ep);
|
await this._setSpeechHandlers(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 */});
|
||||||
@@ -362,7 +344,9 @@ class TaskGather extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _initSpeech(cs, ep) {
|
async _setSpeechHandlers(cs, ep) {
|
||||||
|
if (this._speechHandlersSet) return;
|
||||||
|
this._speechHandlersSet = true;
|
||||||
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
|
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
|
||||||
switch (this.vendor) {
|
switch (this.vendor) {
|
||||||
case 'google':
|
case 'google':
|
||||||
@@ -415,6 +399,36 @@ class TaskGather extends Task {
|
|||||||
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'cobalt':
|
||||||
|
this.bugname = 'cobalt_transcribe';
|
||||||
|
ep.addCustomEventListener(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;
|
||||||
|
|
||||||
case 'ibm':
|
case 'ibm':
|
||||||
this.bugname = 'ibm_transcribe';
|
this.bugname = 'ibm_transcribe';
|
||||||
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||||
@@ -474,6 +488,7 @@ class TaskGather extends Task {
|
|||||||
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');
|
||||||
@@ -517,6 +532,22 @@ class TaskGather extends Task {
|
|||||||
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(() => {
|
||||||
@@ -654,6 +685,9 @@ class TaskGather extends Task {
|
|||||||
}
|
}
|
||||||
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};
|
||||||
@@ -704,8 +738,22 @@ class TaskGather extends Task {
|
|||||||
_onJambonzConnect(_cs, _ep) {
|
_onJambonzConnect(_cs, _ep) {
|
||||||
this.logger.debug('TaskGather:_onJambonzConnect');
|
this.logger.debug('TaskGather:_onJambonzConnect');
|
||||||
}
|
}
|
||||||
_onJambonzError(cs, _ep, evt) {
|
async _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})
|
||||||
|
.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') {
|
||||||
@@ -804,6 +852,7 @@ class TaskGather extends Task {
|
|||||||
}
|
}
|
||||||
clearTimeout(this.interDigitTimer);
|
clearTimeout(this.interDigitTimer);
|
||||||
this._clearTimer();
|
this._clearTimer();
|
||||||
|
this._clearFastRecognitionTimer();
|
||||||
|
|
||||||
if (this.isContinuousAsr && reason.startsWith('speech')) {
|
if (this.isContinuousAsr && reason.startsWith('speech')) {
|
||||||
evt = {
|
evt = {
|
||||||
@@ -817,7 +866,11 @@ class TaskGather extends Task {
|
|||||||
this.logger.debug({evt}, 'TaskGather:resolve buffered results');
|
this.logger.debug({evt}, 'TaskGather:resolve buffered results');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.span.setAttributes({'stt.resolve': reason, 'stt.result': JSON.stringify(evt)});
|
this.span.setAttributes({
|
||||||
|
channel: 1,
|
||||||
|
'stt.resolve': reason,
|
||||||
|
'stt.result': JSON.stringify(evt)
|
||||||
|
});
|
||||||
if (this.needsStt && this.ep && this.ep.connected) {
|
if (this.needsStt && this.ep && this.ep.connected) {
|
||||||
this.ep.stopTranscription({vendor: this.vendor})
|
this.ep.stopTranscription({vendor: this.vendor})
|
||||||
.catch((err) => this.logger.error({err}, 'Error stopping transcription'));
|
.catch((err) => this.logger.error({err}, 'Error stopping transcription'));
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ 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}`;
|
||||||
@@ -102,8 +109,16 @@ 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;
|
||||||
}
|
}
|
||||||
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
|
if (this.fallbackVendor === 'default') {
|
||||||
|
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));
|
||||||
@@ -168,6 +183,41 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -187,15 +237,7 @@ class Lex extends Task {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
|
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
|
||||||
// eslint-disable-next-line no-unused-vars
|
const filePath = await this._fallbackSynthAudio(cs, msg, stats, synthAudio);
|
||||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
|
||||||
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')) {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../ut
|
|||||||
const makeTask = require('./make_task');
|
const makeTask = require('./make_task');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const MAX_PLAY_AUDIO_QUEUE_SIZE = 10;
|
const MAX_PLAY_AUDIO_QUEUE_SIZE = 10;
|
||||||
|
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;
|
||||||
|
|
||||||
[
|
[
|
||||||
@@ -29,6 +31,10 @@ class TaskListen extends Task {
|
|||||||
|
|
||||||
get name() { return TaskName.Listen; }
|
get name() { return TaskName.Listen; }
|
||||||
|
|
||||||
|
set bugname(name) { this._bugname = name; }
|
||||||
|
|
||||||
|
set ignoreCustomerData(val) { this._ignoreCustomerData = val; }
|
||||||
|
|
||||||
async exec(cs, {ep}) {
|
async exec(cs, {ep}) {
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
@@ -65,7 +71,8 @@ class TaskListen extends Task {
|
|||||||
if (this.ep && this.ep.connected) {
|
if (this.ep && this.ep.connected) {
|
||||||
this.logger.debug('TaskListen:kill closing websocket');
|
this.logger.debug('TaskListen:kill closing websocket');
|
||||||
try {
|
try {
|
||||||
await this.ep.forkAudioStop();
|
const args = this._bugname ? [this._bugname] : [];
|
||||||
|
await this.ep.forkAudioStop(...args);
|
||||||
this.logger.debug('TaskListen:kill successfully closed websocket');
|
this.logger.debug('TaskListen:kill successfully closed websocket');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info(err, 'TaskListen:kill');
|
this.logger.info(err, 'TaskListen:kill');
|
||||||
@@ -85,13 +92,16 @@ class TaskListen extends Task {
|
|||||||
|
|
||||||
async updateListen(status) {
|
async updateListen(status) {
|
||||||
if (!this.killed && this.ep && this.ep.connected) {
|
if (!this.killed && this.ep && this.ep.connected) {
|
||||||
|
const args = this._bugname ? [this._bugname] : [];
|
||||||
this.logger.info(`TaskListen:updateListen status ${status}`);
|
this.logger.info(`TaskListen:updateListen status ${status}`);
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ListenStatus.Pause:
|
case ListenStatus.Pause:
|
||||||
await this.ep.forkAudioPause().catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
|
await this.ep.forkAudioPause(...args)
|
||||||
|
.catch((err) => this.logger.info(err, 'TaskListen: error pausing audio'));
|
||||||
break;
|
break;
|
||||||
case ListenStatus.Resume:
|
case ListenStatus.Resume:
|
||||||
await this.ep.forkAudioResume().catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
|
await this.ep.forkAudioResume(...args)
|
||||||
|
.catch((err) => this.logger.info(err, 'TaskListen: error resuming audio'));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,9 +114,13 @@ class TaskListen extends Task {
|
|||||||
|
|
||||||
async _startListening(cs, ep) {
|
async _startListening(cs, ep) {
|
||||||
this._initListeners(ep);
|
this._initListeners(ep);
|
||||||
|
const ci = this.nested ? this.parentTask.sd.callInfo : cs.callInfo.toJSON();
|
||||||
|
if (this._ignoreCustomerData) {
|
||||||
|
delete ci.customerData;
|
||||||
|
}
|
||||||
const metadata = Object.assign(
|
const metadata = Object.assign(
|
||||||
{sampleRate: this.sampleRate, mixType: this.mixType},
|
{sampleRate: this.sampleRate, mixType: this.mixType},
|
||||||
this.nested ? this.parentTask.sd.callInfo : cs.callInfo.toJSON(),
|
ci,
|
||||||
this.metadata);
|
this.metadata);
|
||||||
if (this.hook.auth) {
|
if (this.hook.auth) {
|
||||||
this.logger.debug({username: this.hook.auth.username, password: this.hook.auth.password},
|
this.logger.debug({username: this.hook.auth.username, password: this.hook.auth.password},
|
||||||
@@ -120,6 +134,7 @@ class TaskListen extends Task {
|
|||||||
wsUrl: this.hook.url,
|
wsUrl: this.hook.url,
|
||||||
mixType: this.mixType,
|
mixType: this.mixType,
|
||||||
sampling: this.sampleRate,
|
sampling: this.sampleRate,
|
||||||
|
...(this._bugname && {bugname: this._bugname}),
|
||||||
metadata
|
metadata
|
||||||
});
|
});
|
||||||
this.recordStartTime = moment();
|
this.recordStartTime = moment();
|
||||||
@@ -140,7 +155,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));
|
||||||
@@ -161,12 +176,25 @@ class TaskListen extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onDtmf(ep, evt) {
|
_onDtmf(ep, evt) {
|
||||||
this.logger.debug({evt}, `TaskListen:_onDtmf received dtmf ${evt.dtmf}`);
|
const {dtmf, duration} = evt;
|
||||||
|
this.logger.debug({evt}, `TaskListen:_onDtmf received dtmf ${dtmf}`);
|
||||||
if (this.passDtmf && this.ep?.connected) {
|
if (this.passDtmf && this.ep?.connected) {
|
||||||
const obj = {event: 'dtmf', dtmf: evt.dtmf, duration: evt.duration};
|
const obj = {event: 'dtmf', dtmf, duration};
|
||||||
this.ep.forkAudioSendText(obj)
|
const args = this._bugname ? [this._bugname, obj] : [obj];
|
||||||
|
this.ep.forkAudioSendText(...args)
|
||||||
.catch((err) => this.logger.info({err}, 'TaskListen:_onDtmf error sending dtmf'));
|
.catch((err) => this.logger.info({err}, 'TaskListen:_onDtmf error sending dtmf'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* add a child span for the dtmf event */
|
||||||
|
const msDuration = Math.floor((duration / 8000) * 1000);
|
||||||
|
const {span} = this.startChildSpan(`${DTMF_SPAN_NAME}:${dtmf}`);
|
||||||
|
span.setAttributes({
|
||||||
|
channel: 1,
|
||||||
|
dtmf,
|
||||||
|
duration: `${msDuration}ms`
|
||||||
|
});
|
||||||
|
span.end();
|
||||||
|
|
||||||
if (evt.dtmf === this.finishOnKey) {
|
if (evt.dtmf === this.finishOnKey) {
|
||||||
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
|
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
|
||||||
this.results.digits = evt.dtmf;
|
this.results.digits = evt.dtmf;
|
||||||
@@ -192,7 +220,15 @@ class TaskListen extends Task {
|
|||||||
try {
|
try {
|
||||||
const results = await ep.play(evt.file);
|
const results = await ep.play(evt.file);
|
||||||
logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
|
logger.debug(`Finished playing file, result: ${JSON.stringify(results)}`);
|
||||||
ep.forkAudioSendText({type: 'playDone', data: Object.assign({id: evt.id}, results)});
|
const obj = {
|
||||||
|
type: 'playDone',
|
||||||
|
data: {
|
||||||
|
id: evt.id,
|
||||||
|
...results
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const args = this._bugname ? [this._bugname, obj] : [obj];
|
||||||
|
ep.forkAudioSendText(...args);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({err}, 'Error playing file');
|
logger.error({err}, 'Error playing file');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,23 +23,38 @@ class TaskRestDial extends Task {
|
|||||||
|
|
||||||
get name() { return TaskName.RestDial; }
|
get name() { return TaskName.RestDial; }
|
||||||
|
|
||||||
|
set appJson(app_json) {
|
||||||
|
this.app_json = app_json;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* INVITE has just been sent at this point
|
* INVITE has just been sent at this point
|
||||||
*/
|
*/
|
||||||
async exec(cs) {
|
async exec(cs) {
|
||||||
await super.exec(cs);
|
await super.exec(cs);
|
||||||
|
this.cs = cs;
|
||||||
this.canCancel = true;
|
this.canCancel = true;
|
||||||
|
|
||||||
|
if (this.data.amd) {
|
||||||
|
this.startAmd = cs.startAmd;
|
||||||
|
this.stopAmd = cs.stopAmd;
|
||||||
|
this.on('amd', this._onAmdEvent.bind(this, cs));
|
||||||
|
}
|
||||||
|
|
||||||
this._setCallTimer();
|
this._setCallTimer();
|
||||||
await this.awaitTaskDone();
|
await this.awaitTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
turnOffAmd() {
|
||||||
|
if (this.callSession.ep && this.callSession.ep.amd) this.stopAmd(this.callSession.ep, this);
|
||||||
|
}
|
||||||
|
|
||||||
kill(cs) {
|
kill(cs) {
|
||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
this._clearCallTimer();
|
this._clearCallTimer();
|
||||||
if (this.canCancel && cs?.req) {
|
if (this.canCancel) {
|
||||||
this.canCancel = false;
|
this.canCancel = false;
|
||||||
cs.req.cancel();
|
cs?.req?.cancel();
|
||||||
}
|
}
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
@@ -48,12 +63,13 @@ 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');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const b3 = this.getTracingPropagation();
|
const b3 = this.getTracingPropagation();
|
||||||
const httpHeaders = b3 && {b3};
|
const httpHeaders = b3 && {b3};
|
||||||
const params = {
|
const params = {
|
||||||
...cs.callInfo,
|
...(cs.callInfo.toJSON()),
|
||||||
defaults: {
|
defaults: {
|
||||||
synthesizer: {
|
synthesizer: {
|
||||||
vendor: cs.speechSynthesisVendor,
|
vendor: cs.speechSynthesisVendor,
|
||||||
@@ -66,7 +82,21 @@ class TaskRestDial extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
|
if (this.startAmd) {
|
||||||
|
try {
|
||||||
|
this.startAmd(this.callSession, this.callSession.ep, this, this.data.amd);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.info({err}, 'Rest:dial:Call established - Error calling startAmd');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tasks;
|
||||||
|
if (this.app_json) {
|
||||||
|
this.logger.debug('TaskRestDial: using app_json from task data');
|
||||||
|
tasks = JSON.parse(this.app_json);
|
||||||
|
} else {
|
||||||
|
this.logger.debug({call_hook: this.call_hook}, 'TaskRestDial: retrieving application');
|
||||||
|
tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
|
||||||
|
}
|
||||||
if (tasks && Array.isArray(tasks)) {
|
if (tasks && Array.isArray(tasks)) {
|
||||||
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
|
this.logger.debug({tasks: tasks}, `TaskRestDial: replacing application with ${tasks.length} tasks`);
|
||||||
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
cs.replaceApplication(normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata)));
|
||||||
@@ -98,7 +128,19 @@ 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;
|
||||||
this.kill();
|
if (this.canCancel) {
|
||||||
|
this.canCancel = false;
|
||||||
|
this.cs?.req?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onAmdEvent(cs, evt) {
|
||||||
|
this.logger.info({evt}, 'Rest:dial:_onAmdEvent');
|
||||||
|
const {actionHook} = this.data.amd;
|
||||||
|
this.performHook(cs, actionHook, evt)
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.error({err}, 'Rest:dial:_onAmdEvent - error calling actionHook');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
lib/tasks/say.js
119
lib/tasks/say.js
@@ -36,6 +36,8 @@ class TaskSay extends Task {
|
|||||||
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||||
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.isHandledByPrimaryProvider = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Say; }
|
get name() { return TaskName.Say; }
|
||||||
@@ -48,26 +50,15 @@ class TaskSay extends Task {
|
|||||||
return `${this.name}{${this.text[0]}}`;
|
return `${this.name}{${this.text[0]}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async exec(cs, {ep}) {
|
async _synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label}) {
|
||||||
await super.exec(cs);
|
|
||||||
|
|
||||||
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;
|
||||||
const 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) {
|
||||||
@@ -78,8 +69,17 @@ class TaskSay extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* allow for microsoft custom region voice and api_key to be specified as an override */
|
||||||
|
if (vendor === 'microsoft' && this.options.deploymentId) {
|
||||||
|
credentials = credentials || {};
|
||||||
|
credentials.use_custom_tts = true;
|
||||||
|
credentials.custom_tts_endpoint = this.options.deploymentId;
|
||||||
|
credentials.api_key = this.options.apiKey || credentials.apiKey;
|
||||||
|
credentials.region = this.options.region || credentials.region;
|
||||||
|
voice = this.options.voice || voice;
|
||||||
|
}
|
||||||
|
|
||||||
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({
|
||||||
@@ -109,6 +109,7 @@ class TaskSay extends Task {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
|
const {filePath, servedFromCache, rtt} = await synthAudio(stats, {
|
||||||
|
account_sid: cs.accountSid,
|
||||||
text,
|
text,
|
||||||
vendor,
|
vendor,
|
||||||
language,
|
language,
|
||||||
@@ -148,31 +149,83 @@ 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});
|
||||||
return;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const arr = this.text.map((t) => generateAudio(t));
|
const arr = this.text.map((t) => generateAudio(t));
|
||||||
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
|
return (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');
|
||||||
}
|
}
|
||||||
|
|||||||
139
lib/tasks/stt-task.js
Normal file
139
lib/tasks/stt-task.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
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,
|
||||||
|
removeSpeechListeners,
|
||||||
|
setSpeechCredentialsAtRuntime,
|
||||||
|
compileSonioxTranscripts
|
||||||
|
} = require('../utils/transcription-utils')(logger);
|
||||||
|
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||||
|
this.normalizeTranscription = normalizeTranscription;
|
||||||
|
this.removeSpeechListeners = removeSpeechListeners;
|
||||||
|
this.compileSonioxTranscripts = compileSonioxTranscripts;
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
this.data.recognizer = {hints: [], altLanguages: []};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* buffer for soniox transcripts */
|
||||||
|
this._sonioxTranscripts = [];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initSpeechCredentials(cs, vendor, label) {
|
||||||
|
const {getNuanceAccessToken, getIbmAccessToken} = this.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}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SttTask;
|
||||||
@@ -155,7 +155,7 @@ class Task extends Emitter {
|
|||||||
if (this.actionHook) {
|
if (this.actionHook) {
|
||||||
const type = this.name === TaskName.Redirect ? 'session:redirect' : 'verb:hook';
|
const type = this.name === TaskName.Redirect ? 'session:redirect' : 'verb:hook';
|
||||||
const params = results ? Object.assign(this.cs.callInfo.toJSON(), results) : this.cs.callInfo.toJSON();
|
const params = results ? Object.assign(this.cs.callInfo.toJSON(), results) : this.cs.callInfo.toJSON();
|
||||||
const span = this.startSpan(type, {'hook.url': this.actionHook});
|
const span = this.startSpan(`${type} (${this.actionHook})`);
|
||||||
const b3 = this.getTracingPropagation('b3', span);
|
const b3 = this.getTracingPropagation('b3', span);
|
||||||
const httpHeaders = b3 && {b3};
|
const httpHeaders = b3 && {b3};
|
||||||
span.setAttributes({'http.body': JSON.stringify(params)});
|
span.setAttributes({'http.body': JSON.stringify(params)});
|
||||||
|
|||||||
@@ -1,54 +1,42 @@
|
|||||||
const Task = require('./task');
|
const assert = require('assert');
|
||||||
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');
|
} = require('../utils/constants');
|
||||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
|
const SttTask = require('./stt-task');
|
||||||
|
|
||||||
class TaskTranscribe extends Task {
|
const STT_LISTEN_SPAN_NAME = 'stt-listen';
|
||||||
|
|
||||||
|
class TaskTranscribe extends SttTask {
|
||||||
constructor(logger, opts, parentTask) {
|
constructor(logger, opts, parentTask) {
|
||||||
super(logger, opts);
|
super(logger, opts, parentTask);
|
||||||
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);
|
||||||
|
|
||||||
const recognizer = this.data.recognizer;
|
if (this.data.recognizer) {
|
||||||
this.vendor = recognizer.vendor;
|
this.interim = !!this.data.recognizer.interim;
|
||||||
this.language = recognizer.language;
|
this.separateRecognitionPerChannel = this.data.recognizer.separateRecognitionPerChannel;
|
||||||
this.interim = !!recognizer.interim;
|
}
|
||||||
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
|
|
||||||
|
|
||||||
/* let credentials be supplied in the recognizer object at runtime */
|
this.childSpan = [null, null];
|
||||||
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
|
||||||
|
|
||||||
/* buffer for soniox transcripts */
|
// Continuos asr timeout
|
||||||
this._sonioxTranscripts = [];
|
this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ? this.data.recognizer.asrTimeout * 1000 : 0;
|
||||||
|
this.isContinuousAsr = this.asrTimeout > 0;
|
||||||
recognizer.hints = recognizer.hints || [];
|
/* buffer speech for continuous asr */
|
||||||
recognizer.altLanguages = recognizer.altLanguages || [];
|
this._bufferedTranscripts = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Transcribe; }
|
get name() { return TaskName.Transcribe; }
|
||||||
@@ -56,7 +44,6 @@ class TaskTranscribe extends Task {
|
|||||||
async exec(cs, {ep, ep2}) {
|
async exec(cs, {ep, ep2}) {
|
||||||
super.exec(cs);
|
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) {
|
if (cs.hasGlobalSttHints) {
|
||||||
const {hints, hintsBoost} = cs.globalSttHints;
|
const {hints, hintsBoost} = cs.globalSttHints;
|
||||||
@@ -84,38 +71,46 @@ class TaskTranscribe extends Task {
|
|||||||
this.language = cs.speechRecognizerLanguage;
|
this.language = cs.speechRecognizerLanguage;
|
||||||
if (this.data.recognizer) this.data.recognizer.language = this.language;
|
if (this.data.recognizer) this.data.recognizer.language = this.language;
|
||||||
}
|
}
|
||||||
|
if ('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) {
|
if (!this.data.recognizer.vendor) {
|
||||||
this.data.recognizer.vendor = this.vendor;
|
this.data.recognizer.vendor = this.vendor;
|
||||||
}
|
}
|
||||||
if (!this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
|
||||||
|
if (!this.sttCredentials) {
|
||||||
|
try {
|
||||||
|
this.sttCredentials = await this._initSpeechCredentials(cs, this.vendor, this.label);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
|
||||||
|
await this._fallback();
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* when using cobalt model is required */
|
||||||
|
if (this.vendor === 'cobalt' && !this.data.recognizer.model) {
|
||||||
|
this.notifyError({ msg: 'ASR error', details:'Cobalt requires a model to be specified'});
|
||||||
|
throw new Error('Cobalt requires a model to be specified');
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -132,8 +127,7 @@ class TaskTranscribe extends Task {
|
|||||||
this.removeSpeechListeners(ep);
|
this.removeSpeechListeners(ep);
|
||||||
}
|
}
|
||||||
|
|
||||||
async kill(cs) {
|
async _stopTranscription() {
|
||||||
super.kill(cs);
|
|
||||||
let stopTranscription = false;
|
let stopTranscription = false;
|
||||||
if (this.ep?.connected) {
|
if (this.ep?.connected) {
|
||||||
stopTranscription = true;
|
stopTranscription = true;
|
||||||
@@ -145,6 +139,13 @@ class TaskTranscribe extends Task {
|
|||||||
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();
|
||||||
@@ -152,7 +153,26 @@ class TaskTranscribe extends Task {
|
|||||||
await this.awaitTaskDone();
|
await this.awaitTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _startTranscribing(cs, ep, channel) {
|
async updateTranscribe(status) {
|
||||||
|
if (!this.killed && this.ep && this.ep.connected) {
|
||||||
|
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) return;
|
||||||
|
this._speechHandlersSet = true;
|
||||||
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
|
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
|
||||||
switch (this.vendor) {
|
switch (this.vendor) {
|
||||||
case 'google':
|
case 'google':
|
||||||
@@ -205,6 +225,36 @@ class TaskTranscribe extends Task {
|
|||||||
ep.addCustomEventListener(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 = 'cobalt_transcribe';
|
||||||
|
ep.addCustomEventListener(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 = 'ibm_transcribe';
|
this.bugname = 'ibm_transcribe';
|
||||||
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription,
|
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription,
|
||||||
@@ -226,16 +276,37 @@ class TaskTranscribe extends Task {
|
|||||||
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
|
ep.addCustomEventListener(NvidiaTranscriptionEvents.VadDetected,
|
||||||
this._onVadDetected.bind(this, cs, ep));
|
this._onVadDetected.bind(this, cs, ep));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid vendor ${this.vendor}`);
|
if (this.vendor.startsWith('custom:')) {
|
||||||
|
this.bugname = `${this.vendor}_transcribe`;
|
||||||
|
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
ep.addCustomEventListener(JambonzTranscriptionEvents.Connect, this._onJambonzConnect.bind(this, cs, ep));
|
||||||
|
ep.addCustomEventListener(JambonzTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onJambonzConnectFailure.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 */
|
||||||
ep.addCustomEventListener(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 */
|
||||||
|
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||||
|
this.childSpan[channel - 1] = {span, ctx};
|
||||||
}
|
}
|
||||||
|
|
||||||
async _transcribe(ep) {
|
async _transcribe(ep) {
|
||||||
@@ -244,7 +315,8 @@ class TaskTranscribe extends Task {
|
|||||||
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +357,36 @@ class TaskTranscribe extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isContinuousAsr && evt.is_final) {
|
||||||
|
this._bufferedTranscripts.push(evt);
|
||||||
|
this._startAsrTimer(channel);
|
||||||
|
} else {
|
||||||
|
await this._resolve(channel, evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_compileTranscripts() {
|
||||||
|
assert(this._bufferedTranscripts.length);
|
||||||
|
const evt = this._bufferedTranscripts[0];
|
||||||
|
let t = '';
|
||||||
|
for (const a of this._bufferedTranscripts) {
|
||||||
|
t += ` ${a.alternatives[0].transcript}`;
|
||||||
|
}
|
||||||
|
evt.alternatives[0].transcript = t.trim();
|
||||||
|
return evt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _resolve(channel, evt) {
|
||||||
|
/* we've got a transcript, so end the otel child span for this channel */
|
||||||
|
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||||
|
this.childSpan[channel - 1].span.setAttributes({
|
||||||
|
channel,
|
||||||
|
'stt.resolve': 'transcript',
|
||||||
|
'stt.result': JSON.stringify(evt)
|
||||||
|
});
|
||||||
|
this.childSpan[channel - 1].span.end();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.transcriptionHook) {
|
if (this.transcriptionHook) {
|
||||||
const b3 = this.getTracingPropagation();
|
const b3 = this.getTracingPropagation();
|
||||||
const httpHeaders = b3 && {b3};
|
const httpHeaders = b3 && {b3};
|
||||||
@@ -315,16 +417,44 @@ class TaskTranscribe extends Task {
|
|||||||
this._clearTimer();
|
this._clearTimer();
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
/* start another child span for this channel */
|
||||||
|
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||||
|
this.childSpan[channel - 1] = {span, ctx};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onNoAudio(cs, ep, channel) {
|
_onNoAudio(cs, ep, channel) {
|
||||||
this.logger.debug(`TaskTranscribe:_onNoAudio restarting transcription on channel ${channel}`);
|
this.logger.debug(`TaskTranscribe:_onNoAudio restarting transcription on channel ${channel}`);
|
||||||
|
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||||
|
this.childSpan[channel - 1].span.setAttributes({
|
||||||
|
channel,
|
||||||
|
'stt.resolve': 'timeout'
|
||||||
|
});
|
||||||
|
this.childSpan[channel - 1].span.end();
|
||||||
|
}
|
||||||
this._transcribe(ep);
|
this._transcribe(ep);
|
||||||
|
|
||||||
|
/* start new child span for this channel */
|
||||||
|
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||||
|
this.childSpan[channel - 1] = {span, ctx};
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMaxDurationExceeded(cs, ep, channel) {
|
_onMaxDurationExceeded(cs, ep, channel) {
|
||||||
this.logger.debug(`TaskTranscribe:_onMaxDurationExceeded restarting transcription on channel ${channel}`);
|
this.logger.debug(`TaskTranscribe:_onMaxDurationExceeded restarting transcription on channel ${channel}`);
|
||||||
|
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||||
|
this.childSpan[channel - 1].span.setAttributes({
|
||||||
|
channel,
|
||||||
|
'stt.resolve': 'max duration exceeded'
|
||||||
|
});
|
||||||
|
this.childSpan[channel - 1].span.end();
|
||||||
|
}
|
||||||
|
|
||||||
this._transcribe(ep);
|
this._transcribe(ep);
|
||||||
|
|
||||||
|
/* start new child span for this channel */
|
||||||
|
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
|
||||||
|
this.childSpan[channel - 1] = {span, ctx};
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearTimer() {
|
_clearTimer() {
|
||||||
@@ -337,7 +467,7 @@ class TaskTranscribe extends Task {
|
|||||||
this.logger.debug('TaskTranscribe:_onDeepgramConnect');
|
this.logger.debug('TaskTranscribe:_onDeepgramConnect');
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDeepGramConnectFailure(cs, _ep, _channel, evt) {
|
_onDeepGramConnectFailure(cs, _ep, channel, evt) {
|
||||||
const {reason} = evt;
|
const {reason} = evt;
|
||||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||||
this.logger.info({evt}, 'TaskTranscribe:_onDeepgramConnectFailure');
|
this.logger.info({evt}, 'TaskTranscribe:_onDeepgramConnectFailure');
|
||||||
@@ -348,6 +478,32 @@ class TaskTranscribe extends Task {
|
|||||||
vendor: 'deepgram',
|
vendor: 'deepgram',
|
||||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
|
}).catch((err) => this.logger.info({err}, 'Error generating alert for deepgram connection failure'));
|
||||||
this.notifyError(`Failed connecting to speech vendor deepgram: ${reason}`);
|
this.notifyError(`Failed connecting to speech vendor deepgram: ${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();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onJambonzConnect(_cs, _ep) {
|
||||||
|
this.logger.debug('TaskTranscribe:_onJambonzConnect');
|
||||||
|
}
|
||||||
|
|
||||||
|
_onJambonzConnectFailure(cs, _ep, evt) {
|
||||||
|
const {reason} = evt;
|
||||||
|
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||||
|
this.logger.info({evt}, 'TaskTranscribe:_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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +511,7 @@ class TaskTranscribe extends Task {
|
|||||||
this.logger.debug('TaskTranscribe:_onIbmConnect');
|
this.logger.debug('TaskTranscribe:_onIbmConnect');
|
||||||
}
|
}
|
||||||
|
|
||||||
_onIbmConnectFailure(cs, _ep, _channel, evt) {
|
_onIbmConnectFailure(cs, _ep, channel, evt) {
|
||||||
const {reason} = evt;
|
const {reason} = evt;
|
||||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||||
this.logger.info({evt}, 'TaskTranscribe:_onIbmConnectFailure');
|
this.logger.info({evt}, 'TaskTranscribe:_onIbmConnectFailure');
|
||||||
@@ -366,31 +522,72 @@ class TaskTranscribe extends Task {
|
|||||||
vendor: 'ibm',
|
vendor: 'ibm',
|
||||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
|
}).catch((err) => this.logger.info({err}, 'Error generating alert for IBM connection failure'));
|
||||||
this.notifyError(`Failed connecting to speech vendor IBM: ${reason}`);
|
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();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
_onIbmError(cs, _ep, _channel, evt) {
|
_onIbmError(cs, _ep, _channel, evt) {
|
||||||
this.logger.info({evt}, 'TaskTranscribe:_onIbmError');
|
this.logger.info({evt}, 'TaskTranscribe:_onIbmError');
|
||||||
}
|
}
|
||||||
_onJambonzError(cs, _ep, evt) {
|
async _onJambonzError(cs, _ep, evt) {
|
||||||
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
|
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
|
||||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
|
||||||
|
_ep.stopTranscription({vendor: this.vendor})
|
||||||
|
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
|
||||||
|
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||||
|
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') {
|
if (this.vendor === 'nuance') {
|
||||||
const {code, error} = evt;
|
const {code, error} = evt;
|
||||||
if (code === 404 && error === 'No speech') return this._resolve('timeout');
|
if (code === 404 && error === 'No speech') return this._resolve('timeout');
|
||||||
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
|
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
|
||||||
|
}
|
||||||
|
this.logger.info({evt}, '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}`});
|
||||||
}
|
}
|
||||||
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}`});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_startAsrTimer(channel) {
|
||||||
|
assert(this.isContinuousAsr);
|
||||||
|
this._clearAsrTimer(channel);
|
||||||
|
this._asrTimer = setTimeout(() => {
|
||||||
|
this.logger.debug(`TaskTranscribe:_startAsrTimer - asr timer went off for channel: ${channel}`);
|
||||||
|
const evt = this._compileTranscripts();
|
||||||
|
this._bufferedTranscripts = [];
|
||||||
|
this._resolve(channel, evt);
|
||||||
|
}, this.asrTimeout);
|
||||||
|
this.logger.debug(`TaskTranscribe:_startAsrTimer: set for ${this.asrTimeout}ms for channel ${channel}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearAsrTimer(channel) {
|
||||||
|
if (this._asrTimer) clearTimeout(this._asrTimer);
|
||||||
|
this._asrTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TaskTranscribe;
|
module.exports = TaskTranscribe;
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const {readFile} = require('fs');
|
const {readFile} = require('fs');
|
||||||
const {
|
const {
|
||||||
|
TaskName,
|
||||||
GoogleTranscriptionEvents,
|
GoogleTranscriptionEvents,
|
||||||
AwsTranscriptionEvents,
|
AwsTranscriptionEvents,
|
||||||
AzureTranscriptionEvents,
|
AzureTranscriptionEvents,
|
||||||
|
NuanceTranscriptionEvents,
|
||||||
|
NvidiaTranscriptionEvents,
|
||||||
|
IbmTranscriptionEvents,
|
||||||
|
SonioxTranscriptionEvents,
|
||||||
|
CobaltTranscriptionEvents,
|
||||||
|
DeepgramTranscriptionEvents,
|
||||||
|
JambonzTranscriptionEvents,
|
||||||
AmdEvents,
|
AmdEvents,
|
||||||
AvmdEvents
|
AvmdEvents
|
||||||
} = require('./constants');
|
} = require('./constants');
|
||||||
@@ -47,13 +55,19 @@ 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}`);
|
||||||
|
|
||||||
this.thresholdWordCount = opts.thresholdWordCount || 9;
|
this.thresholdWordCount = opts.thresholdWordCount || 9;
|
||||||
const {normalizeTranscription} = require('./transcription-utils')(logger);
|
const {normalizeTranscription} = require('./transcription-utils')(logger);
|
||||||
this.normalizeTranscription = normalizeTranscription;
|
this.normalizeTranscription = normalizeTranscription;
|
||||||
|
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
|
||||||
|
this.getNuanceAccessToken = getNuanceAccessToken;
|
||||||
|
this.getIbmAccessToken = getIbmAccessToken;
|
||||||
|
const {setChannelVarsForStt} = require('./transcription-utils')(logger);
|
||||||
|
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
noSpeechTimeoutMs = 5000,
|
noSpeechTimeoutMs = 5000,
|
||||||
@@ -229,51 +243,96 @@ module.exports = (logger) => {
|
|||||||
|
|
||||||
const startAmd = async(cs, ep, task, opts) => {
|
const startAmd = async(cs, ep, task, opts) => {
|
||||||
const amd = ep.amd = new Amd(logger, cs, opts);
|
const amd = ep.amd = new Amd(logger, cs, opts);
|
||||||
const {vendor, language, sttCredentials} = amd;
|
const {vendor, language} = amd;
|
||||||
const sttOpts = {};
|
let sttCredentials = amd.sttCredentials;
|
||||||
const hints = voicemailHints[language] || [];
|
const hints = voicemailHints[language] || [];
|
||||||
|
|
||||||
|
if (vendor === 'nuance' && sttCredentials.client_id) {
|
||||||
|
/* get nuance access token */
|
||||||
|
const {getNuanceAccessToken} = amd;
|
||||||
|
const {client_id, secret} = sttCredentials;
|
||||||
|
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
|
||||||
|
logger.debug({client_id}, `Gather:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
|
||||||
|
sttCredentials = {...sttCredentials, access_token};
|
||||||
|
}
|
||||||
|
else if (vendor == 'ibm' && sttCredentials.stt_api_key) {
|
||||||
|
/* get ibm access token */
|
||||||
|
const {getIbmAccessToken} = amd;
|
||||||
|
const {stt_api_key, stt_region} = sttCredentials;
|
||||||
|
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
|
||||||
|
logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
|
||||||
|
sttCredentials = {...sttCredentials, access_token, stt_region};
|
||||||
|
}
|
||||||
|
|
||||||
/* 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}`);
|
||||||
if ('google' === vendor) {
|
const sttOpts = amd.setChannelVarsForStt({name: TaskName.Gather}, sttCredentials, {
|
||||||
sttOpts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(sttCredentials.credentials);
|
vendor,
|
||||||
sttOpts.GOOGLE_SPEECH_USE_ENHANCED = true;
|
hints,
|
||||||
sttOpts.GOOGLE_SPEECH_HINTS = hints.join(',');
|
enhancedModel: true,
|
||||||
if (opts.recognizer?.altLanguages) {
|
altLanguages: opts.recognizer?.altLanguages || [],
|
||||||
sttOpts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
|
initialSpeechTimeoutMs: opts.resolveTimeoutMs,
|
||||||
}
|
});
|
||||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
|
||||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, onEndOfUtterance.bind(null, cs, ep, task));
|
|
||||||
}
|
|
||||||
else if (['aws', 'polly'].includes(vendor)) {
|
|
||||||
Object.assign(sttOpts, {
|
|
||||||
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
|
|
||||||
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
|
|
||||||
AWS_REGION: sttCredentials.region
|
|
||||||
});
|
|
||||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
|
||||||
}
|
|
||||||
else if ('microsoft' === vendor) {
|
|
||||||
Object.assign(sttOpts, {
|
|
||||||
'AZURE_SUBSCRIPTION_KEY': sttCredentials.api_key,
|
|
||||||
'AZURE_REGION': sttCredentials.region
|
|
||||||
});
|
|
||||||
sttOpts.AZURE_SPEECH_HINTS = hints.join(',');
|
|
||||||
if (opts.recognizer?.altLanguages) {
|
|
||||||
sttOpts.AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = opts.recognizer.altLanguages.join(',');
|
|
||||||
}
|
|
||||||
sttOpts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = opts.resolveTimeoutMs || 20000;
|
|
||||||
|
|
||||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, onTranscription.bind(null, cs, ep, task));
|
|
||||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, onNoSpeechDetected.bind(null, cs, ep, task));
|
|
||||||
}
|
|
||||||
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
|
await ep.set(sttOpts).catch((err) => logger.info(err, 'Error setting channel variables'));
|
||||||
|
|
||||||
|
amd.transcriptionHandler = onTranscription.bind(null, cs, ep, task);
|
||||||
|
amd.EndOfUtteranceHandler = onEndOfUtterance.bind(null, cs, ep, task);
|
||||||
|
amd.noSpeechHandler = onNoSpeechDetected.bind(null, cs, ep, task);
|
||||||
|
|
||||||
|
switch (vendor) {
|
||||||
|
case 'google':
|
||||||
|
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, amd.EndOfUtteranceHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'aws':
|
||||||
|
case 'polly':
|
||||||
|
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
case 'microsoft':
|
||||||
|
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, amd.noSpeechHandler);
|
||||||
|
break;
|
||||||
|
case 'nuance':
|
||||||
|
ep.addCustomEventListener(NuanceTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deepgram':
|
||||||
|
ep.addCustomEventListener(DeepgramTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'soniox':
|
||||||
|
amd.bugname = 'soniox_amd_transcribe';
|
||||||
|
ep.addCustomEventListener(SonioxTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ibm':
|
||||||
|
ep.addCustomEventListener(IbmTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nvidia':
|
||||||
|
ep.addCustomEventListener(NvidiaTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cobalt':
|
||||||
|
ep.addCustomEventListener(CobaltTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (vendor.startsWith('custom:')) {
|
||||||
|
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, amd.transcriptionHandler);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Invalid vendor ${this.vendor}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
amd
|
amd
|
||||||
.on(AmdEvents.NoSpeechDetected, (evt) => {
|
.on(AmdEvents.NoSpeechDetected, (evt) => {
|
||||||
task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt});
|
task.emit('amd', {type: AmdEvents.NoSpeechDetected, ...evt});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.stopTranscription({vendor, bugname});
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping transcription');
|
logger.info({err}, 'Error stopping transcription');
|
||||||
}
|
}
|
||||||
@@ -281,7 +340,7 @@ module.exports = (logger) => {
|
|||||||
.on(AmdEvents.HumanDetected, (evt) => {
|
.on(AmdEvents.HumanDetected, (evt) => {
|
||||||
task.emit('amd', {type: AmdEvents.HumanDetected, ...evt});
|
task.emit('amd', {type: AmdEvents.HumanDetected, ...evt});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.stopTranscription({vendor, bugname});
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping transcription');
|
logger.info({err}, 'Error stopping transcription');
|
||||||
}
|
}
|
||||||
@@ -292,7 +351,7 @@ module.exports = (logger) => {
|
|||||||
.on(AmdEvents.DecisionTimeout, (evt) => {
|
.on(AmdEvents.DecisionTimeout, (evt) => {
|
||||||
task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt});
|
task.emit('amd', {type: AmdEvents.DecisionTimeout, ...evt});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.stopTranscription({vendor, bugname});
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping transcription');
|
logger.info({err}, 'Error stopping transcription');
|
||||||
}
|
}
|
||||||
@@ -300,7 +359,7 @@ module.exports = (logger) => {
|
|||||||
.on(AmdEvents.ToneTimeout, (evt) => {
|
.on(AmdEvents.ToneTimeout, (evt) => {
|
||||||
//task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt});
|
//task.emit('amd', {type: AmdEvents.ToneTimeout, ...evt});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.execute('avmd_stop').catch((err) => logger.info(err, 'Error stopping avmd'));
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping avmd');
|
logger.info({err}, 'Error stopping avmd');
|
||||||
}
|
}
|
||||||
@@ -308,7 +367,7 @@ module.exports = (logger) => {
|
|||||||
.on(AmdEvents.MachineStoppedSpeaking, () => {
|
.on(AmdEvents.MachineStoppedSpeaking, () => {
|
||||||
task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking});
|
task.emit('amd', {type: AmdEvents.MachineStoppedSpeaking});
|
||||||
try {
|
try {
|
||||||
ep.connected && ep.stopTranscription({vendor, bugname});
|
stopAmd(ep, task);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, 'Error stopping transcription');
|
logger.info({err}, 'Error stopping transcription');
|
||||||
}
|
}
|
||||||
@@ -327,6 +386,19 @@ module.exports = (logger) => {
|
|||||||
if (ep.amd) {
|
if (ep.amd) {
|
||||||
vendor = ep.amd.vendor;
|
vendor = ep.amd.vendor;
|
||||||
ep.amd.stopAllTimers();
|
ep.amd.stopAllTimers();
|
||||||
|
|
||||||
|
ep.removeListener(GoogleTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(GoogleTranscriptionEvents.EndOfUtterance, ep.amd.EndOfUtteranceHandler);
|
||||||
|
ep.removeListener(AwsTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(AzureTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(AzureTranscriptionEvents.NoSpeechDetected, ep.amd.noSpeechHandler);
|
||||||
|
ep.removeListener(NuanceTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(DeepgramTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(SonioxTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(IbmTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(NvidiaTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
ep.removeListener(JambonzTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||||
|
|
||||||
ep.amd = null;
|
ep.amd = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,20 @@ const {LifeCycleEvents} = require('./constants');
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const app = express();
|
const app = express();
|
||||||
const getString = bent('string');
|
const getString = bent('string');
|
||||||
const AWS = require('aws-sdk');
|
const {
|
||||||
const sns = new AWS.SNS({apiVersion: '2010-03-31'});
|
SNSClient,
|
||||||
const autoscaling = new AWS.AutoScaling({apiVersion: '2011-01-01'});
|
SubscribeCommand,
|
||||||
|
UnsubscribeCommand } = require('@aws-sdk/client-sns');
|
||||||
|
const snsClient = new SNSClient({ region: AWS_REGION, apiVersion: '2010-03-31' });
|
||||||
|
const {
|
||||||
|
AutoScalingClient,
|
||||||
|
DescribeAutoScalingGroupsCommand,
|
||||||
|
CompleteLifecycleActionCommand } = require('@aws-sdk/client-auto-scaling');
|
||||||
|
const autoScalingClient = new AutoScalingClient({ region: AWS_REGION, apiVersion: '2011-01-01' });
|
||||||
const {Parser} = require('xml2js');
|
const {Parser} = require('xml2js');
|
||||||
const parser = new Parser();
|
const parser = new Parser();
|
||||||
const {validatePayload} = require('verify-aws-sns-signature');
|
const {validatePayload} = require('verify-aws-sns-signature');
|
||||||
|
|
||||||
AWS.config.update({region: AWS_REGION});
|
|
||||||
|
|
||||||
class SnsNotifier extends Emitter {
|
class SnsNotifier extends Emitter {
|
||||||
constructor(logger) {
|
constructor(logger) {
|
||||||
super();
|
super();
|
||||||
@@ -69,7 +74,7 @@ class SnsNotifier extends Emitter {
|
|||||||
subscriptionRequestId: this.subscriptionRequestId
|
subscriptionRequestId: this.subscriptionRequestId
|
||||||
}, 'response from SNS SubscribeURL');
|
}, 'response from SNS SubscribeURL');
|
||||||
const data = await this.describeInstance();
|
const data = await this.describeInstance();
|
||||||
this.lifecycleState = data.AutoScalingInstances[0].LifecycleState;
|
this.lifecycleState = data.AutoScalingGroups[0].Instances[0].LifecycleState;
|
||||||
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -135,11 +140,12 @@ class SnsNotifier extends Emitter {
|
|||||||
|
|
||||||
async subscribe() {
|
async subscribe() {
|
||||||
try {
|
try {
|
||||||
const response = await sns.subscribe({
|
const params = {
|
||||||
Protocol: 'http',
|
Protocol: 'http',
|
||||||
TopicArn: AWS_SNS_TOPIC_ARM,
|
TopicArn: AWS_SNS_TOPIC_ARM,
|
||||||
Endpoint: this.snsEndpoint
|
Endpoint: this.snsEndpoint
|
||||||
}).promise();
|
};
|
||||||
|
const response = await snsClient.send(new SubscribeCommand(params));
|
||||||
this.logger.info({response}, `response to SNS subscribe to ${AWS_SNS_TOPIC_ARM}`);
|
this.logger.info({response}, `response to SNS subscribe to ${AWS_SNS_TOPIC_ARM}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error({err}, `Error subscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`);
|
this.logger.error({err}, `Error subscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`);
|
||||||
@@ -149,9 +155,10 @@ class SnsNotifier extends Emitter {
|
|||||||
async unsubscribe() {
|
async unsubscribe() {
|
||||||
if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription');
|
if (!this.subscriptionArn) throw new Error('SnsNotifier#unsubscribe called without an active subscription');
|
||||||
try {
|
try {
|
||||||
const response = await sns.unsubscribe({
|
const params = {
|
||||||
SubscriptionArn: this.subscriptionArn
|
SubscriptionArn: this.subscriptionArn
|
||||||
}).promise();
|
};
|
||||||
|
const response = await snsClient.send(new UnsubscribeCommand(params));
|
||||||
this.logger.info({response}, `response to SNS unsubscribe to ${AWS_SNS_TOPIC_ARM}`);
|
this.logger.info({response}, `response to SNS unsubscribe to ${AWS_SNS_TOPIC_ARM}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`);
|
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`);
|
||||||
@@ -160,26 +167,29 @@ class SnsNotifier extends Emitter {
|
|||||||
|
|
||||||
completeScaleIn() {
|
completeScaleIn() {
|
||||||
assert(this.scaleInParams);
|
assert(this.scaleInParams);
|
||||||
autoscaling.completeLifecycleAction(this.scaleInParams, (err, response) => {
|
autoScalingClient.send(new CompleteLifecycleActionCommand(this.scaleInParams))
|
||||||
if (err) return this.logger.error({err}, 'Error completing scale-in');
|
.then((data) => {
|
||||||
this.logger.info({response}, 'Successfully completed scale-in action');
|
return this.logger.info({data}, 'Successfully completed scale-in action');
|
||||||
});
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.error({err}, 'Error completing scale-in');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describeInstance() {
|
describeInstance() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.instanceId) return reject('instance-id unknown');
|
if (!this.instanceId) return reject('instance-id unknown');
|
||||||
autoscaling.describeAutoScalingInstances({
|
autoScalingClient.send(new DescribeAutoScalingGroupsCommand({
|
||||||
InstanceIds: [this.instanceId]
|
InstanceIds: [this.instanceId]
|
||||||
}, (err, data) => {
|
}))
|
||||||
if (err) {
|
.then((data) => {
|
||||||
|
this.logger.info({data}, 'SnsNotifier: describeInstance');
|
||||||
|
return resolve(data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
this.logger.error({err}, 'Error describing instances');
|
this.logger.error({err}, 'Error describing instances');
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
});
|
||||||
this.logger.info({data}, 'SnsNotifier: describeInstance');
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +203,7 @@ module.exports = async function(logger) {
|
|||||||
process.on('SIGHUP', async() => {
|
process.on('SIGHUP', async() => {
|
||||||
try {
|
try {
|
||||||
const data = await notifier.describeInstance();
|
const data = await notifier.describeInstance();
|
||||||
const state = data.AutoScalingInstances[0].LifecycleState;
|
const state = data.AutoScalingGroups[0].Instances[0].LifecycleState;
|
||||||
if (state !== notifier.lifecycleState) {
|
if (state !== notifier.lifecycleState) {
|
||||||
notifier.lifecycleState = state;
|
notifier.lifecycleState = state;
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
|||||||
@@ -2,17 +2,24 @@ 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) {
|
||||||
let tracer, callSid, linkedSpanId;
|
const {srf} = require('../../');
|
||||||
|
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 {
|
else if (req.srf) {
|
||||||
tracer = req.srf.locals.otel.tracer;
|
|
||||||
callSid = req.locals.callSid;
|
callSid = req.locals.callSid;
|
||||||
|
accountSid = req.get('X-Account-Sid'),
|
||||||
|
applicationSid = req.locals.application_sid;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callSid = req.callSid;
|
||||||
|
accountSid = req.accountSid;
|
||||||
|
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) {
|
||||||
@@ -22,13 +29,20 @@ 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: req.get('X-Account-Sid'),
|
accountSid,
|
||||||
applicationSid: req.locals.application_sid,
|
applicationSid
|
||||||
callId: req.get('Call-ID'),
|
|
||||||
externalCallId: req.get('X-CID')
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,11 @@
|
|||||||
"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",
|
||||||
@@ -90,6 +95,11 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ 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 sqlSpeechCredentials = `SELECT *
|
const sqlSpeechCredentialsForAccount = `SELECT *
|
||||||
FROM speech_credentials
|
FROM speech_credentials
|
||||||
WHERE account_sid = ? `;
|
WHERE account_sid = ? OR (account_sid is NULL AND service_provider_sid =
|
||||||
const sqlSpeechCredentialsForSP = `SELECT *
|
(SELECT service_provider_sid from accounts where account_sid = ?))`;
|
||||||
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 = ?
|
||||||
@@ -49,8 +46,10 @@ 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));
|
||||||
@@ -78,6 +77,14 @@ const speechMapper = (cred) => {
|
|||||||
const o = JSON.parse(decrypt(credential));
|
const o = JSON.parse(decrypt(credential));
|
||||||
obj.api_key = o.api_key;
|
obj.api_key = o.api_key;
|
||||||
}
|
}
|
||||||
|
else if ('nvidia' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.riva_server_uri = o.riva_server_uri;
|
||||||
|
}
|
||||||
|
else if ('cobalt' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.cobalt_server_uri = o.cobalt_server_uri;
|
||||||
|
}
|
||||||
else if (obj.vendor.startsWith('custom:')) {
|
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;
|
||||||
@@ -90,27 +97,28 @@ const speechMapper = (cred) => {
|
|||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bucketCredentialDecrypt = (account) => {
|
||||||
|
const { bucket_credential } = account.account;
|
||||||
|
if (!bucket_credential || bucket_credential.vendor) return;
|
||||||
|
account.account.bucket_credential = JSON.parse(decrypt(bucket_credential));
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = (logger, srf) => {
|
module.exports = (logger, srf) => {
|
||||||
const {pool} = srf.locals.dbHelpers;
|
const {pool} = srf.locals.dbHelpers;
|
||||||
const pp = pool.promise();
|
const pp = pool.promise();
|
||||||
|
|
||||||
const lookupAccountDetails = async(account_sid) => {
|
const lookupAccountDetails = async(account_sid) => {
|
||||||
|
|
||||||
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(sqlSpeechCredentials, account_sid);
|
const [r2] = await pp.query(sqlSpeechCredentialsForAccount, [account_sid, 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 account = r[0];
|
||||||
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
|
bucketCredentialDecrypt(account);
|
||||||
r3.forEach((s) => {
|
|
||||||
if (!speech.find((s2) => s2.vendor === s.vendor)) {
|
|
||||||
speech.push(speechMapper(s));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...r[0],
|
...account,
|
||||||
speech
|
speech
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ 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;
|
||||||
logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
|
srf.locals.serviceUrl = `http://${srf.locals.ipv4}:${port}`;
|
||||||
|
logger.info(`listening for HTTP requests on port ${port}, serviceUrl is ${srf.locals.serviceUrl}`);
|
||||||
resolve({server, app});
|
resolve({server, app});
|
||||||
});
|
});
|
||||||
return server;
|
return server;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const {
|
|||||||
JAMBONES_FREESWITCH,
|
JAMBONES_FREESWITCH,
|
||||||
JAMBONES_REDIS_HOST,
|
JAMBONES_REDIS_HOST,
|
||||||
JAMBONES_REDIS_PORT,
|
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,
|
||||||
@@ -146,7 +147,7 @@ function installSrfLocals(srf, logger) {
|
|||||||
password: JAMBONES_MYSQL_PASSWORD,
|
password: JAMBONES_MYSQL_PASSWORD,
|
||||||
database: JAMBONES_MYSQL_DATABASE,
|
database: JAMBONES_MYSQL_DATABASE,
|
||||||
connectionLimit: JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
connectionLimit: JAMBONES_MYSQL_CONNECTION_LIMIT || 10
|
||||||
}, logger, tracer);
|
}, logger);
|
||||||
const {
|
const {
|
||||||
client,
|
client,
|
||||||
updateCallStatus,
|
updateCallStatus,
|
||||||
@@ -167,7 +168,12 @@ function installSrfLocals(srf, logger) {
|
|||||||
removeFromList,
|
removeFromList,
|
||||||
getListPosition,
|
getListPosition,
|
||||||
lengthOfList,
|
lengthOfList,
|
||||||
} = require('@jambonz/realtimedb-helpers')({
|
addToSortedSet,
|
||||||
|
retrieveFromSortedSet,
|
||||||
|
retrieveByPatternSortedSet,
|
||||||
|
sortedSetLength,
|
||||||
|
sortedSetPositionByPattern
|
||||||
|
} = require('@jambonz/realtimedb-helpers')(JAMBONES_REDIS_SENTINELS || {
|
||||||
host: JAMBONES_REDIS_HOST,
|
host: JAMBONES_REDIS_HOST,
|
||||||
port: JAMBONES_REDIS_PORT || 6379
|
port: JAMBONES_REDIS_PORT || 6379
|
||||||
}, logger, tracer);
|
}, logger, tracer);
|
||||||
@@ -175,7 +181,7 @@ function installSrfLocals(srf, logger) {
|
|||||||
synthAudio,
|
synthAudio,
|
||||||
getNuanceAccessToken,
|
getNuanceAccessToken,
|
||||||
getIbmAccessToken,
|
getIbmAccessToken,
|
||||||
} = require('@jambonz/speech-utils')({
|
} = require('@jambonz/speech-utils')(JAMBONES_REDIS_SENTINELS || {
|
||||||
host: JAMBONES_REDIS_HOST,
|
host: JAMBONES_REDIS_HOST,
|
||||||
port: JAMBONES_REDIS_PORT || 6379
|
port: JAMBONES_REDIS_PORT || 6379
|
||||||
}, logger, tracer);
|
}, logger, tracer);
|
||||||
@@ -228,7 +234,12 @@ function installSrfLocals(srf, logger) {
|
|||||||
lengthOfList,
|
lengthOfList,
|
||||||
getListPosition,
|
getListPosition,
|
||||||
getNuanceAccessToken,
|
getNuanceAccessToken,
|
||||||
getIbmAccessToken
|
getIbmAccessToken,
|
||||||
|
addToSortedSet,
|
||||||
|
retrieveFromSortedSet,
|
||||||
|
retrieveByPatternSortedSet,
|
||||||
|
sortedSetLength,
|
||||||
|
sortedSetPositionByPattern
|
||||||
},
|
},
|
||||||
parentLogger: logger,
|
parentLogger: logger,
|
||||||
getSBC,
|
getSBC,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const RootSpan = require('./call-tracer');
|
|||||||
const uuidv4 = require('uuid-random');
|
const uuidv4 = require('uuid-random');
|
||||||
|
|
||||||
class SingleDialer extends Emitter {
|
class SingleDialer extends Emitter {
|
||||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) {
|
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask}) {
|
||||||
super();
|
super();
|
||||||
assert(target.type);
|
assert(target.type);
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ 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));
|
||||||
}
|
}
|
||||||
@@ -45,6 +46,10 @@ class SingleDialer extends Emitter {
|
|||||||
return this.callInfo.callStatus;
|
return this.callInfo.callStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get applicationSid() {
|
||||||
|
return this.application?.application_sid || this.callInfo?.applicationSid;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* can be used for all http requests within this session
|
* can be used for all http requests within this session
|
||||||
*/
|
*/
|
||||||
@@ -243,9 +248,14 @@ class SingleDialer extends Emitter {
|
|||||||
.on('modify', async(req, res) => {
|
.on('modify', async(req, res) => {
|
||||||
try {
|
try {
|
||||||
if (this.ep) {
|
if (this.ep) {
|
||||||
const newSdp = await this.ep.modify(req.body);
|
if (this.dialTask && this.dialTask.isOnHold) {
|
||||||
res.send(200, {body: newSdp});
|
this.logger.info('dial is onhold, emit event');
|
||||||
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
this.emit('reinvite', req, res);
|
||||||
|
} 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');
|
||||||
@@ -426,11 +436,11 @@ class SingleDialer extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function placeOutdial({
|
function placeOutdial({
|
||||||
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan
|
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask
|
||||||
}) {
|
}) {
|
||||||
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
|
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask
|
||||||
});
|
});
|
||||||
sd.exec(srf, ms, myOpts);
|
sd.exec(srf, ms, myOpts);
|
||||||
return sd;
|
return sd;
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ module.exports = (logger) => {
|
|||||||
method: 'OPTIONS',
|
method: 'OPTIONS',
|
||||||
headers: {
|
headers: {
|
||||||
'X-FS-Status': ms && !dryUpCalls ? 'open' : 'closed',
|
'X-FS-Status': ms && !dryUpCalls ? 'open' : 'closed',
|
||||||
'X-FS-Calls': srf.locals.sessionTracker.count
|
'X-FS-Calls': srf.locals.sessionTracker.count,
|
||||||
|
'X-FS-ServiceUrl': srf.locals.serviceUrl
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
req.on('response', (res) => {
|
req.on('response', (res) => {
|
||||||
|
|||||||
7
lib/utils/sdp-utils.js
Normal file
7
lib/utils/sdp-utils.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const isOnhold = (sdp) => {
|
||||||
|
return sdp && (sdp.includes('a=sendonly') || sdp.includes('a=inactive'));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isOnhold
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ const {
|
|||||||
DeepgramTranscriptionEvents,
|
DeepgramTranscriptionEvents,
|
||||||
SonioxTranscriptionEvents,
|
SonioxTranscriptionEvents,
|
||||||
NvidiaTranscriptionEvents,
|
NvidiaTranscriptionEvents,
|
||||||
|
CobaltTranscriptionEvents,
|
||||||
JambonzTranscriptionEvents
|
JambonzTranscriptionEvents
|
||||||
} = require('./constants');
|
} = require('./constants');
|
||||||
|
|
||||||
@@ -92,6 +93,11 @@ 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'
|
||||||
@@ -225,12 +231,37 @@ const normalizeGoogle = (evt, channel, language) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeCustom = (evt, channel, language) => {
|
const normalizeCobalt = (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: [evt.alternatives[0]]
|
alternatives,
|
||||||
|
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
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -312,9 +343,11 @@ 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);
|
||||||
default:
|
default:
|
||||||
if (vendor.startsWith('custom:')) {
|
if (vendor.startsWith('custom:')) {
|
||||||
return normalizeCustom(evt, channel, language);
|
return normalizeCustom(evt, channel, language, vendor);
|
||||||
}
|
}
|
||||||
logger.error(`Unknown vendor ${vendor}`);
|
logger.error(`Unknown vendor ${vendor}`);
|
||||||
return evt;
|
return evt;
|
||||||
@@ -356,12 +389,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}),
|
||||||
...(rOpts.altLanguages.length > 0 &&
|
...(rOpts.altLanguages?.length > 0 &&
|
||||||
{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}),
|
||||||
@@ -386,14 +419,16 @@ module.exports = (logger) => {
|
|||||||
else if ('microsoft' === vendor) {
|
else if ('microsoft' === vendor) {
|
||||||
opts = {
|
opts = {
|
||||||
...opts,
|
...opts,
|
||||||
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
...(rOpts.hints && 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 && 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(',')}),
|
||||||
...(rOpts.altLanguages && rOpts.altLanguages.length > 0 &&
|
...(rOpts.altLanguages && rOpts.altLanguages.length > 0 &&
|
||||||
{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}),
|
||||||
@@ -401,11 +436,11 @@ module.exports = (logger) => {
|
|||||||
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
|
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
|
||||||
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
|
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
|
||||||
...(sttCredentials && {
|
...(sttCredentials && {
|
||||||
AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key,
|
...(sttCredentials.api_key && {AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key}),
|
||||||
AZURE_REGION: sttCredentials.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) {
|
||||||
@@ -549,6 +584,7 @@ module.exports = (logger) => {
|
|||||||
}
|
}
|
||||||
else if ('nvidia' === vendor) {
|
else if ('nvidia' === vendor) {
|
||||||
const {nvidiaOptions = {}} = rOpts;
|
const {nvidiaOptions = {}} = rOpts;
|
||||||
|
const rivaUri = nvidiaOptions.rivaUri || sttCredentials.riva_server_uri;
|
||||||
opts = {
|
opts = {
|
||||||
...opts,
|
...opts,
|
||||||
...((nvidiaOptions.profanityFilter || rOpts.profanityFilter) && {NVIDIA_PROFANITY_FILTER: 1}),
|
...((nvidiaOptions.profanityFilter || rOpts.profanityFilter) && {NVIDIA_PROFANITY_FILTER: 1}),
|
||||||
@@ -560,7 +596,7 @@ module.exports = (logger) => {
|
|||||||
...(nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: nvidiaOptions.maxAlternatives}),
|
...(nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: nvidiaOptions.maxAlternatives}),
|
||||||
...(!nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: 1}),
|
...(!nvidiaOptions.maxAlternatives && {NVIDIA_MAX_ALTERNATIVES: 1}),
|
||||||
...(rOpts.model && {NVIDIA_MODEL: rOpts.model}),
|
...(rOpts.model && {NVIDIA_MODEL: rOpts.model}),
|
||||||
...(nvidiaOptions.rivaUri && {NVIDIA_RIVA_URI: nvidiaOptions.rivaUri}),
|
...(rivaUri && {NVIDIA_RIVA_URI: rivaUri}),
|
||||||
...(nvidiaOptions.verbatimTranscripts && {NVIDIA_VERBATIM_TRANSCRIPTS: 1}),
|
...(nvidiaOptions.verbatimTranscripts && {NVIDIA_VERBATIM_TRANSCRIPTS: 1}),
|
||||||
...(rOpts.diarization && {NVIDIA_SPEAKER_DIARIZATION: 1}),
|
...(rOpts.diarization && {NVIDIA_SPEAKER_DIARIZATION: 1}),
|
||||||
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
|
...(rOpts.diarization && rOpts.diarizationMaxSpeakers > 0 &&
|
||||||
@@ -576,6 +612,25 @@ 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 (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;
|
||||||
@@ -625,6 +680,9 @@ module.exports = (logger) => {
|
|||||||
|
|
||||||
ep.removeCustomEventListener(SonioxTranscriptionEvents.Transcription);
|
ep.removeCustomEventListener(SonioxTranscriptionEvents.Transcription);
|
||||||
|
|
||||||
|
ep.removeCustomEventListener(CobaltTranscriptionEvents.Transcription);
|
||||||
|
ep.removeCustomEventListener(CobaltTranscriptionEvents.CompileContext);
|
||||||
|
|
||||||
ep.removeCustomEventListener(NvidiaTranscriptionEvents.Transcription);
|
ep.removeCustomEventListener(NvidiaTranscriptionEvents.Transcription);
|
||||||
ep.removeCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete);
|
ep.removeCustomEventListener(NvidiaTranscriptionEvents.TranscriptionComplete);
|
||||||
ep.removeCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech);
|
ep.removeCustomEventListener(NvidiaTranscriptionEvents.StartOfSpeech);
|
||||||
@@ -656,6 +714,10 @@ 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 {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const Websocket = require('ws');
|
|||||||
const snakeCaseKeys = require('./snakecase-keys');
|
const snakeCaseKeys = require('./snakecase-keys');
|
||||||
const {
|
const {
|
||||||
RESPONSE_TIMEOUT_MS,
|
RESPONSE_TIMEOUT_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
|
||||||
@@ -42,6 +43,7 @@ class WsRequestor extends BaseRequestor {
|
|||||||
async request(type, hook, params, httpHeaders = {}) {
|
async request(type, hook, params, httpHeaders = {}) {
|
||||||
assert(HookMsgTypes.includes(type));
|
assert(HookMsgTypes.includes(type));
|
||||||
const url = hook.url || hook;
|
const url = hook.url || hook;
|
||||||
|
const wantsAck = !['call:status', 'verb:status', 'jambonz:error'].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');
|
||||||
@@ -72,11 +74,19 @@ 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`);
|
||||||
this.queuedMsg.push({type, hook, params, httpHeaders});
|
if (wantsAck) {
|
||||||
|
const p = new Promise((resolve, reject) => {
|
||||||
|
this.queuedMsg.push({type, hook, params, httpHeaders, promise: {resolve, reject}});
|
||||||
|
});
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.queuedMsg.push({type, hook, params, httpHeaders});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.connectInProgress = true;
|
this.connectInProgress = true;
|
||||||
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection`);
|
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection for ${type}`);
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
@@ -115,9 +125,14 @@ class WsRequestor extends BaseRequestor {
|
|||||||
|
|
||||||
const sendQueuedMsgs = () => {
|
const sendQueuedMsgs = () => {
|
||||||
if (this.queuedMsg.length > 0) {
|
if (this.queuedMsg.length > 0) {
|
||||||
for (const {type, hook, params, httpHeaders} of this.queuedMsg) {
|
for (const {type, hook, params, httpHeaders, promise} of this.queuedMsg) {
|
||||||
this.logger.debug(`WsRequestor:request - preparing queued ${type} for sending`);
|
this.logger.debug(`WsRequestor:request - preparing queued ${type} for sending`);
|
||||||
setImmediate(this.request.bind(this, type, hook, params, httpHeaders));
|
if (promise) {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -136,8 +151,8 @@ class WsRequestor extends BaseRequestor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* simple notifications */
|
/* simple notifications */
|
||||||
if (['call:status', 'verb:status', 'jambonz:error'].includes(type) || reconnectingWithoutAck) {
|
if (!wantsAck || 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();
|
||||||
});
|
});
|
||||||
@@ -178,9 +193,17 @@ class WsRequestor extends BaseRequestor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_stopPingTimer() {
|
||||||
|
if (this._pingTimer) {
|
||||||
|
clearInterval(this._pingTimer);
|
||||||
|
this._pingTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.closedGracefully = true;
|
this.closedGracefully = true;
|
||||||
this.logger.debug('WsRequestor:close closing socket');
|
this.logger.debug('WsRequestor:close closing socket');
|
||||||
|
this._stopPingTimer();
|
||||||
try {
|
try {
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.close(1000);
|
this.ws.close(1000);
|
||||||
@@ -195,6 +218,7 @@ class WsRequestor extends BaseRequestor {
|
|||||||
|
|
||||||
_connect() {
|
_connect() {
|
||||||
assert(!this.ws);
|
assert(!this.ws);
|
||||||
|
this._stopPingTimer();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const handshakeTimeout = JAMBONES_WS_HANDSHAKE_TIMEOUT_MS ?
|
const handshakeTimeout = JAMBONES_WS_HANDSHAKE_TIMEOUT_MS ?
|
||||||
parseInt(JAMBONES_WS_HANDSHAKE_TIMEOUT_MS) :
|
parseInt(JAMBONES_WS_HANDSHAKE_TIMEOUT_MS) :
|
||||||
@@ -255,10 +279,15 @@ class WsRequestor extends BaseRequestor {
|
|||||||
this.connectInProgress = false;
|
this.connectInProgress = false;
|
||||||
this.connections++;
|
this.connections++;
|
||||||
this.emit('ready', ws);
|
this.emit('ready', ws);
|
||||||
|
|
||||||
|
if (JAMBONES_WS_PING_INTERVAL_MS > 15000) {
|
||||||
|
this._pingTimer = setInterval(() => this.ws?.ping(), JAMBONES_WS_PING_INTERVAL_MS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClose(code) {
|
_onClose(code) {
|
||||||
this.logger.info(`WsRequestor(${this.id}) - closed from far end ${code}`);
|
this.logger.info(`WsRequestor(${this.id}) - closed from far end ${code}`);
|
||||||
|
this._stopPingTimer();
|
||||||
if (this.connections > 0 && code !== 1000) {
|
if (this.connections > 0 && code !== 1000) {
|
||||||
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
|
this.logger.info({url: this.url}, 'WsRequestor - socket closed unexpectedly from remote side');
|
||||||
this.emit('socket-closed');
|
this.emit('socket-closed');
|
||||||
@@ -283,6 +312,7 @@ class WsRequestor extends BaseRequestor {
|
|||||||
_onSocketClosed() {
|
_onSocketClosed() {
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
this.emit('connection-dropped');
|
this.emit('connection-dropped');
|
||||||
|
this._stopPingTimer();
|
||||||
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) {
|
if (this.connections > 0 && this.connections < MAX_RECONNECTS && !this.closedGracefully) {
|
||||||
if (!this._initMsgId) this._clearPendingMessages();
|
if (!this._initMsgId) this._clearPendingMessages();
|
||||||
this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`);
|
this.logger.debug(`WsRequestor:_onSocketClosed waiting ${this.backoffMs} to reconnect`);
|
||||||
|
|||||||
9182
package-lock.json
generated
9182
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jambonz-feature-server",
|
"name": "jambonz-feature-server",
|
||||||
"version": "v0.8.2",
|
"version": "0.8.4",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.16.0"
|
"node": ">= 10.16.0"
|
||||||
@@ -19,19 +19,21 @@
|
|||||||
"bugs": {},
|
"bugs": {},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node app",
|
"start": "node app",
|
||||||
"test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=1 ENCRYPTION_SECRET=foobar DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=error ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:JambonzR0ck$:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
|
"test": "NODE_ENV=test JAMBONES_HOSTING=1 HTTP_POOL=1 JAMBONES_TTS_TRIM_SILENCE=1 ENCRYPTION_SECRET=foobar DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=error ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:JambonzR0ck$:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
|
||||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||||
"jslint": "eslint app.js tracer.js lib",
|
"jslint": "eslint app.js tracer.js lib",
|
||||||
"jslint:fix": "eslint app.js tracer.js lib --fix"
|
"jslint:fix": "eslint app.js tracer.js lib --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jambonz/db-helpers": "^0.7.4",
|
"@aws-sdk/client-auto-scaling": "^3.360.0",
|
||||||
|
"@aws-sdk/client-sns": "^3.360.0",
|
||||||
|
"@jambonz/db-helpers": "^0.9.1",
|
||||||
"@jambonz/http-health-check": "^0.0.1",
|
"@jambonz/http-health-check": "^0.0.1",
|
||||||
"@jambonz/realtimedb-helpers": "^0.7.0",
|
"@jambonz/realtimedb-helpers": "^0.8.6",
|
||||||
"@jambonz/speech-utils": "^0.0.12",
|
"@jambonz/speech-utils": "^0.0.21",
|
||||||
"@jambonz/stats-collector": "^0.1.8",
|
"@jambonz/stats-collector": "^0.1.9",
|
||||||
"@jambonz/time-series": "^0.2.5",
|
"@jambonz/time-series": "^0.2.8",
|
||||||
"@jambonz/verb-specifications": "^0.0.17",
|
"@jambonz/verb-specifications": "^0.0.34",
|
||||||
"@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",
|
||||||
@@ -41,12 +43,11 @@
|
|||||||
"@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": "^2.1313.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.21",
|
"drachtio-fsmrf": "^3.0.27",
|
||||||
"drachtio-srf": "^4.5.23",
|
"drachtio-srf": "^4.5.26",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"ip": "^1.1.8",
|
"ip": "^1.1.8",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
"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.5.0"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"clear-module": "^4.1.2",
|
"clear-module": "^4.1.2",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ test('\'config: listen\'', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||||
@@ -86,7 +86,7 @@ test('\'config: listen - stop\'', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||||
|
|||||||
@@ -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.us/hello-world",
|
"url": "https://public-apps.jambonz.cloud/hello-world",
|
||||||
"method": "POST"
|
"method": "POST"
|
||||||
},
|
},
|
||||||
"from": "15083718299",
|
"from": "15083718299",
|
||||||
@@ -88,11 +88,11 @@ test('test create-call call-hook basic authentication', async(t) => {
|
|||||||
|
|
||||||
let verbs = [
|
let verbs = [
|
||||||
{
|
{
|
||||||
"verb": "say",
|
"verb": "pause",
|
||||||
"text": "hello"
|
"length": 1
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
//THEN
|
//THEN
|
||||||
await p;
|
await p;
|
||||||
|
|
||||||
@@ -106,3 +106,117 @@ test('test create-call call-hook basic authentication', async(t) => {
|
|||||||
t.error(err);
|
t.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('test create-call amd', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
let from = 'create-call-amd';
|
||||||
|
let account_sid = 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f';
|
||||||
|
|
||||||
|
// Give UAS app time to come up
|
||||||
|
const p = sippUac('uas.xml', '172.38.0.10', from);
|
||||||
|
await waitFor(1000);
|
||||||
|
|
||||||
|
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||||
|
post('v1/createCall', {
|
||||||
|
'account_sid':account_sid,
|
||||||
|
"call_hook": {
|
||||||
|
"url": "http://127.0.0.1:3100/",
|
||||||
|
"method": "POST",
|
||||||
|
"username": "username",
|
||||||
|
"password": "password"
|
||||||
|
},
|
||||||
|
"from": from,
|
||||||
|
"to": {
|
||||||
|
"type": "phone",
|
||||||
|
"number": "15583084809"
|
||||||
|
},
|
||||||
|
"amd": {
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
},
|
||||||
|
"speech_recognizer_vendor": "google",
|
||||||
|
"speech_recognizer_language": "en"
|
||||||
|
});
|
||||||
|
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "pause",
|
||||||
|
"length": 7
|
||||||
|
}
|
||||||
|
];
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
//THEN
|
||||||
|
await p;
|
||||||
|
|
||||||
|
let obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`)
|
||||||
|
t.ok(obj.body.type = 'amd_no_speech_detected',
|
||||||
|
'create-call: AMD detected');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test create-call app_json', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
let from = 'create-call-app-json';
|
||||||
|
let account_sid = 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f';
|
||||||
|
|
||||||
|
// Give UAS app time to come up
|
||||||
|
const p = sippUac('uas.xml', '172.38.0.10', from);
|
||||||
|
await waitFor(1000);
|
||||||
|
|
||||||
|
const app_json = `[
|
||||||
|
{
|
||||||
|
"verb": "pause",
|
||||||
|
"length": 7
|
||||||
|
}
|
||||||
|
]`;
|
||||||
|
|
||||||
|
const post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||||
|
post('v1/createCall', {
|
||||||
|
'account_sid':account_sid,
|
||||||
|
"call_hook": {
|
||||||
|
"url": "http://127.0.0.1:3100/",
|
||||||
|
"method": "POST",
|
||||||
|
"username": "username",
|
||||||
|
"password": "password"
|
||||||
|
},
|
||||||
|
app_json,
|
||||||
|
"from": from,
|
||||||
|
"to": {
|
||||||
|
"type": "phone",
|
||||||
|
"number": "15583084809"
|
||||||
|
},
|
||||||
|
"amd": {
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
},
|
||||||
|
"speech_recognizer_vendor": "google",
|
||||||
|
"speech_recognizer_language": "en"
|
||||||
|
});
|
||||||
|
|
||||||
|
//THEN
|
||||||
|
await p;
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
/* SQLEditor (MySQL (2))*/
|
/* SQLEditor (MySQL (2))*/
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS=0;
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS account_static_ips;
|
DROP TABLE IF EXISTS account_static_ips;
|
||||||
@@ -14,8 +13,12 @@ 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_carrier_set_entry;
|
DROP TABLE IF EXISTS lcr_carrier_set_entry;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS lcr_routes;
|
DROP TABLE IF EXISTS lcr_routes;
|
||||||
@@ -52,6 +55,8 @@ DROP TABLE IF EXISTS smpp_addresses;
|
|||||||
|
|
||||||
DROP TABLE IF EXISTS speech_credentials;
|
DROP TABLE IF EXISTS speech_credentials;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS system_information;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS users;
|
DROP TABLE IF EXISTS users;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS smpp_gateways;
|
DROP TABLE IF EXISTS smpp_gateways;
|
||||||
@@ -124,6 +129,16 @@ 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 ,
|
||||||
@@ -136,11 +151,23 @@ PRIMARY KEY (dns_record_sid)
|
|||||||
CREATE TABLE lcr_routes
|
CREATE TABLE lcr_routes
|
||||||
(
|
(
|
||||||
lcr_route_sid CHAR(36),
|
lcr_route_sid CHAR(36),
|
||||||
|
lcr_sid CHAR(36) NOT NULL,
|
||||||
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
|
regex VARCHAR(32) NOT NULL COMMENT 'regex-based pattern match against dialed number, used for LCR routing of PSTN calls',
|
||||||
description VARCHAR(1024),
|
description VARCHAR(1024),
|
||||||
priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted first',
|
priority INTEGER NOT NULL COMMENT 'lower priority routes are attempted first',
|
||||||
PRIMARY KEY (lcr_route_sid)
|
PRIMARY KEY (lcr_route_sid)
|
||||||
) COMMENT='Least cost routing table';
|
) COMMENT='An ordered list of digit patterns in an LCR table. The patterns are tested in sequence until one matches';
|
||||||
|
|
||||||
|
CREATE TABLE lcr
|
||||||
|
(
|
||||||
|
lcr_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
|
name VARCHAR(64) COMMENT 'User-assigned name for this LCR table',
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
default_carrier_set_entry_sid CHAR(36) COMMENT 'default carrier/route to use when no digit match based results are found.',
|
||||||
|
service_provider_sid CHAR(36),
|
||||||
|
account_sid CHAR(36),
|
||||||
|
PRIMARY KEY (lcr_sid)
|
||||||
|
) COMMENT='An LCR (least cost routing) table that is used by a service provider or account to make decisions about routing outbound calls when multiple carriers are available.';
|
||||||
|
|
||||||
CREATE TABLE password_settings
|
CREATE TABLE password_settings
|
||||||
(
|
(
|
||||||
@@ -248,7 +275,10 @@ CREATE TABLE sbc_addresses
|
|||||||
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
sbc_address_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
ipv4 VARCHAR(255) NOT NULL,
|
ipv4 VARCHAR(255) NOT NULL,
|
||||||
port INTEGER NOT NULL DEFAULT 5060,
|
port INTEGER NOT NULL DEFAULT 5060,
|
||||||
|
tls_port INTEGER,
|
||||||
|
wss_port INTEGER,
|
||||||
service_provider_sid CHAR(36),
|
service_provider_sid CHAR(36),
|
||||||
|
last_updated DATETIME,
|
||||||
PRIMARY KEY (sbc_address_sid)
|
PRIMARY KEY (sbc_address_sid)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -304,9 +334,17 @@ 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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE system_information
|
||||||
|
(
|
||||||
|
domain_name VARCHAR(255),
|
||||||
|
sip_domain_name VARCHAR(255),
|
||||||
|
monitoring_domain_name VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE users
|
CREATE TABLE users
|
||||||
(
|
(
|
||||||
user_sid CHAR(36) NOT NULL UNIQUE ,
|
user_sid CHAR(36) NOT NULL UNIQUE ,
|
||||||
@@ -357,6 +395,7 @@ smpp_inbound_password VARCHAR(64),
|
|||||||
register_from_user VARCHAR(128),
|
register_from_user VARCHAR(128),
|
||||||
register_from_domain VARCHAR(255),
|
register_from_domain VARCHAR(255),
|
||||||
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
|
register_public_ip_in_contact BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
register_status VARCHAR(4096),
|
||||||
PRIMARY KEY (voip_carrier_sid)
|
PRIMARY KEY (voip_carrier_sid)
|
||||||
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
) COMMENT='A Carrier or customer PBX that can send or receive calls';
|
||||||
|
|
||||||
@@ -385,7 +424,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 UNIQUE ,
|
number VARCHAR(132) NOT NULL,
|
||||||
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),
|
||||||
@@ -403,6 +442,7 @@ inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound ca
|
|||||||
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN',
|
||||||
voip_carrier_sid CHAR(36) NOT NULL,
|
voip_carrier_sid CHAR(36) NOT NULL,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
protocol ENUM('udp','tcp','tls', 'tls/srtp') DEFAULT 'udp' COMMENT 'Outbound call protocol',
|
||||||
PRIMARY KEY (sip_gateway_sid)
|
PRIMARY KEY (sip_gateway_sid)
|
||||||
) COMMENT='A whitelisted sip gateway used for origination/termination';
|
) COMMENT='A whitelisted sip gateway used for origination/termination';
|
||||||
|
|
||||||
@@ -435,13 +475,16 @@ account_sid CHAR(36) COMMENT 'account that this application belongs to (if null,
|
|||||||
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
|
call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ',
|
||||||
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
|
call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events',
|
||||||
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
|
messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ',
|
||||||
app_json VARCHAR(16384),
|
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 ';
|
||||||
|
|
||||||
@@ -479,6 +522,9 @@ 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';
|
||||||
|
|
||||||
@@ -499,9 +545,20 @@ 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);
|
||||||
|
|
||||||
|
CREATE INDEX lcr_sid_idx ON lcr_routes (lcr_sid);
|
||||||
|
ALTER TABLE lcr_routes ADD FOREIGN KEY lcr_sid_idxfk (lcr_sid) REFERENCES lcr (lcr_sid);
|
||||||
|
|
||||||
|
CREATE INDEX lcr_sid_idx ON lcr (lcr_sid);
|
||||||
|
ALTER TABLE lcr ADD FOREIGN KEY default_carrier_set_entry_sid_idxfk (default_carrier_set_entry_sid) REFERENCES lcr_carrier_set_entry (lcr_carrier_set_entry_sid);
|
||||||
|
|
||||||
|
CREATE INDEX service_provider_sid_idx ON lcr (service_provider_sid);
|
||||||
|
CREATE INDEX account_sid_idx ON lcr (account_sid);
|
||||||
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
|
CREATE INDEX permission_sid_idx ON permissions (permission_sid);
|
||||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
|
CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid);
|
||||||
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
|
CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid);
|
||||||
@@ -555,8 +612,6 @@ 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);
|
||||||
@@ -593,6 +648,8 @@ 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);
|
||||||
@@ -647,5 +704,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
|
|||||||
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
||||||
|
|
||||||
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS=1;
|
SET FOREIGN_KEY_CHECKS=1;
|
||||||
@@ -5,6 +5,8 @@ const getJSON = bent('json')
|
|||||||
const clearModule = require('clear-module');
|
const clearModule = require('clear-module');
|
||||||
const {provisionCallHook} = require('./utils')
|
const {provisionCallHook} = require('./utils')
|
||||||
|
|
||||||
|
const sleepFor = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, p) => {
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||||
});
|
});
|
||||||
@@ -43,10 +45,11 @@ test('\'dial-phone\'', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
||||||
|
await sleepFor(1000);
|
||||||
|
|
||||||
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
|
let account_sid = '622f62e4-303a-49f2-bbe0-eb1e1714e37a';
|
||||||
let post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
let post = bent('http://127.0.0.1:3000/', 'POST', 'json', 201);
|
||||||
@@ -84,7 +87,7 @@ test('\'dial-sip\'', async(t) => {
|
|||||||
try {
|
try {
|
||||||
await connect(srf);
|
await connect(srf);
|
||||||
// wait for fs connected to drachtio server.
|
// wait for fs connected to drachtio server.
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await sleepFor(1000);
|
||||||
// GIVEN
|
// GIVEN
|
||||||
const from = "dial_sip";
|
const from = "dial_sip";
|
||||||
let verbs = [
|
let verbs = [
|
||||||
@@ -102,7 +105,7 @@ test('\'dial-sip\'', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
||||||
@@ -169,7 +172,7 @@ test('\'dial-user\'', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
const p = sippUac('uas-dial.xml', '172.38.0.10', undefined, undefined, 2);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ services:
|
|||||||
ipv4_address: 172.38.0.7
|
ipv4_address: 172.38.0.7
|
||||||
|
|
||||||
drachtio:
|
drachtio:
|
||||||
image: drachtio/drachtio-server:latest
|
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.4.18
|
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:
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ test('\'gather\' test - google', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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`);
|
||||||
@@ -86,7 +86,7 @@ test('\'gather\' test - default (google)', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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`);
|
||||||
@@ -102,6 +102,53 @@ test('\'gather\' test - default (google)', async(t) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('\'config\' test - reset to app defaults', async(t) => {
|
||||||
|
if (!GCP_JSON_KEY) {
|
||||||
|
t.pass('skipping config tests');
|
||||||
|
return t.end();
|
||||||
|
}
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
let verbs = [
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"recognizer": {
|
||||||
|
"vendor": "google",
|
||||||
|
"language": "fr-FR"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "config",
|
||||||
|
"reset": ['recognizer'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"verb": "gather",
|
||||||
|
"input": ["speech"],
|
||||||
|
"timeout": 10,
|
||||||
|
"actionHook": "/actionHook"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
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() === 'i\'d like to speak to customer support',
|
||||||
|
'config: resets recognizer to app defaults');
|
||||||
|
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('\'gather\' test - microsoft', async(t) => {
|
test('\'gather\' test - microsoft', async(t) => {
|
||||||
if (!MICROSOFT_REGION || !MICROSOFT_API_KEY) {
|
if (!MICROSOFT_REGION || !MICROSOFT_API_KEY) {
|
||||||
t.pass('skipping microsoft tests');
|
t.pass('skipping microsoft tests');
|
||||||
@@ -126,7 +173,7 @@ test('\'gather\' test - microsoft', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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`);
|
||||||
@@ -166,7 +213,7 @@ test('\'gather\' test - aws', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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`);
|
||||||
@@ -209,12 +256,12 @@ test('\'gather\' test - deepgram', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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));
|
//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().includes('like to speak to customer support'),
|
||||||
'gather: succeeds when using deepgram credentials');
|
'gather: succeeds when using deepgram credentials');
|
||||||
disconnect();
|
disconnect();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -251,7 +298,7 @@ test('\'gather\' test - soniox', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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`);
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ require('./play-tests');
|
|||||||
require('./sip-refer-tests');
|
require('./sip-refer-tests');
|
||||||
require('./listen-tests');
|
require('./listen-tests');
|
||||||
require('./config-test');
|
require('./config-test');
|
||||||
|
require('./queue-test');
|
||||||
require('./remove-test-db');
|
require('./remove-test-db');
|
||||||
require('./docker_stop');
|
require('./docker_stop');
|
||||||
@@ -35,7 +35,7 @@ test('\'listen-success\'', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-gather-account-creds-success-send-bye.xml', '172.38.0.10', from);
|
||||||
@@ -57,7 +57,7 @@ test('\'listen-success\'', async(t) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('\'listen-maxLength\'', async(t) => {
|
test.skip('\'listen-maxLength\'', async(t) => {
|
||||||
clearModule.all();
|
clearModule.all();
|
||||||
const {srf, disconnect} = require('../app');
|
const {srf, disconnect} = require('../app');
|
||||||
try {
|
try {
|
||||||
@@ -69,13 +69,13 @@ test('\'listen-maxLength\'', async(t) => {
|
|||||||
{
|
{
|
||||||
"verb": "listen",
|
"verb": "listen",
|
||||||
"url": `ws://172.38.0.60:3000/${from}`,
|
"url": `ws://172.38.0.60:3000/${from}`,
|
||||||
"mixType" : "mixed",
|
"mixType" : "stereo",
|
||||||
"timeout": 2,
|
"timeout": 2,
|
||||||
"maxLength": 2
|
"maxLength": 2
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
// 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);
|
||||||
@@ -109,7 +109,7 @@ test('\'listen-pause-resume\'', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
const p = sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
const p = sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ test('\'play\' tests single link in plain text', async(t) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const from = 'play_single_link';
|
const from = 'play_single_link';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
@@ -62,7 +62,7 @@ test('\'play\' tests multi links in array', async(t) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const from = 'play_multi_links_in_array';
|
const from = 'play_multi_links_in_array';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
@@ -100,8 +100,8 @@ test('\'play\' tests single link in conference', async(t) => {
|
|||||||
waitHook: `/customHook`
|
waitHook: `/customHook`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
provisionCustomHook(from, waitHookVerbs)
|
await provisionCustomHook(from, waitHookVerbs)
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
||||||
@@ -141,8 +141,8 @@ test('\'play\' tests multi links in array in conference', async(t) => {
|
|||||||
waitHook: `/customHook`
|
waitHook: `/customHook`
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
provisionCustomHook(from, waitHookVerbs)
|
await provisionCustomHook(from, waitHookVerbs)
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-send-bye.xml', '172.38.0.10', from);
|
||||||
@@ -178,8 +178,8 @@ test('\'play\' tests with seekOffset and actionHook', async(t) => {
|
|||||||
const waitHookVerbs = [];
|
const waitHookVerbs = [];
|
||||||
|
|
||||||
const from = 'play_action_hook';
|
const from = 'play_action_hook';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
provisionCustomHook(from, waitHookVerbs)
|
await provisionCustomHook(from, waitHookVerbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
@@ -218,7 +218,7 @@ test('\'play\' tests with earlymedia', async(t) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const from = 'play_early_media';
|
const from = 'play_early_media';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-invite-expect-183-cancel.xml', '172.38.0.10', from);
|
await sippUac('uac-invite-expect-183-cancel.xml', '172.38.0.10', from);
|
||||||
|
|||||||
127
test/queue-test.js
Normal file
127
test/queue-test.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const { sippUac } = require('./sipp')('test_fs');
|
||||||
|
const clearModule = require('clear-module');
|
||||||
|
const {provisionCallHook, provisionActionHook, provisionAnyHook} = 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleepFor = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
||||||
|
|
||||||
|
test('\'enqueue-dequeue\' tests', async(t) => {
|
||||||
|
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'enqueue',
|
||||||
|
name: 'support',
|
||||||
|
actionHook: '/actionHook'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const verbs2 = [
|
||||||
|
{
|
||||||
|
verb: 'dequeue',
|
||||||
|
name: 'support'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const actionVerbs = [
|
||||||
|
{
|
||||||
|
verb: 'play',
|
||||||
|
url: 'silence_stream://1000',
|
||||||
|
earlyMedia: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'enqueue_success';
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
await provisionActionHook(from, actionVerbs)
|
||||||
|
|
||||||
|
const from2 = 'dequeue_success';
|
||||||
|
await provisionCallHook(from2, verbs2);
|
||||||
|
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
const p1 = sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
|
|
||||||
|
await sleepFor(1000);
|
||||||
|
|
||||||
|
const p2 = sippUac('uac-success-send-bye.xml', '172.38.0.11', from2);
|
||||||
|
await Promise.all([p1, p2]);
|
||||||
|
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj.body.queue_result === 'bridged');
|
||||||
|
t.pass('enqueue-dequeue: succeeds connect');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('\leave\' tests', async(t) => {
|
||||||
|
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'enqueue',
|
||||||
|
name: 'support1',
|
||||||
|
waitHook: '/anyHook/enqueue_success_leave',
|
||||||
|
actionHook: '/actionHook'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const anyHookVerbs = [
|
||||||
|
{
|
||||||
|
verb: 'leave'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const actionVerbs = [
|
||||||
|
{
|
||||||
|
verb: 'play',
|
||||||
|
url: 'silence_stream://1000',
|
||||||
|
earlyMedia: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const from = 'enqueue_success_leave';
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
await provisionAnyHook(from, anyHookVerbs);
|
||||||
|
await provisionActionHook(from, actionVerbs)
|
||||||
|
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
|
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/enqueue_success_leave`);
|
||||||
|
t.ok(obj.body.queue_position === 0);
|
||||||
|
const obj1 = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj1.body.queue_result === 'leave');
|
||||||
|
t.pass('enqueue-dequeue: succeeds connect');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -31,7 +31,7 @@ test('\'say\' tests', async(t) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const from = 'say_test_success';
|
const from = 'say_test_success';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
await sippUac('uac-success-received-bye.xml', '172.38.0.10', from);
|
||||||
@@ -43,3 +43,84 @@ test('\'say\' tests', async(t) => {
|
|||||||
t.error(err);
|
t.error(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('\'config\' reset synthesizer tests', 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'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (MICROSOFT_CUSTOM_API_KEY && MICROSOFT_DEPLOYMENT_ID && MICROSOFT_CUSTOM_REGION && MICROSOFT_CUSTOM_VOICE) {
|
||||||
|
test('\'say\' tests - microsoft custom voice', async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'say',
|
||||||
|
text: 'hello',
|
||||||
|
synthesizer: {
|
||||||
|
vendor: 'microsoft',
|
||||||
|
voice: MICROSOFT_CUSTOM_VOICE,
|
||||||
|
options: {
|
||||||
|
deploymentId: MICROSOFT_DEPLOYMENT_ID,
|
||||||
|
apiKey: MICROSOFT_CUSTOM_API_KEY,
|
||||||
|
region: MICROSOFT_CUSTOM_REGION,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
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 microsoft custom voice');
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ test('\'refer\' tests w/202 and NOTIFY', {timeout: 25000}, async(t) => {
|
|||||||
const noVerbs = [];
|
const noVerbs = [];
|
||||||
|
|
||||||
const from = 'refer_with_notify';
|
const from = 'refer_with_notify';
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
provisionActionHook(from, noVerbs)
|
await provisionActionHook(from, noVerbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-refer-with-notify.xml', '172.38.0.10', from);
|
await sippUac('uac-refer-with-notify.xml', '172.38.0.10', from);
|
||||||
@@ -81,8 +81,8 @@ test('\'refer\' tests w/202 but no NOTIFY', {timeout: 25000}, async(t) => {
|
|||||||
const noVerbs = [];
|
const noVerbs = [];
|
||||||
|
|
||||||
const from = 'refer_no_notify';
|
const from = 'refer_no_notify';
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
provisionActionHook(from, noVerbs)
|
await provisionActionHook(from, noVerbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-refer-no-notify.xml', '172.38.0.10', from);
|
await sippUac('uac-refer-no-notify.xml', '172.38.0.10', from);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ test('sending SIP in-dialog requests tests', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "sip_indialog_test";
|
let from = "sip_indialog_test";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-send-info-during-dialog.xml', '172.38.0.10', from);
|
await sippUac('uac-send-info-during-dialog.xml', '172.38.0.10', from);
|
||||||
const obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
const obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ obj.sippUac = (file, bindAddress, from='sipp', to='16174000000', loop=1) => {
|
|||||||
'-cid_str', `%u-%p@%s-${idx++}`,
|
'-cid_str', `%u-%p@%s-${idx++}`,
|
||||||
'172.38.0.50',
|
'172.38.0.50',
|
||||||
'-key','from', from,
|
'-key','from', from,
|
||||||
'-key','to', to, '-trace_msg'
|
'-key','to', to, '-trace_msg', '-trace_err'
|
||||||
];
|
];
|
||||||
|
|
||||||
if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
|
if (bindAddress) args.splice(5, 0, '--ip', bindAddress);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ test('\'transcribe\' test - google', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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`);
|
||||||
@@ -85,7 +85,7 @@ test('\'transcribe\' test - microsoft', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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`);
|
||||||
@@ -122,7 +122,7 @@ test('\'transcribe\' test - aws', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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`);
|
||||||
@@ -162,11 +162,11 @@ test('\'transcribe\' test - deepgram', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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`);
|
||||||
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().includes('like to speak to customer support'),
|
||||||
'transcribe: succeeds when using deepgram credentials');
|
'transcribe: succeeds when using deepgram credentials');
|
||||||
|
|
||||||
disconnect();
|
disconnect();
|
||||||
@@ -202,14 +202,52 @@ test('\'transcribe\' test - soniox', async(t) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let from = "gather_success";
|
let from = "gather_success";
|
||||||
provisionCallHook(from, verbs);
|
await provisionCallHook(from, verbs);
|
||||||
// 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));
|
//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 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`);
|
||||||
|
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();
|
disconnect();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`error received: ${err}`);
|
console.log(`error received: ${err}`);
|
||||||
|
|||||||
@@ -6,31 +6,40 @@ const bent = require('bent');
|
|||||||
* The function help testcase to register desired jambonz json response for an application call
|
* The function help testcase to register desired jambonz json response for an application call
|
||||||
* When a call has From number match the registered hook event, the desired jambonz json will be responded.
|
* When a call has From number match the registered hook event, the desired jambonz json will be responded.
|
||||||
*/
|
*/
|
||||||
const provisionCallHook = (from, verbs) => {
|
const provisionCallHook = async (from, verbs) => {
|
||||||
const mapping = {
|
const mapping = {
|
||||||
from,
|
from,
|
||||||
data: JSON.stringify(verbs)
|
data: JSON.stringify(verbs)
|
||||||
};
|
};
|
||||||
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
||||||
post('/appMapping', mapping);
|
await post('/appMapping', mapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
const provisionCustomHook = (from, verbs) => {
|
const provisionCustomHook = async(from, verbs) => {
|
||||||
const mapping = {
|
const mapping = {
|
||||||
from,
|
from,
|
||||||
data: JSON.stringify(verbs)
|
data: JSON.stringify(verbs)
|
||||||
};
|
};
|
||||||
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
||||||
post(`/customHookMapping`, mapping);
|
await post(`/customHookMapping`, mapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
const provisionActionHook = (from, verbs) => {
|
const provisionActionHook = async(from, verbs) => {
|
||||||
const mapping = {
|
const mapping = {
|
||||||
from,
|
from,
|
||||||
data: JSON.stringify(verbs)
|
data: JSON.stringify(verbs)
|
||||||
};
|
};
|
||||||
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
||||||
post(`/actionHook`, mapping);
|
await post(`/actionHook`, mapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { provisionCallHook, provisionCustomHook, provisionActionHook}
|
const provisionAnyHook = async(key, verbs) => {
|
||||||
|
const mapping = {
|
||||||
|
key,
|
||||||
|
data: JSON.stringify(verbs)
|
||||||
|
};
|
||||||
|
const post = bent('http://127.0.0.1:3100', 'POST', 'string', 200);
|
||||||
|
await post(`/anyHookMapping`, mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { provisionCallHook, provisionCustomHook, provisionActionHook, provisionAnyHook}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const app = express();
|
const app = express();
|
||||||
const Websocket = require('ws');
|
const Websocket = require('ws');
|
||||||
const listenPort = process.env.HTTP_PORT || 3000;
|
const listenPort = process.env.HTTP_PORT || 3000;
|
||||||
|
const any_hook_json_mapping = new Map();
|
||||||
let json_mapping = new Map();
|
let json_mapping = new Map();
|
||||||
let hook_mapping = new Map();
|
let hook_mapping = new Map();
|
||||||
let ws_packet_count = new Map();
|
let ws_packet_count = new Map();
|
||||||
@@ -61,7 +62,7 @@ app.all('/', (req, res) => {
|
|||||||
console.log(req.body, 'POST /');
|
console.log(req.body, 'POST /');
|
||||||
const key = req.body.from
|
const key = req.body.from
|
||||||
addRequestToMap(key, req, hook_mapping);
|
addRequestToMap(key, req, hook_mapping);
|
||||||
return getJsonFromMap(key, req, res);
|
return getJsonFromMap(json_mapping, key, req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/appMapping', (req, res) => {
|
app.post('/appMapping', (req, res) => {
|
||||||
@@ -106,7 +107,7 @@ app.post('/actionHook', (req, res) => {
|
|||||||
app.all('/customHook', (req, res) => {
|
app.all('/customHook', (req, res) => {
|
||||||
let key = `${req.body.from}_customHook`;;
|
let key = `${req.body.from}_customHook`;;
|
||||||
console.log(req.body, `POST /customHook`);
|
console.log(req.body, `POST /customHook`);
|
||||||
return getJsonFromMap(key, req, res);
|
return getJsonFromMap(json_mapping, key, req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/customHookMapping', (req, res) => {
|
app.post('/customHookMapping', (req, res) => {
|
||||||
@@ -116,6 +117,23 @@ app.post('/customHookMapping', (req, res) => {
|
|||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any Hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
app.all('/anyHook/:key', (req, res) => {
|
||||||
|
let key = req.params.key;
|
||||||
|
console.log(req.body, `POST /anyHook/${key}`);
|
||||||
|
return getJsonFromMap(any_hook_json_mapping, key, req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/anyHookMapping', (req, res) => {
|
||||||
|
let key = req.body.key;
|
||||||
|
console.log(req.body, `POST /anyHookMapping/${key}`);
|
||||||
|
any_hook_json_mapping.set(key, req.body.data);
|
||||||
|
return res.sendStatus(200);
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch Requests
|
// Fetch Requests
|
||||||
app.get('/requests/:key', (req, res) => {
|
app.get('/requests/:key', (req, res) => {
|
||||||
let key = req.params.key;
|
let key = req.params.key;
|
||||||
@@ -162,9 +180,9 @@ app.get('/ws_metadata/:key', (req, res) => {
|
|||||||
* private function
|
* private function
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function getJsonFromMap(key, req, res) {
|
function getJsonFromMap(map, key, req, res) {
|
||||||
if (!json_mapping.has(key)) return res.sendStatus(404);
|
if (!map.has(key)) return res.sendStatus(404);
|
||||||
const retData = JSON.parse(json_mapping.get(key));
|
const retData = JSON.parse(map.get(key));
|
||||||
console.log(retData, ` Response to ${req.method} ${req.url}`);
|
console.log(retData, ` Response to ${req.method} ${req.url}`);
|
||||||
addRequestToMap(key, req, hook_mapping);
|
addRequestToMap(key, req, hook_mapping);
|
||||||
return res.json(retData);
|
return res.json(retData);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ test('basic webhook tests', async(t) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const from = 'sip_decline_test_success';
|
const from = 'sip_decline_test_success';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
await sippUac('uac-expect-603.xml', '172.38.0.10', from);
|
await sippUac('uac-expect-603.xml', '172.38.0.10', from);
|
||||||
t.pass('webhook successfully declines call');
|
t.pass('webhook successfully declines call');
|
||||||
@@ -73,7 +73,7 @@ test('invalid jambonz json create alert tests', async(t) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const from = 'invalid_json_create_alert';
|
const from = 'invalid_json_create_alert';
|
||||||
provisionCallHook(from, verbs)
|
await provisionCallHook(from, verbs)
|
||||||
|
|
||||||
// THEN
|
// THEN
|
||||||
await sippUac('uac-invite-expect-480.xml', '172.38.0.10', from);
|
await sippUac('uac-invite-expect-480.xml', '172.38.0.10', from);
|
||||||
|
|||||||
Reference in New Issue
Block a user