mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-01-25 02:07:56 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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 createHttpListener = require('./lib/utils/http-listener');
|
||||
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) => {
|
||||
if (logger !== instance) {
|
||||
|
||||
@@ -174,5 +174,61 @@
|
||||
"non è raggiungibile",
|
||||
"lascia pure un messaggio",
|
||||
"puoi lasciare un messaggio"
|
||||
],
|
||||
"ja-JP": [
|
||||
"この通話は留守番電話に転送されました",
|
||||
"発信先は現在電話に出ることができません",
|
||||
"発信音の後でメッセージを録音してください",
|
||||
"録音を完了したら電話を切ることができます",
|
||||
"只今電話に出ることができません",
|
||||
"ただ今電話に出ることができません",
|
||||
"ただいま電話に出ることができません",
|
||||
"ピーという発信音の後にお名前とご用件をお話しください",
|
||||
"ファックスを送られる方はスタートボタンを押してください",
|
||||
"FAXを送られる方はスタートボタンを押してください",
|
||||
"おかけになった電話をお呼びしましたが",
|
||||
"お出になりません",
|
||||
"おでになりません",
|
||||
"お掛けになった電話番号は",
|
||||
"おかけになった電話番号は",
|
||||
"お掛けになった電話は",
|
||||
"おかけになった電話は",
|
||||
"現在使われておりません",
|
||||
"番号をお確かめになって",
|
||||
"お掛け直し下さい",
|
||||
"おかけ直し下さい",
|
||||
"おかけ直しください",
|
||||
"こちらはNTTドコモです",
|
||||
"こちらはエーユーです",
|
||||
"こちらはソフトバンクです",
|
||||
"電波の届かない",
|
||||
"電源が入っていない",
|
||||
"掛かりません",
|
||||
"かかりません",
|
||||
"お繋ぎすることが出来ません",
|
||||
"お繋ぎ出来ません",
|
||||
"お繋ぎすることができません",
|
||||
"お繋ぎできません",
|
||||
"おつなぎすることができません",
|
||||
"おつなぎできません",
|
||||
"メッセージを録音",
|
||||
"留守番電話",
|
||||
"お留守番サービス",
|
||||
"留守番",
|
||||
"留守電",
|
||||
"留守",
|
||||
"接続します",
|
||||
"合図の音",
|
||||
"ピーと",
|
||||
"発信音",
|
||||
"ご用件",
|
||||
"伝言",
|
||||
"お話しください",
|
||||
"ファックス",
|
||||
"FAX",
|
||||
"終了",
|
||||
"終了しました",
|
||||
"終了いたしました",
|
||||
"営業時間"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ router.post('/',
|
||||
|
||||
// find handling sbc sip for called 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);
|
||||
if (reg) {
|
||||
sbcAddress = selectHostPort(logger, reg.sbcAddress, 'tcp')[1];
|
||||
@@ -159,7 +159,9 @@ router.post('/',
|
||||
* trunk isn't specified,
|
||||
* 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 callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -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 (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.callerName = callerName;
|
||||
req.locals.callInfo.from = from;
|
||||
req.locals.callInfo.to = to;
|
||||
req.locals.callInfo.originatingSipIp = originatingSipIp;
|
||||
req.locals.callInfo.originatingSipTrunkName = originatingSipTrunkName;
|
||||
if (customerData) req.locals.callInfo.customerData = customerData;
|
||||
delete app.callInfo;
|
||||
}
|
||||
next();
|
||||
|
||||
@@ -658,6 +658,15 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
// disableTtsCache
|
||||
get disableTtsCache() {
|
||||
return this._disableTtsCache || false;
|
||||
}
|
||||
|
||||
set disableTtsCache(d) {
|
||||
this._disableTtsCache = d;
|
||||
}
|
||||
|
||||
getTsStreamingVendor() {
|
||||
let v;
|
||||
if (this.currentTask?.isStreamingTts) {
|
||||
@@ -710,7 +719,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
|
||||
hasGlobalSttPunctuation() {
|
||||
get hasGlobalSttPunctuation() {
|
||||
return this._globalSttPunctuation !== undefined;
|
||||
}
|
||||
|
||||
@@ -1017,8 +1026,6 @@ class CallSession extends Emitter {
|
||||
(type === 'tts' && credential.use_for_tts) ||
|
||||
(type === 'stt' && credential.use_for_stt)
|
||||
)) {
|
||||
this.logger.debug(
|
||||
`${type}: ${credential.vendor} ${credential.label ? `, label: ${credential.label}` : ''} `);
|
||||
if ('google' === vendor) {
|
||||
if (type === 'tts' && !credential.tts_tested_ok ||
|
||||
type === 'stt' && !credential.stt_tested_ok) {
|
||||
@@ -1088,6 +1095,13 @@ class CallSession extends Emitter {
|
||||
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) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
@@ -1119,6 +1133,7 @@ class CallSession extends Emitter {
|
||||
return {
|
||||
api_key: credential.api_key,
|
||||
model_id: credential.model_id,
|
||||
api_uri: credential.api_uri,
|
||||
options: credential.options
|
||||
};
|
||||
}
|
||||
@@ -1167,7 +1182,15 @@ class CallSession extends Emitter {
|
||||
service_version: credential.service_version
|
||||
};
|
||||
}
|
||||
else if ('deepgramriver' === vendor) {
|
||||
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
|
||||
};
|
||||
}
|
||||
else if ('deepgramflux' === vendor) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
api_key: credential.api_key,
|
||||
@@ -1417,7 +1440,11 @@ class CallSession extends Emitter {
|
||||
}
|
||||
else {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1864,7 +1891,7 @@ Duration=${duration} `
|
||||
return;
|
||||
}
|
||||
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', {
|
||||
id,
|
||||
status: 'failed',
|
||||
@@ -2397,7 +2424,7 @@ Duration=${duration} `
|
||||
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) {
|
||||
this.res.send(183, {body: ep.local.sdp});
|
||||
return {ep};
|
||||
@@ -2683,7 +2710,7 @@ Duration=${duration} `
|
||||
*/
|
||||
_onRefer(req, res) {
|
||||
const task = this.currentTask;
|
||||
const sd = task.sd;
|
||||
const sd = task?.sd;
|
||||
if (task && TaskName.Dial === task.name && sd && task.referHook) {
|
||||
task.handleRefer(this, req, res);
|
||||
}
|
||||
@@ -3082,7 +3109,7 @@ Duration=${duration} `
|
||||
task.notifyTtsStreamIsEmpty();
|
||||
} else if (
|
||||
// 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;
|
||||
sayTask.notifyTtsStreamIsEmpty();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ class InboundCallSession extends CallSession {
|
||||
this.req = req;
|
||||
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));
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
|
||||
@@ -49,7 +49,8 @@ class Conference extends Task {
|
||||
this.confName = this.data.name;
|
||||
[
|
||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
|
||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook', 'endConferenceDuration'
|
||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook',
|
||||
'endConferenceDuration', 'distributeDtmf'
|
||||
].forEach((attr) => this[attr] = this.data[attr]);
|
||||
this.record = this.data.record || {};
|
||||
this.statusEvents = [];
|
||||
@@ -356,6 +357,7 @@ class Conference extends Task {
|
||||
//https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod_conference_3965534/
|
||||
// mute | Enter conference muted
|
||||
...((this.joinMuted || this.speakOnlyTo) && {mute: true}),
|
||||
...(this.distributeDtmf && {'dist-dtmf': true})
|
||||
}});
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,8 @@ class TaskConfig extends Task {
|
||||
'boostAudioSignal',
|
||||
'vad',
|
||||
'ttsStream',
|
||||
'autoStreamTts'
|
||||
'autoStreamTts',
|
||||
'disableTtsCache'
|
||||
].forEach((k) => this[k] = this.data[k] || {});
|
||||
|
||||
if ('notifyEvents' in this.data) {
|
||||
@@ -88,6 +89,7 @@ class TaskConfig extends Task {
|
||||
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 hasDisableTtsCache() { return Object.keys(this.data).includes('disableTtsCache'); }
|
||||
|
||||
get summary() {
|
||||
const phrase = [];
|
||||
@@ -125,6 +127,7 @@ class TaskConfig extends Task {
|
||||
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 (this.hasDisableTtsCache) phrase.push(`disableTtsCache ${this.data.disableTtsCache ? 'on' : 'off'}`);
|
||||
return `${this.name}{${phrase.join(',')}}`;
|
||||
}
|
||||
|
||||
@@ -357,6 +360,11 @@ class TaskConfig extends Task {
|
||||
this.logger.info('Config: disabling ttsStream');
|
||||
cs.disableTtsStream();
|
||||
}
|
||||
|
||||
if (this.hasDisableTtsCache) {
|
||||
this.logger.info(`set disableTtsCache = ${this.disableTtsCache}`);
|
||||
cs.disableTtsCache = this.data.disableTtsCache;
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
|
||||
@@ -641,7 +641,9 @@ class TaskDial extends Task {
|
||||
* trunk isn't specified,
|
||||
* 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 callingNumber = str.startsWith('+') ? str.substring(1) : str;
|
||||
const voip_carrier_sid = await lookupCarrierByPhoneNumber(cs.accountSid, callingNumber);
|
||||
@@ -674,7 +676,8 @@ class TaskDial extends Task {
|
||||
rootSpan: cs.rootSpan,
|
||||
startSpan: this.startSpan.bind(this),
|
||||
dialTask: this,
|
||||
onHoldMusic: this.cs.onHoldMusic
|
||||
onHoldMusic: this.cs.onHoldMusic,
|
||||
tmpFiles: this.cs.tmpFiles,
|
||||
});
|
||||
this.dials.set(sd.callSid, sd);
|
||||
|
||||
@@ -775,6 +778,9 @@ class TaskDial extends Task {
|
||||
this.epOther.api('uuid_break', this.epOther.uuid);
|
||||
this.epOther.bridge(sd.ep);
|
||||
}
|
||||
else {
|
||||
this.logger.error('Dial:_connectSingleDial - no other endpoint to bridge!');
|
||||
}
|
||||
this.bridged = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ const {
|
||||
AwsTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
DeepgramTranscriptionEvents,
|
||||
GladiaTranscriptionEvents,
|
||||
SonioxTranscriptionEvents,
|
||||
CobaltTranscriptionEvents,
|
||||
IbmTranscriptionEvents,
|
||||
NvidiaTranscriptionEvents,
|
||||
JambonzTranscriptionEvents,
|
||||
AssemblyAiTranscriptionEvents,
|
||||
DeepgramRiverTranscriptionEvents,
|
||||
HoundifyTranscriptionEvents,
|
||||
DeepgramfluxTranscriptionEvents,
|
||||
VoxistTranscriptionEvents,
|
||||
CartesiaTranscriptionEvents,
|
||||
OpenAITranscriptionEvents,
|
||||
@@ -93,6 +95,8 @@ class TaskGather extends SttTask {
|
||||
|
||||
get needsStt() { return this.input.includes('speech'); }
|
||||
|
||||
get isBackgroundGather() { return this.bugname_prefix === 'background_bargeIn_'; }
|
||||
|
||||
get wantsSingleUtterance() {
|
||||
return this.data.recognizer?.singleUtterance === true;
|
||||
}
|
||||
@@ -227,7 +231,9 @@ class TaskGather extends SttTask {
|
||||
|
||||
const startListening = async(cs, ep) => {
|
||||
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) {
|
||||
try {
|
||||
await this._setSpeechHandlers(cs, ep);
|
||||
@@ -395,6 +401,7 @@ class TaskGather extends SttTask {
|
||||
if (this.digitBuffer.length === 0 && this.needsStt) {
|
||||
// DTMF is higher priority than STT.
|
||||
this.removeCustomEventListeners();
|
||||
this._clearAsrTimer(); //clear ASR timer as we're now using dtmf
|
||||
this._stopTranscribing(ep);
|
||||
}
|
||||
this.digitBuffer += evt.dtmf;
|
||||
@@ -409,6 +416,7 @@ class TaskGather extends SttTask {
|
||||
const ms = this.interDigitTimeout * 1000;
|
||||
this.logger.debug(`starting interdigit timer of ${ms}`);
|
||||
this.interDigitTimer = setTimeout(() => this._resolve('dtmf-interdigit-timeout'), ms);
|
||||
this._clearTimer(); //clear main timer as we're now using interdigit dtmf timer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,16 +474,28 @@ class TaskGather extends SttTask {
|
||||
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.ConnectFailure,
|
||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||
break;
|
||||
|
||||
case 'deepgramriver':
|
||||
this.bugname = `${this.bugname_prefix}deepgramriver_transcribe`;
|
||||
case 'deepgramflux':
|
||||
this.bugname = `${this.bugname_prefix}deepgramflux_transcribe`;
|
||||
this.addCustomEventListener(
|
||||
ep, DeepgramRiverTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep, DeepgramfluxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
this.addCustomEventListener(
|
||||
ep, DeepgramRiverTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
||||
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.ConnectFailure,
|
||||
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 '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;
|
||||
|
||||
case 'soniox':
|
||||
@@ -555,6 +575,18 @@ class TaskGather extends SttTask {
|
||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||
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':
|
||||
this.bugname = `${this.bugname_prefix}voxist_transcribe`;
|
||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Transcription,
|
||||
@@ -702,8 +734,8 @@ class TaskGather extends SttTask {
|
||||
this.logger.debug(`Starting timoutTimer of ${this.timeout}ms`);
|
||||
this._clearTimer();
|
||||
this._timeoutTimer = setTimeout(() => {
|
||||
// If continuousASR in use then extend by the asr window for more transcripts.
|
||||
if (this.isContinuousAsr) this._startAsrTimer();
|
||||
if (this.interDigitTimer) return; // let the inter-digit timer complete
|
||||
else {
|
||||
this._resolve(this.digitBuffer.length >= this.minDigits ? 'dtmf-num-digits' : 'timeout');
|
||||
}
|
||||
@@ -910,7 +942,7 @@ class TaskGather extends SttTask {
|
||||
|
||||
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language,
|
||||
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) {
|
||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
|
||||
@@ -1076,6 +1108,11 @@ class TaskGather extends SttTask {
|
||||
this.cs.requestor.request('verb:hook', this.partialResultHook, Object.assign({speech: evt},
|
||||
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 (evt.vendor.finalWords.length) {
|
||||
this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript');
|
||||
|
||||
@@ -5,6 +5,17 @@ const moment = require('moment');
|
||||
const MAX_PLAY_AUDIO_QUEUE_SIZE = 10;
|
||||
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
|
||||
.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 {
|
||||
constructor(logger, opts, parentTask) {
|
||||
super(logger, opts);
|
||||
@@ -16,10 +27,20 @@ class TaskListen extends Task {
|
||||
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'
|
||||
].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)) {
|
||||
this.metadata[key] = escapeString(this.data.metadata[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.mixType = this.mixType || 'mono';
|
||||
this.sampleRate = this.sampleRate || 8000;
|
||||
this.earlyMedia = this.data.earlyMedia === true;
|
||||
|
||||
@@ -218,7 +218,7 @@ class TaskLlmUltravox_S2S extends Task {
|
||||
async _onServerEvent(_ep, evt) {
|
||||
let endConversation = false;
|
||||
const type = evt.type;
|
||||
this.logger.debug({evt}, 'TaskLlmUltravox_S2S:_onServerEvent');
|
||||
//this.logger.debug({evt}, 'TaskLlmUltravox_S2S:_onServerEvent');
|
||||
|
||||
/* server errors of some sort */
|
||||
if (type === 'error') {
|
||||
|
||||
@@ -6,9 +6,21 @@ class TaskPlay extends Task {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.url = this.data.url.includes('?')
|
||||
? this.data.url.split('?')[0] + '?' + this.data.url.split('?')[1].replaceAll('.', '%2E')
|
||||
: 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.timeoutSecs = this.data.timeoutSecs || -1;
|
||||
this.loop = this.data.loop || 1;
|
||||
|
||||
@@ -19,6 +19,7 @@ class TaskRestDial extends Task {
|
||||
this.timeout = this.data.timeout || 60;
|
||||
this.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
|
||||
this.referHook = this.data.referHook;
|
||||
this.recentCallStatus = 0;
|
||||
|
||||
this.on('connect', this._onConnect.bind(this));
|
||||
this.on('callStatus', this._onCallStatus.bind(this));
|
||||
@@ -57,7 +58,11 @@ class TaskRestDial extends Task {
|
||||
this._clearCallTimer();
|
||||
if (this.canCancel) {
|
||||
this.canCancel = false;
|
||||
cs?.req?.cancel();
|
||||
try {
|
||||
cs?.req?.cancel();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'TaskRestDial: error cancelling call');
|
||||
}
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
@@ -118,7 +123,8 @@ class TaskRestDial extends Task {
|
||||
}
|
||||
|
||||
_onCallStatus(status) {
|
||||
this.logger.debug(`CallStatus: ${status}`);
|
||||
this.logger.debug(`RestDial CallStatus: ${status}`);
|
||||
this.recentCallStatus = status;
|
||||
if (status >= 200) {
|
||||
this.canCancel = false;
|
||||
this._clearCallTimer();
|
||||
@@ -136,11 +142,16 @@ class TaskRestDial extends Task {
|
||||
}
|
||||
|
||||
_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;
|
||||
if (this.canCancel) {
|
||||
if (this.canCancel && this.recentCallStatus < 200) {
|
||||
this.logger.debug('TaskRestDial: cancelling call attempt');
|
||||
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 { 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) => {
|
||||
// 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
|
||||
@@ -259,40 +281,32 @@ class TaskSay extends TtsTask {
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && ep?.connected) {
|
||||
let segment = 0;
|
||||
while (!this.killed && segment < filepath.length) {
|
||||
const filename = filepath[segment];
|
||||
if (cs.isInConference) {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]);
|
||||
await this.playToConfMember(ep, memberId, confName, confUuid, filename);
|
||||
}
|
||||
else {
|
||||
let playbackId;
|
||||
const isStreaming = filepath[segment].startsWith('say:{');
|
||||
const isStreaming = filename.startsWith('say:{');
|
||||
if (isStreaming) {
|
||||
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
|
||||
if (arr) this.logger.debug(`Say:exec sending streaming tts request: ${arr[1].substring(0, 64)}..`);
|
||||
}
|
||||
else {
|
||||
this.logger.debug(`Say:exec sending ${filepath[segment].substring(0, 64)}`);
|
||||
const arr = /^say:\{.*\}\s*(.*)$/.exec(filename);
|
||||
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)}`);
|
||||
}
|
||||
|
||||
const onPlaybackStop = (evt) => {
|
||||
try {
|
||||
this.logger.debug({evt},
|
||||
`Say got playback-stop ${evt.variable_tts_playback_id ? 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},
|
||||
const playbackId = this.getPlaybackId(segment);
|
||||
const isMatch = isMatchingEvent(this.logger, filename, playbackId, evt);
|
||||
if (!isMatch) {
|
||||
this.logger.info({currentPlaybackId: playbackId, stopPlaybackId: evt.variable_tts_playback_id},
|
||||
'Say:exec discarding playback-stop for earlier play');
|
||||
ep.once('playback-stop', this._boundOnPlaybackStop);
|
||||
|
||||
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.notifiedPlayBackStop = true;
|
||||
const tts_error = evt.variable_tts_error;
|
||||
@@ -331,6 +345,7 @@ class TaskSay extends TtsTask {
|
||||
!this.disableTtsCache
|
||||
) {
|
||||
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, {
|
||||
account_sid,
|
||||
vendor,
|
||||
@@ -358,9 +373,17 @@ class TaskSay extends TtsTask {
|
||||
};
|
||||
this._boundOnPlaybackStop = onPlaybackStop.bind(this);
|
||||
|
||||
ep.once('playback-start', (evt) => {
|
||||
const onPlaybackStart = (evt) => {
|
||||
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},
|
||||
`Say got playback-start ${evt.variable_tts_playback_id ? evt.variable_tts_playback_id : ''}`);
|
||||
if (this.otelSpan) {
|
||||
@@ -374,16 +397,21 @@ class TaskSay extends TtsTask {
|
||||
} catch (err) {
|
||||
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
|
||||
this._playPromise = new Promise((resolve, reject) => {
|
||||
this._playResolve = resolve;
|
||||
this._playReject = reject;
|
||||
});
|
||||
const r = await ep.play(filepath[segment]);
|
||||
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'));
|
||||
}
|
||||
try {
|
||||
// wait for playback-stop event received to confirm if the playback is successful
|
||||
await this._playPromise;
|
||||
@@ -400,12 +428,12 @@ class TaskSay extends TtsTask {
|
||||
this._playResolve = null;
|
||||
this._playReject = null;
|
||||
}
|
||||
if (filepath[segment].startsWith('say:{')) {
|
||||
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
|
||||
if (filename.startsWith('say:{')) {
|
||||
const arr = /^say:\{.*\}\s*(.*)$/.exec(filename);
|
||||
if (arr) this.logger.debug(`Say:exec complete playing streaming tts request: ${arr[1].substring(0, 64)}..`);
|
||||
} else {
|
||||
// This log will print spech credentials in say command for tts stream mode
|
||||
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
||||
this.logger.debug(`Say:exec completed play file ${filename}`);
|
||||
}
|
||||
}
|
||||
segment++;
|
||||
|
||||
@@ -203,6 +203,56 @@ class SttTask extends Task {
|
||||
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
||||
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) {
|
||||
|
||||
@@ -6,7 +6,8 @@ const {
|
||||
AwsTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
DeepgramTranscriptionEvents,
|
||||
DeepgramRiverTranscriptionEvents,
|
||||
GladiaTranscriptionEvents,
|
||||
DeepgramfluxTranscriptionEvents,
|
||||
SonioxTranscriptionEvents,
|
||||
CobaltTranscriptionEvents,
|
||||
IbmTranscriptionEvents,
|
||||
@@ -14,6 +15,7 @@ const {
|
||||
JambonzTranscriptionEvents,
|
||||
TranscribeStatus,
|
||||
AssemblyAiTranscriptionEvents,
|
||||
HoundifyTranscriptionEvents,
|
||||
VoxistTranscriptionEvents,
|
||||
CartesiaTranscriptionEvents,
|
||||
OpenAITranscriptionEvents,
|
||||
@@ -237,19 +239,35 @@ class TaskTranscribe extends SttTask {
|
||||
this._onVendorConnect.bind(this, cs, ep));
|
||||
this.addCustomEventListener(ep, DeepgramTranscriptionEvents.ConnectFailure,
|
||||
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 (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true;
|
||||
|
||||
break;
|
||||
case 'deepgramriver':
|
||||
this.bugname = `${this.bugname_prefix}deepgramriver_transcribe`;
|
||||
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.Transcription,
|
||||
case 'deepgramflux':
|
||||
this.bugname = `${this.bugname_prefix}deepgramflux_transcribe`;
|
||||
this.addCustomEventListener(ep, DeepgramfluxTranscriptionEvents.Transcription,
|
||||
this._onTranscription.bind(this, cs, ep, channel));
|
||||
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.Connect,
|
||||
this.addCustomEventListener(ep, DeepgramfluxTranscriptionEvents.Connect,
|
||||
this._onVendorConnect.bind(this, cs, ep));
|
||||
this.addCustomEventListener(ep, DeepgramRiverTranscriptionEvents.ConnectFailure,
|
||||
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;
|
||||
case 'soniox':
|
||||
this.bugname = `${this.bugname_prefix}soniox_transcribe`;
|
||||
@@ -320,6 +338,18 @@ class TaskTranscribe extends SttTask {
|
||||
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||
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':
|
||||
this.bugname = `${this.bugname_prefix}voxist_transcribe`;
|
||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Transcription,
|
||||
|
||||
@@ -3,6 +3,16 @@ const { TaskPreconditions } = require('../utils/constants');
|
||||
const { SpeechCredentialError } = require('../utils/error');
|
||||
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 {
|
||||
|
||||
constructor(logger, data, parentTask) {
|
||||
@@ -22,10 +32,19 @@ class TtsTask extends Task {
|
||||
this.disableTtsCache = this.data.disableTtsCache;
|
||||
this.options = this.synthesizer.options || {};
|
||||
this.instructions = this.data.instructions;
|
||||
this.playbackIds = [];
|
||||
}
|
||||
|
||||
getPlaybackId(offset) {
|
||||
return this.playbackIds[offset];
|
||||
}
|
||||
|
||||
async 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) {
|
||||
this.options = {...cs.synthesizer.options, ...this.options};
|
||||
this.data.synthesizer = this.data.synthesizer || {};
|
||||
@@ -66,11 +85,10 @@ class TtsTask extends Task {
|
||||
}
|
||||
|
||||
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} = credentials;
|
||||
let obj;
|
||||
|
||||
this.logger.debug({credentials},
|
||||
`setTtsStreamingChannelVars: vendor: ${vendor}, language: ${language}, voice: ${voice}`);
|
||||
this.logger.debug(`setTtsStreamingChannelVars: vendor: ${vendor}, language: ${language}, voice: ${voice}`);
|
||||
|
||||
switch (vendor) {
|
||||
case 'deepgram':
|
||||
@@ -91,6 +109,7 @@ class TtsTask extends Task {
|
||||
const {stability, similarity_boost, use_speaker_boost, style, speed} = this.options.voice_settings || {};
|
||||
obj = {
|
||||
ELEVENLABS_API_KEY: api_key,
|
||||
...(api_uri && {ELEVENLABS_API_URI: api_uri}),
|
||||
ELEVENLABS_TTS_STREAMING_MODEL_ID: model_id,
|
||||
ELEVENLABS_TTS_STREAMING_VOICE_ID: voice,
|
||||
// 20/12/2024 - only eleven_turbo_v2_5 support multiple language
|
||||
@@ -280,6 +299,7 @@ class TtsTask extends Task {
|
||||
renderForCaching: preCache
|
||||
});
|
||||
if (!filePath.startsWith('say:')) {
|
||||
this.playbackIds.push(null);
|
||||
this.logger.debug(`Say: file ${filePath}, served from cache ${servedFromCache}`);
|
||||
if (filePath) cs.trackTmpFile(filePath);
|
||||
if (this.otelSpan) {
|
||||
@@ -309,8 +329,16 @@ class TtsTask extends Task {
|
||||
}
|
||||
}
|
||||
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},`);
|
||||
this.notifyStatus({
|
||||
event: 'synthesized-audio',
|
||||
vendor,
|
||||
language,
|
||||
servedFromCache,
|
||||
'id': this.id
|
||||
});
|
||||
return modifiedPath;
|
||||
}
|
||||
return filePath;
|
||||
|
||||
@@ -94,12 +94,20 @@
|
||||
"DeepgramTranscriptionEvents": {
|
||||
"Transcription": "deepgram_transcribe::transcription",
|
||||
"ConnectFailure": "deepgram_transcribe::connect_failed",
|
||||
"Connect": "deepgram_transcribe::connect"
|
||||
"Connect": "deepgram_transcribe::connect",
|
||||
"Error": "deepgram_transcribe::error"
|
||||
},
|
||||
"DeepgramRiverTranscriptionEvents": {
|
||||
"Transcription": "deepgramriver_transcribe::transcription",
|
||||
"ConnectFailure": "deepgramriver_transcribe::connect_failed",
|
||||
"Connect": "deepgramriver_transcribe::connect"
|
||||
"DeepgramfluxTranscriptionEvents": {
|
||||
"Transcription": "deepgramflux_transcribe::transcription",
|
||||
"ConnectFailure": "deepgramflux_transcribe::connect_failed",
|
||||
"Connect": "deepgramflux_transcribe::connect",
|
||||
"Error": "deepgramflux_transcribe::error"
|
||||
},
|
||||
"GladiaTranscriptionEvents": {
|
||||
"Transcription": "gladia_transcribe::transcription",
|
||||
"ConnectFailure": "gladia_transcribe::connect_failed",
|
||||
"Connect": "gladia_transcribe::connect",
|
||||
"Error": "gladia_transcribe::error"
|
||||
},
|
||||
"SonioxTranscriptionEvents": {
|
||||
"Transcription": "soniox_transcribe::transcription",
|
||||
@@ -167,6 +175,12 @@
|
||||
"ConnectFailure": "assemblyai_transcribe::connect_failed",
|
||||
"Connect": "assemblyai_transcribe::connect"
|
||||
},
|
||||
"HoundifyTranscriptionEvents": {
|
||||
"Transcription": "houndify_transcribe::transcription",
|
||||
"Error": "houndify_transcribe::error",
|
||||
"ConnectFailure": "houndify_transcribe::connect_failed",
|
||||
"Connect": "houndify_transcribe::connect"
|
||||
},
|
||||
"VoxistTranscriptionEvents": {
|
||||
"Transcription": "voxist_transcribe::transcription",
|
||||
"Error": "voxist_transcribe::error",
|
||||
|
||||
@@ -81,7 +81,12 @@ const speechMapper = (cred) => {
|
||||
obj.deepgram_tts_uri = o.deepgram_tts_uri;
|
||||
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
|
||||
}
|
||||
else if ('deepgramriver' === obj.vendor) {
|
||||
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;
|
||||
}
|
||||
@@ -101,6 +106,7 @@ const speechMapper = (cred) => {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
obj.model_id = o.model_id;
|
||||
obj.api_uri = o.api_uri;
|
||||
obj.options = o.options;
|
||||
}
|
||||
else if ('playht' === obj.vendor) {
|
||||
@@ -141,6 +147,12 @@ const speechMapper = (cred) => {
|
||||
obj.api_key = o.api_key;
|
||||
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;
|
||||
}
|
||||
else if ('voxist' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
|
||||
@@ -173,7 +173,8 @@ function installSrfLocals(srf, logger, {
|
||||
lookupAccountCapacitiesBySid,
|
||||
lookupSmppGateways,
|
||||
lookupClientByAccountAndUsername,
|
||||
lookupSystemInformation
|
||||
lookupSystemInformation,
|
||||
lookupLcrByAccount
|
||||
} = require('@jambonz/db-helpers')({
|
||||
host: JAMBONES_MYSQL_HOST,
|
||||
user: JAMBONES_MYSQL_USER,
|
||||
@@ -279,7 +280,8 @@ function installSrfLocals(srf, logger, {
|
||||
retrieveByPatternSortedSet,
|
||||
sortedSetLength,
|
||||
sortedSetPositionByPattern,
|
||||
getVerbioAccessToken
|
||||
getVerbioAccessToken,
|
||||
lookupLcrByAccount
|
||||
},
|
||||
parentLogger: logger,
|
||||
getSBC,
|
||||
|
||||
@@ -20,7 +20,7 @@ const { createMediaEndpoint } = require('./media-endpoint');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask,
|
||||
onHoldMusic}) {
|
||||
onHoldMusic, tmpFiles}) {
|
||||
super();
|
||||
assert(target.type);
|
||||
|
||||
@@ -44,6 +44,7 @@ class SingleDialer extends Emitter {
|
||||
this.callSid = crypto.randomUUID();
|
||||
this.dialTask = dialTask;
|
||||
this.onHoldMusic = onHoldMusic;
|
||||
this.tmpFiles = tmpFiles;
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
}
|
||||
@@ -328,7 +329,13 @@ class SingleDialer extends Emitter {
|
||||
*/
|
||||
async kill(Reason) {
|
||||
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) {
|
||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||
this.logger.debug('SingleDialer:kill hanging up called party');
|
||||
@@ -402,7 +409,7 @@ class SingleDialer extends Emitter {
|
||||
tasks,
|
||||
rootSpan: this.rootSpan,
|
||||
req: this.req,
|
||||
tmpFiles: cs.tmpFiles,
|
||||
tmpFiles: this.tmpFiles,
|
||||
});
|
||||
await cs.exec();
|
||||
|
||||
@@ -536,12 +543,12 @@ class SingleDialer extends Emitter {
|
||||
|
||||
function placeOutdial({
|
||||
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask,
|
||||
onHoldMusic
|
||||
onHoldMusic, tmpFiles
|
||||
}) {
|
||||
const myOpts = deepcopy(opts);
|
||||
const sd = new SingleDialer({
|
||||
logger, sbcAddress, target, opts: myOpts, application, callInfo,
|
||||
accountInfo, rootSpan, startSpan, dialTask, onHoldMusic
|
||||
accountInfo, rootSpan, startSpan, dialTask, onHoldMusic, tmpFiles
|
||||
});
|
||||
sd.exec(srf, ms, myOpts);
|
||||
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;
|
||||
@@ -131,6 +131,43 @@ const stickyVars = {
|
||||
'OPENAI_TURN_DETECTION_PREFIX_PADDING_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,20 +376,56 @@ const normalizeDeepgram = (evt, channel, language, shortUtterance) => {
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeDeepgramRiver = (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: evt.end_of_turn_confidence,
|
||||
confidence,
|
||||
end_of_turn_confidence: evt.end_of_turn_confidence,
|
||||
transcript: evt.transcript,
|
||||
...(turnTakingEvent && {turn_taking_event: turnTakingEvent})
|
||||
}
|
||||
],
|
||||
vendor: {
|
||||
name: 'deepgramriver',
|
||||
name: 'deepgramflux',
|
||||
evt: copy
|
||||
}
|
||||
};
|
||||
@@ -570,6 +643,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 copy = JSON.parse(JSON.stringify(evt));
|
||||
return {
|
||||
@@ -669,8 +766,10 @@ module.exports = (logger) => {
|
||||
switch (vendor) {
|
||||
case 'deepgram':
|
||||
return normalizeDeepgram(evt, channel, language, shortUtterance);
|
||||
case 'deepgramriver':
|
||||
return normalizeDeepgramRiver(evt, channel, language, shortUtterance);
|
||||
case 'gladia':
|
||||
return normalizeGladia(evt, channel, language, shortUtterance);
|
||||
case 'deepgramflux':
|
||||
return normalizeDeepgramFlux(evt, channel, language, shortUtterance);
|
||||
case 'microsoft':
|
||||
return normalizeMicrosoft(evt, channel, language, punctuation);
|
||||
case 'google':
|
||||
@@ -689,6 +788,8 @@ module.exports = (logger) => {
|
||||
return normalizeCobalt(evt, channel, language);
|
||||
case 'assemblyai':
|
||||
return normalizeAssemblyAi(evt, channel, language, shortUtterance);
|
||||
case 'houndify':
|
||||
return normalizeHoundify(evt, channel, language, shortUtterance);
|
||||
case 'voxist':
|
||||
return normalizeVoxist(evt, channel, language);
|
||||
case 'cartesia':
|
||||
@@ -965,19 +1066,30 @@ module.exports = (logger) => {
|
||||
...(entityPrompt && {DEEPGRAM_SPEECH_ENTITY_PROMPT: entityPrompt}),
|
||||
};
|
||||
}
|
||||
else if ('deepgramriver' === vendor) {
|
||||
else if ('deepgramflux' === vendor) {
|
||||
const {
|
||||
preflightThreshold,
|
||||
eotThreshold,
|
||||
eotTimeoutMs,
|
||||
mipOptOut
|
||||
mipOptOut,
|
||||
model,
|
||||
eagerEotThreshold,
|
||||
keyterms
|
||||
} = rOpts.deepgramOptions || {};
|
||||
opts = {
|
||||
DEEPGRAM_API_KEY: sttCredentials.api_key,
|
||||
...(preflightThreshold && {DEEPGRAM_SPEECH_PRELIGHT_THRESHOLD: preflightThreshold}),
|
||||
...(eotThreshold && {DEEPGRAM_SPEECH_EOT_THRESHOLD: eotThreshold}),
|
||||
...(eotTimeoutMs && {DEEPGRAM_SPEECH_EOT_TIMEOUT_MS: eotTimeoutMs}),
|
||||
...(mipOptOut && {DEEPGRAM_SPEECH_MIP_OPT_OUT: mipOptOut}),
|
||||
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 ('gladia' === vendor) {
|
||||
const {host, path} = sttCredentials;
|
||||
opts = {
|
||||
GLADIA_SPEECH_HOST: host,
|
||||
GLADIA_SPEECH_PATH: path,
|
||||
};
|
||||
}
|
||||
else if ('soniox' === vendor) {
|
||||
@@ -1106,6 +1218,57 @@ module.exports = (logger) => {
|
||||
{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
|
||||
} = rOpts.houndifyOptions || {};
|
||||
|
||||
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}),
|
||||
...(audioEndpoint && {HOUNDIFY_AUDIO_ENDPOINT: audioEndpoint}),
|
||||
...(customVocabulary && {HOUNDIFY_CUSTOM_VOCABULARY:
|
||||
Array.isArray(customVocabulary) ? customVocabulary.join(',') : customVocabulary}),
|
||||
...(languageModel && {HOUNDIFY_LANGUAGE_MODEL: languageModel}),
|
||||
};
|
||||
}
|
||||
else if ('voxist' === vendor) {
|
||||
opts = {
|
||||
...opts,
|
||||
|
||||
@@ -293,7 +293,7 @@ class WsRequestor extends BaseRequestor {
|
||||
|
||||
/* send the message */
|
||||
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 (this._reconnectPromise) {
|
||||
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": {
|
||||
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
||||
"@aws-sdk/client-sns": "^3.549.0",
|
||||
"@jambonz/db-helpers": "^0.9.16",
|
||||
"@jambonz/db-helpers": "^0.9.18",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/mw-registrar": "^0.2.7",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.15",
|
||||
"@jambonz/speech-utils": "^0.2.19",
|
||||
"@jambonz/speech-utils": "^0.2.25",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/time-series": "^0.2.14",
|
||||
"@jambonz/verb-specifications": "^0.0.113",
|
||||
"@jambonz/verb-specifications": "^0.0.119",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||
@@ -49,7 +49,7 @@
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^4.1.2",
|
||||
"drachtio-srf": "^5.0.5",
|
||||
"drachtio-srf": "^5.0.11",
|
||||
"express": "^4.19.2",
|
||||
"express-validator": "^7.0.1",
|
||||
"moment": "^2.30.1",
|
||||
|
||||
Reference in New Issue
Block a user