mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-15 10:49:07 +00:00
Compare commits
79 Commits
v0.9.5-rc3
...
feat/eleve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b1166194 | ||
|
|
7f1543a0f3 | ||
|
|
83955ba972 | ||
|
|
a5fa5fce5b | ||
|
|
cc1751f500 | ||
|
|
1a1f53aede | ||
|
|
1984b6d3ea | ||
|
|
769b66f57e | ||
|
|
5d8ea34716 | ||
|
|
cf7e89914b | ||
|
|
225d1039db | ||
|
|
14c5bc18cd | ||
|
|
e8817814bf | ||
|
|
cd9f9632ba | ||
|
|
98b845f489 | ||
|
|
f92b1dbc97 | ||
|
|
0442144793 | ||
|
|
2de24af169 | ||
|
|
a884880321 | ||
|
|
b307df79d0 | ||
|
|
77bd11dd47 | ||
|
|
46d56fe546 | ||
|
|
30ab281ea2 | ||
|
|
0869a73052 | ||
|
|
a0a579ccee | ||
|
|
4218653852 | ||
|
|
89cc39f726 | ||
|
|
b231593bff | ||
|
|
4309d25376 | ||
|
|
a00703a067 | ||
|
|
89c985b564 | ||
|
|
b4ed4c8c46 | ||
|
|
581d309f36 | ||
|
|
d1baf2fe37 | ||
|
|
28bf0d3477 | ||
|
|
d2d3b4583e | ||
|
|
854c26db11 | ||
|
|
e77666a1a7 | ||
|
|
5acb19225b | ||
|
|
1d6f84c2d7 | ||
|
|
de9b970a93 | ||
|
|
ec786ef1dd | ||
|
|
a95a6d1683 | ||
|
|
65b3066866 | ||
|
|
057f52e56c | ||
|
|
b46be57eba | ||
|
|
f950d19d1c | ||
|
|
859132bb1c | ||
|
|
acaadceaa2 | ||
|
|
add8d63e8e | ||
|
|
a05b72a420 | ||
|
|
28ff85225f | ||
|
|
f2fe7c4d24 | ||
|
|
97408c7d3b | ||
|
|
db5f0a0dce | ||
|
|
654ccd9d9d | ||
|
|
ea27b20ac5 | ||
|
|
96aa705378 | ||
|
|
5e51849839 | ||
|
|
44f69fa76d | ||
|
|
73c77bea71 | ||
|
|
babc0d0dbb | ||
|
|
6b8d0fe1a8 | ||
|
|
66bb466297 | ||
|
|
1933f4ec0b | ||
|
|
b1089a1ae9 | ||
|
|
93e06d887e | ||
|
|
b478e0ecd2 | ||
|
|
94d43d4b70 | ||
|
|
eb449e9169 | ||
|
|
158d9d7d25 | ||
|
|
5886d1d945 | ||
|
|
352106ec0c | ||
|
|
05a6bf51a7 | ||
|
|
bd1c763e72 | ||
|
|
d831a4ca7f | ||
|
|
0cc6ea987f | ||
|
|
6e7521c91c | ||
|
|
e0e2ade289 |
6
app.js
6
app.js
@@ -29,6 +29,12 @@ const {LifeCycleEvents, FS_UUID_SET_NAME, SystemState, FEATURE_SERVER} = require
|
|||||||
const installSrfLocals = require('./lib/utils/install-srf-locals');
|
const installSrfLocals = require('./lib/utils/install-srf-locals');
|
||||||
const createHttpListener = require('./lib/utils/http-listener');
|
const createHttpListener = require('./lib/utils/http-listener');
|
||||||
const healthCheck = require('@jambonz/http-health-check');
|
const healthCheck = require('@jambonz/http-health-check');
|
||||||
|
const ProcessMonitor = require('./lib/utils/process-monitor');
|
||||||
|
const monitor = new ProcessMonitor(logger);
|
||||||
|
|
||||||
|
// Log startup
|
||||||
|
monitor.logStartup();
|
||||||
|
monitor.setupSignalHandlers();
|
||||||
|
|
||||||
logger.on('level-change', (lvl, _val, prevLvl, _prevVal, instance) => {
|
logger.on('level-change', (lvl, _val, prevLvl, _prevVal, instance) => {
|
||||||
if (logger !== instance) {
|
if (logger !== instance) {
|
||||||
|
|||||||
@@ -174,5 +174,61 @@
|
|||||||
"non è raggiungibile",
|
"non è raggiungibile",
|
||||||
"lascia pure un messaggio",
|
"lascia pure un messaggio",
|
||||||
"puoi lasciare un messaggio"
|
"puoi lasciare un messaggio"
|
||||||
|
],
|
||||||
|
"ja-JP": [
|
||||||
|
"この通話は留守番電話に転送されました",
|
||||||
|
"発信先は現在電話に出ることができません",
|
||||||
|
"発信音の後でメッセージを録音してください",
|
||||||
|
"録音を完了したら電話を切ることができます",
|
||||||
|
"只今電話に出ることができません",
|
||||||
|
"ただ今電話に出ることができません",
|
||||||
|
"ただいま電話に出ることができません",
|
||||||
|
"ピーという発信音の後にお名前とご用件をお話しください",
|
||||||
|
"ファックスを送られる方はスタートボタンを押してください",
|
||||||
|
"FAXを送られる方はスタートボタンを押してください",
|
||||||
|
"おかけになった電話をお呼びしましたが",
|
||||||
|
"お出になりません",
|
||||||
|
"おでになりません",
|
||||||
|
"お掛けになった電話番号は",
|
||||||
|
"おかけになった電話番号は",
|
||||||
|
"お掛けになった電話は",
|
||||||
|
"おかけになった電話は",
|
||||||
|
"現在使われておりません",
|
||||||
|
"番号をお確かめになって",
|
||||||
|
"お掛け直し下さい",
|
||||||
|
"おかけ直し下さい",
|
||||||
|
"おかけ直しください",
|
||||||
|
"こちらはNTTドコモです",
|
||||||
|
"こちらはエーユーです",
|
||||||
|
"こちらはソフトバンクです",
|
||||||
|
"電波の届かない",
|
||||||
|
"電源が入っていない",
|
||||||
|
"掛かりません",
|
||||||
|
"かかりません",
|
||||||
|
"お繋ぎすることが出来ません",
|
||||||
|
"お繋ぎ出来ません",
|
||||||
|
"お繋ぎすることができません",
|
||||||
|
"お繋ぎできません",
|
||||||
|
"おつなぎすることができません",
|
||||||
|
"おつなぎできません",
|
||||||
|
"メッセージを録音",
|
||||||
|
"留守番電話",
|
||||||
|
"お留守番サービス",
|
||||||
|
"留守番",
|
||||||
|
"留守電",
|
||||||
|
"留守",
|
||||||
|
"接続します",
|
||||||
|
"合図の音",
|
||||||
|
"ピーと",
|
||||||
|
"発信音",
|
||||||
|
"ご用件",
|
||||||
|
"伝言",
|
||||||
|
"お話しください",
|
||||||
|
"ファックス",
|
||||||
|
"FAX",
|
||||||
|
"終了",
|
||||||
|
"終了しました",
|
||||||
|
"終了いたしました",
|
||||||
|
"営業時間"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const OPTIONS_PING_INTERVAL = parseInt(process.env.OPTIONS_PING_INTERVAL, 10) ||
|
|||||||
const JAMBONZ_RECORD_WS_BASE_URL = process.env.JAMBONZ_RECORD_WS_BASE_URL || process.env.JAMBONES_RECORD_WS_BASE_URL;
|
const JAMBONZ_RECORD_WS_BASE_URL = process.env.JAMBONZ_RECORD_WS_BASE_URL || process.env.JAMBONES_RECORD_WS_BASE_URL;
|
||||||
const JAMBONZ_RECORD_WS_USERNAME = process.env.JAMBONZ_RECORD_WS_USERNAME || process.env.JAMBONES_RECORD_WS_USERNAME;
|
const JAMBONZ_RECORD_WS_USERNAME = process.env.JAMBONZ_RECORD_WS_USERNAME || process.env.JAMBONES_RECORD_WS_USERNAME;
|
||||||
const JAMBONZ_RECORD_WS_PASSWORD = process.env.JAMBONZ_RECORD_WS_PASSWORD || process.env.JAMBONES_RECORD_WS_PASSWORD;
|
const JAMBONZ_RECORD_WS_PASSWORD = process.env.JAMBONZ_RECORD_WS_PASSWORD || process.env.JAMBONES_RECORD_WS_PASSWORD;
|
||||||
const JAMBONZ_DISABLE_DIAL_PAI_HEADER = process.env.JAMBONZ_DISABLE_DIAL_PAI_HEADER || false;
|
const JAMBONZ_DIAL_PAI_HEADER = process.env.JAMBONZ_DIAL_PAI_HEADER || false;
|
||||||
const JAMBONES_DISABLE_DIRECT_P2P_CALL = process.env.JAMBONES_DISABLE_DIRECT_P2P_CALL || false;
|
const JAMBONES_DISABLE_DIRECT_P2P_CALL = process.env.JAMBONES_DISABLE_DIRECT_P2P_CALL || false;
|
||||||
|
|
||||||
const JAMBONES_EAGERLY_PRE_CACHE_AUDIO = parseInt(process.env.JAMBONES_EAGERLY_PRE_CACHE_AUDIO, 10) || 0;
|
const JAMBONES_EAGERLY_PRE_CACHE_AUDIO = parseInt(process.env.JAMBONES_EAGERLY_PRE_CACHE_AUDIO, 10) || 0;
|
||||||
@@ -225,11 +225,11 @@ module.exports = {
|
|||||||
JAMBONZ_RECORD_WS_BASE_URL,
|
JAMBONZ_RECORD_WS_BASE_URL,
|
||||||
JAMBONZ_RECORD_WS_USERNAME,
|
JAMBONZ_RECORD_WS_USERNAME,
|
||||||
JAMBONZ_RECORD_WS_PASSWORD,
|
JAMBONZ_RECORD_WS_PASSWORD,
|
||||||
JAMBONZ_DISABLE_DIAL_PAI_HEADER,
|
JAMBONZ_DIAL_PAI_HEADER,
|
||||||
JAMBONES_DISABLE_DIRECT_P2P_CALL,
|
JAMBONES_DISABLE_DIRECT_P2P_CALL,
|
||||||
JAMBONES_USE_FREESWITCH_TIMER_FD,
|
JAMBONES_USE_FREESWITCH_TIMER_FD,
|
||||||
JAMBONES_DIAL_SBC_FOR_REGISTERED_USER,
|
JAMBONES_DIAL_SBC_FOR_REGISTERED_USER,
|
||||||
JAMBONES_MEDIA_TIMEOUT_MS,
|
JAMBONES_MEDIA_TIMEOUT_MS,
|
||||||
JAMBONES_MEDIA_HOLD_TIMEOUT_MS,
|
JAMBONES_MEDIA_HOLD_TIMEOUT_MS,
|
||||||
JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS
|
JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ router.post('/',
|
|||||||
|
|
||||||
// find handling sbc sip for called user
|
// find handling sbc sip for called user
|
||||||
if (JAMBONES_DIAL_SBC_FOR_REGISTERED_USER && target.type === 'user') {
|
if (JAMBONES_DIAL_SBC_FOR_REGISTERED_USER && target.type === 'user') {
|
||||||
const { registrar } = srf.locals.dbHelpers;
|
const { registrar} = srf.locals.dbHelpers;
|
||||||
const reg = await registrar.query(target.name);
|
const reg = await registrar.query(target.name);
|
||||||
if (reg) {
|
if (reg) {
|
||||||
sbcAddress = selectHostPort(logger, reg.sbcAddress, 'tcp')[1];
|
sbcAddress = selectHostPort(logger, reg.sbcAddress, 'tcp')[1];
|
||||||
@@ -159,7 +159,9 @@ router.post('/',
|
|||||||
* trunk isn't specified,
|
* trunk isn't specified,
|
||||||
* check if from-number matches any existing numbers on Jambonz
|
* check if from-number matches any existing numbers on Jambonz
|
||||||
* */
|
* */
|
||||||
if (target.type === 'phone' && !target.trunk) {
|
const { lookupLcrByAccount} = srf.locals.dbHelpers;
|
||||||
|
const lcrs = await lookupLcrByAccount(req.body.account_sid);
|
||||||
|
if (target.type === 'phone' && !target.trunk && lcrs.length == 0) {
|
||||||
const str = restDial.from || '';
|
const str = restDial.from || '';
|
||||||
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber);
|
const voip_carrier_sid = await lookupCarrierByPhoneNumber(req.body.account_sid, callingNumber);
|
||||||
|
|||||||
@@ -112,6 +112,14 @@ module.exports = function(srf, logger) {
|
|||||||
req.locals.callingNumber = sipURIs[1];
|
req.locals.callingNumber = sipURIs[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Feature server INVITE request pipelines taking time to finish,
|
||||||
|
// while connecting and fetch application from db and invoking webhook.
|
||||||
|
// call can be canceled without any handling, so we add a listener here
|
||||||
|
req.once('cancel', (sipMsg) => {
|
||||||
|
logger.info(`${callId} got CANCEL request`);
|
||||||
|
req.locals.canceled = true;
|
||||||
|
});
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,13 +370,14 @@ module.exports = function(srf, logger) {
|
|||||||
});
|
});
|
||||||
// if transferred call contains callInfo, let update original data to newly created callInfo in this instance.
|
// if transferred call contains callInfo, let update original data to newly created callInfo in this instance.
|
||||||
if (app.transferredCall && app.callInfo) {
|
if (app.transferredCall && app.callInfo) {
|
||||||
const {direction, callerName, from, to, originatingSipIp, originatingSipTrunkName} = app.callInfo;
|
const {direction, callerName, from, to, originatingSipIp, originatingSipTrunkName, customerData} = app.callInfo;
|
||||||
req.locals.callInfo.direction = direction;
|
req.locals.callInfo.direction = direction;
|
||||||
req.locals.callInfo.callerName = callerName;
|
req.locals.callInfo.callerName = callerName;
|
||||||
req.locals.callInfo.from = from;
|
req.locals.callInfo.from = from;
|
||||||
req.locals.callInfo.to = to;
|
req.locals.callInfo.to = to;
|
||||||
req.locals.callInfo.originatingSipIp = originatingSipIp;
|
req.locals.callInfo.originatingSipIp = originatingSipIp;
|
||||||
req.locals.callInfo.originatingSipTrunkName = originatingSipTrunkName;
|
req.locals.callInfo.originatingSipTrunkName = originatingSipTrunkName;
|
||||||
|
if (customerData) req.locals.callInfo.customerData = customerData;
|
||||||
delete app.callInfo;
|
delete app.callInfo;
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ const {
|
|||||||
RecordState,
|
RecordState,
|
||||||
AllowedSipRecVerbs,
|
AllowedSipRecVerbs,
|
||||||
AllowedConfirmSessionVerbs,
|
AllowedConfirmSessionVerbs,
|
||||||
TtsStreamingEvents
|
TtsStreamingEvents,
|
||||||
} = require('../utils/constants');
|
ListenStatus
|
||||||
|
} = require('../utils/constants.json');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const sessionTracker = require('./session-tracker');
|
const sessionTracker = require('./session-tracker');
|
||||||
@@ -37,6 +38,7 @@ 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';
|
||||||
const { NonFatalTaskError} = require('../utils/error');
|
const { NonFatalTaskError} = require('../utils/error');
|
||||||
const { createMediaEndpoint } = require('../utils/media-endpoint');
|
const { createMediaEndpoint } = require('../utils/media-endpoint');
|
||||||
|
const SttLatencyCalculator = require('../utils/stt-latency-calculator');
|
||||||
const sqlRetrieveQueueEventHook = `SELECT * FROM webhooks
|
const sqlRetrieveQueueEventHook = `SELECT * FROM webhooks
|
||||||
WHERE webhook_sid =
|
WHERE webhook_sid =
|
||||||
(
|
(
|
||||||
@@ -146,6 +148,30 @@ class CallSession extends Emitter {
|
|||||||
this.conversationTurns = [];
|
this.conversationTurns = [];
|
||||||
this.on('userSaid', this._onUserSaid.bind(this));
|
this.on('userSaid', this._onUserSaid.bind(this));
|
||||||
this.on('botSaid', this._onBotSaid.bind(this));
|
this.on('botSaid', this._onBotSaid.bind(this));
|
||||||
|
/**
|
||||||
|
* Support STT latency
|
||||||
|
*/
|
||||||
|
this.sttLatencyCalculator = new SttLatencyCalculator({
|
||||||
|
logger,
|
||||||
|
cs: this
|
||||||
|
});
|
||||||
|
this.on('transcribe-start', () => {
|
||||||
|
this.sttLatencyCalculator.resetTime();
|
||||||
|
});
|
||||||
|
this.on('on-transcription', () => {
|
||||||
|
this.sttLatencyCalculator.onTranscriptionReceived();
|
||||||
|
});
|
||||||
|
this.on('transcribe-stop', () => {
|
||||||
|
this.sttLatencyCalculator.onTranscribeStop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get notifySttLatencyEnabled() {
|
||||||
|
return this._notifySttLatencyEnabled || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set notifySttLatencyEnabled(enabled) {
|
||||||
|
this._notifySttLatencyEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -632,6 +658,15 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disableTtsCache
|
||||||
|
get disableTtsCache() {
|
||||||
|
return this._disableTtsCache || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
set disableTtsCache(d) {
|
||||||
|
this._disableTtsCache = d;
|
||||||
|
}
|
||||||
|
|
||||||
getTsStreamingVendor() {
|
getTsStreamingVendor() {
|
||||||
let v;
|
let v;
|
||||||
if (this.currentTask?.isStreamingTts) {
|
if (this.currentTask?.isStreamingTts) {
|
||||||
@@ -684,7 +719,7 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
hasGlobalSttPunctuation() {
|
get hasGlobalSttPunctuation() {
|
||||||
return this._globalSttPunctuation !== undefined;
|
return this._globalSttPunctuation !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,10 +751,25 @@ class CallSession extends Emitter {
|
|||||||
return this._fillerNoise;
|
return this._fillerNoise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async pauseOrResumeBackgroundListenIfRequired(action, silence = false) {
|
||||||
|
if ((action == 'pauseCallRecording' || action == 'resumeCallRecording') &&
|
||||||
|
this.backgroundTaskManager.isTaskRunning('record')) {
|
||||||
|
this.logger.debug({action, silence}, 'CallSession:pauseOrResumeBackgroundListenIfRequired');
|
||||||
|
const backgroundListenTask = this.backgroundTaskManager.getTask('record');
|
||||||
|
const status = action === 'pauseCallRecording' ? ListenStatus.Pause : ListenStatus.Resume;
|
||||||
|
backgroundListenTask.updateListen(
|
||||||
|
status,
|
||||||
|
silence
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async notifyRecordOptions(opts) {
|
async notifyRecordOptions(opts) {
|
||||||
const {action} = opts;
|
const {action, silence} = opts;
|
||||||
this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
|
this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
|
||||||
|
|
||||||
|
this.pauseOrResumeBackgroundListenIfRequired(action, silence);
|
||||||
|
|
||||||
/* if we have not answered yet, just save the details for later */
|
/* if we have not answered yet, just save the details for later */
|
||||||
if (!this.dlg) {
|
if (!this.dlg) {
|
||||||
if (action === 'startCallRecording') {
|
if (action === 'startCallRecording') {
|
||||||
@@ -900,6 +950,14 @@ class CallSession extends Emitter {
|
|||||||
this.ttsStreamingBuffer?.start();
|
this.ttsStreamingBuffer?.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopTtsStream() {
|
||||||
|
if (this.appIsUsingWebsockets) {
|
||||||
|
this.requestor?.request('tts:streaming-event', '/streaming-event', {event_type: 'stream_closed'})
|
||||||
|
.catch((err) => this.logger.info({err}, 'CallSession:clearTtsStream - Error sending user_interruption'));
|
||||||
|
this.ttsStreamingBuffer?.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async enableBotMode(gather, autoEnable) {
|
async enableBotMode(gather, autoEnable) {
|
||||||
try {
|
try {
|
||||||
let task;
|
let task;
|
||||||
@@ -923,7 +981,7 @@ class CallSession extends Emitter {
|
|||||||
task.sticky = autoEnable;
|
task.sticky = autoEnable;
|
||||||
// listen to the bargein-done from background manager
|
// listen to the bargein-done from background manager
|
||||||
this.backgroundTaskManager.on('bargeIn-done', () => {
|
this.backgroundTaskManager.on('bargeIn-done', () => {
|
||||||
if (this.requestor instanceof WsRequestor) {
|
if (this.appIsUsingWebsockets) {
|
||||||
try {
|
try {
|
||||||
this.kill(true);
|
this.kill(true);
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
@@ -976,8 +1034,6 @@ class CallSession extends Emitter {
|
|||||||
(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.debug(
|
|
||||||
`${type}: ${credential.vendor} ${credential.label ? `, label: ${credential.label}` : ''} `);
|
|
||||||
if ('google' === vendor) {
|
if ('google' === vendor) {
|
||||||
if (type === 'tts' && !credential.tts_tested_ok ||
|
if (type === 'tts' && !credential.tts_tested_ok ||
|
||||||
type === 'stt' && !credential.stt_tested_ok) {
|
type === 'stt' && !credential.stt_tested_ok) {
|
||||||
@@ -1047,6 +1103,13 @@ class CallSession extends Emitter {
|
|||||||
deepgram_stt_use_tls: credential.deepgram_stt_use_tls
|
deepgram_stt_use_tls: credential.deepgram_stt_use_tls
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else if ('gladia' === vendor) {
|
||||||
|
return {
|
||||||
|
speech_credential_sid: credential.speech_credential_sid,
|
||||||
|
api_key: credential.api_key,
|
||||||
|
region: credential.region,
|
||||||
|
};
|
||||||
|
}
|
||||||
else if ('soniox' === vendor) {
|
else if ('soniox' === vendor) {
|
||||||
return {
|
return {
|
||||||
speech_credential_sid: credential.speech_credential_sid,
|
speech_credential_sid: credential.speech_credential_sid,
|
||||||
@@ -1078,6 +1141,8 @@ class CallSession extends Emitter {
|
|||||||
return {
|
return {
|
||||||
api_key: credential.api_key,
|
api_key: credential.api_key,
|
||||||
model_id: credential.model_id,
|
model_id: credential.model_id,
|
||||||
|
stt_model_id: credential.stt_model_id,
|
||||||
|
api_uri: credential.api_uri,
|
||||||
options: credential.options
|
options: credential.options
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1105,6 +1170,13 @@ class CallSession extends Emitter {
|
|||||||
options: credential.options
|
options: credential.options
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else if ('resemble' === vendor) {
|
||||||
|
return {
|
||||||
|
api_key: credential.api_key,
|
||||||
|
resemble_tts_use_tls: credential.resemble_tts_use_tls,
|
||||||
|
resemble_tts_uri: credential.resemble_tts_uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
else if ('inworld' === vendor) {
|
else if ('inworld' === vendor) {
|
||||||
return {
|
return {
|
||||||
api_key: credential.api_key,
|
api_key: credential.api_key,
|
||||||
@@ -1119,6 +1191,21 @@ class CallSession extends Emitter {
|
|||||||
service_version: credential.service_version
|
service_version: credential.service_version
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else if ('houndify' === vendor) {
|
||||||
|
return {
|
||||||
|
speech_credential_sid: credential.speech_credential_sid,
|
||||||
|
client_id: credential.client_id,
|
||||||
|
client_key: credential.client_key,
|
||||||
|
user_id: credential.user_id,
|
||||||
|
houndify_server_uri: credential.houndify_server_uri
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('deepgramflux' === vendor) {
|
||||||
|
return {
|
||||||
|
speech_credential_sid: credential.speech_credential_sid,
|
||||||
|
api_key: credential.api_key,
|
||||||
|
};
|
||||||
|
}
|
||||||
else if ('voxist' === vendor) {
|
else if ('voxist' === vendor) {
|
||||||
return {
|
return {
|
||||||
speech_credential_sid: credential.speech_credential_sid,
|
speech_credential_sid: credential.speech_credential_sid,
|
||||||
@@ -1195,6 +1282,7 @@ class CallSession extends Emitter {
|
|||||||
this.ttsStreamingBuffer.on(TtsStreamingEvents.Pause, this._onTtsStreamingPause.bind(this));
|
this.ttsStreamingBuffer.on(TtsStreamingEvents.Pause, this._onTtsStreamingPause.bind(this));
|
||||||
this.ttsStreamingBuffer.on(TtsStreamingEvents.Resume, this._onTtsStreamingResume.bind(this));
|
this.ttsStreamingBuffer.on(TtsStreamingEvents.Resume, this._onTtsStreamingResume.bind(this));
|
||||||
this.ttsStreamingBuffer.on(TtsStreamingEvents.ConnectFailure, this._onTtsStreamingConnectFailure.bind(this));
|
this.ttsStreamingBuffer.on(TtsStreamingEvents.ConnectFailure, this._onTtsStreamingConnectFailure.bind(this));
|
||||||
|
this.ttsStreamingBuffer.on(TtsStreamingEvents.Connected, this._onTtsStreamingConnected.bind(this));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.logger.info(`CallSession:exec - not a normal call session: ${this.constructor.name}`);
|
this.logger.info(`CallSession:exec - not a normal call session: ${this.constructor.name}`);
|
||||||
@@ -1253,7 +1341,7 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (0 === this.tasks.length &&
|
if (0 === this.tasks.length &&
|
||||||
this.requestor instanceof WsRequestor &&
|
this.appIsUsingWebsockets &&
|
||||||
!this.requestor.closedGracefully &&
|
!this.requestor.closedGracefully &&
|
||||||
!this.callGone &&
|
!this.callGone &&
|
||||||
!this.isConfirmCallSession
|
!this.isConfirmCallSession
|
||||||
@@ -1363,7 +1451,11 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (this.req && !this.dlg) {
|
if (this.req && !this.dlg) {
|
||||||
this.req.cancel();
|
try {
|
||||||
|
this.req.cancel();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, 'CallSession:_lccCallStatus error cancelling request');
|
||||||
|
}
|
||||||
this._callReleased();
|
this._callReleased();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1661,8 +1753,8 @@ Duration=${duration} `
|
|||||||
async _lccWhisper(opts, callSid) {
|
async _lccWhisper(opts, callSid) {
|
||||||
const {whisper} = opts;
|
const {whisper} = opts;
|
||||||
let tasks;
|
let tasks;
|
||||||
const b3 = this.b3;
|
//const b3 = this.b3;
|
||||||
const httpHeaders = b3 && {b3};
|
//const httpHeaders = b3 && {b3};
|
||||||
|
|
||||||
// this whole thing requires us to be in a Dial verb
|
// this whole thing requires us to be in a Dial verb
|
||||||
const task = this.currentTask;
|
const task = this.currentTask;
|
||||||
@@ -1671,12 +1763,15 @@ Duration=${duration} `
|
|||||||
}
|
}
|
||||||
|
|
||||||
// allow user to provide a url object, a url string, an array of tasks, or a single task
|
// allow user to provide a url object, a url string, an array of tasks, or a single task
|
||||||
if (typeof whisper === 'string' || (typeof whisper === 'object' && whisper.url)) {
|
// Disable passing a URL as not functional Sam Machin - 17/07/2025
|
||||||
// retrieve a url
|
//if (typeof whisper === 'string' || (typeof whisper === 'object' && whisper.url && !whisper.verb)) {
|
||||||
const json = await this.requestor(opts.call_hook, this.callInfo.toJSON(), httpHeaders);
|
// // retrieve a url
|
||||||
tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
// const json = await this.requestor(opts.call_hook, this.callInfo.toJSON(), httpHeaders);
|
||||||
}
|
// tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||||
else if (Array.isArray(whisper)) {
|
//}
|
||||||
|
//else if (Array.isArray(whisper)) {
|
||||||
|
|
||||||
|
if (Array.isArray(whisper)) {
|
||||||
// an inline array of tasks
|
// an inline array of tasks
|
||||||
tasks = normalizeJambones(this.logger, whisper).map((tdata) => makeTask(this.logger, tdata));
|
tasks = normalizeJambones(this.logger, whisper).map((tdata) => makeTask(this.logger, tdata));
|
||||||
}
|
}
|
||||||
@@ -1807,7 +1902,7 @@ Duration=${duration} `
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (tokens === undefined) {
|
else if (tokens === undefined) {
|
||||||
this.logger.info({opts}, 'CallSession:_lccTtsTokens - invalid command since id is missing');
|
this.logger.info({opts}, 'CallSession:_lccTtsTokens - invalid command since tokens is missing');
|
||||||
return this.requestor.request('tts:tokens-result', '/tokens-result', {
|
return this.requestor.request('tts:tokens-result', '/tokens-result', {
|
||||||
id,
|
id,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
@@ -1913,6 +2008,17 @@ Duration=${duration} `
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err, opts, callSid}, 'CallSession:updateCall - error updating call');
|
this.logger.info({err, opts, callSid}, 'CallSession:updateCall - error updating call');
|
||||||
|
const {writeAlerts} = this.srf.locals;
|
||||||
|
try {
|
||||||
|
writeAlerts({
|
||||||
|
alert_type: 'error-updating-call',
|
||||||
|
account_sid: this.accountSid,
|
||||||
|
message: err.message,
|
||||||
|
target_sid: callSid
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, 'Error writing error-updating-call alert');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2317,6 +2423,7 @@ Duration=${duration} `
|
|||||||
const ep = await this._createMediaEndpoint({
|
const ep = await this._createMediaEndpoint({
|
||||||
headers: {
|
headers: {
|
||||||
'X-Jambones-Call-ID': this.callId,
|
'X-Jambones-Call-ID': this.callId,
|
||||||
|
'X-Call-Sid': this.callSid,
|
||||||
},
|
},
|
||||||
remoteSdp: this.req.body
|
remoteSdp: this.req.body
|
||||||
});
|
});
|
||||||
@@ -2328,7 +2435,7 @@ Duration=${duration} `
|
|||||||
this.logger.debug(`endpoint was destroyed!! ${this.ep.uuid}`);
|
this.logger.debug(`endpoint was destroyed!! ${this.ep.uuid}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.direction === CallDirection.Inbound) {
|
if (this.direction === CallDirection.Inbound || this.application?.transferredCall) {
|
||||||
if (task.earlyMedia && !this.req.finalResponseSent) {
|
if (task.earlyMedia && !this.req.finalResponseSent) {
|
||||||
this.res.send(183, {body: ep.local.sdp});
|
this.res.send(183, {body: ep.local.sdp});
|
||||||
return {ep};
|
return {ep};
|
||||||
@@ -2460,7 +2567,9 @@ Duration=${duration} `
|
|||||||
this.backgroundTaskManager.stopAll();
|
this.backgroundTaskManager.stopAll();
|
||||||
this.clearOrRestoreActionHookDelayProcessor().catch((err) => {});
|
this.clearOrRestoreActionHookDelayProcessor().catch((err) => {});
|
||||||
|
|
||||||
this.ttsStreamingBuffer?.stop();
|
this.stopTtsStream();
|
||||||
|
|
||||||
|
this.sttLatencyCalculator?.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2612,7 +2721,7 @@ Duration=${duration} `
|
|||||||
*/
|
*/
|
||||||
_onRefer(req, res) {
|
_onRefer(req, res) {
|
||||||
const task = this.currentTask;
|
const task = this.currentTask;
|
||||||
const sd = task.sd;
|
const sd = task?.sd;
|
||||||
if (task && TaskName.Dial === task.name && sd && task.referHook) {
|
if (task && TaskName.Dial === task.name && sd && task.referHook) {
|
||||||
task.handleRefer(this, req, res);
|
task.handleRefer(this, req, res);
|
||||||
}
|
}
|
||||||
@@ -2918,14 +3027,14 @@ Duration=${duration} `
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
_notifyTaskError(obj) {
|
_notifyTaskError(obj) {
|
||||||
if (this.requestor instanceof WsRequestor) {
|
if (this.appIsUsingWebsockets) {
|
||||||
this.requestor.request('jambonz:error', '/error', obj)
|
this.requestor.request('jambonz:error', '/error', obj)
|
||||||
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskError - Error sending'));
|
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskError - Error sending'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_notifyTaskStatus(task, evt) {
|
_notifyTaskStatus(task, evt) {
|
||||||
if (this.notifyEvents && this.requestor instanceof WsRequestor) {
|
if (this.notifyEvents && this.appIsUsingWebsockets) {
|
||||||
const obj = {...evt, id: task.id, name: task.name};
|
const obj = {...evt, id: task.id, name: task.name};
|
||||||
this.requestor.request('verb:status', '/status', obj)
|
this.requestor.request('verb:status', '/status', obj)
|
||||||
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskStatus - Error sending'));
|
.catch((err) => this.logger.debug({err}, 'CallSession:_notifyTaskStatus - Error sending'));
|
||||||
@@ -2977,7 +3086,7 @@ Duration=${duration} `
|
|||||||
}
|
}
|
||||||
|
|
||||||
_clearTasks(backgroundGather, evt) {
|
_clearTasks(backgroundGather, evt) {
|
||||||
if (this.requestor instanceof WsRequestor && !backgroundGather.cleared) {
|
if (this.appIsUsingWebsockets && !backgroundGather.cleared) {
|
||||||
this.logger.debug({evt}, 'CallSession:_clearTasks on event from background gather');
|
this.logger.debug({evt}, 'CallSession:_clearTasks on event from background gather');
|
||||||
try {
|
try {
|
||||||
backgroundGather.cleared = true;
|
backgroundGather.cleared = true;
|
||||||
@@ -3005,10 +3114,20 @@ Duration=${duration} `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onTtsStreamingConnected() {
|
||||||
|
this.requestor?.request('tts:streaming-event', '/streaming-event', {event_type: 'stream_open'})
|
||||||
|
.catch((err) => this.logger.info({err}, 'CallSession:_onTtsStreamingConnected - Error sending'));
|
||||||
|
}
|
||||||
|
|
||||||
_onTtsStreamingEmpty() {
|
_onTtsStreamingEmpty() {
|
||||||
const task = this.currentTask;
|
const task = this.currentTask;
|
||||||
if (task && TaskName.Say === task.name) {
|
if (task && TaskName.Say === task.name) {
|
||||||
task.notifyTtsStreamIsEmpty();
|
task.notifyTtsStreamIsEmpty();
|
||||||
|
} else if (
|
||||||
|
// If Gather nested say task is streaming
|
||||||
|
task && TaskName.Gather === task.name && task.sayTask && task.sayTask.isStreamingTts) {
|
||||||
|
const sayTask = task.sayTask;
|
||||||
|
sayTask.notifyTtsStreamIsEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3103,6 +3222,20 @@ Duration=${duration} `
|
|||||||
return `assistant: ${t.text}`;
|
return `assistant: ${t.text}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startSttLatencyVad() {
|
||||||
|
if (this.notifySttLatencyEnabled) {
|
||||||
|
this.sttLatencyCalculator.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopSttLatencyVad() {
|
||||||
|
this.sttLatencyCalculator.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateSttLatency() {
|
||||||
|
return this.sttLatencyCalculator.calculateLatency();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = CallSession;
|
module.exports = CallSession;
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const CallSession = require('./call-session');
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
class ConfirmCallSession extends CallSession {
|
class ConfirmCallSession extends CallSession {
|
||||||
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo, memberId, confName, rootSpan, req}) {
|
// eslint-disable-next-line max-len
|
||||||
|
constructor({logger, application, dlg, ep, tasks, callInfo, accountInfo, memberId, confName, rootSpan, req, tmpFiles}) {
|
||||||
super({
|
super({
|
||||||
logger,
|
logger,
|
||||||
application,
|
application,
|
||||||
@@ -24,6 +25,7 @@ class ConfirmCallSession extends CallSession {
|
|||||||
this.dlg = dlg;
|
this.dlg = dlg;
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
this.req = req;
|
this.req = req;
|
||||||
|
this.tmpFiles = tmpFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ class InboundCallSession extends CallSession {
|
|||||||
this.req = req;
|
this.req = req;
|
||||||
this.res = res;
|
this.res = res;
|
||||||
|
|
||||||
|
// if the call was canceled before we got here, handle it
|
||||||
|
if (this.req.locals.canceled) {
|
||||||
|
req.locals.logger.info('InboundCallSession: constructor - call was already canceled');
|
||||||
|
this._onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
req.once('cancel', this._onCancel.bind(this));
|
req.once('cancel', this._onCancel.bind(this));
|
||||||
|
|
||||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ class Conference extends Task {
|
|||||||
this.confName = this.data.name;
|
this.confName = this.data.name;
|
||||||
[
|
[
|
||||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
|
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
|
||||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook', 'endConferenceDuration'
|
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook',
|
||||||
|
'endConferenceDuration', 'distributeDtmf'
|
||||||
].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 = [];
|
||||||
@@ -356,6 +357,7 @@ class Conference extends Task {
|
|||||||
//https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod_conference_3965534/
|
//https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod_conference_3965534/
|
||||||
// mute | Enter conference muted
|
// mute | Enter conference muted
|
||||||
...((this.joinMuted || this.speakOnlyTo) && {mute: true}),
|
...((this.joinMuted || this.speakOnlyTo) && {mute: true}),
|
||||||
|
...(this.distributeDtmf && {'dist-dtmf': true})
|
||||||
}});
|
}});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -673,7 +675,8 @@ class Conference extends Task {
|
|||||||
confName: this.confName,
|
confName: this.confName,
|
||||||
tasks,
|
tasks,
|
||||||
rootSpan: cs.rootSpan,
|
rootSpan: cs.rootSpan,
|
||||||
req: cs.req
|
req: cs.req,
|
||||||
|
tmpFiles: cs.tmpFiles,
|
||||||
});
|
});
|
||||||
await this._playSession.exec();
|
await this._playSession.exec();
|
||||||
this._playSession = null;
|
this._playSession = null;
|
||||||
|
|||||||
@@ -18,12 +18,16 @@ class TaskConfig extends Task {
|
|||||||
'boostAudioSignal',
|
'boostAudioSignal',
|
||||||
'vad',
|
'vad',
|
||||||
'ttsStream',
|
'ttsStream',
|
||||||
'autoStreamTts'
|
'autoStreamTts',
|
||||||
|
'disableTtsCache'
|
||||||
].forEach((k) => this[k] = this.data[k] || {});
|
].forEach((k) => this[k] = this.data[k] || {});
|
||||||
|
|
||||||
if ('notifyEvents' in this.data) {
|
if ('notifyEvents' in this.data) {
|
||||||
this.notifyEvents = !!this.data.notifyEvents;
|
this.notifyEvents = !!this.data.notifyEvents;
|
||||||
}
|
}
|
||||||
|
if (this.hasNotifySttLatency) {
|
||||||
|
this.notifySttLatency = !!this.data.notifySttLatency;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.bargeIn.enable) {
|
if (this.bargeIn.enable) {
|
||||||
this.gatherOpts = {
|
this.gatherOpts = {
|
||||||
@@ -83,7 +87,9 @@ class TaskConfig extends Task {
|
|||||||
get hasVad() { return Object.keys(this.vad).length; }
|
get hasVad() { return Object.keys(this.vad).length; }
|
||||||
get hasFillerNoise() { return Object.keys(this.fillerNoise).length; }
|
get hasFillerNoise() { return Object.keys(this.fillerNoise).length; }
|
||||||
get hasReferHook() { return Object.keys(this.data).includes('referHook'); }
|
get hasReferHook() { return Object.keys(this.data).includes('referHook'); }
|
||||||
|
get hasNotifySttLatency() { return Object.keys(this.data).includes('notifySttLatency'); }
|
||||||
get hasTtsStream() { return Object.keys(this.ttsStream).length; }
|
get hasTtsStream() { return Object.keys(this.ttsStream).length; }
|
||||||
|
get hasDisableTtsCache() { return Object.keys(this.data).includes('disableTtsCache'); }
|
||||||
|
|
||||||
get summary() {
|
get summary() {
|
||||||
const phrase = [];
|
const phrase = [];
|
||||||
@@ -112,6 +118,8 @@ class TaskConfig extends Task {
|
|||||||
if (this.hasFillerNoise) phrase.push(`fillerNoise ${this.fillerNoise.enable ? 'on' : 'off'}`);
|
if (this.hasFillerNoise) phrase.push(`fillerNoise ${this.fillerNoise.enable ? 'on' : 'off'}`);
|
||||||
if (this.data.amd) phrase.push('enable amd');
|
if (this.data.amd) phrase.push('enable amd');
|
||||||
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||||
|
if (this.hasNotifySttLatency) phrase.push(
|
||||||
|
`notifySttLatency ${this.notifySttLatency ? 'on' : 'off'}`);
|
||||||
if (this.onHoldMusic) phrase.push(`onHoldMusic: ${this.onHoldMusic}`);
|
if (this.onHoldMusic) phrase.push(`onHoldMusic: ${this.onHoldMusic}`);
|
||||||
if ('boostAudioSignal' in this.data) phrase.push(`setGain ${this.data.boostAudioSignal}`);
|
if ('boostAudioSignal' in this.data) phrase.push(`setGain ${this.data.boostAudioSignal}`);
|
||||||
if (this.hasReferHook) phrase.push('set referHook');
|
if (this.hasReferHook) phrase.push('set referHook');
|
||||||
@@ -119,6 +127,7 @@ class TaskConfig extends Task {
|
|||||||
phrase.push(`${this.ttsStream.enable ? 'enable' : 'disable'} ttsStream`);
|
phrase.push(`${this.ttsStream.enable ? 'enable' : 'disable'} ttsStream`);
|
||||||
}
|
}
|
||||||
if ('autoStreamTts' in this.data) phrase.push(`enable Say.stream value ${this.data.autoStreamTts ? 'on' : 'off'}`);
|
if ('autoStreamTts' in this.data) phrase.push(`enable Say.stream value ${this.data.autoStreamTts ? 'on' : 'off'}`);
|
||||||
|
if (this.hasDisableTtsCache) phrase.push(`disableTtsCache ${this.data.disableTtsCache ? 'on' : 'off'}`);
|
||||||
return `${this.name}{${phrase.join(',')}}`;
|
return `${this.name}{${phrase.join(',')}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +139,11 @@ class TaskConfig extends Task {
|
|||||||
cs.notifyEvents = !!this.data.notifyEvents;
|
cs.notifyEvents = !!this.data.notifyEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.hasNotifySttLatency) {
|
||||||
|
this.logger.debug(`turning notifySttLatency ${this.notifySttLatency ? 'on' : 'off'}`);
|
||||||
|
cs.notifySttLatencyEnabled = this.notifySttLatency;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.onHoldMusic) {
|
if (this.onHoldMusic) {
|
||||||
cs.onHoldMusic = this.onHoldMusic;
|
cs.onHoldMusic = this.onHoldMusic;
|
||||||
}
|
}
|
||||||
@@ -318,7 +332,10 @@ class TaskConfig extends Task {
|
|||||||
voiceMs: this.vad.voiceMs || 250,
|
voiceMs: this.vad.voiceMs || 250,
|
||||||
silenceMs: this.vad.silenceMs || 150,
|
silenceMs: this.vad.silenceMs || 150,
|
||||||
strategy: this.vad.strategy || 'one-shot',
|
strategy: this.vad.strategy || 'one-shot',
|
||||||
mode: (this.vad.mode !== undefined && this.vad.mode !== null) ? this.vad.mode : 2
|
mode: (this.vad.mode !== undefined && this.vad.mode !== null) ? this.vad.mode : 2,
|
||||||
|
vendor: this.vad.vendor || 'silero',
|
||||||
|
threshold: this.vad.threshold || 0.5,
|
||||||
|
speechPadMs: this.vad.speechPadMs || 30,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +360,11 @@ class TaskConfig extends Task {
|
|||||||
this.logger.info('Config: disabling ttsStream');
|
this.logger.info('Config: disabling ttsStream');
|
||||||
cs.disableTtsStream();
|
cs.disableTtsStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.hasDisableTtsCache) {
|
||||||
|
this.logger.info(`set disableTtsCache = ${this.disableTtsCache}`);
|
||||||
|
cs.disableTtsCache = this.data.disableTtsCache;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async kill(cs) {
|
async kill(cs) {
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ const parseDecibels = require('../utils/parse-decibels');
|
|||||||
const debug = require('debug')('jambonz:feature-server');
|
const debug = require('debug')('jambonz:feature-server');
|
||||||
const {parseUri} = require('drachtio-srf');
|
const {parseUri} = require('drachtio-srf');
|
||||||
const {ANCHOR_MEDIA_ALWAYS,
|
const {ANCHOR_MEDIA_ALWAYS,
|
||||||
JAMBONZ_DISABLE_DIAL_PAI_HEADER,
|
JAMBONZ_DIAL_PAI_HEADER,
|
||||||
JAMBONES_DIAL_SBC_FOR_REGISTERED_USER} = require('../config');
|
JAMBONES_DIAL_SBC_FOR_REGISTERED_USER} = require('../config');
|
||||||
const { isOnhold, isOpusFirst } = require('../utils/sdp-utils');
|
const { isOnhold, isOpusFirst, getLeadingCodec } = require('../utils/sdp-utils');
|
||||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
const { selectHostPort } = require('../utils/network');
|
const { selectHostPort } = require('../utils/network');
|
||||||
const { sleepFor } = require('../utils/helpers');
|
const { sleepFor } = require('../utils/helpers');
|
||||||
@@ -109,6 +109,7 @@ class TaskDial extends Task {
|
|||||||
this.tag = this.data.tag;
|
this.tag = this.data.tag;
|
||||||
this.boostAudioSignal = this.data.boostAudioSignal;
|
this.boostAudioSignal = this.data.boostAudioSignal;
|
||||||
this._mediaPath = MediaPath.FullMedia;
|
this._mediaPath = MediaPath.FullMedia;
|
||||||
|
this.forwardPAI = this.data.forwardPAI;
|
||||||
|
|
||||||
if (this.dtmfHook) {
|
if (this.dtmfHook) {
|
||||||
const {parentDtmfCollector, childDtmfCollector} = parseDtmfOptions(logger, this.data.dtmfCapture || {});
|
const {parentDtmfCollector, childDtmfCollector} = parseDtmfOptions(logger, this.data.dtmfCapture || {});
|
||||||
@@ -157,6 +158,7 @@ class TaskDial extends Task {
|
|||||||
|
|
||||||
get canReleaseMedia() {
|
get canReleaseMedia() {
|
||||||
const keepAnchor = this.data.anchorMedia ||
|
const keepAnchor = this.data.anchorMedia ||
|
||||||
|
this.weAreTranscoding ||
|
||||||
this.cs.isBackGroundListen ||
|
this.cs.isBackGroundListen ||
|
||||||
this.cs.onHoldMusic ||
|
this.cs.onHoldMusic ||
|
||||||
ANCHOR_MEDIA_ALWAYS ||
|
ANCHOR_MEDIA_ALWAYS ||
|
||||||
@@ -548,13 +550,14 @@ class TaskDial extends Task {
|
|||||||
let sbcAddress = this.proxy || getSBC();
|
let sbcAddress = this.proxy || getSBC();
|
||||||
const teamsInfo = {};
|
const teamsInfo = {};
|
||||||
let fqdn;
|
let fqdn;
|
||||||
|
const forwardPAI = this.forwardPAI ?? JAMBONZ_DIAL_PAI_HEADER; // dial verb overides env var
|
||||||
|
this.logger.debug(forwardPAI, 'forwardPAI value');
|
||||||
if (!sbcAddress) throw new Error('no SBC found for outbound call');
|
if (!sbcAddress) throw new Error('no SBC found for outbound call');
|
||||||
this.headers = {
|
this.headers = {
|
||||||
'X-Account-Sid': cs.accountSid,
|
'X-Account-Sid': cs.accountSid,
|
||||||
...(req && req.has('X-CID') && {'X-CID': req.get('X-CID')}),
|
...(req && req.has('X-CID') && {'X-CID': req.get('X-CID')}),
|
||||||
...(direction === 'outbound' && callInfo.sbcCallid && {'X-CID': callInfo.sbcCallid}),
|
...(direction === 'outbound' && callInfo.sbcCallid && {'X-CID': callInfo.sbcCallid}),
|
||||||
...(!JAMBONZ_DISABLE_DIAL_PAI_HEADER && req && {
|
...(req && forwardPAI && {
|
||||||
...(req.has('P-Asserted-Identity') && {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
|
...(req.has('P-Asserted-Identity') && {'P-Asserted-Identity': req.get('P-Asserted-Identity')}),
|
||||||
...(req.has('Privacy') && {'Privacy': req.get('Privacy')}),
|
...(req.has('Privacy') && {'Privacy': req.get('Privacy')}),
|
||||||
}),
|
}),
|
||||||
@@ -639,7 +642,9 @@ class TaskDial extends Task {
|
|||||||
* trunk isn't specified,
|
* trunk isn't specified,
|
||||||
* check if number matches any existing numbers
|
* check if number matches any existing numbers
|
||||||
* */
|
* */
|
||||||
if (t.type === 'phone' && !t.trunk) {
|
const { lookupLcrByAccount} = srf.locals.dbHelpers;
|
||||||
|
const lcrs = await lookupLcrByAccount(cs.accountSid);
|
||||||
|
if (t.type === 'phone' && !t.trunk && lcrs.length == 0) {
|
||||||
const str = this.callerId || req.callingNumber || '';
|
const str = this.callerId || req.callingNumber || '';
|
||||||
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
const callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber);
|
const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber);
|
||||||
@@ -672,7 +677,8 @@ class TaskDial extends Task {
|
|||||||
rootSpan: cs.rootSpan,
|
rootSpan: cs.rootSpan,
|
||||||
startSpan: this.startSpan.bind(this),
|
startSpan: this.startSpan.bind(this),
|
||||||
dialTask: this,
|
dialTask: this,
|
||||||
onHoldMusic: this.cs.onHoldMusic
|
onHoldMusic: this.cs.onHoldMusic,
|
||||||
|
tmpFiles: this.cs.tmpFiles,
|
||||||
});
|
});
|
||||||
this.dials.set(sd.callSid, sd);
|
this.dials.set(sd.callSid, sd);
|
||||||
|
|
||||||
@@ -773,6 +779,9 @@ class TaskDial extends Task {
|
|||||||
this.epOther.api('uuid_break', this.epOther.uuid);
|
this.epOther.api('uuid_break', this.epOther.uuid);
|
||||||
this.epOther.bridge(sd.ep);
|
this.epOther.bridge(sd.ep);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
this.logger.error('Dial:_connectSingleDial - no other endpoint to bridge!');
|
||||||
|
}
|
||||||
this.bridged = true;
|
this.bridged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,7 +930,13 @@ class TaskDial extends Task {
|
|||||||
this.logger.info({err}, 'Dial:_selectSingleDial - Error boosting audio signal');
|
this.logger.info({err}, 'Dial:_selectSingleDial - Error boosting audio signal');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* basic determination to see if call is being transcoded */
|
||||||
|
const codecA = getLeadingCodec(this.epOther.local.sdp);
|
||||||
|
const codecB = getLeadingCodec(this.ep.remote.sdp);
|
||||||
|
this.weAreTranscoding = (codecA !== codecB);
|
||||||
|
if (this.weAreTranscoding) {
|
||||||
|
this.logger.info(`Dial:_selectSingleDial - transcoding from ${codecA} (A leg) to ${codecB} (B leg)`);
|
||||||
|
}
|
||||||
/* if we can release the media back to the SBC, do so now */
|
/* if we can release the media back to the SBC, do so now */
|
||||||
if (this.canReleaseMedia || this.shouldExitMediaPathEntirely) {
|
if (this.canReleaseMedia || this.shouldExitMediaPathEntirely) {
|
||||||
setTimeout(this._releaseMedia.bind(this, cs, sd, this.shouldExitMediaPathEntirely), 200);
|
setTimeout(this._releaseMedia.bind(this, cs, sd, this.shouldExitMediaPathEntirely), 200);
|
||||||
@@ -995,7 +1010,7 @@ class TaskDial extends Task {
|
|||||||
this.epOther = null;
|
this.epOther = null;
|
||||||
this._mediaPath = releaseEntirely ? MediaPath.NoMedia : MediaPath.PartialMedia;
|
this._mediaPath = releaseEntirely ? MediaPath.NoMedia : MediaPath.PartialMedia;
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Dial:_releaseMedia - successfully released media from freewitch, media path is now ${this._mediaPath}`);
|
`Dial:_releaseMedia - successfully released media from freeswitch, media path is now ${this._mediaPath}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err}, 'Dial:_releaseMedia error');
|
this.logger.info({err}, 'Dial:_releaseMedia error');
|
||||||
}
|
}
|
||||||
@@ -1004,7 +1019,7 @@ class TaskDial extends Task {
|
|||||||
async reAnchorMedia(cs, sd) {
|
async reAnchorMedia(cs, sd) {
|
||||||
if (cs.ep && sd.ep) return;
|
if (cs.ep && sd.ep) return;
|
||||||
|
|
||||||
this.logger.info('Dial:reAnchorMedia - re-anchoring media to freewitch');
|
this.logger.info('Dial:reAnchorMedia - re-anchoring media to freeswitch');
|
||||||
await Promise.all([sd.reAnchorMedia(this._mediaPath), cs.reAnchorMedia(this._mediaPath)]);
|
await Promise.all([sd.reAnchorMedia(this._mediaPath), cs.reAnchorMedia(this._mediaPath)]);
|
||||||
this.epOther = cs.ep;
|
this.epOther = cs.ep;
|
||||||
|
|
||||||
@@ -1012,7 +1027,7 @@ class TaskDial extends Task {
|
|||||||
|
|
||||||
this._mediaPath = MediaPath.FullMedia;
|
this._mediaPath = MediaPath.FullMedia;
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Dial:_releaseMedia - successfully re-anchored media to freewitch, media path is now ${this._mediaPath}`);
|
`Dial:_releaseMedia - successfully re-anchored media to freeswitch, media path is now ${this._mediaPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle RE-INVITE hold from caller leg.
|
// Handle RE-INVITE hold from caller leg.
|
||||||
@@ -1096,7 +1111,8 @@ class TaskDial extends Task {
|
|||||||
accountInfo: this.cs.accountInfo,
|
accountInfo: this.cs.accountInfo,
|
||||||
tasks,
|
tasks,
|
||||||
rootSpan: this.cs.rootSpan,
|
rootSpan: this.cs.rootSpan,
|
||||||
req: this.cs.req
|
req: this.cs.req,
|
||||||
|
tmpFiles: this.cs.tmpFiles,
|
||||||
});
|
});
|
||||||
await this._onHoldSession.exec();
|
await this._onHoldSession.exec();
|
||||||
this._onHoldSession = null;
|
this._onHoldSession = null;
|
||||||
|
|||||||
@@ -370,7 +370,8 @@ class TaskEnqueue extends Task {
|
|||||||
accountInfo: cs.accountInfo,
|
accountInfo: cs.accountInfo,
|
||||||
tasks: tasksToRun,
|
tasks: tasksToRun,
|
||||||
rootSpan: cs.rootSpan,
|
rootSpan: cs.rootSpan,
|
||||||
req: cs.req
|
req: cs.req,
|
||||||
|
tmpFiles: cs.tmpFiles,
|
||||||
});
|
});
|
||||||
await this._playSession.exec();
|
await this._playSession.exec();
|
||||||
this._playSession = null;
|
this._playSession = null;
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ const {
|
|||||||
AwsTranscriptionEvents,
|
AwsTranscriptionEvents,
|
||||||
AzureTranscriptionEvents,
|
AzureTranscriptionEvents,
|
||||||
DeepgramTranscriptionEvents,
|
DeepgramTranscriptionEvents,
|
||||||
|
ElevenlabsTranscriptionEvents,
|
||||||
|
GladiaTranscriptionEvents,
|
||||||
SonioxTranscriptionEvents,
|
SonioxTranscriptionEvents,
|
||||||
CobaltTranscriptionEvents,
|
CobaltTranscriptionEvents,
|
||||||
IbmTranscriptionEvents,
|
IbmTranscriptionEvents,
|
||||||
NvidiaTranscriptionEvents,
|
NvidiaTranscriptionEvents,
|
||||||
JambonzTranscriptionEvents,
|
JambonzTranscriptionEvents,
|
||||||
AssemblyAiTranscriptionEvents,
|
AssemblyAiTranscriptionEvents,
|
||||||
|
HoundifyTranscriptionEvents,
|
||||||
|
DeepgramfluxTranscriptionEvents,
|
||||||
VoxistTranscriptionEvents,
|
VoxistTranscriptionEvents,
|
||||||
CartesiaTranscriptionEvents,
|
CartesiaTranscriptionEvents,
|
||||||
OpenAITranscriptionEvents,
|
OpenAITranscriptionEvents,
|
||||||
@@ -92,6 +96,8 @@ class TaskGather extends SttTask {
|
|||||||
|
|
||||||
get needsStt() { return this.input.includes('speech'); }
|
get needsStt() { return this.input.includes('speech'); }
|
||||||
|
|
||||||
|
get isBackgroundGather() { return this.bugname_prefix === 'background_bargeIn_'; }
|
||||||
|
|
||||||
get wantsSingleUtterance() {
|
get wantsSingleUtterance() {
|
||||||
return this.data.recognizer?.singleUtterance === true;
|
return this.data.recognizer?.singleUtterance === true;
|
||||||
}
|
}
|
||||||
@@ -226,7 +232,9 @@ class TaskGather extends SttTask {
|
|||||||
|
|
||||||
const startListening = async(cs, ep) => {
|
const startListening = async(cs, ep) => {
|
||||||
this._startTimer();
|
this._startTimer();
|
||||||
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
|
if (this.isContinuousAsr && 0 === this.timeout && !this.isBackgroundGather) {
|
||||||
|
this._startAsrTimer();
|
||||||
|
}
|
||||||
if (this.input.includes('speech') && !this.listenDuringPrompt) {
|
if (this.input.includes('speech') && !this.listenDuringPrompt) {
|
||||||
try {
|
try {
|
||||||
await this._setSpeechHandlers(cs, ep);
|
await this._setSpeechHandlers(cs, ep);
|
||||||
@@ -251,7 +259,7 @@ class TaskGather extends SttTask {
|
|||||||
startDtmfListener();
|
startDtmfListener();
|
||||||
}
|
}
|
||||||
this._stopVad();
|
this._stopVad();
|
||||||
if (!this.killed) {
|
if (!this.killed && !this.resolved) {
|
||||||
startListening(cs, ep);
|
startListening(cs, ep);
|
||||||
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
|
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
|
||||||
this.logger.debug('Gather:exec - starting transcription timers after say completes');
|
this.logger.debug('Gather:exec - starting transcription timers after say completes');
|
||||||
@@ -263,15 +271,22 @@ class TaskGather extends SttTask {
|
|||||||
};
|
};
|
||||||
this.sayTask.span = span;
|
this.sayTask.span = span;
|
||||||
this.sayTask.ctx = ctx;
|
this.sayTask.ctx = ctx;
|
||||||
this.sayTask.exec(cs, {ep}) // kicked off, _not_ waiting for it to complete
|
this.sayTask
|
||||||
|
.exec(cs, {ep}) // kicked off, _not_ waiting for it to complete
|
||||||
|
.then(() => {
|
||||||
|
if (this.sayTask.isStreamingTts) return;
|
||||||
|
this.logger.debug('Gather:exec - nested say task completed');
|
||||||
|
span.end();
|
||||||
|
process();
|
||||||
|
return;
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
process();
|
process();
|
||||||
});
|
});
|
||||||
this.sayTask.on('playDone', (err) => {
|
if (this.sayTask.isStreamingTts && !this.sayTask.closeOnStreamEmpty) {
|
||||||
span.end();
|
// if streaming tts, we do not wait for it to complete if it is not closing the stream automatically
|
||||||
if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
|
|
||||||
process();
|
process();
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
else if (this.playTask) {
|
else if (this.playTask) {
|
||||||
const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`);
|
const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`);
|
||||||
@@ -282,7 +297,7 @@ class TaskGather extends SttTask {
|
|||||||
startDtmfListener();
|
startDtmfListener();
|
||||||
}
|
}
|
||||||
this._stopVad();
|
this._stopVad();
|
||||||
if (!this.killed) {
|
if (!this.killed && !this.resolved) {
|
||||||
startListening(cs, ep);
|
startListening(cs, ep);
|
||||||
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
|
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
|
||||||
this.logger.debug('Gather:exec - starting transcription timers after play completes');
|
this.logger.debug('Gather:exec - starting transcription timers after play completes');
|
||||||
@@ -294,15 +309,17 @@ class TaskGather extends SttTask {
|
|||||||
};
|
};
|
||||||
this.playTask.span = span;
|
this.playTask.span = span;
|
||||||
this.playTask.ctx = ctx;
|
this.playTask.ctx = ctx;
|
||||||
this.playTask.exec(cs, {ep}) // kicked off, _not_ waiting for it to complete
|
this.playTask
|
||||||
|
.exec(cs, {ep}) // kicked off, _not_ waiting for it to complete
|
||||||
|
.then(() => {
|
||||||
|
this.logger.debug('Gather:exec - nested play task completed');
|
||||||
|
span.end();
|
||||||
|
process();
|
||||||
|
return;
|
||||||
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
process();
|
process();
|
||||||
});
|
});
|
||||||
this.playTask.on('playDone', (err) => {
|
|
||||||
span.end();
|
|
||||||
if (err) this.logger.error({err}, 'Gather:exec Error playing url');
|
|
||||||
process();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (this.killed) {
|
if (this.killed) {
|
||||||
@@ -346,6 +363,7 @@ class TaskGather extends SttTask {
|
|||||||
this.sayTask?.span.end();
|
this.sayTask?.span.end();
|
||||||
this._stopVad();
|
this._stopVad();
|
||||||
this._resolve('killed');
|
this._resolve('killed');
|
||||||
|
cs.stopSttLatencyVad();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTaskInProgress(opts) {
|
updateTaskInProgress(opts) {
|
||||||
@@ -361,6 +379,9 @@ class TaskGather extends SttTask {
|
|||||||
|
|
||||||
_onDtmf(cs, ep, evt) {
|
_onDtmf(cs, ep, evt) {
|
||||||
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
||||||
|
if (!this._timeoutTimer && this.timeout > 0) {
|
||||||
|
this._startTimer();
|
||||||
|
}
|
||||||
clearTimeout(this.interDigitTimer);
|
clearTimeout(this.interDigitTimer);
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
if (this.dtmfBargein) {
|
if (this.dtmfBargein) {
|
||||||
@@ -385,12 +406,8 @@ class TaskGather extends SttTask {
|
|||||||
if (this.digitBuffer.length === 0 && this.needsStt) {
|
if (this.digitBuffer.length === 0 && this.needsStt) {
|
||||||
// DTMF is higher priority than STT.
|
// DTMF is higher priority than STT.
|
||||||
this.removeCustomEventListeners();
|
this.removeCustomEventListeners();
|
||||||
ep.stopTranscription({
|
this._clearAsrTimer(); //clear ASR timer as we're now using dtmf
|
||||||
vendor: this.vendor,
|
this._stopTranscribing(ep);
|
||||||
bugname: this.bugname,
|
|
||||||
})
|
|
||||||
.catch((err) => this.logger.error({err},
|
|
||||||
` Received DTMF, Error stopping transcription for vendor ${this.vendor}`));
|
|
||||||
}
|
}
|
||||||
this.digitBuffer += evt.dtmf;
|
this.digitBuffer += evt.dtmf;
|
||||||
const len = this.digitBuffer.length;
|
const len = this.digitBuffer.length;
|
||||||
@@ -404,6 +421,7 @@ class TaskGather extends SttTask {
|
|||||||
const ms = this.interDigitTimeout * 1000;
|
const ms = this.interDigitTimeout * 1000;
|
||||||
this.logger.debug(`starting interdigit timer of ${ms}`);
|
this.logger.debug(`starting interdigit timer of ${ms}`);
|
||||||
this.interDigitTimer = setTimeout(() => this._resolve('dtmf-interdigit-timeout'), ms);
|
this.interDigitTimer = setTimeout(() => this._resolve('dtmf-interdigit-timeout'), ms);
|
||||||
|
this._clearTimer(); //clear main timer as we're now using interdigit dtmf timer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,6 +479,39 @@ class TaskGather extends SttTask {
|
|||||||
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||||
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.ConnectFailure,
|
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.ConnectFailure,
|
||||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deepgramflux':
|
||||||
|
this.bugname = `${this.bugname_prefix}deepgramflux_transcribe`;
|
||||||
|
this.addCustomEventListener(
|
||||||
|
ep, DeepgramfluxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(
|
||||||
|
ep, DeepgramfluxTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, DeepgramfluxTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, DeepgramfluxTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'elevenlabs':
|
||||||
|
this.bugname = `${this.bugname_prefix}elevenlabs_transcribe`;
|
||||||
|
this.addCustomEventListener(
|
||||||
|
ep, ElevenlabsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(
|
||||||
|
ep, ElevenlabsTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, ElevenlabsTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, ElevenlabsTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gladia':
|
||||||
|
this.bugname = `${this.bugname_prefix}gladia_transcribe`;
|
||||||
|
this.addCustomEventListener(
|
||||||
|
ep, GladiaTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, GladiaTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, GladiaTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, GladiaTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'soniox':
|
case 'soniox':
|
||||||
@@ -540,6 +591,18 @@ class TaskGather extends SttTask {
|
|||||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'houndify':
|
||||||
|
this.bugname = `${this.bugname_prefix}houndify_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, HoundifyTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, HoundifyTranscriptionEvents.Error,
|
||||||
|
this._onVendorError.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, HoundifyTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, HoundifyTranscriptionEvents.Connect,
|
||||||
|
this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
break;
|
||||||
|
|
||||||
case 'voxist':
|
case 'voxist':
|
||||||
this.bugname = `${this.bugname_prefix}voxist_transcribe`;
|
this.bugname = `${this.bugname_prefix}voxist_transcribe`;
|
||||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Transcription,
|
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Transcription,
|
||||||
@@ -677,14 +740,18 @@ class TaskGather extends SttTask {
|
|||||||
target_sid: this.cs.callSid
|
target_sid: this.cs.callSid
|
||||||
});
|
});
|
||||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||||
|
|
||||||
|
// Some vendor use single connection, that we cannot use onConnect event to track transcription start
|
||||||
|
this.cs.emit('transcribe-start');
|
||||||
}
|
}
|
||||||
|
|
||||||
_startTimer() {
|
_startTimer() {
|
||||||
if (0 === this.timeout) return;
|
if (0 === this.timeout) return;
|
||||||
|
this.logger.debug(`Starting timoutTimer of ${this.timeout}ms`);
|
||||||
this._clearTimer();
|
this._clearTimer();
|
||||||
this._timeoutTimer = setTimeout(() => {
|
this._timeoutTimer = setTimeout(() => {
|
||||||
|
// If continuousASR in use then extend by the asr window for more transcripts.
|
||||||
if (this.isContinuousAsr) this._startAsrTimer();
|
if (this.isContinuousAsr) this._startAsrTimer();
|
||||||
if (this.interDigitTimer) return; // let the inter-digit timer complete
|
|
||||||
else {
|
else {
|
||||||
this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
|
this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
|
||||||
}
|
}
|
||||||
@@ -831,12 +898,10 @@ class TaskGather extends SttTask {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.sayTask && !this.sayTask.killed) {
|
if (this.sayTask && !this.sayTask.killed) {
|
||||||
this.sayTask.removeAllListeners('playDone');
|
|
||||||
this.sayTask.kill(cs);
|
this.sayTask.kill(cs);
|
||||||
this.sayTask = null;
|
this.sayTask = null;
|
||||||
}
|
}
|
||||||
if (this.playTask && !this.playTask.killed) {
|
if (this.playTask && !this.playTask.killed) {
|
||||||
this.playTask.removeAllListeners('playDone');
|
|
||||||
this.playTask.kill(cs);
|
this.playTask.kill(cs);
|
||||||
this.playTask = null;
|
this.playTask = null;
|
||||||
}
|
}
|
||||||
@@ -860,6 +925,10 @@ class TaskGather extends SttTask {
|
|||||||
if (finished === 'true') return;
|
if (finished === 'true') return;
|
||||||
|
|
||||||
if (this.vendor === 'ibm' && evt?.state === 'listening') return;
|
if (this.vendor === 'ibm' && evt?.state === 'listening') return;
|
||||||
|
|
||||||
|
// emit an event to the call session to track the time transcription is received
|
||||||
|
cs.emit('on-transcription');
|
||||||
|
|
||||||
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
|
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
|
||||||
/* we will only get this when we have set utterance_end_ms */
|
/* we will only get this when we have set utterance_end_ms */
|
||||||
if (this._bufferedTranscripts.length === 0) {
|
if (this._bufferedTranscripts.length === 0) {
|
||||||
@@ -887,7 +956,7 @@ class TaskGather extends SttTask {
|
|||||||
|
|
||||||
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language,
|
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language,
|
||||||
this.shortUtterance, this.data.recognizer.punctuation);
|
this.shortUtterance, this.data.recognizer.punctuation);
|
||||||
//this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription normalized transcript');
|
this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription normalized transcript');
|
||||||
|
|
||||||
if (evt.alternatives.length === 0) {
|
if (evt.alternatives.length === 0) {
|
||||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
|
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
|
||||||
@@ -943,6 +1012,12 @@ class TaskGather extends SttTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// receive a final transcript, calculate the stt latency for this transcript
|
||||||
|
const sttLatency = this.cs.calculateSttLatency();
|
||||||
|
if (!emptyTranscript && sttLatency) {
|
||||||
|
this.stt_latency_ms += `${sttLatency.stt_latency_ms},`;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isContinuousAsr) {
|
if (this.isContinuousAsr) {
|
||||||
/* append the transcript and start listening again for asrTimeout */
|
/* append the transcript and start listening again for asrTimeout */
|
||||||
const t = evt.alternatives[0].transcript;
|
const t = evt.alternatives[0].transcript;
|
||||||
@@ -1047,6 +1122,11 @@ class TaskGather extends SttTask {
|
|||||||
this.cs.requestor.request('verb:hook', this.partialResultHook, Object.assign({speech: evt},
|
this.cs.requestor.request('verb:hook', this.partialResultHook, Object.assign({speech: evt},
|
||||||
this.cs.callInfo, httpHeaders));
|
this.cs.callInfo, httpHeaders));
|
||||||
}
|
}
|
||||||
|
else if (this.vendor === 'deepgramflux' &&
|
||||||
|
['EagerEndOfTurn', 'TurnResumed'].includes(evt.vendor.evt?.event)) {
|
||||||
|
this.logger.debug(`Gather:_onTranscription - deepgramflux event detected: ${evt.event}`);
|
||||||
|
this.performAction({speech: evt, reason: 'speechDetected'}, false);
|
||||||
|
}
|
||||||
if (this.vendor === 'soniox') {
|
if (this.vendor === 'soniox') {
|
||||||
if (evt.vendor.finalWords.length) {
|
if (evt.vendor.finalWords.length) {
|
||||||
this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript');
|
this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript');
|
||||||
@@ -1093,13 +1173,8 @@ class TaskGather extends SttTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _startFallback(cs, ep, evt) {
|
async _startFallback(cs, ep, evt) {
|
||||||
if (this.canFallback) {
|
if (this.canFallback()) {
|
||||||
ep.stopTranscription({
|
this._stopTranscribing(ep);
|
||||||
vendor: this.vendor,
|
|
||||||
bugname: this.bugname,
|
|
||||||
gracefulShutdown: false
|
|
||||||
})
|
|
||||||
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
|
|
||||||
try {
|
try {
|
||||||
this.logger.debug('gather:_startFallback');
|
this.logger.debug('gather:_startFallback');
|
||||||
this.notifyError({ msg: 'ASR error',
|
this.notifyError({ msg: 'ASR error',
|
||||||
@@ -1228,17 +1303,26 @@ class TaskGather extends SttTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _stopTranscribing(ep) {
|
||||||
|
// Fix for https://github.com/jambonz/jambonz-feature-server/issues/1281
|
||||||
|
// We should immediately call stop transcription from gather
|
||||||
|
// so that next gather can start transcription immediately
|
||||||
|
ep.stopTranscription({
|
||||||
|
vendor: this.vendor,
|
||||||
|
bugname: this.bugname,
|
||||||
|
gracefulShutdown: false
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (this.resolved) return;
|
||||||
|
this.logger.error({err}, 'Error stopping transcription');
|
||||||
|
});
|
||||||
|
this.cs.emit('transcribe-stop');
|
||||||
|
}
|
||||||
|
|
||||||
async _resolve(reason, evt) {
|
async _resolve(reason, evt) {
|
||||||
this.logger.info({evt}, `TaskGather:resolve with reason ${reason}`);
|
this.logger.info({evt}, `TaskGather:resolve with reason ${reason}`);
|
||||||
if (this.needsStt && this.ep && this.ep.connected) {
|
if (this.needsStt && this.ep && this.ep.connected) {
|
||||||
this.ep.stopTranscription({
|
this._stopTranscribing(this.ep);
|
||||||
vendor: this.vendor,
|
|
||||||
bugname: this.bugname
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (this.resolved) return;
|
|
||||||
this.logger.error({err}, 'Error stopping transcription');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (this.resolved) {
|
if (this.resolved) {
|
||||||
this.logger.debug('TaskGather:_resolve - already resolved');
|
this.logger.debug('TaskGather:_resolve - already resolved');
|
||||||
@@ -1257,11 +1341,28 @@ class TaskGather extends SttTask {
|
|||||||
this._clearAsrTimer();
|
this._clearAsrTimer();
|
||||||
this._clearFinalAsrTimer();
|
this._clearFinalAsrTimer();
|
||||||
|
|
||||||
|
let sttLatencyMetrics = {};
|
||||||
|
if (this.needsStt) {
|
||||||
|
const sttLatency = this.cs.calculateSttLatency();
|
||||||
|
if (sttLatency) {
|
||||||
|
this.stt_latency_ms = this.stt_latency_ms.endsWith(',') ?
|
||||||
|
this.stt_latency_ms.slice(0, -1) : this.stt_latency_ms;
|
||||||
|
sttLatencyMetrics = {
|
||||||
|
'stt.latency_ms': this.stt_latency_ms,
|
||||||
|
'stt.talkspurts': JSON.stringify(sttLatency.talkspurts),
|
||||||
|
'stt.start_time': sttLatency.stt_start_time,
|
||||||
|
'stt.stop_time': sttLatency.stt_stop_time,
|
||||||
|
'stt.usage': sttLatency.stt_usage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.span.setAttributes({
|
this.span.setAttributes({
|
||||||
channel: 1,
|
channel: 1,
|
||||||
'stt.label': this.label || 'None',
|
'stt.label': this.label || 'None',
|
||||||
'stt.resolve': reason,
|
'stt.resolve': reason,
|
||||||
'stt.result': JSON.stringify(evt)
|
'stt.result': JSON.stringify(evt),
|
||||||
|
...sttLatencyMetrics
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.callSession && this.callSession.callGone) {
|
if (this.callSession && this.callSession.callGone) {
|
||||||
@@ -1289,6 +1390,9 @@ class TaskGather extends SttTask {
|
|||||||
|
|
||||||
let returnedVerbs = false;
|
let returnedVerbs = false;
|
||||||
try {
|
try {
|
||||||
|
const latencies = Object.fromEntries(
|
||||||
|
Object.entries(sttLatencyMetrics).map(([key, value]) => [key.replace('stt.', 'stt_'), value])
|
||||||
|
);
|
||||||
if (reason.startsWith('dtmf')) {
|
if (reason.startsWith('dtmf')) {
|
||||||
if (this.parentTask) this.parentTask.emit('dtmf', evt);
|
if (this.parentTask) this.parentTask.emit('dtmf', evt);
|
||||||
else {
|
else {
|
||||||
@@ -1302,7 +1406,7 @@ class TaskGather extends SttTask {
|
|||||||
else {
|
else {
|
||||||
this.emit('transcription', evt);
|
this.emit('transcription', evt);
|
||||||
this.logger.debug('TaskGather:_resolve - invoking performAction');
|
this.logger.debug('TaskGather:_resolve - invoking performAction');
|
||||||
returnedVerbs = await this.performAction({speech: evt, reason: 'speechDetected'});
|
returnedVerbs = await this.performAction({speech: evt, reason: 'speechDetected', ...latencies});
|
||||||
this.logger.debug({returnedVerbs}, 'TaskGather:_resolve - back from performAction');
|
this.logger.debug({returnedVerbs}, 'TaskGather:_resolve - back from performAction');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1310,20 +1414,20 @@ class TaskGather extends SttTask {
|
|||||||
if (this.parentTask) this.parentTask.emit('timeout', evt);
|
if (this.parentTask) this.parentTask.emit('timeout', evt);
|
||||||
else {
|
else {
|
||||||
this.emit('timeout', evt);
|
this.emit('timeout', evt);
|
||||||
returnedVerbs = await this.performAction({reason: 'timeout'});
|
returnedVerbs = await this.performAction({reason: 'timeout', ...latencies});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (reason.startsWith('stt-error')) {
|
else if (reason.startsWith('stt-error')) {
|
||||||
if (this.parentTask) this.parentTask.emit('stt-error', evt);
|
if (this.parentTask) this.parentTask.emit('stt-error', evt);
|
||||||
else {
|
else {
|
||||||
this.emit('stt-error', evt);
|
this.emit('stt-error', evt);
|
||||||
returnedVerbs = await this.performAction({reason: 'error', details: evt.error});
|
returnedVerbs = await this.performAction({reason: 'error', details: evt.error, ...latencies});
|
||||||
}
|
}
|
||||||
} else if (reason.startsWith('stt-low-confidence')) {
|
} else if (reason.startsWith('stt-low-confidence')) {
|
||||||
if (this.parentTask) this.parentTask.emit('stt-low-confidence', evt);
|
if (this.parentTask) this.parentTask.emit('stt-low-confidence', evt);
|
||||||
else {
|
else {
|
||||||
this.emit('stt-low-confidence', evt);
|
this.emit('stt-low-confidence', evt);
|
||||||
returnedVerbs = await this.performAction({speech:evt, reason: 'stt-low-confidence'});
|
returnedVerbs = await this.performAction({speech:evt, reason: 'stt-low-confidence', ...latencies});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) { /*already logged error*/ }
|
} catch (err) { /*already logged error*/ }
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants');
|
const {TaskName, TaskPreconditions, ListenEvents, ListenStatus} = require('../utils/constants.json');
|
||||||
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';
|
const DTMF_SPAN_NAME = 'dtmf';
|
||||||
|
|
||||||
|
function escapeString(str) {
|
||||||
|
return str
|
||||||
|
.replace(/\\/g, '\\\\') // Escape backslashes
|
||||||
|
.replace(/"/g, '\\"') // Escape double quotes
|
||||||
|
.replace(/[\b]/g, '\\b') // Escape backspace (NOTE: [\b] not \b)
|
||||||
|
.replace(/\f/g, '\\f') // Escape formfeed
|
||||||
|
.replace(/\n/g, '\\n') // Escape newlines
|
||||||
|
.replace(/\r/g, '\\r') // Escape carriage returns
|
||||||
|
.replace(/\t/g, '\\t'); // Escape tabs
|
||||||
|
}
|
||||||
|
|
||||||
class TaskListen extends Task {
|
class TaskListen extends Task {
|
||||||
constructor(logger, opts, parentTask) {
|
constructor(logger, opts, parentTask) {
|
||||||
super(logger, opts);
|
super(logger, opts);
|
||||||
@@ -16,10 +27,21 @@ class TaskListen extends Task {
|
|||||||
this.preconditions = TaskPreconditions.Endpoint;
|
this.preconditions = TaskPreconditions.Endpoint;
|
||||||
|
|
||||||
[
|
[
|
||||||
'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep',
|
'action', 'auth', 'method', 'url', 'finishOnKey', 'maxLength', 'mixType', 'passDtmf', 'playBeep',
|
||||||
'sampleRate', 'timeout', 'transcribe', 'wsAuth', 'disableBidirectionalAudio', 'channel'
|
'sampleRate', 'timeout', 'transcribe', 'wsAuth', 'disableBidirectionalAudio', 'channel'
|
||||||
].forEach((k) => this[k] = this.data[k]);
|
].forEach((k) => this[k] = this.data[k]);
|
||||||
|
|
||||||
|
//Escape JSON special characters in metadata
|
||||||
|
if (this.data.metadata) {
|
||||||
|
this.metadata = {};
|
||||||
|
for (const key in this.data.metadata) {
|
||||||
|
if (this.data.metadata.hasOwnProperty(key)) {
|
||||||
|
const value = this.data.metadata[key];
|
||||||
|
this.metadata[key] = typeof value === 'string' ? escapeString(value) : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.mixType = this.mixType || 'mono';
|
this.mixType = this.mixType || 'mono';
|
||||||
this.sampleRate = this.sampleRate || 8000;
|
this.sampleRate = this.sampleRate || 8000;
|
||||||
this.earlyMedia = this.data.earlyMedia === true;
|
this.earlyMedia = this.data.earlyMedia === true;
|
||||||
@@ -72,7 +94,7 @@ class TaskListen extends Task {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info(err, `TaskListen:exec - error ${this.url}`);
|
this.logger.info(err, `TaskListen:exec - error ${this.url}`);
|
||||||
}
|
}
|
||||||
if (this.transcribeTask) this.transcribeTask.kill();
|
if (this.transcribeTask) this.transcribeTask.kill(cs);
|
||||||
this._removeListeners(ep);
|
this._removeListeners(ep);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,9 +125,12 @@ class TaskListen extends Task {
|
|||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateListen(status) {
|
async updateListen(status, silence = false) {
|
||||||
if (!this.killed && this.ep && this.ep.connected) {
|
if (!this.killed && this.ep && this.ep.connected) {
|
||||||
const args = this._bugname ? [this._bugname] : [];
|
const args = [
|
||||||
|
...(this._bugname ? [this._bugname] : []),
|
||||||
|
...(status === ListenStatus.Pause ? ([silence]) : []),
|
||||||
|
];
|
||||||
this.logger.info(`TaskListen:updateListen status ${status}`);
|
this.logger.info(`TaskListen:updateListen status ${status}`);
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ListenStatus.Pause:
|
case ListenStatus.Pause:
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
async _onServerEvent(_ep, evt) {
|
async _onServerEvent(_ep, evt) {
|
||||||
let endConversation = false;
|
let endConversation = false;
|
||||||
const type = evt.type;
|
const type = evt.type;
|
||||||
this.logger.debug({evt}, 'TaskLlmUltravox_S2S:_onServerEvent');
|
//this.logger.debug({evt}, 'TaskLlmUltravox_S2S:_onServerEvent');
|
||||||
|
|
||||||
/* server errors of some sort */
|
/* server errors of some sort */
|
||||||
if (type === 'error') {
|
if (type === 'error') {
|
||||||
|
|||||||
@@ -6,7 +6,21 @@ class TaskPlay extends Task {
|
|||||||
super(logger, opts);
|
super(logger, opts);
|
||||||
this.preconditions = TaskPreconditions.Endpoint;
|
this.preconditions = TaskPreconditions.Endpoint;
|
||||||
|
|
||||||
this.url = this.data.url;
|
//Cleanup URLs that contain a querystring with a . unless that querystring is the filename
|
||||||
|
// see https://github.com/jambonz/jambonz-feature-server/pull/1293
|
||||||
|
// and https://github.com/jambonz/jambonz-feature-server/issues/1394 for background
|
||||||
|
if (this.data.url.includes('?')) {
|
||||||
|
if (['.mp3', '.wav'].includes(this.data.url.slice(-4))) {
|
||||||
|
this.url = this.data.url;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.url = this.data.url.split('?')[0] + '?' + this.data.url.split('?')[1].replaceAll('.', '%2E');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.url = this.data.url;
|
||||||
|
}
|
||||||
|
|
||||||
this.seekOffset = this.data.seekOffset || -1;
|
this.seekOffset = this.data.seekOffset || -1;
|
||||||
this.timeoutSecs = this.data.timeoutSecs || -1;
|
this.timeoutSecs = this.data.timeoutSecs || -1;
|
||||||
this.loop = this.data.loop || 1;
|
this.loop = this.data.loop || 1;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class TaskRestDial extends Task {
|
|||||||
this.timeout = this.data.timeout || 60;
|
this.timeout = this.data.timeout || 60;
|
||||||
this.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
|
this.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
|
||||||
this.referHook = this.data.referHook;
|
this.referHook = this.data.referHook;
|
||||||
|
this.recentCallStatus = 0;
|
||||||
|
|
||||||
this.on('connect', this._onConnect.bind(this));
|
this.on('connect', this._onConnect.bind(this));
|
||||||
this.on('callStatus', this._onCallStatus.bind(this));
|
this.on('callStatus', this._onCallStatus.bind(this));
|
||||||
@@ -57,7 +58,11 @@ class TaskRestDial extends Task {
|
|||||||
this._clearCallTimer();
|
this._clearCallTimer();
|
||||||
if (this.canCancel) {
|
if (this.canCancel) {
|
||||||
this.canCancel = false;
|
this.canCancel = false;
|
||||||
cs?.req?.cancel();
|
try {
|
||||||
|
cs?.req?.cancel();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, 'TaskRestDial: error cancelling call');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
@@ -118,7 +123,8 @@ class TaskRestDial extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onCallStatus(status) {
|
_onCallStatus(status) {
|
||||||
this.logger.debug(`CallStatus: ${status}`);
|
this.logger.debug(`RestDial CallStatus: ${status}`);
|
||||||
|
this.recentCallStatus = status;
|
||||||
if (status >= 200) {
|
if (status >= 200) {
|
||||||
this.canCancel = false;
|
this.canCancel = false;
|
||||||
this._clearCallTimer();
|
this._clearCallTimer();
|
||||||
@@ -136,11 +142,16 @@ class TaskRestDial extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onCallTimeout() {
|
_onCallTimeout() {
|
||||||
this.logger.debug('TaskRestDial: timeout expired without answer, killing task');
|
this.logger.debug(`TaskRestDial: timeout expired without answer, last status ${this.recentCallStatus}`);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
if (this.canCancel) {
|
if (this.canCancel && this.recentCallStatus < 200) {
|
||||||
|
this.logger.debug('TaskRestDial: cancelling call attempt');
|
||||||
this.canCancel = false;
|
this.canCancel = false;
|
||||||
this.cs?.req?.cancel();
|
try {
|
||||||
|
this.cs?.req?.cancel();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, 'TaskRestDial: error cancelling call');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
110
lib/tasks/say.js
110
lib/tasks/say.js
@@ -2,8 +2,31 @@ const assert = require('assert');
|
|||||||
const TtsTask = require('./tts-task');
|
const TtsTask = require('./tts-task');
|
||||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||||
const pollySSMLSplit = require('polly-ssml-split');
|
const pollySSMLSplit = require('polly-ssml-split');
|
||||||
const { SpeechCredentialError } = require('../utils/error');
|
const { SpeechCredentialError, NonFatalTaskError } = require('../utils/error');
|
||||||
const { sleepFor } = require('../utils/helpers');
|
const { sleepFor } = require('../utils/helpers');
|
||||||
|
const { NON_FANTAL_ERRORS } = require('../utils/constants.json');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discard unmatching responses:
|
||||||
|
* (1) I sent a playback id but get a response with a different playback id
|
||||||
|
* (2) I sent a playback id but get a response with no playback id
|
||||||
|
* (3) I did not send a playback id but get a response with a playback id
|
||||||
|
* (4) I sent a cache file but get a response with a different cache file
|
||||||
|
*/
|
||||||
|
|
||||||
|
const isMatchingEvent = (logger, filename, playbackId, evt) => {
|
||||||
|
|
||||||
|
if (!!playbackId && !!evt.variable_tts_playback_id && evt.variable_tts_playback_id === playbackId) {
|
||||||
|
//logger.debug({filename, playbackId, evt}, 'Say:isMatchingEvent - playbackId matched');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!!filename && !!evt.file && evt.file === filename) {
|
||||||
|
//logger.debug({filename, playbackId, evt}, 'Say:isMatchingEvent - filename matched');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
logger.info({filename, playbackId, evt}, 'Say:isMatchingEvent - no match');
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const breakLengthyTextIfNeeded = (logger, text) => {
|
const breakLengthyTextIfNeeded = (logger, text) => {
|
||||||
// As The text can be used for tts streaming, we need to break lengthy text into smaller chunks
|
// As The text can be used for tts streaming, we need to break lengthy text into smaller chunks
|
||||||
@@ -98,13 +121,11 @@ class TaskSay extends TtsTask {
|
|||||||
}
|
}
|
||||||
if (this.isStreamingTts) await this.handlingStreaming(cs, obj);
|
if (this.isStreamingTts) await this.handlingStreaming(cs, obj);
|
||||||
else await this.handling(cs, obj);
|
else await this.handling(cs, obj);
|
||||||
this.emit('playDone');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof SpeechCredentialError) {
|
if (error instanceof SpeechCredentialError) {
|
||||||
// if say failed due to speech credentials, alarm is writtern and error notification is sent
|
// if say failed due to speech credentials, alarm is writtern and error notification is sent
|
||||||
// finished this say to move to next task.
|
// finished this say to move to next task.
|
||||||
this.logger.info({error}, 'Say failed due to SpeechCredentialError, finished!');
|
this.logger.info({error}, 'Say failed due to SpeechCredentialError, finished!');
|
||||||
this.emit('playDone');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -125,9 +146,6 @@ class TaskSay extends TtsTask {
|
|||||||
|
|
||||||
await cs.startTtsStream();
|
await cs.startTtsStream();
|
||||||
|
|
||||||
cs.requestor?.request('tts:streaming-event', '/streaming-event', {event_type: 'stream_open'})
|
|
||||||
.catch((err) => this.logger.info({err}, 'TaskSay:handlingStreaming - Error sending'));
|
|
||||||
|
|
||||||
if (this.text.length !== 0) {
|
if (this.text.length !== 0) {
|
||||||
this.logger.info('TaskSay:handlingStreaming - sending text to TTS stream');
|
this.logger.info('TaskSay:handlingStreaming - sending text to TTS stream');
|
||||||
for (const t of this.text) {
|
for (const t of this.text) {
|
||||||
@@ -259,40 +277,32 @@ class TaskSay extends TtsTask {
|
|||||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && ep?.connected) {
|
while (!this.killed && (this.loop === 'forever' || this.loop--) && ep?.connected) {
|
||||||
let segment = 0;
|
let segment = 0;
|
||||||
while (!this.killed && segment < filepath.length) {
|
while (!this.killed && segment < filepath.length) {
|
||||||
|
const filename = filepath[segment];
|
||||||
if (cs.isInConference) {
|
if (cs.isInConference) {
|
||||||
const {memberId, confName, confUuid} = cs;
|
const {memberId, confName, confUuid} = cs;
|
||||||
await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]);
|
await this.playToConfMember(ep, memberId, confName, confUuid, filename);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let playbackId;
|
const isStreaming = filename.startsWith('say:{');
|
||||||
const isStreaming = filepath[segment].startsWith('say:{');
|
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
|
const arr = /^say:\{.*\}\s*(.*)$/.exec(filename);
|
||||||
if (arr) this.logger.debug(`Say:exec sending streaming tts request: ${arr[1].substring(0, 64)}..`);
|
if (arr) this.logger.debug(`Say:exec sending streaming tts request ${arr[1].substring(0, 64)}..`);
|
||||||
}
|
else this.logger.debug(`Say:exec sending ${filename.substring(0, 64)}`);
|
||||||
else {
|
|
||||||
this.logger.debug(`Say:exec sending ${filepath[segment].substring(0, 64)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPlaybackStop = (evt) => {
|
const onPlaybackStop = (evt) => {
|
||||||
try {
|
try {
|
||||||
this.logger.debug({evt},
|
const playbackId = this.getPlaybackId(segment);
|
||||||
`Say got playback-stop ${evt.variable_tts_playback_id ? evt.variable_tts_playback_id : ''}`);
|
const isMatch = isMatchingEvent(this.logger, filename, playbackId, evt);
|
||||||
|
if (!isMatch) {
|
||||||
/**
|
this.logger.info({currentPlaybackId: playbackId, stopPlaybackId: evt.variable_tts_playback_id},
|
||||||
* If we got a playback id on both the start and stop events, and they don't match,
|
|
||||||
* then we must have received a playback-stop event for an earlier play request.
|
|
||||||
*/
|
|
||||||
const unmatchedResponse = (!!playbackId && !!evt.variable_tts_playback_id) &&
|
|
||||||
evt.variable_tts_playback_id !== playbackId;
|
|
||||||
if (unmatchedResponse) {
|
|
||||||
this.logger.info({currentPlaybackId: playbackId, stopPPlaybackId: evt.variable_tts_playback_id},
|
|
||||||
'Say:exec discarding playback-stop for earlier play');
|
'Say:exec discarding playback-stop for earlier play');
|
||||||
ep.once('playback-stop', this._boundOnPlaybackStop);
|
ep.once('playback-stop', this._boundOnPlaybackStop);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.logger.debug({evt},
|
||||||
|
`Say got playback-stop ${evt.variable_tts_playback_id ? evt.variable_tts_playback_id : ''}`);
|
||||||
this.notifyStatus({event: 'stop-playback'});
|
this.notifyStatus({event: 'stop-playback'});
|
||||||
this.notifiedPlayBackStop = true;
|
this.notifiedPlayBackStop = true;
|
||||||
const tts_error = evt.variable_tts_error;
|
const tts_error = evt.variable_tts_error;
|
||||||
@@ -331,6 +341,7 @@ class TaskSay extends TtsTask {
|
|||||||
!this.disableTtsCache
|
!this.disableTtsCache
|
||||||
) {
|
) {
|
||||||
const text = parseTextFromSayString(this.text[segment]);
|
const text = parseTextFromSayString(this.text[segment]);
|
||||||
|
this.logger.debug({text, cacheFile: evt.variable_tts_cache_filename}, 'Say:exec cache tts');
|
||||||
addFileToCache(evt.variable_tts_cache_filename, {
|
addFileToCache(evt.variable_tts_cache_filename, {
|
||||||
account_sid,
|
account_sid,
|
||||||
vendor,
|
vendor,
|
||||||
@@ -358,9 +369,17 @@ class TaskSay extends TtsTask {
|
|||||||
};
|
};
|
||||||
this._boundOnPlaybackStop = onPlaybackStop.bind(this);
|
this._boundOnPlaybackStop = onPlaybackStop.bind(this);
|
||||||
|
|
||||||
ep.once('playback-start', (evt) => {
|
const onPlaybackStart = (evt) => {
|
||||||
try {
|
try {
|
||||||
playbackId = evt.variable_tts_playback_id;
|
const playbackId = this.getPlaybackId(segment);
|
||||||
|
const isMatch = isMatchingEvent(this.logger, filename, playbackId, evt);
|
||||||
|
if (!isMatch) {
|
||||||
|
this.logger.info({currentPlaybackId: playbackId, startPlaybackId: evt.variable_tts_playback_id},
|
||||||
|
'Say:exec playback-start - unmatched playback_id');
|
||||||
|
ep.once('playback-start', this._boundOnPlaybackStart);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ep.once('playback-stop', this._boundOnPlaybackStop);
|
||||||
this.logger.debug({evt},
|
this.logger.debug({evt},
|
||||||
`Say got playback-start ${evt.variable_tts_playback_id ? evt.variable_tts_playback_id : ''}`);
|
`Say got playback-start ${evt.variable_tts_playback_id ? evt.variable_tts_playback_id : ''}`);
|
||||||
if (this.otelSpan) {
|
if (this.otelSpan) {
|
||||||
@@ -374,16 +393,29 @@ class TaskSay extends TtsTask {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err}, 'Error handling playback-start event');
|
this.logger.info({err}, 'Error handling playback-start event');
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
ep.once('playback-stop', this._boundOnPlaybackStop);
|
this._boundOnPlaybackStart = onPlaybackStart.bind(this);
|
||||||
|
|
||||||
|
ep.once('playback-start', this._boundOnPlaybackStart);
|
||||||
|
|
||||||
// wait for playback-stop event received to confirm if the playback is successful
|
// wait for playback-stop event received to confirm if the playback is successful
|
||||||
this._playPromise = new Promise((resolve, reject) => {
|
this._playPromise = new Promise((resolve, reject) => {
|
||||||
this._playResolve = resolve;
|
this._playResolve = resolve;
|
||||||
this._playReject = reject;
|
this._playReject = reject;
|
||||||
});
|
});
|
||||||
const r = await ep.play(filepath[segment]);
|
try {
|
||||||
this.logger.debug({r}, 'Say:exec play result');
|
const r = await ep.play(filename);
|
||||||
|
this.logger.debug({r}, 'Say:exec play result');
|
||||||
|
if (r.playbackSeconds == null && r.playbackMilliseconds == null && r.playbackLastOffsetPos == null) {
|
||||||
|
this._playReject(new Error('Playback failed to start'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (NON_FANTAL_ERRORS.includes(err.message)) {
|
||||||
|
throw new NonFatalTaskError(err.message);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// wait for playback-stop event received to confirm if the playback is successful
|
// wait for playback-stop event received to confirm if the playback is successful
|
||||||
await this._playPromise;
|
await this._playPromise;
|
||||||
@@ -400,12 +432,12 @@ class TaskSay extends TtsTask {
|
|||||||
this._playResolve = null;
|
this._playResolve = null;
|
||||||
this._playReject = null;
|
this._playReject = null;
|
||||||
}
|
}
|
||||||
if (filepath[segment].startsWith('say:{')) {
|
if (filename.startsWith('say:{')) {
|
||||||
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
|
const arr = /^say:\{.*\}\s*(.*)$/.exec(filename);
|
||||||
if (arr) this.logger.debug(`Say:exec complete playing streaming tts request: ${arr[1].substring(0, 64)}..`);
|
if (arr) this.logger.debug(`Say:exec complete playing streaming tts request: ${arr[1].substring(0, 64)}..`);
|
||||||
} else {
|
} else {
|
||||||
// This log will print spech credentials in say command for tts stream mode
|
// This log will print spech credentials in say command for tts stream mode
|
||||||
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
this.logger.debug(`Say:exec completed play file ${filename}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
segment++;
|
segment++;
|
||||||
@@ -421,8 +453,8 @@ class TaskSay extends TtsTask {
|
|||||||
const {memberId, confName} = cs;
|
const {memberId, confName} = cs;
|
||||||
this.killPlayToConfMember(this.ep, memberId, confName);
|
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||||
} else if (this.isStreamingTts) {
|
} else if (this.isStreamingTts) {
|
||||||
this.logger.debug('TaskSay:kill - clearing TTS stream for streaming audio');
|
this.logger.debug('TaskSay:kill - stopping TTS stream for streaming audio');
|
||||||
cs.clearTtsStream();
|
cs.stopTtsStream();
|
||||||
} else {
|
} else {
|
||||||
if (!this.notifiedPlayBackStop) {
|
if (!this.notifiedPlayBackStop) {
|
||||||
this.notifyStatus({event: 'stop-playback'});
|
this.notifyStatus({event: 'stop-playback'});
|
||||||
@@ -452,6 +484,7 @@ class TaskSay extends TtsTask {
|
|||||||
.replace('playht_', 'playht.')
|
.replace('playht_', 'playht.')
|
||||||
.replace('cartesia_', 'cartesia.')
|
.replace('cartesia_', 'cartesia.')
|
||||||
.replace('rimelabs_', 'rimelabs.')
|
.replace('rimelabs_', 'rimelabs.')
|
||||||
|
.replace('resemble_', 'resemble.')
|
||||||
.replace('inworld_', 'inworld.')
|
.replace('inworld_', 'inworld.')
|
||||||
.replace('verbio_', 'verbio.')
|
.replace('verbio_', 'verbio.')
|
||||||
.replace('elevenlabs_', 'elevenlabs.');
|
.replace('elevenlabs_', 'elevenlabs.');
|
||||||
@@ -517,6 +550,9 @@ const spanMapping = {
|
|||||||
'rimelabs.name_lookup_time_ms': 'name_lookup_ms',
|
'rimelabs.name_lookup_time_ms': 'name_lookup_ms',
|
||||||
'rimelabs.connect_time_ms': 'connect_ms',
|
'rimelabs.connect_time_ms': 'connect_ms',
|
||||||
'rimelabs.final_response_time_ms': 'final_response_ms',
|
'rimelabs.final_response_time_ms': 'final_response_ms',
|
||||||
|
// Resemble
|
||||||
|
'resemble.connect_time_ms': 'connect_ms',
|
||||||
|
'resemble.final_response_time_ms': 'final_response_ms',
|
||||||
// inworld
|
// inworld
|
||||||
'inworld.name_lookup_time_ms': 'name_lookup_ms',
|
'inworld.name_lookup_time_ms': 'name_lookup_ms',
|
||||||
'inworld.connect_time_ms': 'connect_ms',
|
'inworld.connect_time_ms': 'connect_ms',
|
||||||
|
|||||||
@@ -111,7 +111,12 @@ class TaskSipRefer extends Task {
|
|||||||
/* get IP address of the SBC to use as hostname if needed */
|
/* get IP address of the SBC to use as hostname if needed */
|
||||||
const {host} = parseUri(dlg.remote.uri);
|
const {host} = parseUri(dlg.remote.uri);
|
||||||
|
|
||||||
if (!referTo.startsWith('<') && !referTo.startsWith('sip') && !referTo.startsWith('"')) {
|
if (
|
||||||
|
!referTo.startsWith('<') &&
|
||||||
|
!referTo.startsWith('sip:') &&
|
||||||
|
!referTo.startsWith('"') &&
|
||||||
|
!referTo.startsWith('tel:')
|
||||||
|
) {
|
||||||
/* they may have only provided a phone number/user */
|
/* they may have only provided a phone number/user */
|
||||||
referTo = `sip:${referTo}@${host}`;
|
referTo = `sip:${referTo}@${host}`;
|
||||||
}
|
}
|
||||||
@@ -124,7 +129,12 @@ class TaskSipRefer extends Task {
|
|||||||
if (!referredByDisplayName) {
|
if (!referredByDisplayName) {
|
||||||
referredByDisplayName = cs.req?.callingName;
|
referredByDisplayName = cs.req?.callingName;
|
||||||
}
|
}
|
||||||
if (!referredBy.startsWith('<') && !referredBy.startsWith('sip') && !referredBy.startsWith('"')) {
|
if (
|
||||||
|
!referredBy.startsWith('<') &&
|
||||||
|
!referredBy.startsWith('sip:') &&
|
||||||
|
!referredBy.startsWith('"') &&
|
||||||
|
!referredBy.startsWith('tel:')
|
||||||
|
) {
|
||||||
/* they may have only provided a phone number/user */
|
/* they may have only provided a phone number/user */
|
||||||
referredBy = `${referredByDisplayName ? `"${referredByDisplayName}"` : ''}<sip:${referredBy}@${host}>`;
|
referredBy = `${referredByDisplayName ? `"${referredByDisplayName}"` : ''}<sip:${referredBy}@${host}>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const crypto = require('crypto');
|
|||||||
const { TaskPreconditions, CobaltTranscriptionEvents } = require('../utils/constants');
|
const { TaskPreconditions, CobaltTranscriptionEvents } = require('../utils/constants');
|
||||||
const { SpeechCredentialError } = require('../utils/error');
|
const { SpeechCredentialError } = require('../utils/error');
|
||||||
const {JAMBONES_AWS_TRANSCRIBE_USE_GRPC} = require('../config');
|
const {JAMBONES_AWS_TRANSCRIBE_USE_GRPC} = require('../config');
|
||||||
|
const {TaskName} = require('../utils/constants.json');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Please insert turns here: {{turns:4}}"
|
* "Please insert turns here: {{turns:4}}"
|
||||||
@@ -84,6 +85,9 @@ class SttTask extends Task {
|
|||||||
/*bug name prefix */
|
/*bug name prefix */
|
||||||
this.bugname_prefix = '';
|
this.bugname_prefix = '';
|
||||||
|
|
||||||
|
// stt latency calculator
|
||||||
|
this.stt_latency_ms = '';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async exec(cs, {ep, ep2}) {
|
async exec(cs, {ep, ep2}) {
|
||||||
@@ -91,6 +95,12 @@ class SttTask extends Task {
|
|||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
this.ep2 = ep2;
|
this.ep2 = ep2;
|
||||||
|
|
||||||
|
// start vad from stt latency calculator
|
||||||
|
if (this.name !== TaskName.Gather ||
|
||||||
|
this.name === TaskName.Gather && this.needsStt) {
|
||||||
|
cs.startSttLatencyVad();
|
||||||
|
}
|
||||||
|
|
||||||
// use session preferences if we don't have specific verb-level settings.
|
// use session preferences if we don't have specific verb-level settings.
|
||||||
if (cs.recognizer) {
|
if (cs.recognizer) {
|
||||||
for (const k in cs.recognizer) {
|
for (const k in cs.recognizer) {
|
||||||
@@ -161,7 +171,7 @@ class SttTask extends Task {
|
|||||||
try {
|
try {
|
||||||
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
|
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.canFallback) {
|
if (this.canFallback()) {
|
||||||
this.notifyError(
|
this.notifyError(
|
||||||
{
|
{
|
||||||
msg: 'ASR error', details:`Invalid vendor ${this.vendor}, Error: ${error}`,
|
msg: 'ASR error', details:`Invalid vendor ${this.vendor}, Error: ${error}`,
|
||||||
@@ -193,6 +203,56 @@ class SttTask extends Task {
|
|||||||
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
||||||
this.data.recognizer.punctuation = cs.globalSttPunctuation;
|
this.data.recognizer.punctuation = cs.globalSttPunctuation;
|
||||||
}
|
}
|
||||||
|
if (this.vendor === 'gladia') {
|
||||||
|
const { api_key, region } = this.sttCredentials;
|
||||||
|
const {url} = await this.createGladiaLiveSession({
|
||||||
|
api_key, region,
|
||||||
|
model: this.data.recognizer.model || 'solaria-1',
|
||||||
|
options: this.data.recognizer.gladiaOptions || {}
|
||||||
|
});
|
||||||
|
const {host, pathname, search} = new URL(url);
|
||||||
|
this.sttCredentials.host = host;
|
||||||
|
this.sttCredentials.path = `${pathname}${search}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async createGladiaLiveSession({
|
||||||
|
api_key,
|
||||||
|
region = 'us-west',
|
||||||
|
model = 'solaria-1',
|
||||||
|
options = {},
|
||||||
|
}) {
|
||||||
|
const url = `https://api.gladia.io/v2/live?region=${region}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'x-gladia-key': api_key,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
encoding: 'wav/pcm',
|
||||||
|
bit_depth: 16,
|
||||||
|
sample_rate: 8000,
|
||||||
|
channels: 1,
|
||||||
|
model,
|
||||||
|
...options,
|
||||||
|
messages_config: {
|
||||||
|
receive_final_transcripts: true,
|
||||||
|
receive_speech_events: true,
|
||||||
|
receive_errors: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
this.logger.error({url, status: response.status, error}, 'Error creating Gladia live session');
|
||||||
|
throw new Error(`Error creating Gladia live session: ${response.status} ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
this.logger.debug({url: data.url}, 'Gladia Call registered');
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCustomEventListener(ep, event, handler) {
|
addCustomEventListener(ep, event, handler) {
|
||||||
@@ -200,8 +260,19 @@ class SttTask extends Task {
|
|||||||
ep.addCustomEventListener(event, handler);
|
ep.addCustomEventListener(event, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeCustomEventListeners() {
|
removeCustomEventListeners(ep) {
|
||||||
this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler));
|
if (ep) {
|
||||||
|
// for specific endpoint
|
||||||
|
this.eventHandlers.filter((h) => h.ep === ep).forEach((h) => {
|
||||||
|
h.ep.removeCustomEventListener(h.event, h.handler);
|
||||||
|
});
|
||||||
|
this.eventHandlers = this.eventHandlers.filter((h) => h.ep !== ep);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// for all endpoints
|
||||||
|
this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler));
|
||||||
|
this.eventHandlers = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _initSpeechCredentials(cs, vendor, label) {
|
async _initSpeechCredentials(cs, vendor, label) {
|
||||||
@@ -269,11 +340,13 @@ class SttTask extends Task {
|
|||||||
return credentials;
|
return credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canFallback() {
|
canFallback() {
|
||||||
return this.fallbackVendor && this.isHandledByPrimaryProvider && !this.cs.hasFallbackAsr;
|
return this.fallbackVendor && this.isHandledByPrimaryProvider && !this.cs.hasFallbackAsr;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _initFallback() {
|
// ep is optional for gather or any verb that have single ep,
|
||||||
|
// but transcribe does need as it might has 2 eps
|
||||||
|
async _initFallback(ep) {
|
||||||
assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration');
|
assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration');
|
||||||
this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
|
this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
|
||||||
this.isHandledByPrimaryProvider = false;
|
this.isHandledByPrimaryProvider = false;
|
||||||
@@ -286,7 +359,7 @@ class SttTask extends Task {
|
|||||||
this.data.recognizer.label = this.label;
|
this.data.recognizer.label = this.label;
|
||||||
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
|
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
|
||||||
// cleanup previous listener from previous vendor
|
// cleanup previous listener from previous vendor
|
||||||
this.removeCustomEventListeners();
|
this.removeCustomEventListeners(ep);
|
||||||
}
|
}
|
||||||
|
|
||||||
async compileHintsForCobalt(ep, hostport, model, token, hints) {
|
async compileHintsForCobalt(ep, hostport, model, token, hints) {
|
||||||
@@ -400,7 +473,7 @@ class SttTask extends Task {
|
|||||||
dgOptions.utteranceEndMs = dgOptions.utteranceEndMs || asrTimeout;
|
dgOptions.utteranceEndMs = dgOptions.utteranceEndMs || asrTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onVendorConnect(_cs, _ep) {
|
_onVendorConnect(cs, _ep) {
|
||||||
this.logger.debug(`TaskGather:_on${this.vendor}Connect`);
|
this.logger.debug(`TaskGather:_on${this.vendor}Connect`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const {
|
|||||||
AwsTranscriptionEvents,
|
AwsTranscriptionEvents,
|
||||||
AzureTranscriptionEvents,
|
AzureTranscriptionEvents,
|
||||||
DeepgramTranscriptionEvents,
|
DeepgramTranscriptionEvents,
|
||||||
|
GladiaTranscriptionEvents,
|
||||||
|
DeepgramfluxTranscriptionEvents,
|
||||||
SonioxTranscriptionEvents,
|
SonioxTranscriptionEvents,
|
||||||
CobaltTranscriptionEvents,
|
CobaltTranscriptionEvents,
|
||||||
IbmTranscriptionEvents,
|
IbmTranscriptionEvents,
|
||||||
@@ -13,6 +15,7 @@ const {
|
|||||||
JambonzTranscriptionEvents,
|
JambonzTranscriptionEvents,
|
||||||
TranscribeStatus,
|
TranscribeStatus,
|
||||||
AssemblyAiTranscriptionEvents,
|
AssemblyAiTranscriptionEvents,
|
||||||
|
HoundifyTranscriptionEvents,
|
||||||
VoxistTranscriptionEvents,
|
VoxistTranscriptionEvents,
|
||||||
CartesiaTranscriptionEvents,
|
CartesiaTranscriptionEvents,
|
||||||
OpenAITranscriptionEvents,
|
OpenAITranscriptionEvents,
|
||||||
@@ -67,6 +70,9 @@ class TaskTranscribe extends SttTask {
|
|||||||
this._bufferedTranscripts = [ [], [] ]; // for channel 1 and 2
|
this._bufferedTranscripts = [ [], [] ]; // for channel 1 and 2
|
||||||
this.bugname_prefix = 'transcribe_';
|
this.bugname_prefix = 'transcribe_';
|
||||||
this.paused = false;
|
this.paused = false;
|
||||||
|
// fallback flags
|
||||||
|
this.isHandledByPrimaryProviderForEp1 = true;
|
||||||
|
this.isHandledByPrimaryProviderForEp2 = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Transcribe; }
|
get name() { return TaskName.Transcribe; }
|
||||||
@@ -152,12 +158,15 @@ class TaskTranscribe extends SttTask {
|
|||||||
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cs.emit('transcribe-stop');
|
||||||
|
|
||||||
return stopTranscription;
|
return stopTranscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
async kill(cs) {
|
async kill(cs) {
|
||||||
super.kill(cs);
|
super.kill(cs);
|
||||||
const stopTranscription = this._stopTranscription();
|
const stopTranscription = this._stopTranscription();
|
||||||
|
cs.stopSttLatencyVad();
|
||||||
// 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();
|
||||||
@@ -233,10 +242,35 @@ class TaskTranscribe extends SttTask {
|
|||||||
this._onVendorConnect.bind(this, cs, ep));
|
this._onVendorConnect.bind(this, cs, ep));
|
||||||
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.ConnectFailure,
|
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.ConnectFailure,
|
||||||
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||||
|
|
||||||
|
|
||||||
/* if app sets deepgramOptions.utteranceEndMs they essentially want continuous asr */
|
/* if app sets deepgramOptions.utteranceEndMs they essentially want continuous asr */
|
||||||
//if (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true;
|
//if (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'deepgramflux':
|
||||||
|
this.bugname = `${this.bugname_prefix}deepgramflux_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, DeepgramfluxTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, DeepgramfluxTranscriptionEvents.Connect,
|
||||||
|
this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, DeepgramfluxTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, DeepgramfluxTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'gladia':
|
||||||
|
this.bugname = `${this.bugname_prefix}gladia_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, GladiaTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, GladiaTranscriptionEvents.Connect,
|
||||||
|
this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, GladiaTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, GladiaTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'soniox':
|
case 'soniox':
|
||||||
this.bugname = `${this.bugname_prefix}soniox_transcribe`;
|
this.bugname = `${this.bugname_prefix}soniox_transcribe`;
|
||||||
@@ -307,6 +341,18 @@ class TaskTranscribe extends SttTask {
|
|||||||
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'houndify':
|
||||||
|
this.bugname = `${this.bugname_prefix}houndify_transcribe`;
|
||||||
|
this.addCustomEventListener(ep, HoundifyTranscriptionEvents.Transcription,
|
||||||
|
this._onTranscription.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, HoundifyTranscriptionEvents.Error,
|
||||||
|
this._onVendorError.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, HoundifyTranscriptionEvents.ConnectFailure,
|
||||||
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, HoundifyTranscriptionEvents.Connect,
|
||||||
|
this._onVendorConnect.bind(this, cs, ep));
|
||||||
|
break;
|
||||||
|
|
||||||
case 'voxist':
|
case 'voxist':
|
||||||
this.bugname = `${this.bugname_prefix}voxist_transcribe`;
|
this.bugname = `${this.bugname_prefix}voxist_transcribe`;
|
||||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Transcription,
|
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Transcription,
|
||||||
@@ -423,6 +469,9 @@ class TaskTranscribe extends SttTask {
|
|||||||
bugname: this.bugname,
|
bugname: this.bugname,
|
||||||
hostport: this.hostport
|
hostport: this.hostport
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Some vendor use single connection, that we cannot use onConnect event to track transcription start
|
||||||
|
this.cs.emit('transcribe-start');
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onTranscription(cs, ep, channel, evt, fsEvent) {
|
async _onTranscription(cs, ep, channel, evt, fsEvent) {
|
||||||
@@ -441,6 +490,9 @@ class TaskTranscribe extends SttTask {
|
|||||||
|
|
||||||
if (this.vendor === 'ibm' && evt?.state === 'listening') return;
|
if (this.vendor === 'ibm' && evt?.state === 'listening') return;
|
||||||
|
|
||||||
|
// emit an event to the call session to track the time transcription is received
|
||||||
|
cs.emit('on-transcription');
|
||||||
|
|
||||||
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
|
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
|
||||||
/* we will only get this when we have set utterance_end_ms */
|
/* we will only get this when we have set utterance_end_ms */
|
||||||
|
|
||||||
@@ -602,14 +654,28 @@ class TaskTranscribe extends SttTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _resolve(channel, evt) {
|
async _resolve(channel, evt) {
|
||||||
|
let sttLatencyMetrics = {};
|
||||||
if (evt.is_final) {
|
if (evt.is_final) {
|
||||||
|
const sttLatency = this.cs.calculateSttLatency();
|
||||||
|
if (sttLatency) {
|
||||||
|
sttLatencyMetrics = {
|
||||||
|
'stt.latency_ms': `${sttLatency.stt_latency_ms}`,
|
||||||
|
'stt.talkspurts': JSON.stringify(sttLatency.talkspurts),
|
||||||
|
'stt.start_time': sttLatency.stt_start_time,
|
||||||
|
'stt.stop_time': sttLatency.stt_stop_time,
|
||||||
|
'stt.usage': sttLatency.stt_usage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// time to reset the stt latency
|
||||||
|
this.cs.emit('transcribe-start');
|
||||||
/* we've got a final transcript, so end the otel child span for this channel */
|
/* we've got a final transcript, so end the otel child span for this channel */
|
||||||
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||||
this.childSpan[channel - 1].span.setAttributes({
|
this.childSpan[channel - 1].span.setAttributes({
|
||||||
channel,
|
channel,
|
||||||
'stt.label': this.label || 'None',
|
'stt.label': this.label || 'None',
|
||||||
'stt.resolve': 'transcript',
|
'stt.resolve': 'transcript',
|
||||||
'stt.result': JSON.stringify(evt)
|
'stt.result': JSON.stringify(evt),
|
||||||
|
...sttLatencyMetrics
|
||||||
});
|
});
|
||||||
this.childSpan[channel - 1].span.end();
|
this.childSpan[channel - 1].span.end();
|
||||||
}
|
}
|
||||||
@@ -618,9 +684,13 @@ class TaskTranscribe extends SttTask {
|
|||||||
if (this.transcriptionHook) {
|
if (this.transcriptionHook) {
|
||||||
const b3 = this.getTracingPropagation();
|
const b3 = this.getTracingPropagation();
|
||||||
const httpHeaders = b3 && {b3};
|
const httpHeaders = b3 && {b3};
|
||||||
|
const latencies = Object.fromEntries(
|
||||||
|
Object.entries(sttLatencyMetrics).map(([key, value]) => [key.replace('stt.', 'stt_'), value])
|
||||||
|
);
|
||||||
const payload = {
|
const payload = {
|
||||||
...this.cs.callInfo,
|
...this.cs.callInfo,
|
||||||
...httpHeaders,
|
...httpHeaders,
|
||||||
|
...latencies,
|
||||||
...(evt.alternatives && {speech: evt}),
|
...(evt.alternatives && {speech: evt}),
|
||||||
...(evt.type && {speechEvent: evt})
|
...(evt.type && {speechEvent: evt})
|
||||||
};
|
};
|
||||||
@@ -709,7 +779,7 @@ class TaskTranscribe extends SttTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _startFallback(cs, _ep, evt) {
|
async _startFallback(cs, _ep, evt) {
|
||||||
if (this.canFallback) {
|
if (this.canFallback(_ep)) {
|
||||||
_ep.stopTranscription({
|
_ep.stopTranscription({
|
||||||
vendor: this.vendor,
|
vendor: this.vendor,
|
||||||
bugname: this.bugname,
|
bugname: this.bugname,
|
||||||
@@ -719,7 +789,7 @@ class TaskTranscribe extends SttTask {
|
|||||||
try {
|
try {
|
||||||
this.notifyError({ msg: 'ASR error',
|
this.notifyError({ msg: 'ASR error',
|
||||||
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'in progress'});
|
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'in progress'});
|
||||||
await this._initFallback();
|
await this._initFallback(_ep);
|
||||||
let channel = 1;
|
let channel = 1;
|
||||||
if (this.ep !== _ep) {
|
if (this.ep !== _ep) {
|
||||||
channel = 2;
|
channel = 2;
|
||||||
@@ -828,6 +898,41 @@ class TaskTranscribe extends SttTask {
|
|||||||
if (this._asrTimer) clearTimeout(this._asrTimer);
|
if (this._asrTimer) clearTimeout(this._asrTimer);
|
||||||
this._asrTimer = null;
|
this._asrTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We need to keep track the fallback is happened for each endpoint
|
||||||
|
// override the canFallback and _initFallback methods to make sure that
|
||||||
|
// we only fallback once per endpoint
|
||||||
|
// we want to keep track this on task level instead of endpoint level
|
||||||
|
// because the endpoint instance is used across multiple tasks.
|
||||||
|
canFallback(ep) {
|
||||||
|
let isHandledByPrimaryProvider = this.isHandledByPrimaryProvider;
|
||||||
|
if (ep === this.ep) {
|
||||||
|
isHandledByPrimaryProvider = this.isHandledByPrimaryProviderForEp1;
|
||||||
|
} else if (ep === this.ep2) {
|
||||||
|
isHandledByPrimaryProvider = this.isHandledByPrimaryProviderForEp2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOneOfEndpointAlreadyFallenBack = !!this.ep && !!this.ep2 &&
|
||||||
|
this.isHandledByPrimaryProviderForEp1 !== this.isHandledByPrimaryProviderForEp2;
|
||||||
|
|
||||||
|
// fallback is configured
|
||||||
|
return this.fallbackVendor &&
|
||||||
|
// has this endpoint already fallen back
|
||||||
|
isHandledByPrimaryProvider &&
|
||||||
|
// in global level, is there any fallback is already happened
|
||||||
|
// one fallen endpoint will mark cs.hasFallbackAsr to true,
|
||||||
|
// so if one endpoint was fallen, the other endpoint would be able to fallback.
|
||||||
|
(isOneOfEndpointAlreadyFallenBack || !this.cs.hasFallbackAsr);
|
||||||
|
}
|
||||||
|
|
||||||
|
_initFallback(ep) {
|
||||||
|
if (ep === this.ep) {
|
||||||
|
this.isHandledByPrimaryProviderForEp1 = false;
|
||||||
|
} else if (ep === this.ep2) {
|
||||||
|
this.isHandledByPrimaryProviderForEp2 = false;
|
||||||
|
}
|
||||||
|
return super._initFallback(ep);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = TaskTranscribe;
|
module.exports = TaskTranscribe;
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ const { TaskPreconditions } = require('../utils/constants');
|
|||||||
const { SpeechCredentialError } = require('../utils/error');
|
const { SpeechCredentialError } = require('../utils/error');
|
||||||
const dbUtils = require('../utils/db-utils');
|
const dbUtils = require('../utils/db-utils');
|
||||||
|
|
||||||
|
const extractPlaybackId = (str) => {
|
||||||
|
// Match say:{...} and capture the content inside braces
|
||||||
|
const match = str.match(/say:\{([^}]*)\}/);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
// Look for playback_id=value within the captured content
|
||||||
|
const playbackMatch = match[1].match(/playback_id=([^,]*)/);
|
||||||
|
return playbackMatch ? playbackMatch[1] : null;
|
||||||
|
};
|
||||||
|
|
||||||
class TtsTask extends Task {
|
class TtsTask extends Task {
|
||||||
|
|
||||||
constructor(logger, data, parentTask) {
|
constructor(logger, data, parentTask) {
|
||||||
@@ -22,10 +32,19 @@ class TtsTask extends Task {
|
|||||||
this.disableTtsCache = this.data.disableTtsCache;
|
this.disableTtsCache = this.data.disableTtsCache;
|
||||||
this.options = this.synthesizer.options || {};
|
this.options = this.synthesizer.options || {};
|
||||||
this.instructions = this.data.instructions;
|
this.instructions = this.data.instructions;
|
||||||
|
this.playbackIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlaybackId(offset) {
|
||||||
|
return this.playbackIds[offset];
|
||||||
}
|
}
|
||||||
|
|
||||||
async exec(cs) {
|
async exec(cs) {
|
||||||
super.exec(cs);
|
super.exec(cs);
|
||||||
|
// update disableTtsCache from call session if not set in task
|
||||||
|
if (this.data.disableTtsCache == null) {
|
||||||
|
this.disableTtsCache = cs.disableTtsCache;
|
||||||
|
}
|
||||||
if (cs.synthesizer) {
|
if (cs.synthesizer) {
|
||||||
this.options = {...cs.synthesizer.options, ...this.options};
|
this.options = {...cs.synthesizer.options, ...this.options};
|
||||||
this.data.synthesizer = this.data.synthesizer || {};
|
this.data.synthesizer = this.data.synthesizer || {};
|
||||||
@@ -66,55 +85,66 @@ class TtsTask extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setTtsStreamingChannelVars(vendor, language, voice, credentials, ep) {
|
async setTtsStreamingChannelVars(vendor, language, voice, credentials, ep) {
|
||||||
const {api_key, model_id, custom_tts_streaming_url, auth_token} = credentials;
|
const {api_key, model_id, api_uri, custom_tts_streaming_url, auth_token, options} = credentials;
|
||||||
let obj;
|
// api_key, model_id, api_uri, custom_tts_streaming_url, and auth_token are encoded in the credentials
|
||||||
|
// allow them to be overriden via config, using options
|
||||||
|
// give preference to options passed in via config
|
||||||
|
const local_options = {...JSON.parse(options), ...this.options};
|
||||||
|
const local_voice_settings = {...JSON.parse(options).voice_settings, ...this.options.voice_settings};
|
||||||
|
const local_api_key = local_options.api_key ?? api_key;
|
||||||
|
const local_model_id = local_options.model_id ?? model_id;
|
||||||
|
const local_api_uri = local_options.api_uri ?? api_uri;
|
||||||
|
const local_custom_tts_streaming_url = local_options.custom_tts_streaming_url ?? custom_tts_streaming_url;
|
||||||
|
const local_auth_token = local_options.auth_token ?? auth_token;
|
||||||
|
|
||||||
this.logger.debug({credentials},
|
|
||||||
`setTtsStreamingChannelVars: vendor: ${vendor}, language: ${language}, voice: ${voice}`);
|
let obj;
|
||||||
|
|
||||||
switch (vendor) {
|
switch (vendor) {
|
||||||
case 'deepgram':
|
case 'deepgram':
|
||||||
obj = {
|
obj = {
|
||||||
DEEPGRAM_API_KEY: api_key,
|
DEEPGRAM_API_KEY: local_api_key,
|
||||||
DEEPGRAM_TTS_STREAMING_MODEL: voice
|
DEEPGRAM_TTS_STREAMING_MODEL: voice
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'cartesia':
|
case 'cartesia':
|
||||||
obj = {
|
obj = {
|
||||||
CARTESIA_API_KEY: api_key,
|
CARTESIA_API_KEY: local_api_key,
|
||||||
CARTESIA_TTS_STREAMING_MODEL_ID: model_id,
|
CARTESIA_TTS_STREAMING_MODEL_ID: local_model_id,
|
||||||
CARTESIA_TTS_STREAMING_VOICE_ID: voice,
|
CARTESIA_TTS_STREAMING_VOICE_ID: voice,
|
||||||
CARTESIA_TTS_STREAMING_LANGUAGE: language || 'en',
|
CARTESIA_TTS_STREAMING_LANGUAGE: language || 'en',
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'elevenlabs':
|
case 'elevenlabs':
|
||||||
const {stability, similarity_boost, use_speaker_boost, style, speed} = this.options.voice_settings || {};
|
// eslint-disable-next-line max-len
|
||||||
|
const {stability, similarity_boost, use_speaker_boost, style, speed} = local_voice_settings || {};
|
||||||
obj = {
|
obj = {
|
||||||
ELEVENLABS_API_KEY: api_key,
|
ELEVENLABS_API_KEY: local_api_key,
|
||||||
ELEVENLABS_TTS_STREAMING_MODEL_ID: model_id,
|
...(api_uri && {ELEVENLABS_API_URI: local_api_uri}),
|
||||||
|
ELEVENLABS_TTS_STREAMING_MODEL_ID: local_model_id,
|
||||||
ELEVENLABS_TTS_STREAMING_VOICE_ID: voice,
|
ELEVENLABS_TTS_STREAMING_VOICE_ID: voice,
|
||||||
// 20/12/2024 - only eleven_turbo_v2_5 support multiple language
|
// 20/12/2024 - only eleven_turbo_v2_5 support multiple language
|
||||||
...(['eleven_turbo_v2_5'].includes(model_id) && {ELEVENLABS_TTS_STREAMING_LANGUAGE: language}),
|
...(['eleven_turbo_v2_5'].includes(local_model_id) && {ELEVENLABS_TTS_STREAMING_LANGUAGE: language}),
|
||||||
...(stability && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_STABILITY: stability}),
|
...(stability && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_STABILITY: stability}),
|
||||||
...(similarity_boost && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_SIMILARITY_BOOST: similarity_boost}),
|
...(similarity_boost && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_SIMILARITY_BOOST: similarity_boost}),
|
||||||
...(use_speaker_boost && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_USE_SPEAKER_BOOST: use_speaker_boost}),
|
...(use_speaker_boost && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_USE_SPEAKER_BOOST: use_speaker_boost}),
|
||||||
...(style && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_STYLE: style}),
|
...(style && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_STYLE: style}),
|
||||||
// speed has value 0.7 to 1.2, 1.0 is default, make sure we send the value event it's 0
|
// speed has value 0.7 to 1.2, 1.0 is default, make sure we send the value event it's 0
|
||||||
...(speed !== null && speed !== undefined && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_SPEED: `${speed}`}),
|
...(speed !== null && speed !== undefined && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_SPEED: `${speed}`}),
|
||||||
...(this.options.pronunciation_dictionary_locators &&
|
...(local_options.pronunciation_dictionary_locators &&
|
||||||
Array.isArray(this.options.pronunciation_dictionary_locators) && {
|
Array.isArray(local_options.pronunciation_dictionary_locators) && {
|
||||||
ELEVENLABS_TTS_STREAMING_PRONUNCIATION_DICTIONARY_LOCATORS:
|
ELEVENLABS_TTS_STREAMING_PRONUNCIATION_DICTIONARY_LOCATORS:
|
||||||
JSON.stringify(this.options.pronunciation_dictionary_locators)
|
JSON.stringify(local_options.pronunciation_dictionary_locators)
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'rimelabs':
|
case 'rimelabs':
|
||||||
const {
|
const {
|
||||||
pauseBetweenBrackets, phonemizeBetweenBrackets, inlineSpeedAlpha, speedAlpha, reduceLatency
|
pauseBetweenBrackets, phonemizeBetweenBrackets, inlineSpeedAlpha, speedAlpha, reduceLatency
|
||||||
} = this.options;
|
} = local_options;
|
||||||
obj = {
|
obj = {
|
||||||
RIMELABS_API_KEY: api_key,
|
RIMELABS_API_KEY: local_api_key,
|
||||||
RIMELABS_TTS_STREAMING_MODEL_ID: model_id,
|
RIMELABS_TTS_STREAMING_MODEL_ID: local_model_id,
|
||||||
RIMELABS_TTS_STREAMING_VOICE_ID: voice,
|
RIMELABS_TTS_STREAMING_VOICE_ID: voice,
|
||||||
RIMELABS_TTS_STREAMING_LANGUAGE: language || 'en',
|
RIMELABS_TTS_STREAMING_LANGUAGE: language || 'en',
|
||||||
...(pauseBetweenBrackets && {RIMELABS_TTS_STREAMING_PAUSE_BETWEEN_BRACKETS: pauseBetweenBrackets}),
|
...(pauseBetweenBrackets && {RIMELABS_TTS_STREAMING_PAUSE_BETWEEN_BRACKETS: pauseBetweenBrackets}),
|
||||||
@@ -129,8 +159,8 @@ class TtsTask extends Task {
|
|||||||
if (vendor.startsWith('custom:')) {
|
if (vendor.startsWith('custom:')) {
|
||||||
const use_tls = custom_tts_streaming_url.startsWith('wss://');
|
const use_tls = custom_tts_streaming_url.startsWith('wss://');
|
||||||
obj = {
|
obj = {
|
||||||
CUSTOM_TTS_STREAMING_HOST: custom_tts_streaming_url.replace(/^(ws|wss):\/\//, ''),
|
CUSTOM_TTS_STREAMING_HOST: local_custom_tts_streaming_url.replace(/^(ws|wss):\/\//, ''),
|
||||||
CUSTOM_TTS_STREAMING_API_KEY: auth_token,
|
CUSTOM_TTS_STREAMING_API_KEY: local_auth_token,
|
||||||
CUSTOM_TTS_STREAMING_VOICE_ID: voice,
|
CUSTOM_TTS_STREAMING_VOICE_ID: voice,
|
||||||
CUSTOM_TTS_STREAMING_LANGUAGE: language || 'en',
|
CUSTOM_TTS_STREAMING_LANGUAGE: language || 'en',
|
||||||
CUSTOM_TTS_STREAMING_USE_TLS: use_tls
|
CUSTOM_TTS_STREAMING_USE_TLS: use_tls
|
||||||
@@ -249,9 +279,9 @@ class TtsTask extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* produce an audio segment from the provided text */
|
/* produce an audio segment from the provided text */
|
||||||
const generateAudio = async(text) => {
|
const generateAudio = async(text, index) => {
|
||||||
if (this.killed) return;
|
if (this.killed) return {index, filePath: null};
|
||||||
if (text.startsWith('silence_stream://')) return text;
|
if (text.startsWith('silence_stream://')) return {index, filePath: text};
|
||||||
|
|
||||||
/* otel: trace time for tts */
|
/* otel: trace time for tts */
|
||||||
if (!preCache && !this._disableTracing) {
|
if (!preCache && !this._disableTracing) {
|
||||||
@@ -293,16 +323,35 @@ class TtsTask extends Task {
|
|||||||
vendor,
|
vendor,
|
||||||
language,
|
language,
|
||||||
characters: text.length,
|
characters: text.length,
|
||||||
elapsedTime: rtt
|
elapsedTime: rtt,
|
||||||
|
servedFromCache,
|
||||||
|
'id': this.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (servedFromCache) {
|
||||||
|
this.notifyStatus({
|
||||||
|
event: 'synthesized-audio',
|
||||||
|
vendor,
|
||||||
|
language,
|
||||||
|
servedFromCache,
|
||||||
|
'id': this.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {index, filePath, playbackId: null};
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
const playbackId = extractPlaybackId(filePath);
|
||||||
this.logger.debug('Say: a streaming tts api will be used');
|
this.logger.debug('Say: a streaming tts api will be used');
|
||||||
const modifiedPath = filePath.replace('say:{', `say:{session-uuid=${ep.uuid},`);
|
const modifiedPath = filePath.replace('say:{', `say:{session-uuid=${ep.uuid},`);
|
||||||
return modifiedPath;
|
this.notifyStatus({
|
||||||
|
event: 'synthesized-audio',
|
||||||
|
vendor,
|
||||||
|
language,
|
||||||
|
servedFromCache,
|
||||||
|
'id': this.id
|
||||||
|
});
|
||||||
|
return {index, filePath: modifiedPath, playbackId};
|
||||||
}
|
}
|
||||||
return filePath;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err}, 'Error synthesizing tts');
|
this.logger.info({err}, 'Error synthesizing tts');
|
||||||
if (this.otelSpan) this.otelSpan.end();
|
if (this.otelSpan) this.otelSpan.end();
|
||||||
@@ -317,8 +366,20 @@ class TtsTask extends Task {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const arr = this.text.map((t) => (this._validateURL(t) ? t : generateAudio(t)));
|
// process all text segments in parallel will cause ordering issue
|
||||||
return (await Promise.all(arr)).filter((fp) => fp && fp.length);
|
// so we attach index to each promise result and sort them later
|
||||||
|
|
||||||
|
const arr = this.text.map((t, index) => (this._validateURL(t) ?
|
||||||
|
Promise.resolve({index, filePath: t, playbackId: null}) : generateAudio(t, index)));
|
||||||
|
const results = await Promise.all(arr);
|
||||||
|
const sorted = results.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
return sorted
|
||||||
|
.filter((fp) => fp.filePath && fp.filePath.length)
|
||||||
|
.map((r) => {
|
||||||
|
this.playbackIds.push(r.playbackId);
|
||||||
|
return r.filePath;
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info(err, 'TaskSay:exec error');
|
this.logger.info(err, 'TaskSay:exec error');
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -281,13 +281,17 @@ module.exports = (logger) => {
|
|||||||
|
|
||||||
/* set stt options */
|
/* set stt options */
|
||||||
logger.info(`starting amd for vendor ${vendor} and language ${language}`);
|
logger.info(`starting amd for vendor ${vendor} and language ${language}`);
|
||||||
const sttOpts = amd.setChannelVarsForStt({name: TaskName.Gather}, sttCredentials, language, {
|
/* if opts contains recognizer object use that config for stt, otherwise use defaults */
|
||||||
vendor,
|
const rOpts = opts.recognizer ?
|
||||||
hints,
|
opts.recognizer :
|
||||||
enhancedModel: true,
|
{
|
||||||
altLanguages: opts.recognizer?.altLanguages || [],
|
vendor,
|
||||||
initialSpeechTimeoutMs: opts.resolveTimeoutMs,
|
hints,
|
||||||
});
|
enhancedModel: true,
|
||||||
|
altLanguages: opts.recognizer?.altLanguages || [],
|
||||||
|
initialSpeechTimeoutMs: opts.resolveTimeoutMs,
|
||||||
|
};
|
||||||
|
const sttOpts = amd.setChannelVarsForStt({name: TaskName.Gather}, sttCredentials, language, rOpts);
|
||||||
|
|
||||||
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'));
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class BackgroundTaskManager extends Emitter {
|
|||||||
this.logger.info(`stopping background task: ${type}`);
|
this.logger.info(`stopping background task: ${type}`);
|
||||||
task.removeAllListeners();
|
task.removeAllListeners();
|
||||||
task.span.end();
|
task.span.end();
|
||||||
task.kill();
|
task.kill(this.cs);
|
||||||
// Remove task from managed List
|
// Remove task from managed List
|
||||||
this.tasks.delete(type);
|
this.tasks.delete(type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,26 @@
|
|||||||
"DeepgramTranscriptionEvents": {
|
"DeepgramTranscriptionEvents": {
|
||||||
"Transcription": "deepgram_transcribe::transcription",
|
"Transcription": "deepgram_transcribe::transcription",
|
||||||
"ConnectFailure": "deepgram_transcribe::connect_failed",
|
"ConnectFailure": "deepgram_transcribe::connect_failed",
|
||||||
"Connect": "deepgram_transcribe::connect"
|
"Connect": "deepgram_transcribe::connect",
|
||||||
|
"Error": "deepgram_transcribe::error"
|
||||||
|
},
|
||||||
|
"DeepgramfluxTranscriptionEvents": {
|
||||||
|
"Transcription": "deepgramflux_transcribe::transcription",
|
||||||
|
"ConnectFailure": "deepgramflux_transcribe::connect_failed",
|
||||||
|
"Connect": "deepgramflux_transcribe::connect",
|
||||||
|
"Error": "deepgramflux_transcribe::error"
|
||||||
|
},
|
||||||
|
"ElevenlabsTranscriptionEvents": {
|
||||||
|
"Transcription": "elevenlabs_transcribe::transcription",
|
||||||
|
"ConnectFailure": "elevenlabs_transcribe::connect_failed",
|
||||||
|
"Connect": "elevenlabs_transcribe::connect",
|
||||||
|
"Error": "elevenlabs_transcribe::error"
|
||||||
|
},
|
||||||
|
"GladiaTranscriptionEvents": {
|
||||||
|
"Transcription": "gladia_transcribe::transcription",
|
||||||
|
"ConnectFailure": "gladia_transcribe::connect_failed",
|
||||||
|
"Connect": "gladia_transcribe::connect",
|
||||||
|
"Error": "gladia_transcribe::error"
|
||||||
},
|
},
|
||||||
"SonioxTranscriptionEvents": {
|
"SonioxTranscriptionEvents": {
|
||||||
"Transcription": "soniox_transcribe::transcription",
|
"Transcription": "soniox_transcribe::transcription",
|
||||||
@@ -162,6 +181,12 @@
|
|||||||
"ConnectFailure": "assemblyai_transcribe::connect_failed",
|
"ConnectFailure": "assemblyai_transcribe::connect_failed",
|
||||||
"Connect": "assemblyai_transcribe::connect"
|
"Connect": "assemblyai_transcribe::connect"
|
||||||
},
|
},
|
||||||
|
"HoundifyTranscriptionEvents": {
|
||||||
|
"Transcription": "houndify_transcribe::transcription",
|
||||||
|
"Error": "houndify_transcribe::error",
|
||||||
|
"ConnectFailure": "houndify_transcribe::connect_failed",
|
||||||
|
"Connect": "houndify_transcribe::connect"
|
||||||
|
},
|
||||||
"VoxistTranscriptionEvents": {
|
"VoxistTranscriptionEvents": {
|
||||||
"Transcription": "voxist_transcribe::transcription",
|
"Transcription": "voxist_transcribe::transcription",
|
||||||
"Error": "voxist_transcribe::error",
|
"Error": "voxist_transcribe::error",
|
||||||
@@ -177,6 +202,9 @@
|
|||||||
"VadDetection": {
|
"VadDetection": {
|
||||||
"Detection": "vad_detect:detection"
|
"Detection": "vad_detect:detection"
|
||||||
},
|
},
|
||||||
|
"SileroVadDetection": {
|
||||||
|
"Detection": "vad_silero:detect"
|
||||||
|
},
|
||||||
"ListenEvents": {
|
"ListenEvents": {
|
||||||
"Connect": "mod_audio_fork::connect",
|
"Connect": "mod_audio_fork::connect",
|
||||||
"ConnectFailure": "mod_audio_fork::connect_failed",
|
"ConnectFailure": "mod_audio_fork::connect_failed",
|
||||||
@@ -313,7 +341,8 @@
|
|||||||
"Empty": "tts_streaming::empty",
|
"Empty": "tts_streaming::empty",
|
||||||
"Pause": "tts_streaming::pause",
|
"Pause": "tts_streaming::pause",
|
||||||
"Resume": "tts_streaming::resume",
|
"Resume": "tts_streaming::resume",
|
||||||
"ConnectFailure": "tts_streaming::connect_failed"
|
"ConnectFailure": "tts_streaming::connect_failed",
|
||||||
|
"Connected": "tts_streaming::connected"
|
||||||
},
|
},
|
||||||
"TtsStreamingConnectionStatus": {
|
"TtsStreamingConnectionStatus": {
|
||||||
"NotConnected": "not_connected",
|
"NotConnected": "not_connected",
|
||||||
@@ -333,5 +362,8 @@
|
|||||||
"WS_CLOSE_CODES": {
|
"WS_CLOSE_CODES": {
|
||||||
"NormalClosure": 1000,
|
"NormalClosure": 1000,
|
||||||
"GoingAway": 1001
|
"GoingAway": 1001
|
||||||
}
|
},
|
||||||
|
"NON_FANTAL_ERRORS": [
|
||||||
|
"File Not Found"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,15 @@ const speechMapper = (cred) => {
|
|||||||
obj.deepgram_tts_uri = o.deepgram_tts_uri;
|
obj.deepgram_tts_uri = o.deepgram_tts_uri;
|
||||||
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
|
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
|
||||||
}
|
}
|
||||||
|
else if ('gladia' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
obj.region = o.region;
|
||||||
|
}
|
||||||
|
else if ('deepgramflux' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
}
|
||||||
else if ('soniox' === obj.vendor) {
|
else if ('soniox' === obj.vendor) {
|
||||||
const o = JSON.parse(decrypt(credential));
|
const o = JSON.parse(decrypt(credential));
|
||||||
obj.api_key = o.api_key;
|
obj.api_key = o.api_key;
|
||||||
@@ -97,6 +106,8 @@ const speechMapper = (cred) => {
|
|||||||
const o = JSON.parse(decrypt(credential));
|
const o = JSON.parse(decrypt(credential));
|
||||||
obj.api_key = o.api_key;
|
obj.api_key = o.api_key;
|
||||||
obj.model_id = o.model_id;
|
obj.model_id = o.model_id;
|
||||||
|
obj.stt_model_id = o.stt_model_id;
|
||||||
|
obj.api_uri = o.api_uri;
|
||||||
obj.options = o.options;
|
obj.options = o.options;
|
||||||
}
|
}
|
||||||
else if ('playht' === obj.vendor) {
|
else if ('playht' === obj.vendor) {
|
||||||
@@ -120,6 +131,12 @@ const speechMapper = (cred) => {
|
|||||||
obj.model_id = o.model_id;
|
obj.model_id = o.model_id;
|
||||||
obj.options = o.options;
|
obj.options = o.options;
|
||||||
}
|
}
|
||||||
|
else if ('resemble' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.api_key = o.api_key;
|
||||||
|
obj.resemble_tts_use_tls = o.resemble_tts_use_tls;
|
||||||
|
obj.resemble_tts_uri = o.resemble_tts_uri;
|
||||||
|
}
|
||||||
else if ('inworld' === obj.vendor) {
|
else if ('inworld' === obj.vendor) {
|
||||||
const o = JSON.parse(decrypt(credential));
|
const o = JSON.parse(decrypt(credential));
|
||||||
obj.api_key = o.api_key;
|
obj.api_key = o.api_key;
|
||||||
@@ -131,6 +148,13 @@ const speechMapper = (cred) => {
|
|||||||
obj.api_key = o.api_key;
|
obj.api_key = o.api_key;
|
||||||
obj.service_version = o.service_version;
|
obj.service_version = o.service_version;
|
||||||
}
|
}
|
||||||
|
else if ('houndify' === obj.vendor) {
|
||||||
|
const o = JSON.parse(decrypt(credential));
|
||||||
|
obj.client_id = o.client_id;
|
||||||
|
obj.client_key = o.client_key;
|
||||||
|
obj.user_id = o.user_id;
|
||||||
|
obj.houndify_server_uri = o.houndify_server_uri;
|
||||||
|
}
|
||||||
else if ('voxist' === obj.vendor) {
|
else if ('voxist' === obj.vendor) {
|
||||||
const o = JSON.parse(decrypt(credential));
|
const o = JSON.parse(decrypt(credential));
|
||||||
obj.api_key = o.api_key;
|
obj.api_key = o.api_key;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
constructor(logger, account_sid, hook, secret) {
|
constructor(logger, account_sid, hook, secret) {
|
||||||
super(logger, account_sid, hook, secret);
|
super(logger, account_sid, hook, secret);
|
||||||
|
|
||||||
this.method = hook.method || 'POST';
|
this.method = hook.method?.toUpperCase() || 'POST';
|
||||||
this.authHeader = basicAuth(hook.username, hook.password);
|
this.authHeader = basicAuth(hook.username, hook.password);
|
||||||
this.backoffMs = 500;
|
this.backoffMs = 500;
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
|
|
||||||
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip', 'env_vars', 'args']) : null;
|
const payload = params ? snakeCaseKeys(params, ['customerData', 'sip', 'env_vars', 'args']) : null;
|
||||||
const url = hook.url || hook;
|
const url = hook.url || hook;
|
||||||
const method = hook.method || 'POST';
|
const method = hook.method?.toUpperCase() || 'POST';
|
||||||
let buf = '';
|
let buf = '';
|
||||||
httpHeaders = {
|
httpHeaders = {
|
||||||
...httpHeaders,
|
...httpHeaders,
|
||||||
@@ -119,7 +119,7 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert.ok(url, 'HttpRequestor:request url was not provided');
|
assert.ok(url, 'HttpRequestor:request url was not provided');
|
||||||
assert.ok, (['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
|
assert.ok(['GET', 'POST'].includes(method), `HttpRequestor:request method must be 'GET' or 'POST' not ${method}`);
|
||||||
const startAt = process.hrtime();
|
const startAt = process.hrtime();
|
||||||
|
|
||||||
/* if we have an absolute url, and it is ws then do a websocket connection */
|
/* if we have an absolute url, and it is ws then do a websocket connection */
|
||||||
|
|||||||
@@ -173,7 +173,8 @@ function installSrfLocals(srf, logger, {
|
|||||||
lookupAccountCapacitiesBySid,
|
lookupAccountCapacitiesBySid,
|
||||||
lookupSmppGateways,
|
lookupSmppGateways,
|
||||||
lookupClientByAccountAndUsername,
|
lookupClientByAccountAndUsername,
|
||||||
lookupSystemInformation
|
lookupSystemInformation,
|
||||||
|
lookupLcrByAccount
|
||||||
} = require('@jambonz/db-helpers')({
|
} = require('@jambonz/db-helpers')({
|
||||||
host: JAMBONES_MYSQL_HOST,
|
host: JAMBONES_MYSQL_HOST,
|
||||||
user: JAMBONES_MYSQL_USER,
|
user: JAMBONES_MYSQL_USER,
|
||||||
@@ -279,7 +280,8 @@ function installSrfLocals(srf, logger, {
|
|||||||
retrieveByPatternSortedSet,
|
retrieveByPatternSortedSet,
|
||||||
sortedSetLength,
|
sortedSetLength,
|
||||||
sortedSetPositionByPattern,
|
sortedSetPositionByPattern,
|
||||||
getVerbioAccessToken
|
getVerbioAccessToken,
|
||||||
|
lookupLcrByAccount
|
||||||
},
|
},
|
||||||
parentLogger: logger,
|
parentLogger: logger,
|
||||||
getSBC,
|
getSBC,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const { createMediaEndpoint } = require('./media-endpoint');
|
|||||||
|
|
||||||
class SingleDialer extends Emitter {
|
class SingleDialer extends Emitter {
|
||||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask,
|
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask,
|
||||||
onHoldMusic}) {
|
onHoldMusic, tmpFiles}) {
|
||||||
super();
|
super();
|
||||||
assert(target.type);
|
assert(target.type);
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ class SingleDialer extends Emitter {
|
|||||||
this.callSid = crypto.randomUUID();
|
this.callSid = crypto.randomUUID();
|
||||||
this.dialTask = dialTask;
|
this.dialTask = dialTask;
|
||||||
this.onHoldMusic = onHoldMusic;
|
this.onHoldMusic = onHoldMusic;
|
||||||
|
this.tmpFiles = tmpFiles;
|
||||||
|
|
||||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||||
}
|
}
|
||||||
@@ -328,7 +329,13 @@ class SingleDialer extends Emitter {
|
|||||||
*/
|
*/
|
||||||
async kill(Reason) {
|
async kill(Reason) {
|
||||||
this.killed = true;
|
this.killed = true;
|
||||||
if (this.inviteInProgress) await this.inviteInProgress.cancel();
|
if (this.inviteInProgress) {
|
||||||
|
try {
|
||||||
|
await this.inviteInProgress.cancel();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({err}, 'SingleDialer:kill error cancelling invite');
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (this.dlg && this.dlg.connected) {
|
else if (this.dlg && this.dlg.connected) {
|
||||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||||
this.logger.debug('SingleDialer:kill hanging up called party');
|
this.logger.debug('SingleDialer:kill hanging up called party');
|
||||||
@@ -401,7 +408,8 @@ class SingleDialer extends Emitter {
|
|||||||
accountInfo: this.accountInfo,
|
accountInfo: this.accountInfo,
|
||||||
tasks,
|
tasks,
|
||||||
rootSpan: this.rootSpan,
|
rootSpan: this.rootSpan,
|
||||||
req: this.req
|
req: this.req,
|
||||||
|
tmpFiles: this.tmpFiles,
|
||||||
});
|
});
|
||||||
await cs.exec();
|
await cs.exec();
|
||||||
|
|
||||||
@@ -535,12 +543,12 @@ class SingleDialer extends Emitter {
|
|||||||
|
|
||||||
function placeOutdial({
|
function placeOutdial({
|
||||||
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask,
|
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask,
|
||||||
onHoldMusic
|
onHoldMusic, tmpFiles
|
||||||
}) {
|
}) {
|
||||||
const myOpts = deepcopy(opts);
|
const myOpts = deepcopy(opts);
|
||||||
const sd = new SingleDialer({
|
const sd = new SingleDialer({
|
||||||
logger, sbcAddress, target, opts: myOpts, application, callInfo,
|
logger, sbcAddress, target, opts: myOpts, application, callInfo,
|
||||||
accountInfo, rootSpan, startSpan, dialTask, onHoldMusic
|
accountInfo, rootSpan, startSpan, dialTask, onHoldMusic, tmpFiles
|
||||||
});
|
});
|
||||||
sd.exec(srf, ms, myOpts);
|
sd.exec(srf, ms, myOpts);
|
||||||
return sd;
|
return sd;
|
||||||
|
|||||||
91
lib/utils/process-monitor.js
Normal file
91
lib/utils/process-monitor.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// lib/utils/process-monitor.js
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class ProcessMonitor {
|
||||||
|
constructor(logger) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.packageInfo = this.getPackageInfo();
|
||||||
|
this.processName = this.packageInfo.name || 'unknown-app';
|
||||||
|
}
|
||||||
|
|
||||||
|
getPackageInfo() {
|
||||||
|
try {
|
||||||
|
const packagePath = path.join(process.cwd(), 'package.json');
|
||||||
|
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
return { name: 'unknown', version: 'unknown' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logStartup(additionalInfo = {}) {
|
||||||
|
const startupInfo = {
|
||||||
|
msg: `${this.processName} started`,
|
||||||
|
app_name: this.processName,
|
||||||
|
app_version: this.packageInfo.version,
|
||||||
|
pid: process.pid,
|
||||||
|
ppid: process.ppid,
|
||||||
|
pm2_instance_id: process.env.NODE_APP_INSTANCE || 'not_pm2',
|
||||||
|
pm2_id: process.env.pm_id,
|
||||||
|
is_pm2: !!process.env.PM2,
|
||||||
|
node_version: process.version,
|
||||||
|
uptime: process.uptime(),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...additionalInfo
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.info(startupInfo);
|
||||||
|
return startupInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSignalHandlers() {
|
||||||
|
// Log when we receive signals that would cause restart
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
this.logger.info({
|
||||||
|
msg: 'SIGINT received',
|
||||||
|
app_name: this.processName,
|
||||||
|
pid: process.pid,
|
||||||
|
ppid: process.ppid,
|
||||||
|
uptime: process.uptime(),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
this.logger.info({
|
||||||
|
msg: 'SIGTERM received',
|
||||||
|
app_name: this.processName,
|
||||||
|
pid: process.pid,
|
||||||
|
ppid: process.ppid,
|
||||||
|
uptime: process.uptime(),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
this.logger.error({
|
||||||
|
msg: 'Uncaught exception - process will restart',
|
||||||
|
app_name: this.processName,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
pid: process.pid,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
this.logger.error({
|
||||||
|
msg: 'Unhandled rejection',
|
||||||
|
app_name: this.processName,
|
||||||
|
reason,
|
||||||
|
pid: process.pid,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ProcessMonitor;
|
||||||
@@ -55,11 +55,28 @@ const extractSdpMedia = (sdp) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getLeadingCodec = (sdp) => {
|
||||||
|
if (!sdp) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = sdpTransform.parse(sdp);
|
||||||
|
const audio = parsed.media?.find((m) => m.type === 'audio');
|
||||||
|
|
||||||
|
if (!audio) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return audio.rtp?.[0]?.codec || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isOnhold,
|
isOnhold,
|
||||||
mergeSdpMedia,
|
mergeSdpMedia,
|
||||||
extractSdpMedia,
|
extractSdpMedia,
|
||||||
isOpusFirst,
|
isOpusFirst,
|
||||||
makeOpusFirst,
|
makeOpusFirst,
|
||||||
removeVideoSdp
|
removeVideoSdp,
|
||||||
|
getLeadingCodec
|
||||||
};
|
};
|
||||||
|
|||||||
197
lib/utils/stt-latency-calculator.js
Normal file
197
lib/utils/stt-latency-calculator.js
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
const { assert } = require('console');
|
||||||
|
const Emitter = require('events');
|
||||||
|
const {
|
||||||
|
VadDetection,
|
||||||
|
SileroVadDetection
|
||||||
|
} = require('../utils/constants.json');
|
||||||
|
|
||||||
|
class SttLatencyCalculator extends Emitter {
|
||||||
|
constructor({ logger, cs}) {
|
||||||
|
super();
|
||||||
|
this.logger = logger;
|
||||||
|
this.cs = cs;
|
||||||
|
this.isRunning = false;
|
||||||
|
this.isInTalkSpurt = false;
|
||||||
|
this.start_talking_time = 0;
|
||||||
|
this.talkspurts = [];
|
||||||
|
this.vendor = this.cs.vad?.vendor || 'silero';
|
||||||
|
this.stt_start_time = 0;
|
||||||
|
this.stt_stop_time = 0;
|
||||||
|
this.stt_on_transcription_time = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set sttStartTime(time) {
|
||||||
|
this.stt_start_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sttStartTime() {
|
||||||
|
return this.stt_start_time || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set sttStopTime(time) {
|
||||||
|
this.stt_stop_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sttStopTime() {
|
||||||
|
return this.stt_stop_time || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set sttOnTranscriptionTime(time) {
|
||||||
|
this.stt_on_transcription_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sttOnTranscriptionTime() {
|
||||||
|
return this.stt_on_transcription_time || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onVadDetected(_ep, _evt, fsEvent) {
|
||||||
|
if (fsEvent.getHeader('detected-event') === 'stop_talking') {
|
||||||
|
if (this.isInTalkSpurt) {
|
||||||
|
this.talkspurts.push({
|
||||||
|
start: this.start_talking_time,
|
||||||
|
stop: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.start_talking_time = 0;
|
||||||
|
this.isInTalkSpurt = false;
|
||||||
|
} else if (fsEvent.getHeader('detected-event') === 'start_talking') {
|
||||||
|
this.start_talking_time = Date.now();
|
||||||
|
this.isInTalkSpurt = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_startVad() {
|
||||||
|
assert(!this.isRunning, 'Latency calculator is already running');
|
||||||
|
assert(this.cs.ep, 'Callsession has no endpoint to start the latency calculator');
|
||||||
|
const ep = this.cs.ep;
|
||||||
|
if (!ep.sttLatencyVadHandler) {
|
||||||
|
ep.sttLatencyVadHandler = this._onVadDetected.bind(this, ep);
|
||||||
|
if (this.vendor === 'silero') {
|
||||||
|
ep.addCustomEventListener(SileroVadDetection.Detection, ep.sttLatencyVadHandler);
|
||||||
|
} else {
|
||||||
|
ep.addCustomEventListener(VadDetection.Detection, ep.sttLatencyVadHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.stop_talking_time = 0;
|
||||||
|
this.start_talking_time = 0;
|
||||||
|
this.vad = {
|
||||||
|
...(this.cs.vad || {}),
|
||||||
|
strategy: 'continuous',
|
||||||
|
bugname: 'stt-latency-calculator-vad',
|
||||||
|
vendor: this.vendor
|
||||||
|
};
|
||||||
|
|
||||||
|
ep.startVadDetection(this.vad);
|
||||||
|
this.isRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopVad() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
this.logger.warn('Latency calculator is still running, stopping VAD detection');
|
||||||
|
const ep = this.cs.ep;
|
||||||
|
ep.stopVadDetection(this.vad);
|
||||||
|
if (ep.sttLatencyVadHandler) {
|
||||||
|
if (this.vendor === 'silero') {
|
||||||
|
this.ep?.removeCustomEventListener(SileroVadDetection.Detection, ep.sttLatencyVadHandler);
|
||||||
|
} else {
|
||||||
|
this.ep?.removeCustomEventListener(VadDetection.Detection, ep.sttLatencyVadHandler);
|
||||||
|
}
|
||||||
|
ep.sttLatencyVadHandler = null;
|
||||||
|
}
|
||||||
|
this.isRunning = false;
|
||||||
|
this.logger.info('STT Latency Calculator stopped');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
this.logger.warn('Latency calculator is already running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.cs.ep) {
|
||||||
|
this.logger.error('Callsession has no endpoint to start the latency calculator');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._startVad();
|
||||||
|
this.logger.debug('STT Latency Calculator started');
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this._stopVad();
|
||||||
|
}
|
||||||
|
|
||||||
|
toUnixTimestamp(date) {
|
||||||
|
return Math.floor(date / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateLatency() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
this.logger.debug('Latency calculator is not running, cannot calculate latency, returning default values');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stt_stop_time = this.stt_stop_time || Date.now();
|
||||||
|
if (this.isInTalkSpurt) {
|
||||||
|
this.talkspurts.push({
|
||||||
|
start: this.start_talking_time,
|
||||||
|
stop: stt_stop_time
|
||||||
|
});
|
||||||
|
this.isInTalkSpurt = false;
|
||||||
|
this.start_talking_time = 0;
|
||||||
|
}
|
||||||
|
const stt_on_transcription_time = this.stt_on_transcription_time || stt_stop_time;
|
||||||
|
const start_talking_time = this.talkspurts[0]?.start;
|
||||||
|
let lastIdx = this.talkspurts.length - 1;
|
||||||
|
lastIdx = lastIdx < 0 ? 0 : lastIdx;
|
||||||
|
const stop_talking_time = this.talkspurts[lastIdx]?.stop || stt_stop_time;
|
||||||
|
|
||||||
|
return {
|
||||||
|
stt_start_time: this.toUnixTimestamp(this.stt_start_time),
|
||||||
|
stt_stop_time: this.toUnixTimestamp(stt_stop_time),
|
||||||
|
start_talking_time: this.toUnixTimestamp(start_talking_time),
|
||||||
|
stop_talking_time: this.toUnixTimestamp(stop_talking_time),
|
||||||
|
stt_latency: parseFloat((Math.abs(stt_on_transcription_time - stop_talking_time)) / 1000).toFixed(2),
|
||||||
|
stt_latency_ms: Math.abs(stt_on_transcription_time - stop_talking_time),
|
||||||
|
stt_usage: parseFloat((stt_stop_time - this.stt_start_time) / 1000).toFixed(2),
|
||||||
|
talkspurts: this.talkspurts.map((ts) =>
|
||||||
|
([this.toUnixTimestamp(ts.start || 0), this.toUnixTimestamp(ts.stop || 0)]))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTime() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.stt_start_time = Date.now();
|
||||||
|
this.stt_stop_time = 0;
|
||||||
|
this.stt_on_transcription_time = 0;
|
||||||
|
this.clearTalkspurts();
|
||||||
|
this.logger.info('STT Latency Calculator reset');
|
||||||
|
}
|
||||||
|
|
||||||
|
onTranscriptionReceived() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.stt_on_transcription_time = Date.now();
|
||||||
|
this.logger.debug(`CallSession:on-transcription set to ${this.stt_on_transcription_time}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onTranscribeStop() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.stt_stop_time = Date.now();
|
||||||
|
this.logger.debug(`CallSession:transcribe-stop set to ${this.stt_stop_time}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTalkspurts() {
|
||||||
|
this.talkspurts = [];
|
||||||
|
if (!this.isInTalkSpurt) {
|
||||||
|
this.start_talking_time = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SttLatencyCalculator;
|
||||||
@@ -131,6 +131,43 @@ const stickyVars = {
|
|||||||
'OPENAI_TURN_DETECTION_PREFIX_PADDING_MS',
|
'OPENAI_TURN_DETECTION_PREFIX_PADDING_MS',
|
||||||
'OPENAI_TURN_DETECTION_SILENCE_DURATION_MS',
|
'OPENAI_TURN_DETECTION_SILENCE_DURATION_MS',
|
||||||
],
|
],
|
||||||
|
houndify: [
|
||||||
|
'HOUNDIFY_CLIENT_ID',
|
||||||
|
'HOUNDIFY_CLIENT_KEY',
|
||||||
|
'HOUNDIFY_USER_ID',
|
||||||
|
'HOUNDIFY_MAX_SILENCE_SECONDS',
|
||||||
|
'HOUNDIFY_MAX_SILENCE_AFTER_FULL_QUERY_SECONDS',
|
||||||
|
'HOUNDIFY_MAX_SILENCE_AFTER_PARTIAL_QUERY_SECONDS',
|
||||||
|
'HOUNDIFY_VAD_SENSITIVITY',
|
||||||
|
'HOUNDIFY_VAD_TIMEOUT',
|
||||||
|
'HOUNDIFY_VAD_MODE',
|
||||||
|
'HOUNDIFY_VAD_VOICE_MS',
|
||||||
|
'HOUNDIFY_VAD_SILENCE_MS',
|
||||||
|
'HOUNDIFY_VAD_DEBUG',
|
||||||
|
'HOUNDIFY_AUDIO_FORMAT',
|
||||||
|
'HOUNDIFY_ENABLE_NOISE_REDUCTION',
|
||||||
|
'HOUNDIFY_AUDIO_ENDPOINT',
|
||||||
|
'HOUNDIFY_ENABLE_PROFANITY_FILTER',
|
||||||
|
'HOUNDIFY_ENABLE_PUNCTUATION',
|
||||||
|
'HOUNDIFY_ENABLE_CAPITALIZATION',
|
||||||
|
'HOUNDIFY_CONFIDENCE_THRESHOLD',
|
||||||
|
'HOUNDIFY_ENABLE_DISFLUENCY_FILTER',
|
||||||
|
'HOUNDIFY_MAX_RESULTS',
|
||||||
|
'HOUNDIFY_ENABLE_WORD_TIMESTAMPS',
|
||||||
|
'HOUNDIFY_MAX_ALTERNATIVES',
|
||||||
|
'HOUNDIFY_PARTIAL_TRANSCRIPT_INTERVAL',
|
||||||
|
'HOUNDIFY_SESSION_TIMEOUT',
|
||||||
|
'HOUNDIFY_CONNECTION_TIMEOUT',
|
||||||
|
'HOUNDIFY_LATITUDE',
|
||||||
|
'HOUNDIFY_LONGITUDE',
|
||||||
|
'HOUNDIFY_CITY',
|
||||||
|
'HOUNDIFY_STATE',
|
||||||
|
'HOUNDIFY_COUNTRY',
|
||||||
|
'HOUNDIFY_TIMEZONE',
|
||||||
|
'HOUNDIFY_DOMAIN',
|
||||||
|
'HOUNDIFY_CUSTOM_VOCABULARY',
|
||||||
|
'HOUNDIFY_LANGUAGE_MODEL'
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -339,6 +376,61 @@ const normalizeDeepgram = (evt, channel, language, shortUtterance) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeGladia = (evt, channel, language, shortUtterance) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
|
||||||
|
// Handle Gladia transcript format
|
||||||
|
if (evt.type === 'transcript' && evt.data && evt.data.utterance) {
|
||||||
|
const utterance = evt.data.utterance;
|
||||||
|
const alternatives = [{
|
||||||
|
confidence: utterance.confidence || 0,
|
||||||
|
transcript: utterance.text || '',
|
||||||
|
}];
|
||||||
|
|
||||||
|
return {
|
||||||
|
language_code: utterance.language || language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.data.is_final || false,
|
||||||
|
alternatives,
|
||||||
|
vendor: {
|
||||||
|
name: 'gladia',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeDeepgramFlux = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
|
||||||
|
let turnTakingEvent;
|
||||||
|
if (['StartOfTurn', 'EagerEndOfTurn', 'TurnResumed', 'EndOfTurn'].includes(evt.event)) {
|
||||||
|
turnTakingEvent = evt.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* calculate total confidence based on word-level confidence */
|
||||||
|
const realWords = (evt.words || [])
|
||||||
|
.filter((w) => ![',.!?;'].includes(w.word));
|
||||||
|
const confidence = realWords.length > 0 ? realWords.reduce((acc, w) => acc + w.confidence, 0) / realWords.length : 0;
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.event === 'EndOfTurn',
|
||||||
|
alternatives: [
|
||||||
|
{
|
||||||
|
confidence,
|
||||||
|
end_of_turn_confidence: evt.end_of_turn_confidence,
|
||||||
|
transcript: evt.transcript,
|
||||||
|
...(turnTakingEvent && {turn_taking_event: turnTakingEvent})
|
||||||
|
}
|
||||||
|
],
|
||||||
|
vendor: {
|
||||||
|
name: 'deepgramflux',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeNvidia = (evt, channel, language) => {
|
const normalizeNvidia = (evt, channel, language) => {
|
||||||
const copy = JSON.parse(JSON.stringify(evt));
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
const alternatives = (evt.alternatives || [])
|
const alternatives = (evt.alternatives || [])
|
||||||
@@ -453,6 +545,23 @@ const normalizeVerbio = (evt, channel, language) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeElevenlabs = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final: evt.message_type === 'committed_transcript',
|
||||||
|
alternatives: [{
|
||||||
|
confidence: 1.0,
|
||||||
|
transcript: evt.text,
|
||||||
|
}],
|
||||||
|
vendor: {
|
||||||
|
name: 'elevenlabs',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeMicrosoft = (evt, channel, language, punctuation = true) => {
|
const normalizeMicrosoft = (evt, channel, language, punctuation = true) => {
|
||||||
const copy = JSON.parse(JSON.stringify(evt));
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
const nbest = evt.NBest;
|
const nbest = evt.NBest;
|
||||||
@@ -551,6 +660,30 @@ const normalizeAssemblyAi = (evt, channel, language) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeHoundify = (evt, channel, language) => {
|
||||||
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
|
const alternatives = [];
|
||||||
|
const is_final = evt.ResultsAreFinal && evt.ResultsAreFinal[0] === true;
|
||||||
|
if (evt.Disambiguation && evt.Disambiguation.ChoiceData && evt.Disambiguation.ChoiceData.length > 0) {
|
||||||
|
// Handle Houndify Voice Search Result format
|
||||||
|
const choiceData = evt.Disambiguation.ChoiceData[0];
|
||||||
|
alternatives.push({
|
||||||
|
confidence: choiceData.ConfidenceScore || choiceData.ASRConfidence || 0.0,
|
||||||
|
transcript: choiceData.FormattedTranscription || choiceData.Transcription || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
language_code: language,
|
||||||
|
channel_tag: channel,
|
||||||
|
is_final,
|
||||||
|
alternatives,
|
||||||
|
vendor: {
|
||||||
|
name: 'houndify',
|
||||||
|
evt: copy
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeVoxist = (evt, channel, language) => {
|
const normalizeVoxist = (evt, channel, language) => {
|
||||||
const copy = JSON.parse(JSON.stringify(evt));
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
return {
|
return {
|
||||||
@@ -650,6 +783,12 @@ module.exports = (logger) => {
|
|||||||
switch (vendor) {
|
switch (vendor) {
|
||||||
case 'deepgram':
|
case 'deepgram':
|
||||||
return normalizeDeepgram(evt, channel, language, shortUtterance);
|
return normalizeDeepgram(evt, channel, language, shortUtterance);
|
||||||
|
case 'gladia':
|
||||||
|
return normalizeGladia(evt, channel, language, shortUtterance);
|
||||||
|
case 'deepgramflux':
|
||||||
|
return normalizeDeepgramFlux(evt, channel, language, shortUtterance);
|
||||||
|
case 'elevenlabs':
|
||||||
|
return normalizeElevenlabs(evt, channel, language);
|
||||||
case 'microsoft':
|
case 'microsoft':
|
||||||
return normalizeMicrosoft(evt, channel, language, punctuation);
|
return normalizeMicrosoft(evt, channel, language, punctuation);
|
||||||
case 'google':
|
case 'google':
|
||||||
@@ -668,6 +807,8 @@ module.exports = (logger) => {
|
|||||||
return normalizeCobalt(evt, channel, language);
|
return normalizeCobalt(evt, channel, language);
|
||||||
case 'assemblyai':
|
case 'assemblyai':
|
||||||
return normalizeAssemblyAi(evt, channel, language, shortUtterance);
|
return normalizeAssemblyAi(evt, channel, language, shortUtterance);
|
||||||
|
case 'houndify':
|
||||||
|
return normalizeHoundify(evt, channel, language, shortUtterance);
|
||||||
case 'voxist':
|
case 'voxist':
|
||||||
return normalizeVoxist(evt, channel, language);
|
return normalizeVoxist(evt, channel, language);
|
||||||
case 'cartesia':
|
case 'cartesia':
|
||||||
@@ -764,12 +905,15 @@ module.exports = (logger) => {
|
|||||||
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
|
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId,
|
||||||
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
|
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey,
|
||||||
AWS_REGION: sttCredentials.region,
|
AWS_REGION: sttCredentials.region,
|
||||||
AWS_SECURITY_TOKEN: sttCredentials.securityToken
|
AWS_SECURITY_TOKEN: sttCredentials.securityToken,
|
||||||
|
AWS_SESSION_TOKEN: sttCredentials.sessionToken ? sttCredentials.sessionToken : sttCredentials.securityToken
|
||||||
}),
|
}),
|
||||||
...(awsOptions.accessKey && {AWS_ACCESS_KEY_ID: awsOptions.accessKey}),
|
...(awsOptions.accessKey && {AWS_ACCESS_KEY_ID: awsOptions.accessKey}),
|
||||||
...(awsOptions.secretKey && {AWS_SECRET_ACCESS_KEY: awsOptions.secretKey}),
|
...(awsOptions.secretKey && {AWS_SECRET_ACCESS_KEY: awsOptions.secretKey}),
|
||||||
...(awsOptions.region && {AWS_REGION: awsOptions.region}),
|
...(awsOptions.region && {AWS_REGION: awsOptions.region}),
|
||||||
...(awsOptions.securityToken && {AWS_SECURITY_TOKEN: awsOptions.securityToken}),
|
...(awsOptions.securityToken && {AWS_SECURITY_TOKEN: awsOptions.securityToken}),
|
||||||
|
...(awsOptions.sessionToken && {AWS_SESSION_TOKEN: awsOptions.sessionToken ?
|
||||||
|
awsOptions.sessionToken : awsOptions.securityToken}),
|
||||||
...(awsOptions.languageModelName && {AWS_LANGUAGE_MODEL_NAME: awsOptions.languageModelName}),
|
...(awsOptions.languageModelName && {AWS_LANGUAGE_MODEL_NAME: awsOptions.languageModelName}),
|
||||||
...(awsOptions.piiEntityTypes?.length && {AWS_PII_ENTITY_TYPES: awsOptions.piiEntityTypes.join(',')}),
|
...(awsOptions.piiEntityTypes?.length && {AWS_PII_ENTITY_TYPES: awsOptions.piiEntityTypes.join(',')}),
|
||||||
...(awsOptions.piiIdentifyEntities && {AWS_PII_IDENTIFY_ENTITIES: true}),
|
...(awsOptions.piiIdentifyEntities && {AWS_PII_IDENTIFY_ENTITIES: true}),
|
||||||
@@ -795,7 +939,7 @@ module.exports = (logger) => {
|
|||||||
...(rOpts.initialSpeechTimeoutMs > 0 &&
|
...(rOpts.initialSpeechTimeoutMs > 0 &&
|
||||||
{AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}),
|
{AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}),
|
||||||
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
|
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
|
||||||
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
|
...(azureOptions.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
|
||||||
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
|
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
|
||||||
...(azureOptions.speechSegmentationSilenceTimeoutMs &&
|
...(azureOptions.speechSegmentationSilenceTimeoutMs &&
|
||||||
{AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS: azureOptions.speechSegmentationSilenceTimeoutMs}),
|
{AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS: azureOptions.speechSegmentationSilenceTimeoutMs}),
|
||||||
@@ -870,6 +1014,14 @@ module.exports = (logger) => {
|
|||||||
const deepgramUri = deepgramOptions.deepgramSttUri || sttCredentials.deepgram_stt_uri;
|
const deepgramUri = deepgramOptions.deepgramSttUri || sttCredentials.deepgram_stt_uri;
|
||||||
const useTls = deepgramOptions.deepgramSttUseTls || sttCredentials.deepgram_stt_use_tls;
|
const useTls = deepgramOptions.deepgramSttUseTls || sttCredentials.deepgram_stt_use_tls;
|
||||||
|
|
||||||
|
// DH (2025-08-11) entity_prompt is currently limited to 100 words
|
||||||
|
const entityPrompt = deepgramOptions.entityPrompt ?
|
||||||
|
deepgramOptions.entityPrompt
|
||||||
|
.split(/\s+/)
|
||||||
|
.slice(0, 100)
|
||||||
|
.join(' ')
|
||||||
|
: undefined;
|
||||||
|
|
||||||
/* default to a sensible model if not supplied */
|
/* default to a sensible model if not supplied */
|
||||||
if (!model) {
|
if (!model) {
|
||||||
model = selectDefaultDeepgramModel(task, language);
|
model = selectDefaultDeepgramModel(task, language);
|
||||||
@@ -928,7 +1080,53 @@ module.exports = (logger) => {
|
|||||||
...(deepgramOptions.fillerWords) &&
|
...(deepgramOptions.fillerWords) &&
|
||||||
{DEEPGRAM_SPEECH_FILLER_WORDS: deepgramOptions.fillerWords},
|
{DEEPGRAM_SPEECH_FILLER_WORDS: deepgramOptions.fillerWords},
|
||||||
...((Array.isArray(deepgramOptions.keyterms) && deepgramOptions.keyterms.length > 0) &&
|
...((Array.isArray(deepgramOptions.keyterms) && deepgramOptions.keyterms.length > 0) &&
|
||||||
{DEEPGRAM_SPEECH_KEYTERMS: deepgramOptions.keyterms.join(',')})
|
{DEEPGRAM_SPEECH_KEYTERMS: deepgramOptions.keyterms.join(',')}),
|
||||||
|
...(deepgramOptions.mipOptOut && {DEEPGRAM_SPEECH_MIP_OPT_OUT: deepgramOptions.mipOptOut}),
|
||||||
|
...(entityPrompt && {DEEPGRAM_SPEECH_ENTITY_PROMPT: entityPrompt}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('deepgramflux' === vendor) {
|
||||||
|
const {
|
||||||
|
eotThreshold,
|
||||||
|
eotTimeoutMs,
|
||||||
|
mipOptOut,
|
||||||
|
model,
|
||||||
|
eagerEotThreshold,
|
||||||
|
keyterms
|
||||||
|
} = rOpts.deepgramOptions || {};
|
||||||
|
opts = {
|
||||||
|
DEEPGRAMFLUX_API_KEY: sttCredentials.api_key,
|
||||||
|
DEEPGRAMFLUX_SPEECH_MODEL: model || 'flux-general-en',
|
||||||
|
...(eotThreshold && {DEEPGRAMFLUX_SPEECH_EOT_THRESHOLD: eotThreshold}),
|
||||||
|
...(eotTimeoutMs && {DEEPGRAMFLUX_SPEECH_EOT_TIMEOUT_MS: eotTimeoutMs}),
|
||||||
|
...(mipOptOut && {DEEPGRAMFLUX_SPEECH_MIP_OPT_OUT: mipOptOut}),
|
||||||
|
...(eagerEotThreshold && {DEEPGRAMFLUX_SPEECH_EAGER_EOT_THRESHOLD: eagerEotThreshold}),
|
||||||
|
...(keyterms && keyterms.length > 0 && {DEEPGRAMFLUX_SPEECH_KEYTERMS: keyterms.join(',')}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('elevenlabs' === vendor) {
|
||||||
|
const {api_key, stt_model_id} = sttCredentials;
|
||||||
|
const {includeTimestamps, commitStrategy, vadSilenceThresholdSecs, vadThreshold,
|
||||||
|
minSpeechDurationMs, minSilenceDurationMs, enableLogging} = rOpts.elevenlabsOptions || {};
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
ELEVENLABS_API_KEY: api_key,
|
||||||
|
ELEVENLABS_MODEL_ID: stt_model_id,
|
||||||
|
ELEVENLABS_INCLUDE_TIMESTAMPS: includeTimestamps === true ? true : false,
|
||||||
|
...(commitStrategy && {ELEVENLABS_COMMIT_STRATEGY: commitStrategy}),
|
||||||
|
...(vadSilenceThresholdSecs && {ELEVENLABS_VAD_SILENCE_THRESHOLD_SECS: vadSilenceThresholdSecs}),
|
||||||
|
...(vadThreshold && {ELEVENLABS_VAD_THRESHOLD: vadThreshold}),
|
||||||
|
...(minSpeechDurationMs && {ELEVENLABS_MIN_SPEECH_DURATION_MS: minSpeechDurationMs}),
|
||||||
|
...(minSilenceDurationMs && {ELEVENLABS_MIN_SILENCE_DURATION_MS: minSilenceDurationMs}),
|
||||||
|
...(enableLogging && {ELEVENLABS_ENABLE_LOGGING: enableLogging ? 1 : 0}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if ('gladia' === vendor) {
|
||||||
|
const {host, path} = sttCredentials;
|
||||||
|
opts = {
|
||||||
|
GLADIA_SPEECH_HOST: host,
|
||||||
|
GLADIA_SPEECH_PATH: path,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if ('soniox' === vendor) {
|
else if ('soniox' === vendor) {
|
||||||
@@ -1057,6 +1255,61 @@ module.exports = (logger) => {
|
|||||||
{ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)})
|
{ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else if ('houndify' === vendor) {
|
||||||
|
const {
|
||||||
|
latitude, longitude, city, state, country, timeZone, domain, audioEndpoint,
|
||||||
|
maxSilenceSeconds, maxSilenceAfterFullQuerySeconds, maxSilenceAfterPartialQuerySeconds,
|
||||||
|
vadSensitivity, vadTimeout, vadMode, vadVoiceMs, vadSilenceMs, vadDebug,
|
||||||
|
audioFormat, enableNoiseReduction, enableProfanityFilter, enablePunctuation,
|
||||||
|
enableCapitalization, confidenceThreshold, enableDisfluencyFilter,
|
||||||
|
maxResults, enableWordTimestamps, maxAlternatives, partialTranscriptInterval,
|
||||||
|
sessionTimeout, connectionTimeout, customVocabulary, languageModel,
|
||||||
|
requestInfo, sampleRate
|
||||||
|
} = rOpts.houndifyOptions || {};
|
||||||
|
const audioEndpointUri = audioEndpoint || sttCredentials.houndify_server_uri;
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
...opts,
|
||||||
|
HOUNDIFY_CLIENT_ID: sttCredentials.client_id,
|
||||||
|
HOUNDIFY_CLIENT_KEY: sttCredentials.client_key,
|
||||||
|
HOUNDIFY_USER_ID: sttCredentials.user_id,
|
||||||
|
HOUNDIFY_MAX_SILENCE_SECONDS: maxSilenceSeconds || 5,
|
||||||
|
HOUNDIFY_MAX_SILENCE_AFTER_FULL_QUERY_SECONDS: maxSilenceAfterFullQuerySeconds || 1,
|
||||||
|
HOUNDIFY_MAX_SILENCE_AFTER_PARTIAL_QUERY_SECONDS: maxSilenceAfterPartialQuerySeconds || 1.5,
|
||||||
|
...(vadSensitivity && {HOUNDIFY_VAD_SENSITIVITY: vadSensitivity}),
|
||||||
|
...(vadTimeout && {HOUNDIFY_VAD_TIMEOUT: vadTimeout}),
|
||||||
|
...(vadMode && {HOUNDIFY_VAD_MODE: vadMode}),
|
||||||
|
...(vadVoiceMs && {HOUNDIFY_VAD_VOICE_MS: vadVoiceMs}),
|
||||||
|
...(vadSilenceMs && {HOUNDIFY_VAD_SILENCE_MS: vadSilenceMs}),
|
||||||
|
...(vadDebug && {HOUNDIFY_VAD_DEBUG: vadDebug}),
|
||||||
|
...(audioFormat && {HOUNDIFY_AUDIO_FORMAT: audioFormat}),
|
||||||
|
...(enableNoiseReduction && {HOUNDIFY_ENABLE_NOISE_REDUCTION: enableNoiseReduction}),
|
||||||
|
...(enableProfanityFilter && {HOUNDIFY_ENABLE_PROFANITY_FILTER: enableProfanityFilter}),
|
||||||
|
...(enablePunctuation && {HOUNDIFY_ENABLE_PUNCTUATION: enablePunctuation}),
|
||||||
|
...(enableCapitalization && {HOUNDIFY_ENABLE_CAPITALIZATION: enableCapitalization}),
|
||||||
|
...(confidenceThreshold && {HOUNDIFY_CONFIDENCE_THRESHOLD: confidenceThreshold}),
|
||||||
|
...(enableDisfluencyFilter && {HOUNDIFY_ENABLE_DISFLUENCY_FILTER: enableDisfluencyFilter}),
|
||||||
|
...(maxResults && {HOUNDIFY_MAX_RESULTS: maxResults}),
|
||||||
|
...(enableWordTimestamps && {HOUNDIFY_ENABLE_WORD_TIMESTAMPS: enableWordTimestamps}),
|
||||||
|
...(maxAlternatives && {HOUNDIFY_MAX_ALTERNATIVES: maxAlternatives}),
|
||||||
|
...(partialTranscriptInterval && {HOUNDIFY_PARTIAL_TRANSCRIPT_INTERVAL: partialTranscriptInterval}),
|
||||||
|
...(sessionTimeout && {HOUNDIFY_SESSION_TIMEOUT: sessionTimeout}),
|
||||||
|
...(connectionTimeout && {HOUNDIFY_CONNECTION_TIMEOUT: connectionTimeout}),
|
||||||
|
...(latitude && {HOUNDIFY_LATITUDE: latitude}),
|
||||||
|
...(longitude && {HOUNDIFY_LONGITUDE: longitude}),
|
||||||
|
...(city && {HOUNDIFY_CITY: city}),
|
||||||
|
...(state && {HOUNDIFY_STATE: state}),
|
||||||
|
...(country && {HOUNDIFY_COUNTRY: country}),
|
||||||
|
...(timeZone && {HOUNDIFY_TIMEZONE: timeZone}),
|
||||||
|
...(domain && {HOUNDIFY_DOMAIN: domain}),
|
||||||
|
...(audioEndpointUri && {HOUNDIFY_AUDIO_ENDPOINT: audioEndpointUri}),
|
||||||
|
...(customVocabulary && {HOUNDIFY_CUSTOM_VOCABULARY:
|
||||||
|
Array.isArray(customVocabulary) ? customVocabulary.join(',') : customVocabulary}),
|
||||||
|
...(languageModel && {HOUNDIFY_LANGUAGE_MODEL: languageModel}),
|
||||||
|
...(requestInfo && {HOUNDIFY_REQUEST_INFO: JSON.stringify(requestInfo)}),
|
||||||
|
...(sampleRate && {HOUNDIFY_SAMPLING_RATE: sampleRate}),
|
||||||
|
};
|
||||||
|
}
|
||||||
else if ('voxist' === vendor) {
|
else if ('voxist' === vendor) {
|
||||||
opts = {
|
opts = {
|
||||||
...opts,
|
...opts,
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
this.removeCustomEventListeners();
|
this.removeCustomEventListeners();
|
||||||
if (this.ep) {
|
if (this.ep) {
|
||||||
this._api(this.ep, [this.ep.uuid, 'close'])
|
this._api(this.ep, [this.ep.uuid, 'stop'])
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
this.logger.info({ err }, 'TtsStreamingBuffer:stop Error closing TTS streaming')
|
this.logger.info({ err }, 'TtsStreamingBuffer:stop Error closing TTS streaming')
|
||||||
);
|
);
|
||||||
@@ -193,10 +193,7 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
this.logger.debug('TtsStreamingBuffer:_feedQueue TTS stream is not open or no endpoint available');
|
this.logger.debug('TtsStreamingBuffer:_feedQueue TTS stream is not open or no endpoint available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (this._connectionStatus !== TtsStreamingConnectionStatus.Connected) {
|
||||||
this._connectionStatus === TtsStreamingConnectionStatus.NotConnected ||
|
|
||||||
this._connectionStatus === TtsStreamingConnectionStatus.Failed
|
|
||||||
) {
|
|
||||||
this.logger.debug('TtsStreamingBuffer:_feedQueue TTS stream is not connected');
|
this.logger.debug('TtsStreamingBuffer:_feedQueue TTS stream is not connected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -278,6 +275,14 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
}
|
}
|
||||||
const chunk = combinedText.slice(0, chunkEnd);
|
const chunk = combinedText.slice(0, chunkEnd);
|
||||||
|
|
||||||
|
// Check if the chunk is only whitespace before processing the queue
|
||||||
|
// If so, wait for more meaningful text
|
||||||
|
if (isWhitespace(chunk)) {
|
||||||
|
this.logger.debug('TtsStreamingBuffer:_feedQueue chunk is only whitespace, waiting for more text');
|
||||||
|
this._setTimerIfNeeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Now we iterate over the queue items
|
// Now we iterate over the queue items
|
||||||
// and deduct their lengths until we've accounted for chunkEnd characters.
|
// and deduct their lengths until we've accounted for chunkEnd characters.
|
||||||
let remaining = chunkEnd;
|
let remaining = chunkEnd;
|
||||||
@@ -301,6 +306,14 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
this.bufferedLength -= chunkEnd;
|
this.bufferedLength -= chunkEnd;
|
||||||
|
|
||||||
const modifiedChunk = chunk.replace(/\n\n/g, '\n \n');
|
const modifiedChunk = chunk.replace(/\n\n/g, '\n \n');
|
||||||
|
|
||||||
|
if (isWhitespace(modifiedChunk)) {
|
||||||
|
this.logger.debug('TtsStreamingBuffer:_feedQueue modified chunk is only whitespace, restoring queue');
|
||||||
|
this.queue.unshift({ type: 'text', value: chunk });
|
||||||
|
this.bufferedLength += chunkEnd;
|
||||||
|
this._setTimerIfNeeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.logger.debug(`TtsStreamingBuffer:_feedQueue sending chunk to tts: ${modifiedChunk}`);
|
this.logger.debug(`TtsStreamingBuffer:_feedQueue sending chunk to tts: ${modifiedChunk}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -349,6 +362,7 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
if (this.queue.length > 0) {
|
if (this.queue.length > 0) {
|
||||||
await this._feedQueue();
|
await this._feedQueue();
|
||||||
}
|
}
|
||||||
|
this.emit(TtsStreamingEvents.Connected, { vendor });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onConnectFailure(vendor) {
|
_onConnectFailure(vendor) {
|
||||||
@@ -399,6 +413,7 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
|
|
||||||
removeCustomEventListeners() {
|
removeCustomEventListeners() {
|
||||||
this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler));
|
this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler));
|
||||||
|
this.eventHandlers.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
_initHandlers(ep) {
|
_initHandlers(ep) {
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ class WsRequestor extends BaseRequestor {
|
|||||||
|
|
||||||
/* send the message */
|
/* send the message */
|
||||||
this.ws.send(JSON.stringify(obj), async() => {
|
this.ws.send(JSON.stringify(obj), async() => {
|
||||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
if (obj.type !== 'llm:event') this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||||
// If session:reconnect is waiting for ack, hold here until ack to send queuedMsgs
|
// If session:reconnect is waiting for ack, hold here until ack to send queuedMsgs
|
||||||
if (this._reconnectPromise) {
|
if (this._reconnectPromise) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
474
package-lock.json
generated
474
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -27,14 +27,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
||||||
"@aws-sdk/client-sns": "^3.549.0",
|
"@aws-sdk/client-sns": "^3.549.0",
|
||||||
"@jambonz/db-helpers": "^0.9.12",
|
"@jambonz/db-helpers": "^0.9.18",
|
||||||
"@jambonz/http-health-check": "^0.0.1",
|
"@jambonz/http-health-check": "^0.0.1",
|
||||||
"@jambonz/mw-registrar": "^0.2.7",
|
"@jambonz/mw-registrar": "^0.2.7",
|
||||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
"@jambonz/realtimedb-helpers": "^0.8.15",
|
||||||
"@jambonz/speech-utils": "^0.2.15",
|
"@jambonz/speech-utils": "^0.2.26",
|
||||||
"@jambonz/stats-collector": "^0.1.10",
|
"@jambonz/stats-collector": "^0.1.10",
|
||||||
"@jambonz/time-series": "^0.2.14",
|
"@jambonz/time-series": "^0.2.14",
|
||||||
"@jambonz/verb-specifications": "^0.0.108",
|
"@jambonz/verb-specifications": "^0.0.122",
|
||||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||||
"@opentelemetry/api": "^1.8.0",
|
"@opentelemetry/api": "^1.8.0",
|
||||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||||
@@ -48,13 +48,13 @@
|
|||||||
"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": "^4.0.4",
|
"drachtio-fsmrf": "^4.1.2",
|
||||||
"drachtio-srf": "^5.0.5",
|
"drachtio-srf": "^5.0.14",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.0.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"parse-url": "^9.2.0",
|
"parse-url": "^9.2.0",
|
||||||
"pino": "^8.20.0",
|
"pino": "^10.1.0",
|
||||||
"polly-ssml-split": "^0.1.0",
|
"polly-ssml-split": "^0.1.0",
|
||||||
"sdp-transform": "^2.15.0",
|
"sdp-transform": "^2.15.0",
|
||||||
"short-uuid": "^5.1.0",
|
"short-uuid": "^5.1.0",
|
||||||
|
|||||||
@@ -58,6 +58,46 @@ test('\'refer\' tests w/202 and NOTIFY', {timeout: 25000}, async(t) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('\'refer\' tests tel:', {timeout: 25000}, async(t) => {
|
||||||
|
clearModule.all();
|
||||||
|
const {srf, disconnect} = require('../app');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connect(srf);
|
||||||
|
|
||||||
|
// GIVEN
|
||||||
|
const verbs = [
|
||||||
|
{
|
||||||
|
verb: 'say',
|
||||||
|
text: 'silence_stream://100'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
verb: 'sip:refer',
|
||||||
|
referTo: 'tel:+1234567890',
|
||||||
|
actionHook: '/actionHook'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const noVerbs = [];
|
||||||
|
|
||||||
|
const from = 'refer_with_tel';
|
||||||
|
await provisionCallHook(from, verbs);
|
||||||
|
await provisionActionHook(from, noVerbs)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
await sippUac('uac-refer-with-notify.xml', '172.38.0.10', from);
|
||||||
|
t.pass('refer: successfully received 202 Accepted');
|
||||||
|
await sleepFor(1000);
|
||||||
|
const obj = await getJSON(`http:127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||||
|
t.ok(obj.body.final_referred_call_status === 200, 'refer: successfully received NOTIFY with 200 OK');
|
||||||
|
// console.log(`obj: ${JSON.stringify(obj)}`);
|
||||||
|
disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`error received: ${err}`);
|
||||||
|
disconnect();
|
||||||
|
t.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('\'refer\' tests w/202 but no NOTIFY', {timeout: 25000}, async(t) => {
|
test('\'refer\' tests w/202 but no NOTIFY', {timeout: 25000}, async(t) => {
|
||||||
clearModule.all();
|
clearModule.all();
|
||||||
const {srf, disconnect} = require('../app');
|
const {srf, disconnect} = require('../app');
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ test('invalid jambonz json create alert tests', async(t) => {
|
|||||||
{account_sid: 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f', page: 1, page_size: 25, days: 7});
|
{account_sid: 'bb845d4b-83a9-4cde-a6e9-50f3743bab3f', page: 1, page_size: 25, days: 7});
|
||||||
let checked = false;
|
let checked = false;
|
||||||
for (let i = 0; i < data.total; i++) {
|
for (let i = 0; i < data.total; i++) {
|
||||||
checked = data.data[i].message === 'malformed jambonz payload: must be array'
|
checked = data.data[i].message === 'malformed jambonz payload: must be array';
|
||||||
|
if (checked) break;
|
||||||
}
|
}
|
||||||
t.ok(checked, 'alert is raised as expected');
|
t.ok(checked, 'alert is raised as expected');
|
||||||
disconnect();
|
disconnect();
|
||||||
|
|||||||
Reference in New Issue
Block a user