mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-15 02:39:35 +00:00
Compare commits
25 Commits
v0.9.5-rc5
...
feat/drach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b05e5d026 | ||
|
|
1d6f84c2d7 | ||
|
|
de9b970a93 | ||
|
|
ec786ef1dd | ||
|
|
a95a6d1683 | ||
|
|
65b3066866 | ||
|
|
057f52e56c | ||
|
|
b46be57eba | ||
|
|
f950d19d1c | ||
|
|
859132bb1c | ||
|
|
acaadceaa2 | ||
|
|
add8d63e8e | ||
|
|
a05b72a420 | ||
|
|
febe1ac3b3 | ||
|
|
28ff85225f | ||
|
|
f2fe7c4d24 | ||
|
|
97408c7d3b | ||
|
|
db5f0a0dce | ||
|
|
654ccd9d9d | ||
|
|
ea27b20ac5 | ||
|
|
96aa705378 | ||
|
|
5e51849839 | ||
|
|
44f69fa76d | ||
|
|
73c77bea71 | ||
|
|
babc0d0dbb |
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",
|
||||||
|
"終了",
|
||||||
|
"終了しました",
|
||||||
|
"終了いたしました",
|
||||||
|
"営業時間"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -710,7 +710,7 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
hasGlobalSttPunctuation() {
|
get hasGlobalSttPunctuation() {
|
||||||
return this._globalSttPunctuation !== undefined;
|
return this._globalSttPunctuation !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1017,8 +1017,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) {
|
||||||
@@ -1146,6 +1144,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,
|
||||||
@@ -1410,7 +1415,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1857,7 +1866,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',
|
||||||
@@ -2676,7 +2685,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);
|
||||||
}
|
}
|
||||||
@@ -3075,7 +3084,7 @@ Duration=${duration} `
|
|||||||
task.notifyTtsStreamIsEmpty();
|
task.notifyTtsStreamIsEmpty();
|
||||||
} else if (
|
} else if (
|
||||||
// If Gather nested say task is streaming
|
// If Gather nested say task is streaming
|
||||||
TaskName.Gather === task.name && task.sayTask && task.sayTask.isStreamingTts) {
|
task && TaskName.Gather === task.name && task.sayTask && task.sayTask.isStreamingTts) {
|
||||||
const sayTask = task.sayTask;
|
const sayTask = task.sayTask;
|
||||||
sayTask.notifyTtsStreamIsEmpty();
|
sayTask.notifyTtsStreamIsEmpty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -641,7 +641,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);
|
||||||
@@ -674,7 +676,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);
|
||||||
|
|
||||||
@@ -775,6 +778,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -368,6 +368,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) {
|
||||||
@@ -392,6 +395,7 @@ 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();
|
||||||
|
this._clearAsrTimer(); //clear ASR timer as we're now using dtmf
|
||||||
this._stopTranscribing(ep);
|
this._stopTranscribing(ep);
|
||||||
}
|
}
|
||||||
this.digitBuffer += evt.dtmf;
|
this.digitBuffer += evt.dtmf;
|
||||||
@@ -406,6 +410,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +468,7 @@ 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;
|
break;
|
||||||
|
|
||||||
case 'deepgramriver':
|
case 'deepgramriver':
|
||||||
@@ -473,6 +479,7 @@ class TaskGather extends SttTask {
|
|||||||
ep, DeepgramRiverTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
ep, DeepgramRiverTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||||
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.ConnectFailure,
|
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.ConnectFailure,
|
||||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||||
|
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'soniox':
|
case 'soniox':
|
||||||
@@ -696,10 +703,11 @@ class TaskGather extends SttTask {
|
|||||||
|
|
||||||
_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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,28 @@ const pollySSMLSplit = require('polly-ssml-split');
|
|||||||
const { SpeechCredentialError } = require('../utils/error');
|
const { SpeechCredentialError } = require('../utils/error');
|
||||||
const { sleepFor } = require('../utils/helpers');
|
const { sleepFor } = require('../utils/helpers');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
// HIGH_WATER_BUFFER_SIZE defined in tts-streaming-buffer.js
|
// HIGH_WATER_BUFFER_SIZE defined in tts-streaming-buffer.js
|
||||||
@@ -259,40 +281,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 +345,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 +373,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,15 +397,17 @@ 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]);
|
const r = await ep.play(filename);
|
||||||
this.logger.debug({r}, 'Say:exec play result');
|
this.logger.debug({r}, 'Say:exec play result');
|
||||||
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
|
||||||
@@ -400,12 +425,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++;
|
||||||
@@ -452,6 +477,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 +543,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',
|
||||||
|
|||||||
@@ -237,6 +237,8 @@ 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;
|
||||||
@@ -250,6 +252,8 @@ class TaskTranscribe extends SttTask {
|
|||||||
this._onVendorConnect.bind(this, cs, ep));
|
this._onVendorConnect.bind(this, cs, ep));
|
||||||
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.ConnectFailure,
|
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.ConnectFailure,
|
||||||
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
|
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.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`;
|
||||||
|
|||||||
@@ -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,6 +32,11 @@ 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) {
|
||||||
@@ -69,8 +84,7 @@ class TtsTask extends Task {
|
|||||||
const {api_key, model_id, custom_tts_streaming_url, auth_token} = credentials;
|
const {api_key, model_id, custom_tts_streaming_url, auth_token} = credentials;
|
||||||
let obj;
|
let obj;
|
||||||
|
|
||||||
this.logger.debug({credentials},
|
this.logger.debug(`setTtsStreamingChannelVars: vendor: ${vendor}, language: ${language}, voice: ${voice}`);
|
||||||
`setTtsStreamingChannelVars: vendor: ${vendor}, language: ${language}, voice: ${voice}`);
|
|
||||||
|
|
||||||
switch (vendor) {
|
switch (vendor) {
|
||||||
case 'deepgram':
|
case 'deepgram':
|
||||||
@@ -280,6 +294,7 @@ class TtsTask extends Task {
|
|||||||
renderForCaching: preCache
|
renderForCaching: preCache
|
||||||
});
|
});
|
||||||
if (!filePath.startsWith('say:')) {
|
if (!filePath.startsWith('say:')) {
|
||||||
|
this.playbackIds.push(null);
|
||||||
this.logger.debug(`Say: file ${filePath}, served from cache ${servedFromCache}`);
|
this.logger.debug(`Say: file ${filePath}, served from cache ${servedFromCache}`);
|
||||||
if (filePath) cs.trackTmpFile(filePath);
|
if (filePath) cs.trackTmpFile(filePath);
|
||||||
if (this.otelSpan) {
|
if (this.otelSpan) {
|
||||||
@@ -293,13 +308,32 @@ 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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.logger.debug('Say: a streaming tts api will be used');
|
this.playbackIds.push(extractPlaybackId(filePath));
|
||||||
|
this.logger.debug({playbackIds: this.playbackIds}, '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},`);
|
||||||
|
this.notifyStatus({
|
||||||
|
event: 'synthesized-audio',
|
||||||
|
vendor,
|
||||||
|
language,
|
||||||
|
servedFromCache,
|
||||||
|
'id': this.id
|
||||||
|
});
|
||||||
return modifiedPath;
|
return modifiedPath;
|
||||||
}
|
}
|
||||||
return filePath;
|
return filePath;
|
||||||
|
|||||||
@@ -94,12 +94,14 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"DeepgramRiverTranscriptionEvents": {
|
"DeepgramRiverTranscriptionEvents": {
|
||||||
"Transcription": "deepgramriver_transcribe::transcription",
|
"Transcription": "deepgramriver_transcribe::transcription",
|
||||||
"ConnectFailure": "deepgramriver_transcribe::connect_failed",
|
"ConnectFailure": "deepgramriver_transcribe::connect_failed",
|
||||||
"Connect": "deepgramriver_transcribe::connect"
|
"Connect": "deepgramriver_transcribe::connect",
|
||||||
|
"Error": "deepgramriver_transcribe::error"
|
||||||
},
|
},
|
||||||
"SonioxTranscriptionEvents": {
|
"SonioxTranscriptionEvents": {
|
||||||
"Transcription": "soniox_transcribe::transcription",
|
"Transcription": "soniox_transcribe::transcription",
|
||||||
|
|||||||
@@ -124,6 +124,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;
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -402,7 +409,7 @@ class SingleDialer extends Emitter {
|
|||||||
tasks,
|
tasks,
|
||||||
rootSpan: this.rootSpan,
|
rootSpan: this.rootSpan,
|
||||||
req: this.req,
|
req: this.req,
|
||||||
tmpFiles: cs.tmpFiles,
|
tmpFiles: this.tmpFiles,
|
||||||
});
|
});
|
||||||
await cs.exec();
|
await cs.exec();
|
||||||
|
|
||||||
@@ -536,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;
|
||||||
@@ -785,12 +785,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}),
|
||||||
@@ -891,6 +894,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);
|
||||||
@@ -949,7 +960,9 @@ 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 ('deepgramriver' === vendor) {
|
else if ('deepgramriver' === vendor) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
5437
package-lock.json
generated
5437
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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.16",
|
"@jambonz/db-helpers": "^0.9.17",
|
||||||
"@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.15",
|
"@jambonz/realtimedb-helpers": "^0.8.15",
|
||||||
"@jambonz/speech-utils": "^0.2.15",
|
"@jambonz/speech-utils": "^0.2.23",
|
||||||
"@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.111",
|
"@jambonz/verb-specifications": "^0.0.113",
|
||||||
"@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",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"deepcopy": "^2.1.0",
|
"deepcopy": "^2.1.0",
|
||||||
"drachtio-fsmrf": "^4.1.2",
|
"drachtio-fsmrf": "^4.1.2",
|
||||||
"drachtio-srf": "^5.0.5",
|
"drachtio-srf": "^5.0.11",
|
||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user