mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-03-31 21:26:49 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8b1c429e0 |
@@ -13,7 +13,7 @@ Configuration is provided via environment variables:
|
|||||||
|AWS_ACCESS_KEY_ID| aws access key id, used for TTS/STT as well SNS notifications|no|
|
|AWS_ACCESS_KEY_ID| aws access key id, used for TTS/STT as well SNS notifications|no|
|
||||||
|AWS_REGION| aws region| no|
|
|AWS_REGION| aws region| no|
|
||||||
|AWS_SECRET_ACCESS_KEY| aws secret access key, used per above|no|
|
|AWS_SECRET_ACCESS_KEY| aws secret access key, used per above|no|
|
||||||
|AWS_SNS_TOPIC_ARN| aws sns topic arn that scale-in lifecycle notifications will be published to|no|
|
|AWS_SNS_TOPIC_ARM| aws sns topic arn that scale-in lifecycle notifications will be published to|no|
|
||||||
|DRACHTIO_HOST| ip address of drachtio server (typically '127.0.0.1')|yes|
|
|DRACHTIO_HOST| ip address of drachtio server (typically '127.0.0.1')|yes|
|
||||||
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|
|DRACHTIO_PORT| listening port of drachtio server for control connections (typically 9022)|yes|
|
||||||
|DRACHTIO_SECRET| shared secret|yes|
|
|DRACHTIO_SECRET| shared secret|yes|
|
||||||
@@ -72,7 +72,7 @@ module.exports = {
|
|||||||
STATS_PORT: 8125,
|
STATS_PORT: 8125,
|
||||||
STATS_PROTOCOL: 'tcp',
|
STATS_PROTOCOL: 'tcp',
|
||||||
STATS_TELEGRAF: 1,
|
STATS_TELEGRAF: 1,
|
||||||
AWS_SNS_TOPIC_ARN: 'arn:aws:sns:us-west-1:xxxxxxxxxxx:terraform-20201107200347128600000002',
|
AWS_SNS_TOPIC_ARM: 'arn:aws:sns:us-west-1:xxxxxxxxxxx:terraform-20201107200347128600000002',
|
||||||
JAMBONES_NETWORK_CIDR: '172.31.0.0/16',
|
JAMBONES_NETWORK_CIDR: '172.31.0.0/16',
|
||||||
JAMBONES_MYSQL_HOST: 'aurora-cluster-jambonz.cluster-yyyyyyyyyyy.us-west-1.rds.amazonaws.com',
|
JAMBONES_MYSQL_HOST: 'aurora-cluster-jambonz.cluster-yyyyyyyyyyy.us-west-1.rds.amazonaws.com',
|
||||||
JAMBONES_MYSQL_USER: 'admin',
|
JAMBONES_MYSQL_USER: 'admin',
|
||||||
|
|||||||
81
app.js
81
app.js
@@ -27,54 +27,8 @@ const pino = require('pino');
|
|||||||
const logger = pino(opts, pino.destination({sync: false}));
|
const logger = pino(opts, pino.destination({sync: false}));
|
||||||
const {LifeCycleEvents, FS_UUID_SET_NAME, SystemState, FEATURE_SERVER} = require('./lib/utils/constants');
|
const {LifeCycleEvents, FS_UUID_SET_NAME, SystemState, FEATURE_SERVER} = require('./lib/utils/constants');
|
||||||
const installSrfLocals = require('./lib/utils/install-srf-locals');
|
const installSrfLocals = require('./lib/utils/install-srf-locals');
|
||||||
const createHttpListener = require('./lib/utils/http-listener');
|
installSrfLocals(srf, logger);
|
||||||
const healthCheck = require('@jambonz/http-health-check');
|
|
||||||
|
|
||||||
// Install the srf locals
|
|
||||||
installSrfLocals(srf, logger, {
|
|
||||||
onFreeswitchConnect: (wraper) => {
|
|
||||||
// Only connect to drachtio if freeswitch is connected
|
|
||||||
logger.info(`connected to freeswitch at ${wraper.ms.address}, start drachtio server`);
|
|
||||||
if (DRACHTIO_HOST) {
|
|
||||||
srf.connect({host: DRACHTIO_HOST, port: DRACHTIO_PORT, secret: DRACHTIO_SECRET });
|
|
||||||
srf.on('connect', (err, hp) => {
|
|
||||||
const arr = /^(.*)\/(.*)$/.exec(hp.split(',').pop());
|
|
||||||
srf.locals.localSipAddress = `${arr[2]}`;
|
|
||||||
logger.info(`connected to drachtio listening on ${hp}, local sip address is ${srf.locals.localSipAddress}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
logger.info(`listening for drachtio requests on port ${DRACHTIO_PORT}`);
|
|
||||||
srf.listen({port: DRACHTIO_PORT, secret: DRACHTIO_SECRET});
|
|
||||||
}
|
|
||||||
// Start Http server
|
|
||||||
createHttpListener(logger, srf)
|
|
||||||
.then(({server, app}) => {
|
|
||||||
httpServer = server;
|
|
||||||
healthCheck({app, logger, path: '/', fn: getCount});
|
|
||||||
return {server, app};
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error(err, 'Error creating http listener');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onFreeswitchDisconnect: (wraper) => {
|
|
||||||
// check if all freeswitch connections are lost, disconnect drachtio server
|
|
||||||
logger.info(`lost connection to freeswitch at ${wraper.ms.address}`);
|
|
||||||
const ms = srf.locals.getFreeswitch();
|
|
||||||
if (!ms) {
|
|
||||||
logger.info('no freeswitch connections, stopping drachtio server');
|
|
||||||
disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (NODE_ENV === 'test') {
|
|
||||||
srf.on('error', (err) => {
|
|
||||||
logger.info(err, 'Error connecting to drachtio');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init services
|
|
||||||
const writeSystemAlerts = srf.locals?.writeSystemAlerts;
|
const writeSystemAlerts = srf.locals?.writeSystemAlerts;
|
||||||
if (writeSystemAlerts) {
|
if (writeSystemAlerts) {
|
||||||
writeSystemAlerts({
|
writeSystemAlerts({
|
||||||
@@ -100,6 +54,24 @@ const {
|
|||||||
const InboundCallSession = require('./lib/session/inbound-call-session');
|
const InboundCallSession = require('./lib/session/inbound-call-session');
|
||||||
const SipRecCallSession = require('./lib/session/siprec-call-session');
|
const SipRecCallSession = require('./lib/session/siprec-call-session');
|
||||||
|
|
||||||
|
if (DRACHTIO_HOST) {
|
||||||
|
srf.connect({host: DRACHTIO_HOST, port: DRACHTIO_PORT, secret: DRACHTIO_SECRET });
|
||||||
|
srf.on('connect', (err, hp) => {
|
||||||
|
const arr = /^(.*)\/(.*)$/.exec(hp.split(',').pop());
|
||||||
|
srf.locals.localSipAddress = `${arr[2]}`;
|
||||||
|
logger.info(`connected to drachtio listening on ${hp}, local sip address is ${srf.locals.localSipAddress}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.info(`listening for drachtio requests on port ${DRACHTIO_PORT}`);
|
||||||
|
srf.listen({port: DRACHTIO_PORT, secret: DRACHTIO_SECRET});
|
||||||
|
}
|
||||||
|
if (NODE_ENV === 'test') {
|
||||||
|
srf.on('error', (err) => {
|
||||||
|
logger.info(err, 'Error connecting to drachtio');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
srf.use('invite', [
|
srf.use('invite', [
|
||||||
initLocals,
|
initLocals,
|
||||||
createRootSpan,
|
createRootSpan,
|
||||||
@@ -125,9 +97,21 @@ sessionTracker.on('idle', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const getCount = () => sessionTracker.count;
|
const getCount = () => sessionTracker.count;
|
||||||
|
const healthCheck = require('@jambonz/http-health-check');
|
||||||
let httpServer;
|
let httpServer;
|
||||||
|
|
||||||
|
const createHttpListener = require('./lib/utils/http-listener');
|
||||||
|
createHttpListener(logger, srf)
|
||||||
|
.then(({server, app}) => {
|
||||||
|
httpServer = server;
|
||||||
|
healthCheck({app, logger, path: '/', fn: getCount});
|
||||||
|
return {server, app};
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(err, 'Error creating http listener');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const monInterval = setInterval(async() => {
|
const monInterval = setInterval(async() => {
|
||||||
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
|
srf.locals.stats.gauge('fs.sip.calls.count', sessionTracker.count);
|
||||||
try {
|
try {
|
||||||
@@ -149,7 +133,6 @@ const disconnect = () => {
|
|||||||
httpServer?.on('close', resolve);
|
httpServer?.on('close', resolve);
|
||||||
httpServer?.close();
|
httpServer?.close();
|
||||||
srf.disconnect();
|
srf.disconnect();
|
||||||
srf.removeAllListeners();
|
|
||||||
srf.locals.mediaservers?.forEach((ms) => ms.disconnect());
|
srf.locals.mediaservers?.forEach((ms) => ms.disconnect());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const AWS_REGION = process.env.AWS_REGION;
|
|||||||
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
|
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
|
||||||
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
|
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
|
||||||
const AWS_SNS_PORT = parseInt(process.env.AWS_SNS_PORT, 10) || 3001;
|
const AWS_SNS_PORT = parseInt(process.env.AWS_SNS_PORT, 10) || 3001;
|
||||||
const AWS_SNS_TOPIC_ARN = process.env.AWS_SNS_TOPIC_ARN;
|
const AWS_SNS_TOPIC_ARM = process.env.AWS_SNS_TOPIC_ARM;
|
||||||
const AWS_SNS_PORT_MAX = parseInt(process.env.AWS_SNS_PORT_MAX, 10) || 3005;
|
const AWS_SNS_PORT_MAX = parseInt(process.env.AWS_SNS_PORT_MAX, 10) || 3005;
|
||||||
|
|
||||||
const GCP_JSON_KEY = process.env.GCP_JSON_KEY;
|
const GCP_JSON_KEY = process.env.GCP_JSON_KEY;
|
||||||
@@ -139,9 +139,6 @@ const JAMBONES_USE_FREESWITCH_TIMER_FD = process.env.JAMBONES_USE_FREESWITCH_TIM
|
|||||||
const JAMBONES_DIAL_SBC_FOR_REGISTERED_USER = process.env.JAMBONES_DIAL_SBC_FOR_REGISTERED_USER || false;
|
const JAMBONES_DIAL_SBC_FOR_REGISTERED_USER = process.env.JAMBONES_DIAL_SBC_FOR_REGISTERED_USER || false;
|
||||||
const JAMBONES_MEDIA_TIMEOUT_MS = process.env.JAMBONES_MEDIA_TIMEOUT_MS || 0;
|
const JAMBONES_MEDIA_TIMEOUT_MS = process.env.JAMBONES_MEDIA_TIMEOUT_MS || 0;
|
||||||
const JAMBONES_MEDIA_HOLD_TIMEOUT_MS = process.env.JAMBONES_MEDIA_HOLD_TIMEOUT_MS || 0;
|
const JAMBONES_MEDIA_HOLD_TIMEOUT_MS = process.env.JAMBONES_MEDIA_HOLD_TIMEOUT_MS || 0;
|
||||||
// jambonz
|
|
||||||
const JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS =
|
|
||||||
process.env.JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
JAMBONES_MYSQL_HOST,
|
JAMBONES_MYSQL_HOST,
|
||||||
@@ -192,7 +189,7 @@ module.exports = {
|
|||||||
AWS_ACCESS_KEY_ID,
|
AWS_ACCESS_KEY_ID,
|
||||||
AWS_SECRET_ACCESS_KEY,
|
AWS_SECRET_ACCESS_KEY,
|
||||||
AWS_SNS_PORT,
|
AWS_SNS_PORT,
|
||||||
AWS_SNS_TOPIC_ARN,
|
AWS_SNS_TOPIC_ARM,
|
||||||
AWS_SNS_PORT_MAX,
|
AWS_SNS_PORT_MAX,
|
||||||
|
|
||||||
ANCHOR_MEDIA_ALWAYS,
|
ANCHOR_MEDIA_ALWAYS,
|
||||||
@@ -230,6 +227,5 @@ module.exports = {
|
|||||||
JAMBONES_USE_FREESWITCH_TIMER_FD,
|
JAMBONES_USE_FREESWITCH_TIMER_FD,
|
||||||
JAMBONES_DIAL_SBC_FOR_REGISTERED_USER,
|
JAMBONES_DIAL_SBC_FOR_REGISTERED_USER,
|
||||||
JAMBONES_MEDIA_TIMEOUT_MS,
|
JAMBONES_MEDIA_TIMEOUT_MS,
|
||||||
JAMBONES_MEDIA_HOLD_TIMEOUT_MS,
|
JAMBONES_MEDIA_HOLD_TIMEOUT_MS
|
||||||
JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ router.post('/',
|
|||||||
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
|
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
|
||||||
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
|
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
|
||||||
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat}),
|
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat}),
|
||||||
...(target.proxy && {'X-SIP-Proxy': target.proxy}),
|
|
||||||
...target.headers
|
...target.headers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ const {
|
|||||||
AWS_REGION,
|
AWS_REGION,
|
||||||
JAMBONES_USE_FREESWITCH_TIMER_FD,
|
JAMBONES_USE_FREESWITCH_TIMER_FD,
|
||||||
JAMBONES_MEDIA_TIMEOUT_MS,
|
JAMBONES_MEDIA_TIMEOUT_MS,
|
||||||
JAMBONES_MEDIA_HOLD_TIMEOUT_MS,
|
JAMBONES_MEDIA_HOLD_TIMEOUT_MS
|
||||||
JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS
|
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
const BackgroundTaskManager = require('../utils/background-task-manager');
|
const BackgroundTaskManager = require('../utils/background-task-manager');
|
||||||
@@ -39,7 +38,6 @@ const dbUtils = require('../utils/db-utils');
|
|||||||
const BADPRECONDITIONS = 'preconditions not met';
|
const BADPRECONDITIONS = 'preconditions not met';
|
||||||
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
|
const CALLER_CANCELLED_ERR_MSG = 'Response not sent due to unknown transaction';
|
||||||
const { NonFatalTaskError} = require('../utils/error');
|
const { NonFatalTaskError} = require('../utils/error');
|
||||||
const { sleepFor } = require('../utils/helpers');
|
|
||||||
const sqlRetrieveQueueEventHook = `SELECT * FROM webhooks
|
const sqlRetrieveQueueEventHook = `SELECT * FROM webhooks
|
||||||
WHERE webhook_sid =
|
WHERE webhook_sid =
|
||||||
(
|
(
|
||||||
@@ -135,15 +133,6 @@ class CallSession extends Emitter {
|
|||||||
this.requestor.on('handover', handover.bind(this));
|
this.requestor.on('handover', handover.bind(this));
|
||||||
this.requestor.on('reconnect-error', this._onSessionReconnectError.bind(this));
|
this.requestor.on('reconnect-error', this._onSessionReconnectError.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Currently this is used for openai STT, which has a prompt paramater and
|
|
||||||
* we have an experimental feature where you can send the conversation
|
|
||||||
* history in the prompt
|
|
||||||
*/
|
|
||||||
this.conversationTurns = [];
|
|
||||||
this.on('userSaid', this._onUserSaid.bind(this));
|
|
||||||
this.on('botSaid', this._onBotSaid.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -962,7 +951,7 @@ 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(
|
this.logger.info(
|
||||||
`${type}: ${credential.vendor} ${credential.label ? `, label: ${credential.label}` : ''} `);
|
`${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 ||
|
||||||
@@ -1095,12 +1084,6 @@ class CallSession extends Emitter {
|
|||||||
api_key: credential.api_key
|
api_key: credential.api_key
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if ('voxist' === vendor) {
|
|
||||||
return {
|
|
||||||
speech_credential_sid: credential.speech_credential_sid,
|
|
||||||
api_key: credential.api_key
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if ('whisper' === vendor) {
|
else if ('whisper' === vendor) {
|
||||||
return {
|
return {
|
||||||
api_key: credential.api_key,
|
api_key: credential.api_key,
|
||||||
@@ -1115,17 +1098,12 @@ class CallSession extends Emitter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if ('speechmatics' === vendor) {
|
else if ('speechmatics' === vendor) {
|
||||||
|
this.logger.info({credential}, 'CallSession:getSpeechCredentials - speechmatics credential');
|
||||||
return {
|
return {
|
||||||
api_key: credential.api_key,
|
api_key: credential.api_key,
|
||||||
speechmatics_stt_uri: credential.speechmatics_stt_uri,
|
speechmatics_stt_uri: credential.speechmatics_stt_uri,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if ('openai' === vendor) {
|
|
||||||
return {
|
|
||||||
api_key: credential.api_key,
|
|
||||||
model_id: credential.model_id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (vendor.startsWith('custom:')) {
|
else if (vendor.startsWith('custom:')) {
|
||||||
return {
|
return {
|
||||||
speech_credential_sid: credential.speech_credential_sid,
|
speech_credential_sid: credential.speech_credential_sid,
|
||||||
@@ -1254,7 +1232,7 @@ class CallSession extends Emitter {
|
|||||||
this.logger.info('CallSession:exec all tasks complete');
|
this.logger.info('CallSession:exec all tasks complete');
|
||||||
this._stopping = true;
|
this._stopping = true;
|
||||||
this._onTasksDone();
|
this._onTasksDone();
|
||||||
await this._clearResources();
|
this._clearResources();
|
||||||
|
|
||||||
|
|
||||||
if (!this.isConfirmCallSession && !this.isSmsCallSession) sessionTracker.remove(this.callSid);
|
if (!this.isConfirmCallSession && !this.isSmsCallSession) sessionTracker.remove(this.callSid);
|
||||||
@@ -1325,7 +1303,7 @@ class CallSession extends Emitter {
|
|||||||
_lccCallStatus(opts) {
|
_lccCallStatus(opts) {
|
||||||
if (opts.call_status === CallStatus.Completed && this.dlg) {
|
if (opts.call_status === CallStatus.Completed && this.dlg) {
|
||||||
this.logger.info('CallSession:_lccCallStatus hanging up call due to request from api');
|
this.logger.info('CallSession:_lccCallStatus hanging up call due to request from api');
|
||||||
this._jambonzHangup();
|
this._callerHungup();
|
||||||
}
|
}
|
||||||
else if (opts.call_status === CallStatus.NoAnswer) {
|
else if (opts.call_status === CallStatus.NoAnswer) {
|
||||||
if (this.direction === CallDirection.Inbound) {
|
if (this.direction === CallDirection.Inbound) {
|
||||||
@@ -1459,7 +1437,7 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
if (tasks) {
|
if (tasks) {
|
||||||
const t = normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata));
|
const t = normalizeJambones(this.logger, tasks).map((tdata) => makeTask(this.logger, tdata));
|
||||||
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list');
|
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_lccCallHook new task list');
|
||||||
this.replaceApplication(t);
|
this.replaceApplication(t);
|
||||||
if (this.wakeupResolver) {
|
if (this.wakeupResolver) {
|
||||||
//this.logger.debug({resolution}, 'CallSession:_onCommand - got commands, waking up..');
|
//this.logger.debug({resolution}, 'CallSession:_onCommand - got commands, waking up..');
|
||||||
@@ -1503,7 +1481,7 @@ class CallSession extends Emitter {
|
|||||||
this.backgroundTaskManager.getTask('transcribe').updateTranscribe(opts.transcribe_status);
|
this.backgroundTaskManager.getTask('transcribe').updateTranscribe(opts.transcribe_status);
|
||||||
}
|
}
|
||||||
const task = this.currentTask;
|
const task = this.currentTask;
|
||||||
if (!task || ![TaskName.Dial, TaskName.Transcribe, TaskName.Listen].includes(task.name)) {
|
if (!task || ![TaskName.Dial, TaskName.Transcribe].includes(task.name)) {
|
||||||
return this.logger.info(`CallSession:_lccTranscribeStatus - invalid transcribe_status in task ${task.name}`);
|
return this.logger.info(`CallSession:_lccTranscribeStatus - invalid transcribe_status in task ${task.name}`);
|
||||||
}
|
}
|
||||||
const transcribeTask = task.name === TaskName.Transcribe ? task : task.transcribeTask;
|
const transcribeTask = task.name === TaskName.Transcribe ? task : task.transcribeTask;
|
||||||
@@ -1726,10 +1704,10 @@ Duration=${duration} `
|
|||||||
this.currentTask.ep :
|
this.currentTask.ep :
|
||||||
this.ep;
|
this.ep;
|
||||||
const db = parseDecibels(opts);
|
const db = parseDecibels(opts);
|
||||||
this.logger.debug(`_lccBoostAudioSignal: boosting audio signal by ${db} dB`);
|
this.logger.info(`_lccBoostAudioSignal: boosting audio signal by ${db} dB`);
|
||||||
const args = [ep.uuid, 'setGain', db];
|
const args = [ep.uuid, 'setGain', db];
|
||||||
const response = await ep.api('uuid_dub', args);
|
const response = await ep.api('uuid_dub', args);
|
||||||
this.logger.debug({response}, '_lccBoostAudioSignal: response from freeswitch');
|
this.logger.info({response}, '_lccBoostAudioSignal: response from freeswitch');
|
||||||
}
|
}
|
||||||
|
|
||||||
async _lccMediaPath(desiredPath) {
|
async _lccMediaPath(desiredPath) {
|
||||||
@@ -1782,6 +1760,7 @@ Duration=${duration} `
|
|||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
res = await this.ttsStreamingBuffer?.bufferTokens(tokens);
|
res = await this.ttsStreamingBuffer?.bufferTokens(tokens);
|
||||||
|
this.logger.info({id, res}, 'CallSession:_lccTtsTokens - tts:tokens-result');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info(err, 'CallSession:_lccTtsTokens');
|
this.logger.info(err, 'CallSession:_lccTtsTokens');
|
||||||
}
|
}
|
||||||
@@ -1814,66 +1793,62 @@ Duration=${duration} `
|
|||||||
async updateCall(opts, callSid) {
|
async updateCall(opts, callSid) {
|
||||||
this.logger.debug(opts, 'CallSession:updateCall');
|
this.logger.debug(opts, 'CallSession:updateCall');
|
||||||
|
|
||||||
try {
|
if (opts.call_status) {
|
||||||
if (opts.call_status) {
|
return this._lccCallStatus(opts);
|
||||||
return this._lccCallStatus(opts);
|
}
|
||||||
}
|
if (opts.call_hook || opts.child_call_hook) {
|
||||||
if (opts.call_hook || opts.child_call_hook) {
|
return await this._lccCallHook(opts);
|
||||||
return await this._lccCallHook(opts);
|
}
|
||||||
}
|
if (opts.listen_status || opts.stream_status) {
|
||||||
if (opts.listen_status || opts.stream_status) {
|
await this._lccListenStatus(opts);
|
||||||
await this._lccListenStatus(opts);
|
}
|
||||||
}
|
if (opts.transcribe_status) {
|
||||||
if (opts.transcribe_status) {
|
await this._lccTranscribeStatus(opts);
|
||||||
await this._lccTranscribeStatus(opts);
|
}
|
||||||
}
|
else if (opts.mute_status) {
|
||||||
else if (opts.mute_status) {
|
await this._lccMuteStatus(opts.mute_status === 'mute', callSid);
|
||||||
await this._lccMuteStatus(opts.mute_status === 'mute', callSid);
|
}
|
||||||
}
|
else if (opts.conf_hold_status) {
|
||||||
else if (opts.conf_hold_status) {
|
await this._lccConfHoldStatus(opts);
|
||||||
await this._lccConfHoldStatus(opts);
|
}
|
||||||
}
|
else if (opts.conf_mute_status) {
|
||||||
else if (opts.conf_mute_status) {
|
await this._lccConfMuteStatus(opts);
|
||||||
await this._lccConfMuteStatus(opts);
|
}
|
||||||
}
|
else if (opts.sip_request) {
|
||||||
else if (opts.sip_request) {
|
const res = await this._lccSipRequest(opts, callSid);
|
||||||
const res = await this._lccSipRequest(opts, callSid);
|
return {status: res.status, reason: res.reason};
|
||||||
return {status: res.status, reason: res.reason};
|
} else if (opts.dtmf) {
|
||||||
} else if (opts.dtmf) {
|
await this._lccDtmf(opts, callSid);
|
||||||
await this._lccDtmf(opts, callSid);
|
}
|
||||||
}
|
else if (opts.record) {
|
||||||
else if (opts.record) {
|
await this.notifyRecordOptions(opts.record);
|
||||||
await this.notifyRecordOptions(opts.record);
|
}
|
||||||
}
|
else if (opts.tag) {
|
||||||
else if (opts.tag) {
|
return this._lccTag(opts);
|
||||||
return this._lccTag(opts);
|
}
|
||||||
}
|
else if (opts.conferenceParticipantAction) {
|
||||||
else if (opts.conferenceParticipantAction) {
|
return this._lccConferenceParticipantAction(opts.conferenceParticipantAction);
|
||||||
return this._lccConferenceParticipantAction(opts.conferenceParticipantAction);
|
}
|
||||||
}
|
else if (opts.dub) {
|
||||||
else if (opts.dub) {
|
return this._lccDub(opts.dub, callSid);
|
||||||
return this._lccDub(opts.dub, callSid);
|
}
|
||||||
}
|
else if (opts.boostAudioSignal) {
|
||||||
else if (opts.boostAudioSignal) {
|
return this._lccBoostAudioSignal(opts, callSid);
|
||||||
return this._lccBoostAudioSignal(opts, callSid);
|
}
|
||||||
}
|
else if (opts.media_path) {
|
||||||
else if (opts.media_path) {
|
return this._lccMediaPath(opts.media_path, callSid);
|
||||||
return this._lccMediaPath(opts.media_path, callSid);
|
}
|
||||||
}
|
else if (opts.llm_tool_output) {
|
||||||
else if (opts.llm_tool_output) {
|
return this._lccToolOutput(opts.tool_call_id, opts.llm_tool_output, callSid);
|
||||||
return this._lccToolOutput(opts.tool_call_id, opts.llm_tool_output, callSid);
|
}
|
||||||
}
|
else if (opts.llm_update) {
|
||||||
else if (opts.llm_update) {
|
return this._lccLlmUpdate(opts.llm_update, callSid);
|
||||||
return this._lccLlmUpdate(opts.llm_update, callSid);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// whisper may be the only thing we are asked to do, or it may that
|
// whisper may be the only thing we are asked to do, or it may that
|
||||||
// we are doing a whisper after having muted, paused recording etc..
|
// we are doing a whisper after having muted, paused recording etc..
|
||||||
if (opts.whisper) {
|
if (opts.whisper) {
|
||||||
return this._lccWhisper(opts, callSid);
|
return this._lccWhisper(opts, callSid);
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.info({err, opts, callSid}, 'CallSession:updateCall - error updating call');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1916,7 +1891,7 @@ Duration=${duration} `
|
|||||||
this.tasks = tasks;
|
this.tasks = tasks;
|
||||||
this.taskIdx = 0;
|
this.taskIdx = 0;
|
||||||
this.stackIdx++;
|
this.stackIdx++;
|
||||||
this.logger.debug({tasks: listTaskNames(tasks)},
|
this.logger.info({tasks: listTaskNames(tasks)},
|
||||||
`CallSession:replaceApplication reset with ${tasks.length} new tasks, stack depth is ${this.stackIdx}`);
|
`CallSession:replaceApplication reset with ${tasks.length} new tasks, stack depth is ${this.stackIdx}`);
|
||||||
if (this.currentTask) {
|
if (this.currentTask) {
|
||||||
this.currentTask.kill(this, KillReason.Replaced);
|
this.currentTask.kill(this, KillReason.Replaced);
|
||||||
@@ -1931,7 +1906,7 @@ Duration=${duration} `
|
|||||||
|
|
||||||
kill(onBackgroundGatherBargein = false) {
|
kill(onBackgroundGatherBargein = false) {
|
||||||
if (this.isConfirmCallSession) this.logger.debug('CallSession:kill (ConfirmSession)');
|
if (this.isConfirmCallSession) this.logger.debug('CallSession:kill (ConfirmSession)');
|
||||||
else this.logger.debug('CallSession:kill');
|
else this.logger.info('CallSession:kill');
|
||||||
this._endVerbHookSpan();
|
this._endVerbHookSpan();
|
||||||
if (this.currentTask) {
|
if (this.currentTask) {
|
||||||
this.currentTask.kill(this);
|
this.currentTask.kill(this);
|
||||||
@@ -1996,7 +1971,7 @@ Duration=${duration} `
|
|||||||
task.synthesizer.label :
|
task.synthesizer.label :
|
||||||
this.speechSynthesisLabel;
|
this.speechSynthesisLabel;
|
||||||
|
|
||||||
this.logger.debug({vendor, language, voice, label},
|
this.logger.info({vendor, language, voice, label},
|
||||||
'CallSession:_preCacheAudio - precaching audio for future prompt');
|
'CallSession:_preCacheAudio - precaching audio for future prompt');
|
||||||
task._synthesizeWithSpecificVendor(this, this.ep, {vendor, language, voice, label, preCache: true})
|
task._synthesizeWithSpecificVendor(this, this.ep, {vendor, language, voice, label, preCache: true})
|
||||||
.catch((err) => this.logger.error(err, 'CallSession:_preCacheAudio - error precaching audio'));
|
.catch((err) => this.logger.error(err, 'CallSession:_preCacheAudio - error precaching audio'));
|
||||||
@@ -2066,7 +2041,7 @@ Duration=${duration} `
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _onCommand({msgid, command, call_sid, queueCommand, tool_call_id, data}) {
|
async _onCommand({msgid, command, call_sid, queueCommand, tool_call_id, data}) {
|
||||||
this.logger.debug({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command');
|
this.logger.info({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command');
|
||||||
let resolution;
|
let resolution;
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 'redirect':
|
case 'redirect':
|
||||||
@@ -2075,18 +2050,18 @@ Duration=${duration} `
|
|||||||
const t = normalizeJambones(this.logger, data)
|
const t = normalizeJambones(this.logger, data)
|
||||||
.map((tdata) => makeTask(this.logger, tdata));
|
.map((tdata) => makeTask(this.logger, tdata));
|
||||||
if (!queueCommand) {
|
if (!queueCommand) {
|
||||||
this.logger.debug({tasks: listTaskNames(t)}, 'CallSession:_onCommand new task list');
|
this.logger.info({tasks: listTaskNames(t)}, 'CallSession:_onCommand new task list');
|
||||||
this.replaceApplication(t);
|
this.replaceApplication(t);
|
||||||
}
|
}
|
||||||
else if (JAMBONES_INJECT_CONTENT) {
|
else if (JAMBONES_INJECT_CONTENT) {
|
||||||
if (JAMBONES_EAGERLY_PRE_CACHE_AUDIO) this._preCacheAudio(t);
|
if (JAMBONES_EAGERLY_PRE_CACHE_AUDIO) this._preCacheAudio(t);
|
||||||
this._injectTasks(t);
|
this._injectTasks(t);
|
||||||
this.logger.debug({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (JAMBONES_EAGERLY_PRE_CACHE_AUDIO) this._preCacheAudio(t);
|
if (JAMBONES_EAGERLY_PRE_CACHE_AUDIO) this._preCacheAudio(t);
|
||||||
this.tasks.push(...t);
|
this.tasks.push(...t);
|
||||||
this.logger.debug({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
this.logger.info({tasks: listTaskNames(this.tasks)}, 'CallSession:_onCommand - updated task list');
|
||||||
}
|
}
|
||||||
resolution = {reason: 'received command, new tasks', queue: queueCommand, command};
|
resolution = {reason: 'received command, new tasks', queue: queueCommand, command};
|
||||||
resolution.command = listTaskNames(t);
|
resolution.command = listTaskNames(t);
|
||||||
@@ -2353,32 +2328,10 @@ Duration=${duration} `
|
|||||||
// Destroy previous ep if it's still running.
|
// Destroy previous ep if it's still running.
|
||||||
if (this.ep?.connected) this.ep.destroy();
|
if (this.ep?.connected) this.ep.destroy();
|
||||||
|
|
||||||
/* Codec negotiation issue explanation:
|
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||||
*
|
|
||||||
* Problem scenario:
|
|
||||||
* 1. Initial negotiation:
|
|
||||||
* - FreeSWITCH → SBC: offers multiple codecs (PCMU, PCMA, G722)
|
|
||||||
* - SBC → Callee: passes all codecs (PCMU, PCMA, G722)
|
|
||||||
* - Callee → SBC: responds with PCMA (its supported codec)
|
|
||||||
* - SBC → FreeSWITCH: responds with PCMU (after transcoding)
|
|
||||||
*
|
|
||||||
* 2. After endpoint replacement:
|
|
||||||
* - If we only offer PCMU in the new endpoint
|
|
||||||
* - FreeSWITCH → SBC: offers only PCMU
|
|
||||||
* - SBC → Callee: offers only PCMU
|
|
||||||
* - Call fails: Callee rejects since it only supports PCMA
|
|
||||||
*
|
|
||||||
* Solution:
|
|
||||||
* Always have FreeSWITCH offer multiple codecs to the SBC, don't pass remote sdp here to ensure
|
|
||||||
* the SBC can reoffer the same codecs that the callee originally accepted.
|
|
||||||
* This prevents call failures during media renegotiation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.ep = await this.ms.createEndpoint();
|
|
||||||
this._configMsEndpoint();
|
this._configMsEndpoint();
|
||||||
|
|
||||||
const sdp = await this.dlg.modify(this.ep.local.sdp);
|
await this.dlg.modify(this.ep.local.sdp);
|
||||||
await this.ep.modify(sdp);
|
|
||||||
this.logger.debug('CallSession:replaceEndpoint completed');
|
this.logger.debug('CallSession:replaceEndpoint completed');
|
||||||
return this.ep;
|
return this.ep;
|
||||||
}
|
}
|
||||||
@@ -2386,13 +2339,9 @@ Duration=${duration} `
|
|||||||
/**
|
/**
|
||||||
* Hang up the call and free the media endpoint
|
* Hang up the call and free the media endpoint
|
||||||
*/
|
*/
|
||||||
async _clearResources() {
|
_clearResources() {
|
||||||
for (const resource of [this.dlg, this.ep, this.ep2]) {
|
for (const resource of [this.dlg, this.ep, this.ep2]) {
|
||||||
try {
|
if (resource && resource.connected) resource.destroy();
|
||||||
if (resource && resource.connected) await resource.destroy();
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.info({err}, 'CallSession:_clearResources - error clearing resources');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.dlg = null;
|
this.dlg = null;
|
||||||
this.ep = null;
|
this.ep = null;
|
||||||
@@ -2519,14 +2468,12 @@ Duration=${duration} `
|
|||||||
} else if (sip_method === 'MESSAGE') {
|
} else if (sip_method === 'MESSAGE') {
|
||||||
res.send(202);
|
res.send(202);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`CallSession:_onRequestWithinDialog unsupported method: ${req.method}`);
|
this.logger.info(`CallSession:_onRequestWithinDialog unsported method: ${req.method}`);
|
||||||
res.send(501);
|
res.send(501);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const params = {sip_method, sip_body: req.body, sip_headers: req.headers};
|
const params = {sip_method, sip_body: req.body, sip_headers: req.headers};
|
||||||
this.currentTask.performHook(this, this.sipRequestWithinDialogHook, params).catch((err) => {
|
this.currentTask.performHook(this, this.sipRequestWithinDialogHook, params);
|
||||||
this.logger.error({err}, 'CallSession:_onRequestWithinDialog - error calling sipRequestWithinDialogHook');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onReinvite(req, res) {
|
async _onReinvite(req, res) {
|
||||||
@@ -2537,7 +2484,7 @@ Duration=${duration} `
|
|||||||
res.send(200, {body: this.ep.local.sdp});
|
res.send(200, {body: this.ep.local.sdp});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (this.currentTask && this.currentTask.name === TaskName.Dial && this.currentTask.isOnHoldEnabled) {
|
if (this.currentTask.name === TaskName.Dial && this.currentTask.isOnHoldEnabled) {
|
||||||
this.logger.info('onholdMusic reINVITE after media has been released');
|
this.logger.info('onholdMusic reINVITE after media has been released');
|
||||||
await this.currentTask.handleReinviteAfterMediaReleased(req, res);
|
await this.currentTask.handleReinviteAfterMediaReleased(req, res);
|
||||||
} else {
|
} else {
|
||||||
@@ -2606,7 +2553,7 @@ Duration=${duration} `
|
|||||||
if (json && Array.isArray(json)) {
|
if (json && Array.isArray(json)) {
|
||||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||||
if (tasks && tasks.length > 0) {
|
if (tasks && tasks.length > 0) {
|
||||||
this.logger.debug('CallSession:handleRefer received REFER, get new tasks');
|
this.logger.info('CallSession:handleRefer received REFER, get new tasks');
|
||||||
this.replaceApplication(tasks);
|
this.replaceApplication(tasks);
|
||||||
if (this.wakeupResolver) {
|
if (this.wakeupResolver) {
|
||||||
this.wakeupResolver({reason: 'CallSession: referHook new taks'});
|
this.wakeupResolver({reason: 'CallSession: referHook new taks'});
|
||||||
@@ -2653,14 +2600,14 @@ Duration=${duration} `
|
|||||||
if (typeof this.queueEventHookRequestor === 'undefined') {
|
if (typeof this.queueEventHookRequestor === 'undefined') {
|
||||||
const pp = this._pool.promise();
|
const pp = this._pool.promise();
|
||||||
try {
|
try {
|
||||||
this.logger.debug({accountSid: this.accountSid}, 'performQueueWebhook: looking up account');
|
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: looking up account');
|
||||||
const [r] = await pp.query(sqlRetrieveQueueEventHook, [this.accountSid]);
|
const [r] = await pp.query(sqlRetrieveQueueEventHook, [this.accountSid]);
|
||||||
if (0 === r.length) {
|
if (0 === r.length) {
|
||||||
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: no webhook provisioned');
|
this.logger.info({accountSid: this.accountSid}, 'performQueueWebhook: no webhook provisioned');
|
||||||
this.queueEventHookRequestor = null;
|
this.queueEventHookRequestor = null;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.logger.debug({accountSid: this.accountSid, webhook: r[0]}, 'performQueueWebhook: webhook found');
|
this.logger.info({accountSid: this.accountSid, webhook: r[0]}, 'performQueueWebhook: webhook found');
|
||||||
this.queueEventHookRequestor = new HttpRequestor(this.logger, this.accountSid,
|
this.queueEventHookRequestor = new HttpRequestor(this.logger, this.accountSid,
|
||||||
r[0], this.webhook_secret);
|
r[0], this.webhook_secret);
|
||||||
this.queueEventHook = r[0];
|
this.queueEventHook = r[0];
|
||||||
@@ -2674,7 +2621,7 @@ Duration=${duration} `
|
|||||||
|
|
||||||
/* send webhook */
|
/* send webhook */
|
||||||
const params = {...obj, ...this.callInfo.toJSON()};
|
const params = {...obj, ...this.callInfo.toJSON()};
|
||||||
this.logger.debug({accountSid: this.accountSid, params}, 'performQueueWebhook: sending webhook');
|
this.logger.info({accountSid: this.accountSid, params}, 'performQueueWebhook: sending webhook');
|
||||||
this.queueEventHookRequestor.request('queue:status', this.queueEventHook, params)
|
this.queueEventHookRequestor.request('queue:status', this.queueEventHook, params)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.logger.info({err, accountSid: this.accountSid, obj}, 'Error sending queue notification event');
|
this.logger.info({err, accountSid: this.accountSid, obj}, 'Error sending queue notification event');
|
||||||
@@ -2809,7 +2756,7 @@ Duration=${duration} `
|
|||||||
async handleReinviteAfterMediaReleased(req, res) {
|
async handleReinviteAfterMediaReleased(req, res) {
|
||||||
assert(this.dlg && this.dlg.connected && !this.ep);
|
assert(this.dlg && this.dlg.connected && !this.ep);
|
||||||
const sdp = await this.dlg.modify(req.body);
|
const sdp = await this.dlg.modify(req.body);
|
||||||
this.logger.debug({sdp}, 'CallSession:handleReinviteAfterMediaReleased - reinvite to A leg returned sdp');
|
this.logger.info({sdp}, 'CallSession:handleReinviteAfterMediaReleased - reinvite to A leg returned sdp');
|
||||||
res.send(200, {body: sdp});
|
res.send(200, {body: sdp});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2881,24 +2828,6 @@ Duration=${duration} `
|
|||||||
if (Object.keys(opts).length > 0) {
|
if (Object.keys(opts).length > 0) {
|
||||||
this.ep.set(opts);
|
this.ep.set(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
const origDestroy = this.ep.destroy.bind(this.ep);
|
|
||||||
this.ep.destroy = async() => {
|
|
||||||
try {
|
|
||||||
if (this.currentTask?.name === TaskName.Transcribe && JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS) {
|
|
||||||
// transcribe task is being used, wait for some time before destroy
|
|
||||||
// if final transcription is received but endpoint is already closed,
|
|
||||||
// freeswitch module will not be able to send the transcription
|
|
||||||
|
|
||||||
this.logger.debug('callSession:_configMsEndpoint -' +
|
|
||||||
' transcribe task, wait for some time before destroy');
|
|
||||||
await sleepFor(JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS);
|
|
||||||
}
|
|
||||||
await origDestroy();
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(err, 'callSession:_configMsEndpoint - error destroying endpoint');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _handleMediaTimeout(evt) {
|
async _handleMediaTimeout(evt) {
|
||||||
@@ -2944,7 +2873,7 @@ Duration=${duration} `
|
|||||||
_awaitCommandsOrHangup() {
|
_awaitCommandsOrHangup() {
|
||||||
assert(!this.wakeupResolver);
|
assert(!this.wakeupResolver);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.logger.debug('_awaitCommandsOrHangup - waiting...');
|
this.logger.info('_awaitCommandsOrHangup - waiting...');
|
||||||
this.wakeupResolver = resolve;
|
this.wakeupResolver = resolve;
|
||||||
|
|
||||||
if (this._actionHookDelayProcessor) {
|
if (this._actionHookDelayProcessor) {
|
||||||
@@ -2964,7 +2893,7 @@ Duration=${duration} `
|
|||||||
this.ep.play(this.fillerNoise.url);
|
this.ep.play(this.fillerNoise.url);
|
||||||
this.ep.once('playback-start', (evt) => {
|
this.ep.once('playback-start', (evt) => {
|
||||||
if (evt.file === this.fillerNoise.url && !this._isPlayingFillerNoise) {
|
if (evt.file === this.fillerNoise.url && !this._isPlayingFillerNoise) {
|
||||||
this.logger.debug('CallSession:_awaitCommandsOrHangup - filler noise started');
|
this.logger.info('CallSession:_awaitCommandsOrHangup - filler noise started');
|
||||||
this.ep.api('uuid_break', this.ep.uuid)
|
this.ep.api('uuid_break', this.ep.uuid)
|
||||||
.catch((err) => this.logger.info(err, 'Error killing filler noise'));
|
.catch((err) => this.logger.info(err, 'Error killing filler noise'));
|
||||||
}
|
}
|
||||||
@@ -2975,7 +2904,7 @@ Duration=${duration} `
|
|||||||
|
|
||||||
_clearTasks(backgroundGather, evt) {
|
_clearTasks(backgroundGather, evt) {
|
||||||
if (this.requestor instanceof WsRequestor && !backgroundGather.cleared) {
|
if (this.requestor instanceof WsRequestor && !backgroundGather.cleared) {
|
||||||
this.logger.debug({evt}, 'CallSession:_clearTasks on event from background gather');
|
this.logger.info({evt}, 'CallSession:_clearTasks on event from background gather');
|
||||||
try {
|
try {
|
||||||
backgroundGather.cleared = true;
|
backgroundGather.cleared = true;
|
||||||
this.kill(true);
|
this.kill(true);
|
||||||
@@ -3053,43 +2982,6 @@ Duration=${duration} `
|
|||||||
this._jambonzHangup('Max Call Duration');
|
this._jambonzHangup('Max Call Duration');
|
||||||
this._maxCallDurationTimer = null;
|
this._maxCallDurationTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUserSaid(transcript) {
|
|
||||||
const count = this.conversationTurns.length;
|
|
||||||
if (count === 0 || this.conversationTurns[count - 1].type === 'assistant') {
|
|
||||||
this.conversationTurns.push({
|
|
||||||
type: 'user',
|
|
||||||
text: transcript
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.conversationTurns[count - 1].text += ` ${transcript}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onBotSaid(transcript) {
|
|
||||||
const count = this.conversationTurns.length;
|
|
||||||
if (count === 0 || this.conversationTurns[count - 1].type === 'user') {
|
|
||||||
this.conversationTurns.push({
|
|
||||||
type: 'assistant',
|
|
||||||
text: transcript
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.conversationTurns[count - 1].text += ` ${transcript}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedConversation(numTurns) {
|
|
||||||
const turns = this.conversationTurns.slice(-numTurns);
|
|
||||||
if (turns.length === 0) return null;
|
|
||||||
return turns.map((t) => {
|
|
||||||
if (t.type === 'user') {
|
|
||||||
return `user: ${t.text}`;
|
|
||||||
}
|
|
||||||
return `assistant: ${t.text}`;
|
|
||||||
}).join('\n');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = CallSession;
|
module.exports = CallSession;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class RestCallSession extends CallSession {
|
|||||||
this.callInfo.callTerminationBy = terminatedBy;
|
this.callInfo.callTerminationBy = terminatedBy;
|
||||||
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
const duration = moment().diff(this.dlg.connectTime, 'seconds');
|
||||||
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
this.emit('callStatusChange', {callStatus: CallStatus.Completed, duration});
|
||||||
this.logger.info(`RestCallSession: called party hung up by ${terminatedBy}`);
|
this.logger.debug(`RestCallSession: called party hung up by ${terminatedBy}`);
|
||||||
this._callReleased();
|
this._callReleased();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,11 +83,7 @@ class Conference extends Task {
|
|||||||
// reset answer time if we were transferred from another feature server
|
// reset answer time if we were transferred from another feature server
|
||||||
if (this.connectTime) dlg.connectTime = this.connectTime;
|
if (this.connectTime) dlg.connectTime = this.connectTime;
|
||||||
|
|
||||||
if (cs.sipRequestWithinDialogHook) {
|
|
||||||
/* remove any existing listener to escape from duplicating events */
|
|
||||||
this._removeSipIndialogRequestListener(this.dlg);
|
|
||||||
this._initSipIndialogRequestListener(cs, dlg);
|
|
||||||
}
|
|
||||||
this.ep.on('destroy', this._kicked.bind(this, cs, dlg));
|
this.ep.on('destroy', this._kicked.bind(this, cs, dlg));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -107,7 +103,6 @@ class Conference extends Task {
|
|||||||
|
|
||||||
this.logger.debug(`Conference:exec - conference ${this.confName} is over`);
|
this.logger.debug(`Conference:exec - conference ${this.confName} is over`);
|
||||||
if (this.callMoved !== false) await this.performAction(this.results);
|
if (this.callMoved !== false) await this.performAction(this.results);
|
||||||
this._removeSipIndialogRequestListener(dlg);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info(err, `TaskConference:exec - error in conference ${this.confName}`);
|
this.logger.info(err, `TaskConference:exec - error in conference ${this.confName}`);
|
||||||
}
|
}
|
||||||
@@ -421,20 +416,6 @@ class Conference extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_initSipIndialogRequestListener(cs, dlg) {
|
|
||||||
dlg.on('info', this._onRequestWithinDialog.bind(this, cs));
|
|
||||||
dlg.on('message', this._onRequestWithinDialog.bind(this, cs));
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeSipIndialogRequestListener(dlg) {
|
|
||||||
dlg && dlg.removeAllListeners('message');
|
|
||||||
dlg && dlg.removeAllListeners('info');
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRequestWithinDialog(cs, req, res) {
|
|
||||||
cs._onRequestWithinDialog(req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The conference we have been waiting for has started.
|
* The conference we have been waiting for has started.
|
||||||
* It may be on this server or a different one, and we are
|
* It may be on this server or a different one, and we are
|
||||||
|
|||||||
@@ -187,20 +187,18 @@ class TaskConfig extends Task {
|
|||||||
: cs.speechRecognizerVendor;
|
: cs.speechRecognizerVendor;
|
||||||
cs.speechRecognizerLabel = this.recognizer.label === 'default'
|
cs.speechRecognizerLabel = this.recognizer.label === 'default'
|
||||||
? cs.speechRecognizerLabel : this.recognizer.label;
|
? cs.speechRecognizerLabel : this.recognizer.label;
|
||||||
cs.speechRecognizerLanguage = this.recognizer.language !== undefined && this.recognizer.language !== 'default'
|
cs.speechRecognizerLanguage = this.recognizer.language !== 'default'
|
||||||
? this.recognizer.language
|
? this.recognizer.language
|
||||||
: cs.speechRecognizerLanguage;
|
: cs.speechRecognizerLanguage;
|
||||||
|
|
||||||
//fallback
|
//fallback
|
||||||
cs.fallbackSpeechRecognizerVendor = this.recognizer.fallbackVendor !== undefined &&
|
cs.fallbackSpeechRecognizerVendor = this.recognizer.fallbackVendor !== 'default'
|
||||||
this.recognizer.fallbackVendor !== 'default'
|
|
||||||
? this.recognizer.fallbackVendor
|
? this.recognizer.fallbackVendor
|
||||||
: cs.fallbackSpeechRecognizerVendor;
|
: cs.fallbackSpeechRecognizerVendor;
|
||||||
cs.fallbackSpeechRecognizerLabel = this.recognizer.fallbackLabel === 'default' ?
|
cs.fallbackSpeechRecognizerLabel = this.recognizer.fallbackLabel === 'default' ?
|
||||||
cs.fallbackSpeechRecognizerLabel :
|
cs.fallbackSpeechRecognizerLabel :
|
||||||
this.recognizer.fallbackLabel;
|
this.recognizer.fallbackLabel;
|
||||||
cs.fallbackSpeechRecognizerLanguage = this.recognizer.fallbackLanguage !== undefined &&
|
cs.fallbackSpeechRecognizerLanguage = this.recognizer.fallbackLanguage !== 'default'
|
||||||
this.recognizer.fallbackLanguage !== 'default'
|
|
||||||
? this.recognizer.fallbackLanguage
|
? this.recognizer.fallbackLanguage
|
||||||
: cs.fallbackSpeechRecognizerLanguage;
|
: cs.fallbackSpeechRecognizerLanguage;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ const {TaskName, TaskPreconditions, DequeueResults, BONG_TONE} = require('../uti
|
|||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const { sleepFor } = require('../utils/helpers');
|
|
||||||
|
const sleepFor = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
||||||
|
|
||||||
const getUrl = (cs) => `${cs.srf.locals.serviceUrl}/v1/dequeue/${cs.callSid}`;
|
const getUrl = (cs) => `${cs.srf.locals.serviceUrl}/v1/dequeue/${cs.callSid}`;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ const {ANCHOR_MEDIA_ALWAYS,
|
|||||||
const { isOnhold, isOpusFirst } = require('../utils/sdp-utils');
|
const { isOnhold, isOpusFirst } = require('../utils/sdp-utils');
|
||||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||||
const { selectHostPort } = require('../utils/network');
|
const { selectHostPort } = require('../utils/network');
|
||||||
const { sleepFor } = require('../utils/helpers');
|
|
||||||
|
|
||||||
function parseDtmfOptions(logger, dtmfCapture) {
|
function parseDtmfOptions(logger, dtmfCapture) {
|
||||||
let parentDtmfCollector, childDtmfCollector;
|
let parentDtmfCollector, childDtmfCollector;
|
||||||
@@ -87,6 +86,8 @@ function filterAndLimit(logger, tasks) {
|
|||||||
return unique;
|
return unique;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sleepFor = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
||||||
|
|
||||||
class TaskDial extends Task {
|
class TaskDial extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
super(logger, opts);
|
super(logger, opts);
|
||||||
@@ -229,10 +230,10 @@ class TaskDial extends Task {
|
|||||||
try {
|
try {
|
||||||
await this.epOther.play(this.dialMusic);
|
await this.epOther.play(this.dialMusic);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err, `TaskDial:exec error playing dialMusic ${this.dialMusic}`);
|
this.logger.error(err, `TaskDial:exec error playing ${this.dialMusic}`);
|
||||||
await sleepFor(1000);
|
await sleepFor(1000);
|
||||||
}
|
}
|
||||||
} while (!this.killed && !this.bridged && this._mediaPath === MediaPath.FullMedia);
|
} while (!this.killed || !this.bridged);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -498,7 +499,7 @@ class TaskDial extends Task {
|
|||||||
dlg && dlg.removeAllListeners('info');
|
dlg && dlg.removeAllListeners('info');
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestWithinDialog(cs, req, res) {
|
async _onRequestWithinDialog(cs, req, res) {
|
||||||
cs._onRequestWithinDialog(req, res);
|
cs._onRequestWithinDialog(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,8 +551,7 @@ class TaskDial extends Task {
|
|||||||
proxy: `sip:${sbcAddress}`,
|
proxy: `sip:${sbcAddress}`,
|
||||||
callingNumber: this.callerId || fromUri.user,
|
callingNumber: this.callerId || fromUri.user,
|
||||||
...(this.callerName && {callingName: this.callerName}),
|
...(this.callerName && {callingName: this.callerName}),
|
||||||
opusFirst: isOpusFirst(this.cs.ep.remote.sdp),
|
opusFirst: isOpusFirst(this.cs.ep.remote.sdp)
|
||||||
isVideoCall: this.cs.ep.remote.sdp.includes('m=video')
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const t = this.target.find((t) => t.type === 'teams');
|
const t = this.target.find((t) => t.type === 'teams');
|
||||||
@@ -871,11 +871,7 @@ class TaskDial extends Task {
|
|||||||
|
|
||||||
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
|
if (this.parentDtmfCollector) this._installDtmfDetection(cs, cs.dlg);
|
||||||
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
|
if (this.childDtmfCollector) this._installDtmfDetection(cs, this.dlg);
|
||||||
if (cs.sipRequestWithinDialogHook) {
|
if (cs.sipRequestWithinDialogHook) this._initSipIndialogRequestListener(cs, this.dlg);
|
||||||
/* remove any existing listener to escape from duplicating events */
|
|
||||||
this._removeSipIndialogRequestListener(this.dlg);
|
|
||||||
this._initSipIndialogRequestListener(cs, this.dlg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep: this.epOther, ep2:this.ep});
|
if (this.transcribeTask) this.transcribeTask.exec(cs, {ep: this.epOther, ep2:this.ep});
|
||||||
if (this.listenTask) this.listenTask.exec(cs, {ep: this.listenTask.channel === 2 ? this.ep : this.epOther});
|
if (this.listenTask) this.listenTask.exec(cs, {ep: this.listenTask.channel === 2 ? this.ep : this.epOther});
|
||||||
@@ -908,7 +904,7 @@ class TaskDial extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handleMediaTimeout(evt) {
|
_handleMediaTimeout(evt) {
|
||||||
if (evt?.reason === 'MEDIA_TIMEOUT' && this.sd && this.bridged) {
|
if (evt.reason === 'MEDIA_TIMEOUT' && this.sd && this.bridged) {
|
||||||
this.kill(this.cs, KillReason.MediaTimeout);
|
this.kill(this.cs, KillReason.MediaTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ const {
|
|||||||
NvidiaTranscriptionEvents,
|
NvidiaTranscriptionEvents,
|
||||||
JambonzTranscriptionEvents,
|
JambonzTranscriptionEvents,
|
||||||
AssemblyAiTranscriptionEvents,
|
AssemblyAiTranscriptionEvents,
|
||||||
VoxistTranscriptionEvents,
|
|
||||||
OpenAITranscriptionEvents,
|
|
||||||
VadDetection,
|
VadDetection,
|
||||||
VerbioTranscriptionEvents,
|
VerbioTranscriptionEvents,
|
||||||
SpeechmaticsTranscriptionEvents
|
SpeechmaticsTranscriptionEvents
|
||||||
@@ -84,7 +82,6 @@ class TaskGather extends SttTask {
|
|||||||
this._bufferedTranscripts = [];
|
this._bufferedTranscripts = [];
|
||||||
this.partialTranscriptsCount = 0;
|
this.partialTranscriptsCount = 0;
|
||||||
this.bugname_prefix = 'gather_';
|
this.bugname_prefix = 'gather_';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Gather; }
|
get name() { return TaskName.Gather; }
|
||||||
@@ -112,12 +109,6 @@ class TaskGather extends SttTask {
|
|||||||
return this.fillerNoise.startDelaySecs;
|
return this.fillerNoise.startDelaySecs;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isStreamingTts() { return this.sayTask && this.sayTask.isStreamingTts; }
|
|
||||||
|
|
||||||
getTtsVendorData() {
|
|
||||||
if (this.sayTask) return this.sayTask.getTtsVendorData(this.cs);
|
|
||||||
}
|
|
||||||
|
|
||||||
get summary() {
|
get summary() {
|
||||||
let s = `${this.name}{`;
|
let s = `${this.name}{`;
|
||||||
if (this.input.length === 2) s += 'inputs=[speech,digits],';
|
if (this.input.length === 2) s += 'inputs=[speech,digits],';
|
||||||
@@ -241,7 +232,6 @@ class TaskGather extends SttTask {
|
|||||||
const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`);
|
const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`);
|
||||||
const process = () => {
|
const process = () => {
|
||||||
this.logger.debug('Gather: nested say task completed');
|
this.logger.debug('Gather: nested say task completed');
|
||||||
this.playComplete = true;
|
|
||||||
if (!this.listenDuringPrompt) {
|
if (!this.listenDuringPrompt) {
|
||||||
startDtmfListener();
|
startDtmfListener();
|
||||||
}
|
}
|
||||||
@@ -272,7 +262,6 @@ class TaskGather extends SttTask {
|
|||||||
const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`);
|
const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`);
|
||||||
const process = () => {
|
const process = () => {
|
||||||
this.logger.debug('Gather: nested play task completed');
|
this.logger.debug('Gather: nested play task completed');
|
||||||
this.playComplete = true;
|
|
||||||
if (!this.listenDuringPrompt) {
|
if (!this.listenDuringPrompt) {
|
||||||
startDtmfListener();
|
startDtmfListener();
|
||||||
}
|
}
|
||||||
@@ -535,17 +524,6 @@ class TaskGather extends SttTask {
|
|||||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'voxist':
|
|
||||||
this.bugname = `${this.bugname_prefix}voxist_transcribe`;
|
|
||||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Transcription,
|
|
||||||
this._onTranscription.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(
|
|
||||||
ep, VoxistTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.ConnectFailure,
|
|
||||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'speechmatics':
|
case 'speechmatics':
|
||||||
this.bugname = `${this.bugname_prefix}speechmatics_transcribe`;
|
this.bugname = `${this.bugname_prefix}speechmatics_transcribe`;
|
||||||
this.addCustomEventListener(
|
this.addCustomEventListener(
|
||||||
@@ -563,31 +541,6 @@ class TaskGather extends SttTask {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'openai':
|
|
||||||
this.bugname = `${this.bugname_prefix}openai_transcribe`;
|
|
||||||
this.addCustomEventListener(
|
|
||||||
ep, OpenAITranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(
|
|
||||||
ep, OpenAITranscriptionEvents.SpeechStarted, this._onOpenAISpeechStarted.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(
|
|
||||||
ep, OpenAITranscriptionEvents.SpeechStopped, this._onOpenAISpeechStopped.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(ep, OpenAITranscriptionEvents.Connect,
|
|
||||||
this._onVendorConnect.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(ep, OpenAITranscriptionEvents.ConnectFailure,
|
|
||||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(ep, OpenAITranscriptionEvents.Error,
|
|
||||||
this._onOpenAIErrror.bind(this, cs, ep));
|
|
||||||
|
|
||||||
/* openai delta transcripts are useful only for minBargeinWordCount eval */
|
|
||||||
if (this.minBargeinWordCount > 1) {
|
|
||||||
this.openaiPartials = [];
|
|
||||||
opts.OPENAI_WANT_PARTIALS = 1;
|
|
||||||
this.addCustomEventListener(
|
|
||||||
ep, OpenAITranscriptionEvents.PartialTranscript, this._onOpenAIPartialTranscript.bind(this, cs, ep));
|
|
||||||
}
|
|
||||||
this.modelSupportsConversationTracking = opts.OPENAI_MODEL !== 'whisper-1';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (this.vendor.startsWith('custom:')) {
|
if (this.vendor.startsWith('custom:')) {
|
||||||
this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`;
|
this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`;
|
||||||
@@ -619,25 +572,6 @@ class TaskGather extends SttTask {
|
|||||||
bugname: this.bugname
|
bugname: this.bugname
|
||||||
}, 'Gather:_startTranscribing');
|
}, 'Gather:_startTranscribing');
|
||||||
|
|
||||||
|
|
||||||
/* special feature for openai: we can provide a prompt that includes recent conversation history */
|
|
||||||
let prompt;
|
|
||||||
if (this.vendor === 'openai') {
|
|
||||||
if (this.modelSupportsConversationTracking) {
|
|
||||||
prompt = this.formatOpenAIPrompt(this.cs, {
|
|
||||||
prompt: this.data.recognizer?.openaiOptions?.prompt,
|
|
||||||
hintsTemplate: this.data.recognizer?.openaiOptions?.promptTemplates?.hintsTemplate,
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
conversationHistoryTemplate: this.data.recognizer?.openaiOptions?.promptTemplates?.conversationHistoryTemplate,
|
|
||||||
hints: this.data.recognizer?.hints,
|
|
||||||
});
|
|
||||||
this.logger.debug({prompt}, 'Gather:_startTranscribing - created an openai prompt');
|
|
||||||
}
|
|
||||||
else if (this.data.recognizer?.hints?.length > 0) {
|
|
||||||
prompt = this.data.recognizer?.hints.join(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: we don't need to ask deepgram for interim results, because they
|
* Note: we don't need to ask deepgram for interim results, because they
|
||||||
* already send us words as they are finalized (is_final=true) even before
|
* already send us words as they are finalized (is_final=true) even before
|
||||||
@@ -649,7 +583,6 @@ class TaskGather extends SttTask {
|
|||||||
interim: this.interim,
|
interim: this.interim,
|
||||||
bugname: this.bugname,
|
bugname: this.bugname,
|
||||||
hostport: this.hostport,
|
hostport: this.hostport,
|
||||||
prompt
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
const {writeAlerts, AlertType} = this.cs.srf.locals;
|
const {writeAlerts, AlertType} = this.cs.srf.locals;
|
||||||
this.logger.error(err, 'TaskGather:_startTranscribing error');
|
this.logger.error(err, 'TaskGather:_startTranscribing error');
|
||||||
@@ -691,13 +624,6 @@ class TaskGather extends SttTask {
|
|||||||
this._asrTimer = setTimeout(() => {
|
this._asrTimer = setTimeout(() => {
|
||||||
this.logger.debug('_startAsrTimer - asr timer went off');
|
this.logger.debug('_startAsrTimer - asr timer went off');
|
||||||
const evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language, this.vendor);
|
const evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language, this.vendor);
|
||||||
|
|
||||||
/* special case for speechmatics - keep listening if we dont have any transcripts */
|
|
||||||
if (this.vendor === 'speechmatics' && this._bufferedTranscripts.length === 0) {
|
|
||||||
this.logger.debug('Gather:_startAsrTimer - speechmatics, no transcripts yet, keep listening');
|
|
||||||
this._startAsrTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout', evt);
|
this._resolve(this._bufferedTranscripts.length > 0 ? 'speech' : 'timeout', evt);
|
||||||
}, this.asrTimeout);
|
}, this.asrTimeout);
|
||||||
this.logger.debug(`_startAsrTimer: set for ${this.asrTimeout}ms`);
|
this.logger.debug(`_startAsrTimer: set for ${this.asrTimeout}ms`);
|
||||||
@@ -830,11 +756,7 @@ class TaskGather extends SttTask {
|
|||||||
const bugname = fsEvent.getHeader('media-bugname');
|
const bugname = fsEvent.getHeader('media-bugname');
|
||||||
const finished = fsEvent.getHeader('transcription-session-finished');
|
const finished = fsEvent.getHeader('transcription-session-finished');
|
||||||
this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription raw transcript');
|
this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription raw transcript');
|
||||||
if (bugname && this.bugname !== bugname) {
|
if (bugname && this.bugname !== bugname) return;
|
||||||
this.logger.debug(
|
|
||||||
`Gather:_onTranscription - ignoring transcript from ${bugname} because our bug is ${this.bugname}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (finished === 'true') return;
|
if (finished === 'true') return;
|
||||||
|
|
||||||
if (this.vendor === 'ibm' && evt?.state === 'listening') return;
|
if (this.vendor === 'ibm' && evt?.state === 'listening') return;
|
||||||
@@ -844,16 +766,10 @@ class TaskGather extends SttTask {
|
|||||||
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram but no buffered transcripts');
|
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram but no buffered transcripts');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const utteranceTime = evt.last_word_end;
|
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram, return buffered transcript');
|
||||||
if (utteranceTime && this._dgTimeOfLastUnprocessedWord && utteranceTime < this._dgTimeOfLastUnprocessedWord) {
|
evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language, this.vendor);
|
||||||
this.logger.debug('Gather:_onTranscription - got UtteranceEnd with unprocessed words, continue listening');
|
this._bufferedTranscripts = [];
|
||||||
}
|
this._resolve('speech', evt);
|
||||||
else {
|
|
||||||
this.logger.debug('Gather:_onTranscription - got UtteranceEnd from deepgram, return buffered transcript');
|
|
||||||
evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language, this.vendor);
|
|
||||||
this._bufferedTranscripts = [];
|
|
||||||
this._resolve('speech', evt);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -864,7 +780,7 @@ class TaskGather extends SttTask {
|
|||||||
|
|
||||||
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language,
|
evt = this.normalizeTranscription(evt, this.vendor, 1, this.language,
|
||||||
this.shortUtterance, this.data.recognizer.punctuation);
|
this.shortUtterance, this.data.recognizer.punctuation);
|
||||||
//this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription normalized transcript');
|
this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription normalized transcript');
|
||||||
|
|
||||||
if (evt.alternatives.length === 0) {
|
if (evt.alternatives.length === 0) {
|
||||||
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
|
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
|
||||||
@@ -872,6 +788,8 @@ class TaskGather extends SttTask {
|
|||||||
}
|
}
|
||||||
const confidence = evt.alternatives[0].confidence;
|
const confidence = evt.alternatives[0].confidence;
|
||||||
const minConfidence = this.data.recognizer?.minConfidence;
|
const minConfidence = this.data.recognizer?.minConfidence;
|
||||||
|
this.logger.debug({evt},
|
||||||
|
`TaskGather:_onTranscription - confidence (${confidence}), minConfidence (${minConfidence})`);
|
||||||
if (confidence && minConfidence && confidence < minConfidence) {
|
if (confidence && minConfidence && confidence < minConfidence) {
|
||||||
this.logger.info({evt},
|
this.logger.info({evt},
|
||||||
'TaskGather:_onTranscription - Transcript confidence ' +
|
'TaskGather:_onTranscription - Transcript confidence ' +
|
||||||
@@ -987,21 +905,8 @@ class TaskGather extends SttTask {
|
|||||||
if (originalEvent.is_final && evt.alternatives[0].transcript !== '') {
|
if (originalEvent.is_final && evt.alternatives[0].transcript !== '') {
|
||||||
this.logger.debug({evt}, 'Gather:_onTranscription - buffering a completed (partial) deepgram transcript');
|
this.logger.debug({evt}, 'Gather:_onTranscription - buffering a completed (partial) deepgram transcript');
|
||||||
this._bufferedTranscripts.push(evt);
|
this._bufferedTranscripts.push(evt);
|
||||||
this._dgTimeOfLastUnprocessedWord = null;
|
|
||||||
}
|
|
||||||
if (evt.alternatives[0].transcript === '') {
|
|
||||||
emptyTranscript = true;
|
|
||||||
}
|
|
||||||
else if (!originalEvent.is_final) {
|
|
||||||
/* Deepgram: we have unprocessed words-save last word end time so we can later compare to UtteranceEnd */
|
|
||||||
const words = originalEvent.channel.alternatives[0].words;
|
|
||||||
if (words?.length > 0) {
|
|
||||||
this._dgTimeOfLastUnprocessedWord = words.slice(-1)[0].end;
|
|
||||||
this.logger.debug(
|
|
||||||
`TaskGather:_onTranscription - saving word end time: ${this._dgTimeOfLastUnprocessedWord}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if (evt.alternatives[0].transcript === '') emptyTranscript = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!emptyTranscript) {
|
if (!emptyTranscript) {
|
||||||
@@ -1137,33 +1042,6 @@ class TaskGather extends SttTask {
|
|||||||
this._onVendorError(cs, _ep, {error: JSON.stringify(e)});
|
this._onVendorError(cs, _ep, {error: JSON.stringify(e)});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onOpenAIErrror(cs, _ep, evt) {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const {message, ...e} = evt;
|
|
||||||
this._onVendorError(cs, _ep, {error: JSON.stringify(e)});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _onOpenAISpeechStarted(cs, _ep, evt) {
|
|
||||||
this.logger.debug({evt}, 'TaskGather:_onOpenAISpeechStarted');
|
|
||||||
}
|
|
||||||
|
|
||||||
async _onOpenAISpeechStopped(cs, _ep, evt) {
|
|
||||||
this.logger.debug({evt}, 'TaskGather:_onOpenAISpeechStopped');
|
|
||||||
}
|
|
||||||
|
|
||||||
async _onOpenAIPartialTranscript(cs, _ep, evt) {
|
|
||||||
if (!this.playComplete) {
|
|
||||||
const words = evt.delta.split(' ').filter((w) => /[A-Za-z0-0]/.test(w));
|
|
||||||
this.openaiPartials.push(...words);
|
|
||||||
this.logger.debug({words, partials: this.openaiPartials, evt}, 'TaskGather:_onOpenAIPartialTranscript - words');
|
|
||||||
if (this.openaiPartials.length >= this.minBargeinWordCount) {
|
|
||||||
this.logger.debug({partials: this.openaiPartials}, 'killing audio due to speech (openai)');
|
|
||||||
this._killAudio(cs);
|
|
||||||
this.notifyStatus({event: 'speech-bargein-detected', words: this.openaiPartials});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _onVendorError(cs, _ep, evt) {
|
async _onVendorError(cs, _ep, evt) {
|
||||||
super._onVendorError(cs, _ep, evt);
|
super._onVendorError(cs, _ep, evt);
|
||||||
if (!(await this._startFallback(cs, _ep, evt))) {
|
if (!(await this._startFallback(cs, _ep, evt))) {
|
||||||
@@ -1273,7 +1151,6 @@ class TaskGather extends SttTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (reason.startsWith('speech')) {
|
else if (reason.startsWith('speech')) {
|
||||||
this.cs.emit('userSaid', evt.alternatives[0].transcript);
|
|
||||||
if (this.parentTask) this.parentTask.emit('transcription', evt);
|
if (this.parentTask) this.parentTask.emit('transcription', evt);
|
||||||
else {
|
else {
|
||||||
this.emit('transcription', evt);
|
this.emit('transcription', evt);
|
||||||
@@ -1299,7 +1176,7 @@ class TaskGather extends SttTask {
|
|||||||
if (this.parentTask) this.parentTask.emit('stt-low-confidence', evt);
|
if (this.parentTask) this.parentTask.emit('stt-low-confidence', evt);
|
||||||
else {
|
else {
|
||||||
this.emit('stt-low-confidence', evt);
|
this.emit('stt-low-confidence', evt);
|
||||||
returnedVerbs = await this.performAction({speech:evt, reason: 'stt-low-confidence'});
|
returnedVerbs = await this.performAction({reason: 'stt-low-confidence'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) { /*already logged error*/ }
|
} catch (err) { /*already logged error*/ }
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ class TaskListen extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_onConnect(ep) {
|
_onConnect(ep) {
|
||||||
this.logger.info('TaskListen:_onConnect');
|
this.logger.debug('TaskListen:_onConnect');
|
||||||
}
|
}
|
||||||
_onConnectFailure(ep, evt) {
|
_onConnectFailure(ep, evt) {
|
||||||
this.logger.info(evt, 'TaskListen:_onConnectFailure');
|
this.logger.info(evt, 'TaskListen:_onConnectFailure');
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const {TaskPreconditions} = require('../../utils/constants');
|
|||||||
const TaskLlmOpenAI_S2S = require('./llms/openai_s2s');
|
const TaskLlmOpenAI_S2S = require('./llms/openai_s2s');
|
||||||
const TaskLlmVoiceAgent_S2S = require('./llms/voice_agent_s2s');
|
const TaskLlmVoiceAgent_S2S = require('./llms/voice_agent_s2s');
|
||||||
const TaskLlmUltravox_S2S = require('./llms/ultravox_s2s');
|
const TaskLlmUltravox_S2S = require('./llms/ultravox_s2s');
|
||||||
const TaskLlmElevenlabs_S2S = require('./llms/elevenlabs_s2s');
|
|
||||||
|
|
||||||
class TaskLlm extends Task {
|
class TaskLlm extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
@@ -55,10 +54,6 @@ class TaskLlm extends Task {
|
|||||||
llm = new TaskLlmUltravox_S2S(this.logger, this.data, this);
|
llm = new TaskLlmUltravox_S2S(this.logger, this.data, this);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'elevenlabs':
|
|
||||||
llm = new TaskLlmElevenlabs_S2S(this.logger, this.data, this);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported vendor ${this.vendor} for LLM`);
|
throw new Error(`Unsupported vendor ${this.vendor} for LLM`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,304 +0,0 @@
|
|||||||
const Task = require('../../task');
|
|
||||||
const TaskName = 'Llm_Elevenlabs_s2s';
|
|
||||||
const {LlmEvents_Elevenlabs} = require('../../../utils/constants');
|
|
||||||
const {request} = require('undici');
|
|
||||||
const ClientEvent = 'client.event';
|
|
||||||
const SessionDelete = 'session.delete';
|
|
||||||
|
|
||||||
const elevenlabs_server_events = [
|
|
||||||
'conversation_initiation_metadata',
|
|
||||||
'user_transcript',
|
|
||||||
'agent_response',
|
|
||||||
'client_tool_call'
|
|
||||||
];
|
|
||||||
|
|
||||||
const expandWildcards = (events) => {
|
|
||||||
const expandedEvents = [];
|
|
||||||
|
|
||||||
events.forEach((evt) => {
|
|
||||||
if (evt.endsWith('.*')) {
|
|
||||||
const prefix = evt.slice(0, -2); // Remove the wildcard ".*"
|
|
||||||
const matchingEvents = elevenlabs_server_events.filter((e) => e.startsWith(prefix));
|
|
||||||
expandedEvents.push(...matchingEvents);
|
|
||||||
} else {
|
|
||||||
expandedEvents.push(evt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return expandedEvents;
|
|
||||||
};
|
|
||||||
|
|
||||||
class TaskLlmElevenlabs_S2S extends Task {
|
|
||||||
constructor(logger, opts, parentTask) {
|
|
||||||
super(logger, opts, parentTask);
|
|
||||||
this.parent = parentTask;
|
|
||||||
|
|
||||||
this.vendor = this.parent.vendor;
|
|
||||||
this.auth = this.parent.auth;
|
|
||||||
|
|
||||||
const {agent_id, api_key} = this.auth || {};
|
|
||||||
if (!agent_id) throw new Error('auth.agent_id is required for Elevenlabs S2S');
|
|
||||||
|
|
||||||
this.agent_id = agent_id;
|
|
||||||
this.api_key = api_key;
|
|
||||||
this.actionHook = this.data.actionHook;
|
|
||||||
this.eventHook = this.data.eventHook;
|
|
||||||
this.toolHook = this.data.toolHook;
|
|
||||||
const {
|
|
||||||
conversation_initiation_client_data,
|
|
||||||
input_sample_rate = 16000,
|
|
||||||
output_sample_rate = 16000
|
|
||||||
} = this.data.llmOptions;
|
|
||||||
this.conversation_initiation_client_data = conversation_initiation_client_data;
|
|
||||||
this.input_sample_rate = input_sample_rate;
|
|
||||||
this.output_sample_rate = output_sample_rate;
|
|
||||||
this.results = {
|
|
||||||
completionReason: 'normal conversation end'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* only one of these will have items,
|
|
||||||
* if includeEvents, then these are the events to include
|
|
||||||
* if excludeEvents, then these are the events to exclude
|
|
||||||
*/
|
|
||||||
this.includeEvents = [];
|
|
||||||
this.excludeEvents = [];
|
|
||||||
|
|
||||||
/* default to all events if user did not specify */
|
|
||||||
this._populateEvents(this.data.events || elevenlabs_server_events);
|
|
||||||
|
|
||||||
this.addCustomEventListener = parentTask.addCustomEventListener.bind(parentTask);
|
|
||||||
this.removeCustomEventListeners = parentTask.removeCustomEventListeners.bind(parentTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() { return TaskName; }
|
|
||||||
|
|
||||||
async getSignedUrl() {
|
|
||||||
if (!this.api_key) {
|
|
||||||
return {
|
|
||||||
host: 'api.elevenlabs.io',
|
|
||||||
path: `/v1/convai/conversation?agent_id=${this.agent_id}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const {statusCode, body} = await request(
|
|
||||||
`https://api.elevenlabs.io/v1/convai/conversation/get_signed_url?agent_id=${this.agent_id}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'xi-api-key': this.api_key
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const data = await body.json();
|
|
||||||
if (statusCode !== 200 || !data?.signed_url) {
|
|
||||||
this.logger.error({statusCode, data}, 'Elevenlabs Error registering call');
|
|
||||||
throw new Error(`Elevenlabs Error registering call: ${data.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(data.signed_url);
|
|
||||||
return {
|
|
||||||
host: url.hostname,
|
|
||||||
path: url.pathname + url.search,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async _api(ep, args) {
|
|
||||||
const res = await ep.api('uuid_elevenlabs_s2s', `^^|${args.join('|')}`);
|
|
||||||
if (!res.body?.startsWith('+OK')) {
|
|
||||||
throw new Error({args}, `Error calling uuid_elevenlabs_s2s: ${res.body}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async exec(cs, {ep}) {
|
|
||||||
await super.exec(cs);
|
|
||||||
await this._startListening(cs, ep);
|
|
||||||
|
|
||||||
await this.awaitTaskDone();
|
|
||||||
|
|
||||||
/* note: the parent llm verb started the span, which is why this is necessary */
|
|
||||||
await this.parent.performAction(this.results);
|
|
||||||
|
|
||||||
this._unregisterHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
async kill(cs) {
|
|
||||||
super.kill(cs);
|
|
||||||
|
|
||||||
this._api(cs.ep, [cs.ep.uuid, SessionDelete])
|
|
||||||
.catch((err) => this.logger.info({err}, 'TaskLlmElevenlabs_S2S:kill - error deleting session'));
|
|
||||||
|
|
||||||
this.notifyTaskDone();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send function call output to the Elevenlabs server in the form of conversation.item.create
|
|
||||||
* per https://elevenlabs.io/docs/conversational-ai/api-reference/conversational-ai/websocket
|
|
||||||
*/
|
|
||||||
async processToolOutput(ep, tool_call_id, rawData) {
|
|
||||||
try {
|
|
||||||
const {data} = rawData;
|
|
||||||
this.logger.debug({tool_call_id, data}, 'TaskLlmElevenlabs_S2S:processToolOutput');
|
|
||||||
|
|
||||||
if (!data.type || data.type !== 'client_tool_result') {
|
|
||||||
this.logger.info({data},
|
|
||||||
'TaskLlmElevenlabs_S2S:processToolOutput - invalid tool output, must be client_tool_result');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)]);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.info({err}, 'TaskLlmElevenlabs_S2S:processToolOutput');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a session.update to the Elevenlabs server
|
|
||||||
* Note: creating and deleting conversation items also supported as well as interrupting the assistant
|
|
||||||
*/
|
|
||||||
async processLlmUpdate(ep, data, _callSid) {
|
|
||||||
this.logger.debug({data, _callSid}, 'TaskLlmElevenlabs_S2S:processLlmUpdate, ignored');
|
|
||||||
}
|
|
||||||
|
|
||||||
async _startListening(cs, ep) {
|
|
||||||
this._registerHandlers(ep);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const {host, path} = await this.getSignedUrl();
|
|
||||||
const args = this.conversation_initiation_client_data ?
|
|
||||||
[ep.uuid, 'session.create', this.input_sample_rate, this.output_sample_rate, host, path] :
|
|
||||||
[ep.uuid, 'session.create', this.input_sample_rate, this.output_sample_rate, host, path, 'no_initial_config'];
|
|
||||||
await this._api(ep, args);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error({err}, 'TaskLlmElevenlabs_S2S:_startListening');
|
|
||||||
this.notifyTaskDone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _sendClientEvent(ep, obj) {
|
|
||||||
let ok = true;
|
|
||||||
this.logger.debug({obj}, 'TaskLlmElevenlabs_S2S:_sendClientEvent');
|
|
||||||
try {
|
|
||||||
const args = [ep.uuid, ClientEvent, JSON.stringify(obj)];
|
|
||||||
await this._api(ep, args);
|
|
||||||
} catch (err) {
|
|
||||||
ok = false;
|
|
||||||
this.logger.error({err}, 'TaskLlmElevenlabs_S2S:_sendClientEvent - Error');
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _sendInitialMessage(ep) {
|
|
||||||
if (this.conversation_initiation_client_data) {
|
|
||||||
if (!await this._sendClientEvent(ep, {
|
|
||||||
type: 'conversation_initiation_client_data',
|
|
||||||
...this.conversation_initiation_client_data
|
|
||||||
})) {
|
|
||||||
this.notifyTaskDone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_registerHandlers(ep) {
|
|
||||||
this.addCustomEventListener(ep, LlmEvents_Elevenlabs.Connect, this._onConnect.bind(this, ep));
|
|
||||||
this.addCustomEventListener(ep, LlmEvents_Elevenlabs.ConnectFailure, this._onConnectFailure.bind(this, ep));
|
|
||||||
this.addCustomEventListener(ep, LlmEvents_Elevenlabs.Disconnect, this._onDisconnect.bind(this, ep));
|
|
||||||
this.addCustomEventListener(ep, LlmEvents_Elevenlabs.ServerEvent, this._onServerEvent.bind(this, ep));
|
|
||||||
}
|
|
||||||
|
|
||||||
_unregisterHandlers() {
|
|
||||||
this.removeCustomEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onError(ep, evt) {
|
|
||||||
this.logger.info({evt}, 'TaskLlmElevenlabs_S2S:_onError');
|
|
||||||
this.notifyTaskDone();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onConnect(ep) {
|
|
||||||
this.logger.debug('TaskLlmElevenlabs_S2S:_onConnect');
|
|
||||||
this._sendInitialMessage(ep);
|
|
||||||
}
|
|
||||||
_onConnectFailure(_ep, evt) {
|
|
||||||
this.logger.info(evt, 'TaskLlmElevenlabs_S2S:_onConnectFailure');
|
|
||||||
this.results = {completionReason: 'connection failure'};
|
|
||||||
this.notifyTaskDone();
|
|
||||||
}
|
|
||||||
_onDisconnect(_ep, evt) {
|
|
||||||
this.logger.info(evt, 'TaskLlmElevenlabs_S2S:_onConnectFailure');
|
|
||||||
this.results = {completionReason: 'disconnect from remote end'};
|
|
||||||
this.notifyTaskDone();
|
|
||||||
}
|
|
||||||
async _onServerEvent(ep, evt) {
|
|
||||||
let endConversation = false;
|
|
||||||
const type = evt.type;
|
|
||||||
this.logger.info({evt}, 'TaskLlmElevenlabs_S2S:_onServerEvent');
|
|
||||||
|
|
||||||
if (type === 'error') {
|
|
||||||
endConversation = true;
|
|
||||||
this.results = {
|
|
||||||
completionReason: 'server error',
|
|
||||||
error: evt.error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tool calls */
|
|
||||||
else if (type === 'client_tool_call') {
|
|
||||||
this.logger.debug({evt}, 'TaskLlmElevenlabs_S2S:_onServerEvent - function_call');
|
|
||||||
if (!this.toolHook) {
|
|
||||||
this.logger.warn({evt}, 'TaskLlmElevenlabs_S2S:_onServerEvent - no toolHook defined!');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const {client_tool_call} = evt;
|
|
||||||
const {tool_name: name, tool_call_id: call_id, parameters: args} = client_tool_call;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.parent.sendToolHook(call_id, {name, args});
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.info({err, evt}, 'TaskLlmElevenlabs_S2S - error calling function');
|
|
||||||
this.results = {
|
|
||||||
completionReason: 'client error calling function',
|
|
||||||
error: err
|
|
||||||
};
|
|
||||||
endConversation = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* check whether we should notify on this event */
|
|
||||||
if (this.includeEvents.length > 0 ? this.includeEvents.includes(type) : !this.excludeEvents.includes(type)) {
|
|
||||||
this.parent.sendEventHook(evt)
|
|
||||||
.catch((err) => this.logger.info({err},
|
|
||||||
'TaskLlmElevenlabs_S2S:_onServerEvent - error sending event hook'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endConversation) {
|
|
||||||
this.logger.info({results: this.results},
|
|
||||||
'TaskLlmElevenlabs_S2S:_onServerEvent - ending conversation due to error');
|
|
||||||
this.notifyTaskDone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_populateEvents(events) {
|
|
||||||
if (events.includes('all')) {
|
|
||||||
/* work by excluding specific events */
|
|
||||||
const exclude = events
|
|
||||||
.filter((evt) => evt.startsWith('-'))
|
|
||||||
.map((evt) => evt.slice(1));
|
|
||||||
if (exclude.length === 0) this.includeEvents = elevenlabs_server_events;
|
|
||||||
else this.excludeEvents = expandWildcards(exclude);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/* work by including specific events */
|
|
||||||
const include = events
|
|
||||||
.filter((evt) => !evt.startsWith('-'));
|
|
||||||
this.includeEvents = expandWildcards(include);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.debug({
|
|
||||||
includeEvents: this.includeEvents,
|
|
||||||
excludeEvents: this.excludeEvents
|
|
||||||
}, 'TaskLlmElevenlabs_S2S:_populateEvents');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = TaskLlmElevenlabs_S2S;
|
|
||||||
@@ -4,7 +4,6 @@ const {request} = require('undici');
|
|||||||
const {LlmEvents_Ultravox} = require('../../../utils/constants');
|
const {LlmEvents_Ultravox} = require('../../../utils/constants');
|
||||||
|
|
||||||
const ultravox_server_events = [
|
const ultravox_server_events = [
|
||||||
'createCall',
|
|
||||||
'pong',
|
'pong',
|
||||||
'state',
|
'state',
|
||||||
'transcript',
|
'transcript',
|
||||||
@@ -39,10 +38,6 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
this.eventHook = this.data.eventHook;
|
this.eventHook = this.data.eventHook;
|
||||||
this.toolHook = this.data.toolHook;
|
this.toolHook = this.data.toolHook;
|
||||||
|
|
||||||
this.results = {
|
|
||||||
completionReason: 'normal conversation end'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* only one of these will have items,
|
* only one of these will have items,
|
||||||
* if includeEvents, then these are the events to include
|
* if includeEvents, then these are the events to include
|
||||||
@@ -89,11 +84,11 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
});
|
});
|
||||||
const data = await body.json();
|
const data = await body.json();
|
||||||
if (statusCode !== 201 || !data?.joinUrl) {
|
if (statusCode !== 201 || !data?.joinUrl) {
|
||||||
this.logger.info({statusCode, data}, 'Ultravox Error registering call');
|
this.logger.error({statusCode, data}, 'Ultravox Error registering call');
|
||||||
throw new Error(`Ultravox Error registering call:${statusCode} - ${data.detail}`);
|
throw new Error(`Ultravox Error registering call: ${data.message}`);
|
||||||
}
|
}
|
||||||
this.logger.debug({joinUrl: data.joinUrl}, 'Ultravox Call registered');
|
this.logger.info({joinUrl: data.joinUrl}, 'Ultravox Call registered');
|
||||||
return data;
|
return data.joinUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
_unregisterHandlers() {
|
_unregisterHandlers() {
|
||||||
@@ -110,21 +105,15 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
async _startListening(cs, ep) {
|
async _startListening(cs, ep) {
|
||||||
this._registerHandlers(ep);
|
this._registerHandlers(ep);
|
||||||
|
|
||||||
|
const joinUrl = await this.createCall();
|
||||||
|
// split the joinUrl into host and path
|
||||||
|
const {host, pathname, search} = new URL(joinUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await this.createCall();
|
|
||||||
const {joinUrl} = data;
|
|
||||||
// split the joinUrl into host and path
|
|
||||||
const {host, pathname, search} = new URL(joinUrl);
|
|
||||||
const args = [ep.uuid, 'session.create', host, pathname + search];
|
const args = [ep.uuid, 'session.create', host, pathname + search];
|
||||||
await this._api(ep, args);
|
await this._api(ep, args);
|
||||||
// Notify the application that the session has been created with detail information
|
|
||||||
this._sendLlmEvent('createCall', {
|
|
||||||
type: 'createCall',
|
|
||||||
...data
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err}, 'TaskLlmUltraVox_S2S:_startListening - Error sending createCall');
|
this.logger.error({err}, 'TaskLlmUltraVox_S2S:_startListening');
|
||||||
this.results = {completionReason: `connection failure - ${err}`};
|
|
||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +141,7 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onConnect(ep) {
|
_onConnect(ep) {
|
||||||
this.logger.info('TaskLlmUltravox_S2S:_onConnect');
|
this.logger.debug('TaskLlmUltravox_S2S:_onConnect');
|
||||||
}
|
}
|
||||||
_onConnectFailure(_ep, evt) {
|
_onConnectFailure(_ep, evt) {
|
||||||
this.logger.info(evt, 'TaskLlmUltravox_S2S:_onConnectFailure');
|
this.logger.info(evt, 'TaskLlmUltravox_S2S:_onConnectFailure');
|
||||||
@@ -168,7 +157,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.info({evt}, 'TaskLlmUltravox_S2S:_onServerEvent');
|
||||||
|
|
||||||
/* server errors of some sort */
|
/* server errors of some sort */
|
||||||
if (type === 'error') {
|
if (type === 'error') {
|
||||||
@@ -183,7 +172,7 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
else if (type === 'client_tool_invocation') {
|
else if (type === 'client_tool_invocation') {
|
||||||
this.logger.debug({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - function_call');
|
this.logger.debug({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - function_call');
|
||||||
if (!this.toolHook) {
|
if (!this.toolHook) {
|
||||||
this.logger.info({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - no toolHook defined!');
|
this.logger.warn({evt}, 'TaskLlmUltravox_S2S:_onServerEvent - no toolHook defined!');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const {toolName: name, invocationId: call_id, parameters: args} = evt;
|
const {toolName: name, invocationId: call_id, parameters: args} = evt;
|
||||||
@@ -201,38 +190,16 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sendLlmEvent(type, evt);
|
|
||||||
|
|
||||||
if (endConversation) {
|
|
||||||
this.logger.info({results: this.results},
|
|
||||||
'TaskLlmUltravox_S2S:_onServerEvent - ending conversation due to error');
|
|
||||||
this.notifyTaskDone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendLlmEvent(type, evt) {
|
|
||||||
/* check whether we should notify on this event */
|
/* check whether we should notify on this event */
|
||||||
if (this.includeEvents.length > 0 ? this.includeEvents.includes(type) : !this.excludeEvents.includes(type)) {
|
if (this.includeEvents.length > 0 ? this.includeEvents.includes(type) : !this.excludeEvents.includes(type)) {
|
||||||
this.parent.sendEventHook(evt)
|
this.parent.sendEventHook(evt)
|
||||||
.catch((err) => this.logger.info({err}, 'TaskLlmUltravox_S2S:_onServerEvent - error sending event hook'));
|
.catch((err) => this.logger.info({err}, 'TaskLlmUltravox_S2S:_onServerEvent - error sending event hook'));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async processLlmUpdate(ep, data, _callSid) {
|
if (endConversation) {
|
||||||
try {
|
this.logger.info({results: this.results},
|
||||||
this.logger.debug({data, _callSid}, 'TaskLlmUltravox_S2S:processLlmUpdate');
|
'TaskLlmUltravox_S2S:_onServerEvent - ending conversation due to error');
|
||||||
|
this.notifyTaskDone();
|
||||||
if (!data.type || ![
|
|
||||||
'input_text_message'
|
|
||||||
].includes(data.type)) {
|
|
||||||
this.logger.info({data},
|
|
||||||
'TaskLlmUltravox_S2S:processLlmUpdate - invalid mid-call request, only input_text_message supported');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)]);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.info({err, data}, 'TaskLlmUltravox_S2S:processLlmUpdate - Error processing LLM update');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +215,7 @@ class TaskLlmUltravox_S2S extends Task {
|
|||||||
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)]);
|
await this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)]);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err, data}, 'TaskLlmUltravox_S2S:processToolOutput - Error processing tool output');
|
this.logger.info({err}, 'TaskLlmUltravox_S2S:processToolOutput');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class TaskLlmVoiceAgent_S2S extends Task {
|
|||||||
this.parent = parentTask;
|
this.parent = parentTask;
|
||||||
|
|
||||||
this.vendor = this.parent.vendor;
|
this.vendor = this.parent.vendor;
|
||||||
this.model = this.parent.model || 'voice-agent';
|
this.model = this.parent.model;
|
||||||
this.auth = this.parent.auth;
|
this.auth = this.parent.auth;
|
||||||
this.connectionOptions = this.parent.connectOptions;
|
this.connectionOptions = this.parent.connectOptions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const {TaskName} = require('../utils/constants');
|
const {TaskName} = require('../utils/constants');
|
||||||
const WsRequestor = require('../utils/ws-requestor');
|
const WsRequestor = require('../utils/ws-requestor');
|
||||||
const URL = require('url');
|
|
||||||
const HttpRequestor = require('../utils/http-requestor');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirects to a new application
|
* Redirects to a new application
|
||||||
@@ -26,21 +24,6 @@ class TaskRedirect extends Task {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info(err, `Task:performAction error redirecting to ${this.actionHook}`);
|
this.logger.info(err, `Task:performAction error redirecting to ${this.actionHook}`);
|
||||||
}
|
}
|
||||||
} else if (cs.application.requestor._isAbsoluteUrl(this.actionHook)) {
|
|
||||||
const baseUrl = this.cs.application.requestor.baseUrl;
|
|
||||||
const newUrl = URL.parse(this.actionHook);
|
|
||||||
const newBaseUrl = newUrl.protocol + '//' + newUrl.host;
|
|
||||||
if (baseUrl != newBaseUrl) {
|
|
||||||
try {
|
|
||||||
this.logger.info(`Task:redirect updating base url to ${newBaseUrl}`);
|
|
||||||
const newRequestor = new HttpRequestor(this.logger, cs.accountSid, {url: this.actionHook},
|
|
||||||
cs.accountInfo.account.webhook_secret);
|
|
||||||
this.cs.requestor.removeAllListeners();
|
|
||||||
this.cs.application.requestor = newRequestor;
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.info(err, `Task:redirect error updating base url to ${this.actionHook}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await this.performAction();
|
await this.performAction();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class TaskSay extends TtsTask {
|
|||||||
throw new SpeechCredentialError(
|
throw new SpeechCredentialError(
|
||||||
`No text-to-speech service credentials for ${vendor} with labels: ${label} have been configured`);
|
`No text-to-speech service credentials for ${vendor} with labels: ${label} have been configured`);
|
||||||
}
|
}
|
||||||
this.ep = ep;
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await this.setTtsStreamingChannelVars(vendor, language, voice, credentials, ep);
|
await this.setTtsStreamingChannelVars(vendor, language, voice, credentials, ep);
|
||||||
@@ -213,7 +213,7 @@ class TaskSay extends TtsTask {
|
|||||||
ep.once('playback-start', (evt) => {
|
ep.once('playback-start', (evt) => {
|
||||||
this.logger.debug({evt}, 'Say got playback-start');
|
this.logger.debug({evt}, 'Say got playback-start');
|
||||||
if (this.otelSpan) {
|
if (this.otelSpan) {
|
||||||
this._addStreamingTtsAttributes(this.otelSpan, evt, vendor);
|
this._addStreamingTtsAttributes(this.otelSpan, evt);
|
||||||
this.otelSpan.end();
|
this.otelSpan.end();
|
||||||
this.otelSpan = null;
|
this.otelSpan = null;
|
||||||
if (evt.variable_tts_cache_filename) {
|
if (evt.variable_tts_cache_filename) {
|
||||||
@@ -240,7 +240,6 @@ class TaskSay extends TtsTask {
|
|||||||
language,
|
language,
|
||||||
voice,
|
voice,
|
||||||
engine,
|
engine,
|
||||||
model: this.model || this.model_id,
|
|
||||||
text
|
text
|
||||||
}).catch((err) => this.logger.info({err}, 'Error adding file to cache'));
|
}).catch((err) => this.logger.info({err}, 'Error adding file to cache'));
|
||||||
}
|
}
|
||||||
@@ -292,10 +291,8 @@ class TaskSay extends TtsTask {
|
|||||||
if (cs.isInConference) {
|
if (cs.isInConference) {
|
||||||
const {memberId, confName} = cs;
|
const {memberId, confName} = cs;
|
||||||
this.killPlayToConfMember(this.ep, memberId, confName);
|
this.killPlayToConfMember(this.ep, memberId, confName);
|
||||||
} else if (this.isStreamingTts) {
|
}
|
||||||
this.logger.debug('TaskSay:kill - clearing TTS stream for streaming audio');
|
else {
|
||||||
cs.clearTtsStream();
|
|
||||||
} else {
|
|
||||||
this.notifyStatus({event: 'kill-playback'});
|
this.notifyStatus({event: 'kill-playback'});
|
||||||
this.ep.api('uuid_break', this.ep.uuid);
|
this.ep.api('uuid_break', this.ep.uuid);
|
||||||
}
|
}
|
||||||
@@ -310,7 +307,7 @@ class TaskSay extends TtsTask {
|
|||||||
this.notifyTaskDone();
|
this.notifyTaskDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
_addStreamingTtsAttributes(span, evt, vendor) {
|
_addStreamingTtsAttributes(span, evt) {
|
||||||
const attrs = {'tts.cached': false};
|
const attrs = {'tts.cached': false};
|
||||||
for (const [key, value] of Object.entries(evt)) {
|
for (const [key, value] of Object.entries(evt)) {
|
||||||
if (key.startsWith('variable_tts_')) {
|
if (key.startsWith('variable_tts_')) {
|
||||||
@@ -324,9 +321,6 @@ class TaskSay extends TtsTask {
|
|||||||
.replace('elevenlabs_', 'elevenlabs.');
|
.replace('elevenlabs_', 'elevenlabs.');
|
||||||
if (spanMapping[newKey]) newKey = spanMapping[newKey];
|
if (spanMapping[newKey]) newKey = spanMapping[newKey];
|
||||||
attrs[newKey] = value;
|
attrs[newKey] = value;
|
||||||
if (key === 'variable_tts_time_to_first_byte_ms' && value) {
|
|
||||||
this.cs.srf.locals.stats.histogram('tts.response_time', value, [`vendor:${vendor}`]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete attrs['cache_filename']; //no value in adding this to the span
|
delete attrs['cache_filename']; //no value in adding this to the span
|
||||||
|
|||||||
@@ -5,30 +5,6 @@ const { TaskPreconditions, CobaltTranscriptionEvents } = require('../utils/const
|
|||||||
const { SpeechCredentialError } = require('../utils/error');
|
const { SpeechCredentialError } = require('../utils/error');
|
||||||
const {JAMBONES_AWS_TRANSCRIBE_USE_GRPC} = require('../config');
|
const {JAMBONES_AWS_TRANSCRIBE_USE_GRPC} = require('../config');
|
||||||
|
|
||||||
/**
|
|
||||||
* "Please insert turns here: {{turns:4}}"
|
|
||||||
// -> { processed: 'Please insert turns here: {{turns}}', turns: 4 }
|
|
||||||
|
|
||||||
processTurnString("Please insert turns here: {{turns}}"));
|
|
||||||
// -> { processed: 'Please insert turns here: {{turns}}', turns: null }
|
|
||||||
*/
|
|
||||||
const processTurnString = (input) => {
|
|
||||||
const regex = /\{\{turns(?::(\d+))?\}\}/;
|
|
||||||
const match = input.match(regex);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
return {
|
|
||||||
processed: input,
|
|
||||||
turns: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const turns = match[1] ? parseInt(match[1], 10) : null;
|
|
||||||
const processed = input.replace(regex, '{{turns}}');
|
|
||||||
|
|
||||||
return { processed, turns };
|
|
||||||
};
|
|
||||||
|
|
||||||
class SttTask extends Task {
|
class SttTask extends Task {
|
||||||
|
|
||||||
constructor(logger, data, parentTask) {
|
constructor(logger, data, parentTask) {
|
||||||
@@ -314,57 +290,6 @@ class SttTask extends Task {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
formatOpenAIPrompt(cs, {prompt, hintsTemplate, conversationHistoryTemplate, hints}) {
|
|
||||||
let conversationHistoryPrompt, hintsPrompt;
|
|
||||||
|
|
||||||
/* generate conversation history from template */
|
|
||||||
if (conversationHistoryTemplate) {
|
|
||||||
const {processed, turns} = processTurnString(conversationHistoryTemplate);
|
|
||||||
this.logger.debug({processed, turns}, 'SttTask: processed conversation history template');
|
|
||||||
conversationHistoryPrompt = cs.getFormattedConversation(turns || 4);
|
|
||||||
//this.logger.debug({conversationHistoryPrompt}, 'SttTask: conversation history');
|
|
||||||
if (conversationHistoryPrompt) {
|
|
||||||
conversationHistoryPrompt = processed.replace('{{turns}}', `\n${conversationHistoryPrompt}\nuser: `);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* generate hints from template */
|
|
||||||
if (hintsTemplate && Array.isArray(hints) && hints.length > 0) {
|
|
||||||
hintsPrompt = hintsTemplate.replace('{{hints}}', hints);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* combine into final prompt */
|
|
||||||
let finalPrompt = prompt || '';
|
|
||||||
if (hintsPrompt) {
|
|
||||||
finalPrompt = `${finalPrompt}\n${hintsPrompt}`;
|
|
||||||
}
|
|
||||||
if (conversationHistoryPrompt) {
|
|
||||||
finalPrompt = `${finalPrompt}\n${conversationHistoryPrompt}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.debug({
|
|
||||||
finalPrompt,
|
|
||||||
hints,
|
|
||||||
hintsPrompt,
|
|
||||||
conversationHistoryTemplate,
|
|
||||||
conversationHistoryPrompt
|
|
||||||
}, 'SttTask: formatted OpenAI prompt');
|
|
||||||
return finalPrompt?.trimStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* some STT engines will keep listening after a final response, so no need to restart */
|
|
||||||
doesVendorContinueListeningAfterFinalTranscript(vendor) {
|
|
||||||
return (vendor.startsWith('custom:') || [
|
|
||||||
'soniox',
|
|
||||||
'aws',
|
|
||||||
'microsoft',
|
|
||||||
'deepgram',
|
|
||||||
'google',
|
|
||||||
'speechmatics',
|
|
||||||
'openai',
|
|
||||||
].includes(vendor));
|
|
||||||
}
|
|
||||||
|
|
||||||
_onCompileContext(ep, key, evt) {
|
_onCompileContext(ep, key, evt) {
|
||||||
const {addKey} = this.cs.srf.locals.dbHelpers;
|
const {addKey} = this.cs.srf.locals.dbHelpers;
|
||||||
this.logger.debug({evt}, `received cobalt compile context event, will cache under ${key}`);
|
this.logger.debug({evt}, `received cobalt compile context event, will cache under ${key}`);
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ const {
|
|||||||
JambonzTranscriptionEvents,
|
JambonzTranscriptionEvents,
|
||||||
TranscribeStatus,
|
TranscribeStatus,
|
||||||
AssemblyAiTranscriptionEvents,
|
AssemblyAiTranscriptionEvents,
|
||||||
VoxistTranscriptionEvents,
|
|
||||||
OpenAITranscriptionEvents,
|
|
||||||
VerbioTranscriptionEvents,
|
VerbioTranscriptionEvents,
|
||||||
SpeechmaticsTranscriptionEvents
|
SpeechmaticsTranscriptionEvents
|
||||||
} = require('../utils/constants.json');
|
} = require('../utils/constants.json');
|
||||||
@@ -31,6 +29,7 @@ class TaskTranscribe extends SttTask {
|
|||||||
this.transcriptionHook = this.data.transcriptionHook;
|
this.transcriptionHook = this.data.transcriptionHook;
|
||||||
this.translationHook = this.data.translationHook;
|
this.translationHook = this.data.translationHook;
|
||||||
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||||
|
|
||||||
if (this.data.recognizer) {
|
if (this.data.recognizer) {
|
||||||
this.interim = !!this.data.recognizer.interim;
|
this.interim = !!this.data.recognizer.interim;
|
||||||
this.separateRecognitionPerChannel = this.data.recognizer.separateRecognitionPerChannel;
|
this.separateRecognitionPerChannel = this.data.recognizer.separateRecognitionPerChannel;
|
||||||
@@ -105,7 +104,7 @@ class TaskTranscribe extends SttTask {
|
|||||||
|
|
||||||
if (cs.hasGlobalSttHints) {
|
if (cs.hasGlobalSttHints) {
|
||||||
const {hints, hintsBoost} = cs.globalSttHints;
|
const {hints, hintsBoost} = cs.globalSttHints;
|
||||||
this.data.recognizer.hints = this.data.recognizer?.hints?.concat(hints);
|
this.data.recognizer.hints = this.data.recognizer.hints.concat(hints);
|
||||||
if (!this.data.recognizer.hintsBoost && hintsBoost) this.data.recognizer.hintsBoost = hintsBoost;
|
if (!this.data.recognizer.hintsBoost && hintsBoost) this.data.recognizer.hintsBoost = hintsBoost;
|
||||||
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
|
this.logger.debug({hints: this.data.recognizer.hints, hintsBoost: this.data.recognizer.hintsBoost},
|
||||||
'Transcribe:exec - applying global sttHints');
|
'Transcribe:exec - applying global sttHints');
|
||||||
@@ -301,17 +300,6 @@ class TaskTranscribe extends SttTask {
|
|||||||
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'voxist':
|
|
||||||
this.bugname = `${this.bugname_prefix}voxist_transcribe`;
|
|
||||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Transcription,
|
|
||||||
this._onTranscription.bind(this, cs, ep, channel));
|
|
||||||
this.addCustomEventListener(ep,
|
|
||||||
VoxistTranscriptionEvents.Connect, this._onVendorConnect.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(ep, VoxistTranscriptionEvents.ConnectFailure,
|
|
||||||
this._onVendorConnectFailure.bind(this, cs, ep, channel));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'speechmatics':
|
case 'speechmatics':
|
||||||
this.bugname = `${this.bugname_prefix}speechmatics_transcribe`;
|
this.bugname = `${this.bugname_prefix}speechmatics_transcribe`;
|
||||||
this.addCustomEventListener(
|
this.addCustomEventListener(
|
||||||
@@ -330,20 +318,6 @@ class TaskTranscribe extends SttTask {
|
|||||||
this._onSpeechmaticsError.bind(this, cs, ep));
|
this._onSpeechmaticsError.bind(this, cs, ep));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'openai':
|
|
||||||
this.bugname = `${this.bugname_prefix}openai_transcribe`;
|
|
||||||
this.addCustomEventListener(
|
|
||||||
ep, OpenAITranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep, channel));
|
|
||||||
this.addCustomEventListener(ep, OpenAITranscriptionEvents.Connect,
|
|
||||||
this._onVendorConnect.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(ep, OpenAITranscriptionEvents.ConnectFailure,
|
|
||||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
|
||||||
this.addCustomEventListener(ep, OpenAITranscriptionEvents.Error,
|
|
||||||
this._onOpenAIErrror.bind(this, cs, ep));
|
|
||||||
|
|
||||||
this.modelSupportsConversationTracking = opts.OPENAI_MODEL !== 'whisper-1';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (this.vendor.startsWith('custom:')) {
|
if (this.vendor.startsWith('custom:')) {
|
||||||
this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`;
|
this.bugname = `${this.bugname_prefix}${this.vendor}_transcribe`;
|
||||||
@@ -379,25 +353,6 @@ class TaskTranscribe extends SttTask {
|
|||||||
async _transcribe(ep) {
|
async _transcribe(ep) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`TaskTranscribe:_transcribe - starting transcription vendor ${this.vendor} bugname ${this.bugname}`);
|
`TaskTranscribe:_transcribe - starting transcription vendor ${this.vendor} bugname ${this.bugname}`);
|
||||||
|
|
||||||
/* special feature for openai: we can provide a prompt that includes recent conversation history */
|
|
||||||
let prompt;
|
|
||||||
if (this.vendor === 'openai') {
|
|
||||||
if (this.modelSupportsConversationTracking) {
|
|
||||||
prompt = this.formatOpenAIPrompt(this.cs, {
|
|
||||||
prompt: this.data.recognizer?.openaiOptions?.prompt,
|
|
||||||
hintsTemplate: this.data.recognizer?.openaiOptions?.promptTemplates?.hintsTemplate,
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
conversationHistoryTemplate: this.data.recognizer?.openaiOptions?.promptTemplates?.conversationHistoryTemplate,
|
|
||||||
hints: this.data.recognizer?.hints,
|
|
||||||
});
|
|
||||||
this.logger.debug({prompt}, 'Gather:_startTranscribing - created an openai prompt');
|
|
||||||
}
|
|
||||||
else if (this.data.recognizer?.hints?.length > 0) {
|
|
||||||
prompt = this.data.recognizer?.hints.join(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ep.startTranscription({
|
await ep.startTranscription({
|
||||||
vendor: this.vendor,
|
vendor: this.vendor,
|
||||||
interim: this.interim ? true : false,
|
interim: this.interim ? true : false,
|
||||||
@@ -489,9 +444,8 @@ class TaskTranscribe extends SttTask {
|
|||||||
this._startAsrTimer(channel);
|
this._startAsrTimer(channel);
|
||||||
|
|
||||||
/* some STT engines will keep listening after a final response, so no need to restart */
|
/* some STT engines will keep listening after a final response, so no need to restart */
|
||||||
if (!this.doesVendorContinueListeningAfterFinalTranscript(this.vendor)) {
|
if (!['soniox', 'aws', 'microsoft', 'deepgram', 'google', 'speechmatics']
|
||||||
this._startTranscribing(cs, ep, channel);
|
.includes(this.vendor)) this._startTranscribing(cs, ep, channel);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (this.vendor === 'soniox') {
|
if (this.vendor === 'soniox') {
|
||||||
@@ -514,7 +468,9 @@ class TaskTranscribe extends SttTask {
|
|||||||
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - sending final transcript');
|
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - sending final transcript');
|
||||||
this._resolve(channel, evt);
|
this._resolve(channel, evt);
|
||||||
|
|
||||||
if (!this.doesVendorContinueListeningAfterFinalTranscript(this.vendor)) {
|
/* some STT engines will keep listening after a final response, so no need to restart */
|
||||||
|
if (!['soniox', 'aws', 'microsoft', 'deepgram', 'google', 'speechmatics'].includes(this.vendor) &&
|
||||||
|
!this.vendor.startsWith('custom:')) {
|
||||||
this.logger.debug('TaskTranscribe:_onTranscription - restarting transcribe');
|
this.logger.debug('TaskTranscribe:_onTranscription - restarting transcribe');
|
||||||
this._startTranscribing(cs, ep, channel);
|
this._startTranscribing(cs, ep, channel);
|
||||||
}
|
}
|
||||||
@@ -765,12 +721,6 @@ class TaskTranscribe extends SttTask {
|
|||||||
this._onVendorError(cs, _ep, {error: JSON.stringify(e)});
|
this._onVendorError(cs, _ep, {error: JSON.stringify(e)});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onOpenAIErrror(cs, _ep, evt) {
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const {message, ...e} = evt;
|
|
||||||
this._onVendorError(cs, _ep, {error: JSON.stringify(e)});
|
|
||||||
}
|
|
||||||
|
|
||||||
_startAsrTimer(channel) {
|
_startAsrTimer(channel) {
|
||||||
if (this.vendor === 'deepgram') return; // no need
|
if (this.vendor === 'deepgram') return; // no need
|
||||||
assert(this.isContinuousAsr);
|
assert(this.isContinuousAsr);
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ class TtsTask extends Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullText = Array.isArray(this.text) ? this.text.join(' ') : this.text;
|
|
||||||
if (fullText.length > 0) {
|
|
||||||
cs.emit('botSaid', fullText);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTtsVendorData(cs) {
|
getTtsVendorData(cs) {
|
||||||
@@ -65,6 +60,7 @@ class TtsTask extends Task {
|
|||||||
|
|
||||||
async setTtsStreamingChannelVars(vendor, language, voice, credentials, ep) {
|
async setTtsStreamingChannelVars(vendor, language, voice, credentials, ep) {
|
||||||
const {api_key, model_id, custom_tts_streaming_url, auth_token} = credentials;
|
const {api_key, model_id, custom_tts_streaming_url, auth_token} = credentials;
|
||||||
|
const {stability, similarity_boost, use_speaker_boost, style} = this.options;
|
||||||
let obj;
|
let obj;
|
||||||
|
|
||||||
this.logger.debug({credentials},
|
this.logger.debug({credentials},
|
||||||
@@ -86,7 +82,6 @@ class TtsTask extends Task {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'elevenlabs':
|
case 'elevenlabs':
|
||||||
const {stability, similarity_boost, use_speaker_boost, style, speed} = this.options.voice_settings || {};
|
|
||||||
obj = {
|
obj = {
|
||||||
ELEVENLABS_API_KEY: api_key,
|
ELEVENLABS_API_KEY: api_key,
|
||||||
ELEVENLABS_TTS_STREAMING_MODEL_ID: model_id,
|
ELEVENLABS_TTS_STREAMING_MODEL_ID: model_id,
|
||||||
@@ -96,14 +91,7 @@ class TtsTask extends Task {
|
|||||||
...(stability && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_STABILITY: stability}),
|
...(stability && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_STABILITY: stability}),
|
||||||
...(similarity_boost && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_SIMILARITY_BOOST: similarity_boost}),
|
...(similarity_boost && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_SIMILARITY_BOOST: similarity_boost}),
|
||||||
...(use_speaker_boost && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_USE_SPEAKER_BOOST: use_speaker_boost}),
|
...(use_speaker_boost && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_USE_SPEAKER_BOOST: use_speaker_boost}),
|
||||||
...(style && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_STYLE: style}),
|
...(style && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_STYLE: style})
|
||||||
// speed has value 0.7 to 1.2, 1.0 is default, make sure we send the value event it's 0
|
|
||||||
...(speed !== null && speed !== undefined && {ELEVENLABS_TTS_STREAMING_VOICE_SETTINGS_SPEED: `${speed}`}),
|
|
||||||
...(this.options.pronunciation_dictionary_locators &&
|
|
||||||
Array.isArray(this.options.pronunciation_dictionary_locators) && {
|
|
||||||
ELEVENLABS_TTS_STREAMING_PRONUNCIATION_DICTIONARY_LOCATORS:
|
|
||||||
JSON.stringify(this.options.pronunciation_dictionary_locators)
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'rimelabs':
|
case 'rimelabs':
|
||||||
@@ -137,7 +125,7 @@ class TtsTask extends Task {
|
|||||||
throw new Error(`vendor ${vendor} is not supported for tts streaming yet`);
|
throw new Error(`vendor ${vendor} is not supported for tts streaming yet`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.logger.debug({vendor, credentials, obj}, 'setTtsStreamingChannelVars');
|
this.logger.info({vendor, credentials, obj}, 'setTtsStreamingChannelVars');
|
||||||
|
|
||||||
await ep.set(obj);
|
await ep.set(obj);
|
||||||
}
|
}
|
||||||
@@ -155,14 +143,15 @@ class TtsTask extends Task {
|
|||||||
`No text-to-speech service credentials for ${vendor} with labels: ${label} have been configured`);
|
`No text-to-speech service credentials for ${vendor} with labels: ${label} have been configured`);
|
||||||
}
|
}
|
||||||
/* parse Nuance voices into name and model */
|
/* parse Nuance voices into name and model */
|
||||||
|
let model;
|
||||||
if (vendor === 'nuance' && voice) {
|
if (vendor === 'nuance' && voice) {
|
||||||
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
|
const arr = /([A-Za-z-]*)\s+-\s+(enhanced|standard)/.exec(voice);
|
||||||
if (arr) {
|
if (arr) {
|
||||||
voice = arr[1];
|
voice = arr[1];
|
||||||
this.model = arr[2];
|
model = arr[2];
|
||||||
}
|
}
|
||||||
} else if (vendor === 'deepgram') {
|
} else if (vendor === 'deepgram') {
|
||||||
this.model = voice;
|
model = voice;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* allow for microsoft custom region voice and api_key to be specified as an override */
|
/* allow for microsoft custom region voice and api_key to be specified as an override */
|
||||||
@@ -204,12 +193,8 @@ class TtsTask extends Task {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (vendor === 'cartesia') {
|
|
||||||
credentials.model_id = this.options.model_id || credentials.model_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.model_id = credentials.model_id;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* note on cache_speech_handles. This was found to be risky.
|
* note on cache_speech_handles. This was found to be risky.
|
||||||
* It can cause a crash in the following sequence on a single call:
|
* It can cause a crash in the following sequence on a single call:
|
||||||
@@ -230,8 +215,7 @@ class TtsTask extends Task {
|
|||||||
// If vendor is changed from the previous one, then reset the cache_speech_handles flag
|
// If vendor is changed from the previous one, then reset the cache_speech_handles flag
|
||||||
//cs.currentTtsVendor = vendor;
|
//cs.currentTtsVendor = vendor;
|
||||||
|
|
||||||
if (!preCache && !this._disableTracing)
|
if (!preCache && !this._disableTracing) this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
|
||||||
this.logger.debug({vendor, language, voice, model: this.model}, 'TaskSay:exec');
|
|
||||||
try {
|
try {
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
writeAlerts({
|
writeAlerts({
|
||||||
@@ -266,7 +250,7 @@ class TtsTask extends Task {
|
|||||||
language,
|
language,
|
||||||
voice,
|
voice,
|
||||||
engine,
|
engine,
|
||||||
model: this.model,
|
model,
|
||||||
salt,
|
salt,
|
||||||
credentials,
|
credentials,
|
||||||
options: this.options,
|
options: this.options,
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ if (VMD_HINTS_FILE) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Amd extends Emitter {
|
class Amd extends Emitter {
|
||||||
constructor(logger, cs, opts) {
|
constructor(logger, cs, opts) {
|
||||||
super();
|
super();
|
||||||
@@ -69,8 +68,6 @@ class Amd extends Emitter {
|
|||||||
this.getIbmAccessToken = getIbmAccessToken;
|
this.getIbmAccessToken = getIbmAccessToken;
|
||||||
const {setChannelVarsForStt} = require('./transcription-utils')(logger);
|
const {setChannelVarsForStt} = require('./transcription-utils')(logger);
|
||||||
this.setChannelVarsForStt = setChannelVarsForStt;
|
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||||
this.digitCount = opts.digitCount || 0;
|
|
||||||
this.numberRegEx = RegExp(`[0-9]{${this.digitCount}}`);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
noSpeechTimeoutMs = 5000,
|
noSpeechTimeoutMs = 5000,
|
||||||
@@ -166,14 +163,6 @@ class Amd extends Emitter {
|
|||||||
language: t.language_code
|
language: t.language_code
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (this.digitCount != 0 && this.numberRegEx.test(t.alternatives[0].transcript)) {
|
|
||||||
/* a string of numbers is typically a machine */
|
|
||||||
this.emit(this.decision = AmdEvents.MachineDetected, {
|
|
||||||
reason: 'digit count',
|
|
||||||
greeting: t.alternatives[0].transcript,
|
|
||||||
language: t.language_code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (final && wordCount < this.thresholdWordCount) {
|
else if (final && wordCount < this.thresholdWordCount) {
|
||||||
/* a short greeting is typically a human */
|
/* a short greeting is typically a human */
|
||||||
this.emit(this.decision = AmdEvents.HumanDetected, {
|
this.emit(this.decision = AmdEvents.HumanDetected, {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const assert = require('assert');
|
|||||||
const {
|
const {
|
||||||
AWS_REGION,
|
AWS_REGION,
|
||||||
AWS_SNS_PORT: PORT,
|
AWS_SNS_PORT: PORT,
|
||||||
AWS_SNS_TOPIC_ARN,
|
AWS_SNS_TOPIC_ARM,
|
||||||
AWS_SNS_PORT_MAX,
|
AWS_SNS_PORT_MAX,
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
const {LifeCycleEvents} = require('./constants');
|
const {LifeCycleEvents} = require('./constants');
|
||||||
@@ -55,12 +55,12 @@ class SnsNotifier extends Emitter {
|
|||||||
async _handlePost(req, res) {
|
async _handlePost(req, res) {
|
||||||
try {
|
try {
|
||||||
const parsedBody = JSON.parse(req.body);
|
const parsedBody = JSON.parse(req.body);
|
||||||
this.logger.info({headers: req.headers, body: parsedBody}, 'Received HTTP POST from AWS');
|
this.logger.debug({headers: req.headers, body: parsedBody}, 'Received HTTP POST from AWS');
|
||||||
if (!validatePayload(parsedBody)) {
|
if (!validatePayload(parsedBody)) {
|
||||||
this.logger.info('incoming AWS SNS HTTP POST failed signature validation');
|
this.logger.info('incoming AWS SNS HTTP POST failed signature validation');
|
||||||
return res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
}
|
}
|
||||||
this.logger.info('incoming HTTP POST passed validation');
|
this.logger.debug('incoming HTTP POST passed validation');
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
|
|
||||||
switch (parsedBody.Type) {
|
switch (parsedBody.Type) {
|
||||||
@@ -74,18 +74,7 @@ class SnsNotifier extends Emitter {
|
|||||||
subscriptionRequestId: this.subscriptionRequestId
|
subscriptionRequestId: this.subscriptionRequestId
|
||||||
}, 'response from SNS SubscribeURL');
|
}, 'response from SNS SubscribeURL');
|
||||||
const data = await this.describeInstance();
|
const data = await this.describeInstance();
|
||||||
|
this.lifecycleState = data.AutoScalingGroups[0].Instances[0].LifecycleState;
|
||||||
const group = data.AutoScalingGroups.find((group) =>
|
|
||||||
group.Instances && group.Instances.some((instance) => instance.InstanceId === this.instanceId)
|
|
||||||
);
|
|
||||||
if (!group) {
|
|
||||||
this.logger.error('Current instance not found in any Auto Scaling group', data);
|
|
||||||
} else {
|
|
||||||
const instance = group.Instances.find((instance) => instance.InstanceId === this.instanceId);
|
|
||||||
this.lifecycleState = instance.LifecycleState;
|
|
||||||
}
|
|
||||||
|
|
||||||
//this.lifecycleState = data.AutoScalingGroups[0].Instances[0].LifecycleState;
|
|
||||||
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
this.emit('SubscriptionConfirmation', {publicIp: this.publicIp});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -105,7 +94,7 @@ class SnsNotifier extends Emitter {
|
|||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.logger.info(`SnsNotifier - instance ${msg.EC2InstanceId} is scaling in (not us)`);
|
this.logger.debug(`SnsNotifier - instance ${msg.EC2InstanceId} is scaling in (not us)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -122,7 +111,7 @@ class SnsNotifier extends Emitter {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
this.logger.info('SnsNotifier: retrieving instance data');
|
this.logger.debug('SnsNotifier: retrieving instance data');
|
||||||
this.instanceId = await getString('http://169.254.169.254/latest/meta-data/instance-id');
|
this.instanceId = await getString('http://169.254.169.254/latest/meta-data/instance-id');
|
||||||
this.publicIp = await getString('http://169.254.169.254/latest/meta-data/public-ipv4');
|
this.publicIp = await getString('http://169.254.169.254/latest/meta-data/public-ipv4');
|
||||||
this.logger.info({
|
this.logger.info({
|
||||||
@@ -153,13 +142,13 @@ class SnsNotifier extends Emitter {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
Protocol: 'http',
|
Protocol: 'http',
|
||||||
TopicArn: AWS_SNS_TOPIC_ARN,
|
TopicArn: AWS_SNS_TOPIC_ARM,
|
||||||
Endpoint: this.snsEndpoint
|
Endpoint: this.snsEndpoint
|
||||||
};
|
};
|
||||||
const response = await snsClient.send(new SubscribeCommand(params));
|
const response = await snsClient.send(new SubscribeCommand(params));
|
||||||
this.logger.info({response}, `response to SNS subscribe to ${AWS_SNS_TOPIC_ARN}`);
|
this.logger.info({response}, `response to SNS subscribe to ${AWS_SNS_TOPIC_ARM}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error({err}, `Error subscribing to SNS topic arn ${AWS_SNS_TOPIC_ARN}`);
|
this.logger.error({err}, `Error subscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +159,9 @@ class SnsNotifier extends Emitter {
|
|||||||
SubscriptionArn: this.subscriptionArn
|
SubscriptionArn: this.subscriptionArn
|
||||||
};
|
};
|
||||||
const response = await snsClient.send(new UnsubscribeCommand(params));
|
const response = await snsClient.send(new UnsubscribeCommand(params));
|
||||||
this.logger.info({response}, `response to SNS unsubscribe to ${AWS_SNS_TOPIC_ARN}`);
|
this.logger.info({response}, `response to SNS unsubscribe to ${AWS_SNS_TOPIC_ARM}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${AWS_SNS_TOPIC_ARN}`);
|
this.logger.error({err}, `Error unsubscribing to SNS topic arn ${AWS_SNS_TOPIC_ARM}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class BackgroundTaskManager extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initiate Listen
|
// Initiate Listen
|
||||||
async _initListen(opts, bugname = 'jambonz-background-listen', ignoreCustomerData = false, type = 'listen') {
|
async _initListen(opts, bugname = 'jambonz-background-listen', ignoreCustomerData = true, type = 'listen') {
|
||||||
let task;
|
let task;
|
||||||
try {
|
try {
|
||||||
const t = normalizeJambones(this.logger, [opts]);
|
const t = normalizeJambones(this.logger, [opts]);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const Emitter = require('events');
|
const Emitter = require('events');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const parseUrl = require('parse-url');
|
|
||||||
const timeSeries = require('@jambonz/time-series');
|
const timeSeries = require('@jambonz/time-series');
|
||||||
const {NODE_ENV, JAMBONES_TIME_SERIES_HOST} = require('../config');
|
const {NODE_ENV, JAMBONES_TIME_SERIES_HOST} = require('../config');
|
||||||
let alerter ;
|
let alerter ;
|
||||||
@@ -22,10 +21,6 @@ class BaseRequestor extends Emitter {
|
|||||||
const {stats} = require('../../').srf.locals;
|
const {stats} = require('../../').srf.locals;
|
||||||
this.stats = stats;
|
this.stats = stats;
|
||||||
|
|
||||||
const u = this._parsedUrl = parseUrl(this.url);
|
|
||||||
if (u.port) this._baseUrl = `${u.protocol}://${u.resource}:${u.port}`;
|
|
||||||
else this._baseUrl = `${u.protocol}://${u.resource}`;
|
|
||||||
|
|
||||||
if (!alerter) {
|
if (!alerter) {
|
||||||
alerter = timeSeries(logger, {
|
alerter = timeSeries(logger, {
|
||||||
host: JAMBONES_TIME_SERIES_HOST,
|
host: JAMBONES_TIME_SERIES_HOST,
|
||||||
@@ -35,10 +30,6 @@ class BaseRequestor extends Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get baseUrl() {
|
|
||||||
return this._baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
get Alerter() {
|
get Alerter() {
|
||||||
return alerter;
|
return alerter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"Tag": "tag",
|
"Tag": "tag",
|
||||||
"Transcribe": "transcribe"
|
"Transcribe": "transcribe"
|
||||||
},
|
},
|
||||||
"AllowedSipRecVerbs": ["answer", "config", "gather", "transcribe", "listen", "tag", "hangup", "sip:decline"],
|
"AllowedSipRecVerbs": ["answer", "config", "gather", "transcribe", "listen", "tag"],
|
||||||
"AllowedConfirmSessionVerbs": ["config", "gather", "plays", "say", "tag"],
|
"AllowedConfirmSessionVerbs": ["config", "gather", "plays", "say", "tag"],
|
||||||
"CallStatus": {
|
"CallStatus": {
|
||||||
"Trying": "trying",
|
"Trying": "trying",
|
||||||
@@ -137,18 +137,6 @@
|
|||||||
"Connect": "speechmatics_transcribe::connect",
|
"Connect": "speechmatics_transcribe::connect",
|
||||||
"Error": "speechmatics_transcribe::error"
|
"Error": "speechmatics_transcribe::error"
|
||||||
},
|
},
|
||||||
"OpenAITranscriptionEvents": {
|
|
||||||
"Transcription": "openai_transcribe::transcription",
|
|
||||||
"Translation": "openai_transcribe::translation",
|
|
||||||
"SpeechStarted": "openai_transcribe::speech_started",
|
|
||||||
"SpeechStopped": "openai_transcribe::speech_stopped",
|
|
||||||
"PartialTranscript": "openai_transcribe::partial_transcript",
|
|
||||||
"Info": "openai_transcribe::info",
|
|
||||||
"RecognitionStarted": "openai_transcribe::recognition_started",
|
|
||||||
"ConnectFailure": "openai_transcribe::connect_failed",
|
|
||||||
"Connect": "openai_transcribe::connect",
|
|
||||||
"Error": "openai_transcribe::error"
|
|
||||||
},
|
|
||||||
"JambonzTranscriptionEvents": {
|
"JambonzTranscriptionEvents": {
|
||||||
"Transcription": "jambonz_transcribe::transcription",
|
"Transcription": "jambonz_transcribe::transcription",
|
||||||
"ConnectFailure": "jambonz_transcribe::connect_failed",
|
"ConnectFailure": "jambonz_transcribe::connect_failed",
|
||||||
@@ -161,12 +149,6 @@
|
|||||||
"ConnectFailure": "assemblyai_transcribe::connect_failed",
|
"ConnectFailure": "assemblyai_transcribe::connect_failed",
|
||||||
"Connect": "assemblyai_transcribe::connect"
|
"Connect": "assemblyai_transcribe::connect"
|
||||||
},
|
},
|
||||||
"VoxistTranscriptionEvents": {
|
|
||||||
"Transcription": "voxist_transcribe::transcription",
|
|
||||||
"Error": "voxist_transcribe::error",
|
|
||||||
"ConnectFailure": "voxist_transcribe::connect_failed",
|
|
||||||
"Connect": "voxist_transcribe::connect"
|
|
||||||
},
|
|
||||||
"VadDetection": {
|
"VadDetection": {
|
||||||
"Detection": "vad_detect:detection"
|
"Detection": "vad_detect:detection"
|
||||||
},
|
},
|
||||||
@@ -194,13 +176,6 @@
|
|||||||
"Disconnect": "openai_s2s::disconnect",
|
"Disconnect": "openai_s2s::disconnect",
|
||||||
"ServerEvent": "openai_s2s::server_event"
|
"ServerEvent": "openai_s2s::server_event"
|
||||||
},
|
},
|
||||||
"LlmEvents_Elevenlabs": {
|
|
||||||
"Error": "error",
|
|
||||||
"Connect": "elevenlabs_s2s::connect",
|
|
||||||
"ConnectFailure": "elevenlabs_s2s::connect_failed",
|
|
||||||
"Disconnect": "elevenlabs_s2s::disconnect",
|
|
||||||
"ServerEvent": "elevenlabs_s2s::server_event"
|
|
||||||
},
|
|
||||||
"LlmEvents_VoiceAgent": {
|
"LlmEvents_VoiceAgent": {
|
||||||
"Error": "error",
|
"Error": "error",
|
||||||
"Connect": "voice_agent_s2s::connect",
|
"Connect": "voice_agent_s2s::connect",
|
||||||
|
|||||||
@@ -122,10 +122,6 @@ const speechMapper = (cred) => {
|
|||||||
const o = JSON.parse(decrypt(credential));
|
const o = JSON.parse(decrypt(credential));
|
||||||
obj.api_key = o.api_key;
|
obj.api_key = o.api_key;
|
||||||
}
|
}
|
||||||
else if ('voxist' === obj.vendor) {
|
|
||||||
const o = JSON.parse(decrypt(credential));
|
|
||||||
obj.api_key = o.api_key;
|
|
||||||
}
|
|
||||||
else if ('whisper' === obj.vendor) {
|
else if ('whisper' === 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;
|
||||||
@@ -142,11 +138,6 @@ const speechMapper = (cred) => {
|
|||||||
obj.api_key = o.api_key;
|
obj.api_key = o.api_key;
|
||||||
obj.speechmatics_stt_uri = o.speechmatics_stt_uri;
|
obj.speechmatics_stt_uri = o.speechmatics_stt_uri;
|
||||||
}
|
}
|
||||||
else if ('openai' === obj.vendor) {
|
|
||||||
const o = JSON.parse(decrypt(credential));
|
|
||||||
obj.api_key = o.api_key;
|
|
||||||
obj.model_id = o.model_id;
|
|
||||||
}
|
|
||||||
else if (obj.vendor.startsWith('custom:')) {
|
else if (obj.vendor.startsWith('custom:')) {
|
||||||
const o = JSON.parse(decrypt(credential));
|
const o = JSON.parse(decrypt(credential));
|
||||||
obj.auth_token = o.auth_token;
|
obj.auth_token = o.auth_token;
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
const sleepFor = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
|
||||||
module.exports = {
|
|
||||||
sleepFor
|
|
||||||
};
|
|
||||||
@@ -48,6 +48,8 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
assert(['GET', 'POST'].includes(this.method));
|
assert(['GET', 'POST'].includes(this.method));
|
||||||
|
|
||||||
const u = this._parsedUrl = parseUrl(this.url);
|
const u = this._parsedUrl = parseUrl(this.url);
|
||||||
|
if (u.port) this._baseUrl = `${u.protocol}://${u.resource}:${u.port}`;
|
||||||
|
else this._baseUrl = `${u.protocol}://${u.resource}`;
|
||||||
this._protocol = u.protocol;
|
this._protocol = u.protocol;
|
||||||
this._resource = u.resource;
|
this._resource = u.resource;
|
||||||
this._port = u.port;
|
this._port = u.port;
|
||||||
@@ -55,18 +57,18 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
this._usePools = HTTP_POOL && parseInt(HTTP_POOL);
|
this._usePools = HTTP_POOL && parseInt(HTTP_POOL);
|
||||||
|
|
||||||
if (this._usePools) {
|
if (this._usePools) {
|
||||||
if (pools.has(this.baseUrl)) {
|
if (pools.has(this._baseUrl)) {
|
||||||
this.client = pools.get(this.baseUrl);
|
this.client = pools.get(this._baseUrl);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const connections = HTTP_POOLSIZE ? parseInt(HTTP_POOLSIZE) : 10;
|
const connections = HTTP_POOLSIZE ? parseInt(HTTP_POOLSIZE) : 10;
|
||||||
const pipelining = HTTP_PIPELINING ? parseInt(HTTP_PIPELINING) : 1;
|
const pipelining = HTTP_PIPELINING ? parseInt(HTTP_PIPELINING) : 1;
|
||||||
const pool = this.client = new Pool(this.baseUrl, {
|
const pool = this.client = new Pool(this._baseUrl, {
|
||||||
connections,
|
connections,
|
||||||
pipelining
|
pipelining
|
||||||
});
|
});
|
||||||
pools.set(this.baseUrl, pool);
|
pools.set(this._baseUrl, pool);
|
||||||
this.logger.debug(`HttpRequestor:created pool for ${this.baseUrl}`);
|
this.logger.debug(`HttpRequestor:created pool for ${this._baseUrl}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -87,6 +89,10 @@ class HttpRequestor extends BaseRequestor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get baseUrl() {
|
||||||
|
return this._baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
if (!this._usePools && !this.client?.closed) this.client.close();
|
if (!this._usePools && !this.client?.closed) this.client.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,26 +31,18 @@ function getLocalIp() {
|
|||||||
return '127.0.0.1'; // Fallback to localhost if no suitable interface found
|
return '127.0.0.1'; // Fallback to localhost if no suitable interface found
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMS(logger, wrapper, ms, {
|
function initMS(logger, wrapper, ms) {
|
||||||
onFreeswitchConnect,
|
|
||||||
onFreeswitchDisconnect
|
|
||||||
}) {
|
|
||||||
Object.assign(wrapper, {ms, active: true, connects: 1});
|
Object.assign(wrapper, {ms, active: true, connects: 1});
|
||||||
logger.info(`connected to freeswitch at ${ms.address}`);
|
logger.info(`connected to freeswitch at ${ms.address}`);
|
||||||
|
|
||||||
onFreeswitchConnect(wrapper);
|
|
||||||
|
|
||||||
ms.conn
|
ms.conn
|
||||||
.on('esl::end', () => {
|
.on('esl::end', () => {
|
||||||
wrapper.active = false;
|
wrapper.active = false;
|
||||||
wrapper.connects = 0;
|
|
||||||
logger.info(`lost connection to freeswitch at ${ms.address}`);
|
logger.info(`lost connection to freeswitch at ${ms.address}`);
|
||||||
onFreeswitchDisconnect(wrapper);
|
|
||||||
ms.removeAllListeners();
|
|
||||||
})
|
})
|
||||||
.on('esl::ready', () => {
|
.on('esl::ready', () => {
|
||||||
if (wrapper.connects > 0) {
|
if (wrapper.connects > 0) {
|
||||||
logger.info(`esl::ready connected to freeswitch at ${ms.address}`);
|
logger.info(`connected to freeswitch at ${ms.address}`);
|
||||||
}
|
}
|
||||||
wrapper.connects = 1;
|
wrapper.connects = 1;
|
||||||
wrapper.active = true;
|
wrapper.active = true;
|
||||||
@@ -64,10 +56,7 @@ function initMS(logger, wrapper, ms, {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function installSrfLocals(srf, logger, {
|
function installSrfLocals(srf, logger) {
|
||||||
onFreeswitchConnect = () => {},
|
|
||||||
onFreeswitchDisconnect = () => {}
|
|
||||||
}) {
|
|
||||||
logger.debug('installing srf locals');
|
logger.debug('installing srf locals');
|
||||||
assert(!srf.locals.dbHelpers);
|
assert(!srf.locals.dbHelpers);
|
||||||
const {tracer} = srf.locals.otel;
|
const {tracer} = srf.locals.otel;
|
||||||
@@ -102,10 +91,7 @@ function installSrfLocals(srf, logger, {
|
|||||||
mediaservers.push(val);
|
mediaservers.push(val);
|
||||||
try {
|
try {
|
||||||
const ms = await mrf.connect(fs);
|
const ms = await mrf.connect(fs);
|
||||||
initMS(logger, val, ms, {
|
initMS(logger, val, ms);
|
||||||
onFreeswitchConnect,
|
|
||||||
onFreeswitchDisconnect
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
logger.info({err}, `failed connecting to freeswitch at ${fs.address}, will retry shortly: ${err.message}`);
|
logger.info({err}, `failed connecting to freeswitch at ${fs.address}, will retry shortly: ${err.message}`);
|
||||||
@@ -116,15 +102,9 @@ function installSrfLocals(srf, logger, {
|
|||||||
for (const val of mediaservers) {
|
for (const val of mediaservers) {
|
||||||
if (val.connects === 0) {
|
if (val.connects === 0) {
|
||||||
try {
|
try {
|
||||||
// make sure all listeners are removed before reconnecting
|
|
||||||
val.ms?.disconnect();
|
|
||||||
val.ms = null;
|
|
||||||
logger.info({mediaserver: val.opts}, 'Retrying initial connection to media server');
|
logger.info({mediaserver: val.opts}, 'Retrying initial connection to media server');
|
||||||
const ms = await mrf.connect(val.opts);
|
const ms = await mrf.connect(val.opts);
|
||||||
initMS(logger, val, ms, {
|
initMS(logger, val, ms);
|
||||||
onFreeswitchConnect,
|
|
||||||
onFreeswitchDisconnect
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info({err}, `failed connecting to freeswitch at ${val.opts.address}, will retry shortly`);
|
logger.info({err}, `failed connecting to freeswitch at ${val.opts.address}, will retry shortly`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,12 @@ const RootSpan = require('./call-tracer');
|
|||||||
const uuidv4 = require('uuid-random');
|
const uuidv4 = require('uuid-random');
|
||||||
const HttpRequestor = require('./http-requestor');
|
const HttpRequestor = require('./http-requestor');
|
||||||
const WsRequestor = require('./ws-requestor');
|
const WsRequestor = require('./ws-requestor');
|
||||||
const {makeOpusFirst, removeVideoSdp} = require('./sdp-utils');
|
const {makeOpusFirst} = require('./sdp-utils');
|
||||||
const {
|
const {
|
||||||
JAMBONES_USE_FREESWITCH_TIMER_FD,
|
JAMBONES_USE_FREESWITCH_TIMER_FD,
|
||||||
JAMBONES_MEDIA_TIMEOUT_MS,
|
JAMBONES_MEDIA_TIMEOUT_MS,
|
||||||
JAMBONES_MEDIA_HOLD_TIMEOUT_MS,
|
JAMBONES_MEDIA_HOLD_TIMEOUT_MS
|
||||||
JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS
|
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
const { sleepFor } = require('./helpers');
|
|
||||||
|
|
||||||
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,
|
||||||
@@ -154,21 +152,15 @@ class SingleDialer extends Emitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let lastSdp;
|
let lastSdp;
|
||||||
const connectStream = async(remoteSdp, isVideoCall) => {
|
const connectStream = async(remoteSdp) => {
|
||||||
if (remoteSdp === lastSdp) return;
|
if (remoteSdp === lastSdp) return;
|
||||||
if (process.env.JAMBONES_VIDEO_CALLS_ENABLED_IN_FS && !isVideoCall) {
|
|
||||||
remoteSdp = removeVideoSdp(remoteSdp);
|
|
||||||
}
|
|
||||||
lastSdp = remoteSdp;
|
lastSdp = remoteSdp;
|
||||||
return this.ep.modify(remoteSdp);
|
return this.ep.modify(remoteSdp);
|
||||||
};
|
};
|
||||||
let localSdp = this.ep.local.sdp;
|
|
||||||
if (process.env.JAMBONES_VIDEO_CALLS_ENABLED_IN_FS && !opts.isVideoCall) {
|
|
||||||
localSdp = removeVideoSdp(localSdp);
|
|
||||||
}
|
|
||||||
Object.assign(opts, {
|
Object.assign(opts, {
|
||||||
proxy: `sip:${this.sbcAddress}`,
|
proxy: `sip:${this.sbcAddress}`,
|
||||||
localSdp: opts.opusFirst ? makeOpusFirst(localSdp) : localSdp
|
localSdp: opts.opusFirst ? makeOpusFirst(this.ep.local.sdp) : this.ep.local.sdp
|
||||||
});
|
});
|
||||||
if (this.target.auth) opts.auth = this.target.auth;
|
if (this.target.auth) opts.auth = this.target.auth;
|
||||||
inviteSpan = this.startSpan('invite', {
|
inviteSpan = this.startSpan('invite', {
|
||||||
@@ -230,13 +222,13 @@ class SingleDialer extends Emitter {
|
|||||||
status.callStatus = CallStatus.EarlyMedia;
|
status.callStatus = CallStatus.EarlyMedia;
|
||||||
this.emit('earlyMedia');
|
this.emit('earlyMedia');
|
||||||
}
|
}
|
||||||
connectStream(prov.body, opts.isVideoCall);
|
connectStream(prov.body);
|
||||||
}
|
}
|
||||||
else status.callStatus = CallStatus.Ringing;
|
else status.callStatus = CallStatus.Ringing;
|
||||||
this.emit('callStatusChange', status);
|
this.emit('callStatusChange', status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await connectStream(this.dlg.remote.sdp, opts.isVideoCall);
|
await connectStream(this.dlg.remote.sdp);
|
||||||
this.dlg.callSid = this.callSid;
|
this.dlg.callSid = this.callSid;
|
||||||
this.inviteInProgress = null;
|
this.inviteInProgress = null;
|
||||||
this.emit('callStatusChange', {
|
this.emit('callStatusChange', {
|
||||||
@@ -279,12 +271,7 @@ class SingleDialer extends Emitter {
|
|||||||
this.logger.info('dial is onhold, emit event');
|
this.logger.info('dial is onhold, emit event');
|
||||||
this.emit('reinvite', req, res);
|
this.emit('reinvite', req, res);
|
||||||
} else {
|
} else {
|
||||||
let newSdp = await this.ep.modify(req.body);
|
const newSdp = await this.ep.modify(req.body);
|
||||||
// in case of reINVITE if video call is enabled in FS and the call is not a video call,
|
|
||||||
// remove video media from the SDP
|
|
||||||
if (process.env.JAMBONES_VIDEO_CALLS_ENABLED_IN_FS && !this.opts?.isVideoCall) {
|
|
||||||
newSdp = removeVideoSdp(newSdp);
|
|
||||||
}
|
|
||||||
res.send(200, {body: newSdp});
|
res.send(200, {body: newSdp});
|
||||||
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
||||||
}
|
}
|
||||||
@@ -371,24 +358,6 @@ class SingleDialer extends Emitter {
|
|||||||
this.logger.info(err, 'place-outdial:_configMsEndpoint - error enable inband DTMF');
|
this.logger.info(err, 'place-outdial:_configMsEndpoint - error enable inband DTMF');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const origDestroy = this.ep.destroy.bind(this.ep);
|
|
||||||
this.ep.destroy = async() => {
|
|
||||||
try {
|
|
||||||
if (this.dialTask.transcribeTask && JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS) {
|
|
||||||
// transcribe task is being used, wait for some time before destroy
|
|
||||||
// if final transcription is received but endpoint is already closed,
|
|
||||||
// freeswitch module will not be able to send the transcription
|
|
||||||
|
|
||||||
this.logger.info('SingleDialer:_configMsEndpoint -' +
|
|
||||||
' Dial with transcribe task, wait for some time before destroy');
|
|
||||||
await sleepFor(JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS);
|
|
||||||
}
|
|
||||||
await origDestroy();
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(err, 'SingleDialer:_configMsEndpoint - error destroying endpoint');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -440,10 +409,7 @@ class SingleDialer extends Emitter {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.debug(err, 'SingleDialer:_executeApp: error');
|
this.logger.debug(err, 'SingleDialer:_executeApp: error');
|
||||||
this.emit('decline');
|
this.emit('decline');
|
||||||
if (this.dlg.connected) {
|
if (this.dlg.connected) this.dlg.destroy();
|
||||||
this.dlg.destroy();
|
|
||||||
this.ep.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,8 +536,7 @@ function placeOutdial({
|
|||||||
}) {
|
}) {
|
||||||
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, myOpts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask, onHoldMusic
|
||||||
accountInfo, rootSpan, startSpan, dialTask, onHoldMusic
|
|
||||||
});
|
});
|
||||||
sd.exec(srf, ms, myOpts);
|
sd.exec(srf, ms, myOpts);
|
||||||
return sd;
|
return sd;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const {
|
|||||||
JAMBONES_SBCS,
|
JAMBONES_SBCS,
|
||||||
K8S,
|
K8S,
|
||||||
K8S_SBC_SIP_SERVICE_NAME,
|
K8S_SBC_SIP_SERVICE_NAME,
|
||||||
AWS_SNS_TOPIC_ARN,
|
AWS_SNS_TOPIC_ARM,
|
||||||
OPTIONS_PING_INTERVAL,
|
OPTIONS_PING_INTERVAL,
|
||||||
AWS_REGION,
|
AWS_REGION,
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
@@ -35,7 +35,7 @@ module.exports = (logger) => {
|
|||||||
// listen for SNS lifecycle changes
|
// listen for SNS lifecycle changes
|
||||||
let lifecycleEmitter = new Emitter();
|
let lifecycleEmitter = new Emitter();
|
||||||
let dryUpCalls = false;
|
let dryUpCalls = false;
|
||||||
if (AWS_SNS_TOPIC_ARN && AWS_REGION) {
|
if (AWS_SNS_TOPIC_ARM && AWS_REGION) {
|
||||||
|
|
||||||
(async function() {
|
(async function() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -35,12 +35,6 @@ const makeOpusFirst = (sdp) => {
|
|||||||
}
|
}
|
||||||
return sdpTransform.write(parsedSdp);
|
return sdpTransform.write(parsedSdp);
|
||||||
};
|
};
|
||||||
const removeVideoSdp = (sdp) => {
|
|
||||||
const parsedSdp = sdpTransform.parse(sdp);
|
|
||||||
// Filter out video media sections, keeping only non-video media
|
|
||||||
parsedSdp.media = parsedSdp.media.filter((media) => media.type !== 'video');
|
|
||||||
return sdpTransform.write(parsedSdp);
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractSdpMedia = (sdp) => {
|
const extractSdpMedia = (sdp) => {
|
||||||
const parsedSdp1 = sdpTransform.parse(sdp);
|
const parsedSdp1 = sdpTransform.parse(sdp);
|
||||||
@@ -60,6 +54,5 @@ module.exports = {
|
|||||||
mergeSdpMedia,
|
mergeSdpMedia,
|
||||||
extractSdpMedia,
|
extractSdpMedia,
|
||||||
isOpusFirst,
|
isOpusFirst,
|
||||||
makeOpusFirst,
|
makeOpusFirst
|
||||||
removeVideoSdp
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const stickyVars = {
|
|||||||
'DEEPGRAM_SPEECH_TIER',
|
'DEEPGRAM_SPEECH_TIER',
|
||||||
'DEEPGRAM_SPEECH_MODEL',
|
'DEEPGRAM_SPEECH_MODEL',
|
||||||
'DEEPGRAM_SPEECH_ENABLE_SMART_FORMAT',
|
'DEEPGRAM_SPEECH_ENABLE_SMART_FORMAT',
|
||||||
'DEEPGRAM_SPEECH_ENABLE_NO_DELAY',
|
|
||||||
'DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
|
'DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION',
|
||||||
'DEEPGRAM_SPEECH_PROFANITY_FILTER',
|
'DEEPGRAM_SPEECH_PROFANITY_FILTER',
|
||||||
'DEEPGRAM_SPEECH_REDACT',
|
'DEEPGRAM_SPEECH_REDACT',
|
||||||
@@ -45,8 +44,7 @@ const stickyVars = {
|
|||||||
'DEEPGRAM_SPEECH_VAD_TURNOFF',
|
'DEEPGRAM_SPEECH_VAD_TURNOFF',
|
||||||
'DEEPGRAM_SPEECH_TAG',
|
'DEEPGRAM_SPEECH_TAG',
|
||||||
'DEEPGRAM_SPEECH_MODEL_VERSION',
|
'DEEPGRAM_SPEECH_MODEL_VERSION',
|
||||||
'DEEPGRAM_SPEECH_FILLER_WORDS',
|
'DEEPGRAM_SPEECH_FILLER_WORDS'
|
||||||
'DEEPGRAM_SPEECH_KEYTERMS',
|
|
||||||
],
|
],
|
||||||
aws: [
|
aws: [
|
||||||
'AWS_VOCABULARY_NAME',
|
'AWS_VOCABULARY_NAME',
|
||||||
@@ -107,9 +105,6 @@ const stickyVars = {
|
|||||||
'ASSEMBLYAI_API_KEY',
|
'ASSEMBLYAI_API_KEY',
|
||||||
'ASSEMBLYAI_WORD_BOOST'
|
'ASSEMBLYAI_WORD_BOOST'
|
||||||
],
|
],
|
||||||
voxist: [
|
|
||||||
'VOXIST_API_KEY',
|
|
||||||
],
|
|
||||||
speechmatics: [
|
speechmatics: [
|
||||||
'SPEECHMATICS_API_KEY',
|
'SPEECHMATICS_API_KEY',
|
||||||
'SPEECHMATICS_HOST',
|
'SPEECHMATICS_HOST',
|
||||||
@@ -117,16 +112,7 @@ const stickyVars = {
|
|||||||
'SPEECHMATICS_SPEECH_HINTS',
|
'SPEECHMATICS_SPEECH_HINTS',
|
||||||
'SPEECHMATICS_TRANSLATION_LANGUAGES',
|
'SPEECHMATICS_TRANSLATION_LANGUAGES',
|
||||||
'SPEECHMATICS_TRANSLATION_PARTIALS'
|
'SPEECHMATICS_TRANSLATION_PARTIALS'
|
||||||
],
|
]
|
||||||
openai: [
|
|
||||||
'OPENAI_API_KEY',
|
|
||||||
'OPENAI_MODEL',
|
|
||||||
'OPENAI_INPUT_AUDIO_NOISE_REDUCTION',
|
|
||||||
'OPENAI_TURN_DETECTION_TYPE',
|
|
||||||
'OPENAI_TURN_DETECTION_THRESHOLD',
|
|
||||||
'OPENAI_TURN_DETECTION_PREFIX_PADDING_MS',
|
|
||||||
'OPENAI_TURN_DETECTION_SILENCE_DURATION_MS',
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -313,18 +299,13 @@ const normalizeDeepgram = (evt, channel, language, shortUtterance) => {
|
|||||||
confidence: alt.confidence,
|
confidence: alt.confidence,
|
||||||
transcript: alt.transcript,
|
transcript: alt.transcript,
|
||||||
}));
|
}));
|
||||||
/**
|
|
||||||
* Some models (nova-2-general) return the detected language in the
|
|
||||||
* alternatives.languages array if the language is set as multi.
|
|
||||||
* If the language is detected, we use it as the language_code.
|
|
||||||
*/
|
|
||||||
const detectedLanguage = evt.channel?.alternatives?.[0]?.languages?.[0];
|
|
||||||
/**
|
/**
|
||||||
* note difference between is_final and speech_final in Deepgram:
|
* note difference between is_final and speech_final in Deepgram:
|
||||||
* https://developers.deepgram.com/docs/understand-endpointing-interim-results
|
* https://developers.deepgram.com/docs/understand-endpointing-interim-results
|
||||||
*/
|
*/
|
||||||
return {
|
return {
|
||||||
language_code: detectedLanguage || language,
|
language_code: language,
|
||||||
channel_tag: channel,
|
channel_tag: channel,
|
||||||
is_final: shortUtterance ? evt.is_final : evt.speech_final,
|
is_final: shortUtterance ? evt.is_final : evt.speech_final,
|
||||||
alternatives: alternatives.length ? [alternatives[0]] : [],
|
alternatives: alternatives.length ? [alternatives[0]] : [],
|
||||||
@@ -536,25 +517,6 @@ const normalizeAssemblyAi = (evt, channel, language) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeVoxist = (evt, channel, language) => {
|
|
||||||
const copy = JSON.parse(JSON.stringify(evt));
|
|
||||||
return {
|
|
||||||
language_code: language,
|
|
||||||
channel_tag: channel,
|
|
||||||
is_final: evt.type === 'final',
|
|
||||||
alternatives: [
|
|
||||||
{
|
|
||||||
confidence: 1.00,
|
|
||||||
transcript: evt.text,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vendor: {
|
|
||||||
name: 'voxist',
|
|
||||||
evt: copy
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeSpeechmatics = (evt, channel, language) => {
|
const normalizeSpeechmatics = (evt, channel, language) => {
|
||||||
const copy = JSON.parse(JSON.stringify(evt));
|
const copy = JSON.parse(JSON.stringify(evt));
|
||||||
const is_final = evt.message === 'AddTranscript';
|
const is_final = evt.message === 'AddTranscript';
|
||||||
@@ -580,35 +542,6 @@ const normalizeSpeechmatics = (evt, channel, language) => {
|
|||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateConfidence = (logprobsArray) => {
|
|
||||||
// Sum the individual log probabilities
|
|
||||||
const totalLogProb = logprobsArray.reduce((sum, tokenInfo) => sum + tokenInfo.logprob, 0);
|
|
||||||
|
|
||||||
// Convert the total log probability back to a regular probability
|
|
||||||
const confidence = Math.exp(totalLogProb);
|
|
||||||
return confidence;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeOpenAI = (evt, channel, language) => {
|
|
||||||
const copy = JSON.parse(JSON.stringify(evt));
|
|
||||||
const obj = {
|
|
||||||
language_code: language,
|
|
||||||
channel_tag: channel,
|
|
||||||
is_final: true,
|
|
||||||
alternatives: [
|
|
||||||
{
|
|
||||||
transcript: evt.transcript,
|
|
||||||
confidence: evt.logprobs ? calculateConfidence(evt.logprobs) : 1.0,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vendor: {
|
|
||||||
name: 'openai',
|
|
||||||
evt: copy
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = (logger) => {
|
module.exports = (logger) => {
|
||||||
const normalizeTranscription = (evt, vendor, channel, language, shortUtterance, punctuation) => {
|
const normalizeTranscription = (evt, vendor, channel, language, shortUtterance, punctuation) => {
|
||||||
|
|
||||||
@@ -634,14 +567,10 @@ module.exports = (logger) => {
|
|||||||
return normalizeCobalt(evt, channel, language);
|
return normalizeCobalt(evt, channel, language);
|
||||||
case 'assemblyai':
|
case 'assemblyai':
|
||||||
return normalizeAssemblyAi(evt, channel, language, shortUtterance);
|
return normalizeAssemblyAi(evt, channel, language, shortUtterance);
|
||||||
case 'voxist':
|
|
||||||
return normalizeVoxist(evt, channel, language);
|
|
||||||
case 'verbio':
|
case 'verbio':
|
||||||
return normalizeVerbio(evt, channel, language);
|
return normalizeVerbio(evt, channel, language);
|
||||||
case 'speechmatics':
|
case 'speechmatics':
|
||||||
return normalizeSpeechmatics(evt, channel, language);
|
return normalizeSpeechmatics(evt, channel, language);
|
||||||
case 'openai':
|
|
||||||
return normalizeOpenAI(evt, channel, language);
|
|
||||||
default:
|
default:
|
||||||
if (vendor.startsWith('custom:')) {
|
if (vendor.startsWith('custom:')) {
|
||||||
return normalizeCustom(evt, channel, language, vendor);
|
return normalizeCustom(evt, channel, language, vendor);
|
||||||
@@ -776,8 +705,6 @@ module.exports = (logger) => {
|
|||||||
//azureSttEndpointId overrides sttCredentials.custom_stt_endpoint
|
//azureSttEndpointId overrides sttCredentials.custom_stt_endpoint
|
||||||
...(rOpts.azureSttEndpointId &&
|
...(rOpts.azureSttEndpointId &&
|
||||||
{AZURE_SERVICE_ENDPOINT_ID: rOpts.azureSttEndpointId}),
|
{AZURE_SERVICE_ENDPOINT_ID: rOpts.azureSttEndpointId}),
|
||||||
...(azureOptions.speechRecognitionMode &&
|
|
||||||
{AZURE_RECOGNITION_MODE: azureOptions.speechRecognitionMode}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if ('nuance' === vendor) {
|
else if ('nuance' === vendor) {
|
||||||
@@ -829,7 +756,7 @@ module.exports = (logger) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if ('deepgram' === vendor) {
|
else if ('deepgram' === vendor) {
|
||||||
let model = rOpts.deepgramOptions?.model || rOpts.model;
|
let {model} = rOpts;
|
||||||
const {deepgramOptions = {}} = rOpts;
|
const {deepgramOptions = {}} = rOpts;
|
||||||
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;
|
||||||
@@ -851,8 +778,6 @@ module.exports = (logger) => {
|
|||||||
{DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1},
|
{DEEPGRAM_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 1},
|
||||||
...(deepgramOptions.smartFormatting) &&
|
...(deepgramOptions.smartFormatting) &&
|
||||||
{DEEPGRAM_SPEECH_ENABLE_SMART_FORMAT: 1},
|
{DEEPGRAM_SPEECH_ENABLE_SMART_FORMAT: 1},
|
||||||
...(deepgramOptions.noDelay) &&
|
|
||||||
{DEEPGRAM_SPEECH_ENABLE_NO_DELAY: 1},
|
|
||||||
...(deepgramOptions.profanityFilter) &&
|
...(deepgramOptions.profanityFilter) &&
|
||||||
{DEEPGRAM_SPEECH_PROFANITY_FILTER: 1},
|
{DEEPGRAM_SPEECH_PROFANITY_FILTER: 1},
|
||||||
...(deepgramOptions.redact) &&
|
...(deepgramOptions.redact) &&
|
||||||
@@ -890,9 +815,7 @@ module.exports = (logger) => {
|
|||||||
...(deepgramOptions.version) &&
|
...(deepgramOptions.version) &&
|
||||||
{DEEPGRAM_SPEECH_MODEL_VERSION: deepgramOptions.version},
|
{DEEPGRAM_SPEECH_MODEL_VERSION: deepgramOptions.version},
|
||||||
...(deepgramOptions.fillerWords) &&
|
...(deepgramOptions.fillerWords) &&
|
||||||
{DEEPGRAM_SPEECH_FILLER_WORDS: deepgramOptions.fillerWords},
|
{DEEPGRAM_SPEECH_FILLER_WORDS: deepgramOptions.fillerWords}
|
||||||
...((Array.isArray(deepgramOptions.keyterms) && deepgramOptions.keyterms.length > 0) &&
|
|
||||||
{DEEPGRAM_SPEECH_KEYTERMS: deepgramOptions.keyterms.join(',')})
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if ('soniox' === vendor) {
|
else if ('soniox' === vendor) {
|
||||||
@@ -1001,43 +924,6 @@ module.exports = (logger) => {
|
|||||||
{ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)})
|
{ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if ('voxist' === vendor) {
|
|
||||||
opts = {
|
|
||||||
...opts,
|
|
||||||
...(sttCredentials.api_key) &&
|
|
||||||
{VOXIST_API_KEY: sttCredentials.api_key},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if ('openai' === vendor) {
|
|
||||||
const {openaiOptions = {}} = rOpts;
|
|
||||||
const model = openaiOptions.model || rOpts.model || sttCredentials.model_id || 'whisper-1';
|
|
||||||
const apiKey = openaiOptions.apiKey || sttCredentials.api_key;
|
|
||||||
|
|
||||||
opts = {
|
|
||||||
OPENAI_MODEL: model,
|
|
||||||
OPENAI_API_KEY: apiKey,
|
|
||||||
...opts,
|
|
||||||
...(openaiOptions.prompt && {OPENAI_PROMPT: openaiOptions.prompt}),
|
|
||||||
...(openaiOptions.input_audio_noise_reduction &&
|
|
||||||
{OPENAI_INPUT_AUDIO_NOISE_REDUCTION: openaiOptions.input_audio_noise_reduction}),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (openaiOptions.turn_detection) {
|
|
||||||
opts = {
|
|
||||||
...opts,
|
|
||||||
OPENAI_TURN_DETECTION_TYPE: openaiOptions.turn_detection.type,
|
|
||||||
...(openaiOptions.turn_detection.threshold && {
|
|
||||||
OPENAI_TURN_DETECTION_THRESHOLD: openaiOptions.turn_detection.threshold
|
|
||||||
}),
|
|
||||||
...(openaiOptions.turn_detection.prefix_padding_ms && {
|
|
||||||
OPENAI_TURN_DETECTION_PREFIX_PADDING_MS: openaiOptions.turn_detection.prefix_padding_ms
|
|
||||||
}),
|
|
||||||
...(openaiOptions.turn_detection.silence_duration_ms && {
|
|
||||||
OPENAI_TURN_DETECTION_SILENCE_DURATION_MS: openaiOptions.turn_detection.silence_duration_ms
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ('verbio' === vendor) {
|
else if ('verbio' === vendor) {
|
||||||
const {verbioOptions = {}} = rOpts;
|
const {verbioOptions = {}} = rOpts;
|
||||||
opts = {
|
opts = {
|
||||||
|
|||||||
@@ -4,50 +4,37 @@ const {
|
|||||||
TtsStreamingEvents,
|
TtsStreamingEvents,
|
||||||
TtsStreamingConnectionStatus
|
TtsStreamingConnectionStatus
|
||||||
} = require('../utils/constants');
|
} = require('../utils/constants');
|
||||||
|
|
||||||
const MAX_CHUNK_SIZE = 1800;
|
const MAX_CHUNK_SIZE = 1800;
|
||||||
const HIGH_WATER_BUFFER_SIZE = 1000;
|
const HIGH_WATER_BUFFER_SIZE = 1000;
|
||||||
const LOW_WATER_BUFFER_SIZE = 200;
|
const LOW_WATER_BUFFER_SIZE = 200;
|
||||||
const TIMEOUT_RETRY_MSECS = 3000;
|
const TIMEOUT_RETRY_MSECS = 3000;
|
||||||
|
|
||||||
|
|
||||||
const isWhitespace = (str) => /^\s*$/.test(str);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Each queue item is an object:
|
|
||||||
* - { type: 'text', value: '…' } for text tokens.
|
|
||||||
* - { type: 'flush' } for a flush command.
|
|
||||||
*/
|
|
||||||
class TtsStreamingBuffer extends Emitter {
|
class TtsStreamingBuffer extends Emitter {
|
||||||
constructor(cs) {
|
constructor(cs) {
|
||||||
super();
|
super();
|
||||||
this.cs = cs;
|
this.cs = cs;
|
||||||
this.logger = cs.logger;
|
this.logger = cs.logger;
|
||||||
|
|
||||||
// Use an array to hold our structured items.
|
this.tokens = '';
|
||||||
this.queue = [];
|
|
||||||
// Track total number of characters in text items.
|
|
||||||
this.bufferedLength = 0;
|
|
||||||
this.eventHandlers = [];
|
this.eventHandlers = [];
|
||||||
this._isFull = false;
|
this._isFull = false;
|
||||||
this._connectionStatus = TtsStreamingConnectionStatus.NotConnected;
|
this._connectionStatus = TtsStreamingConnectionStatus.NotConnected;
|
||||||
|
this._flushPending = false;
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
// Record the last time the text buffer was updated.
|
|
||||||
this.lastUpdateTime = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isEmpty() {
|
get isEmpty() {
|
||||||
return this.queue.length === 0;
|
return this.tokens.length === 0;
|
||||||
}
|
|
||||||
|
|
||||||
get size() {
|
|
||||||
return this.bufferedLength;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFull() {
|
get isFull() {
|
||||||
return this._isFull;
|
return this._isFull;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.tokens.length;
|
||||||
|
}
|
||||||
|
|
||||||
get ep() {
|
get ep() {
|
||||||
return this.cs?.ep;
|
return this.cs?.ep;
|
||||||
}
|
}
|
||||||
@@ -55,8 +42,7 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
async start() {
|
async start() {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
this._connectionStatus === TtsStreamingConnectionStatus.NotConnected,
|
this._connectionStatus === TtsStreamingConnectionStatus.NotConnected,
|
||||||
'TtsStreamingBuffer:start already started, or has failed'
|
'TtsStreamingBuffer:start already started, or has failed');
|
||||||
);
|
|
||||||
|
|
||||||
this.vendor = this.cs.getTsStreamingVendor();
|
this.vendor = this.cs.getTsStreamingVendor();
|
||||||
if (!this.vendor) {
|
if (!this.vendor) {
|
||||||
@@ -69,9 +55,9 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
this._connectionStatus = TtsStreamingConnectionStatus.Connecting;
|
this._connectionStatus = TtsStreamingConnectionStatus.Connecting;
|
||||||
try {
|
try {
|
||||||
if (this.eventHandlers.length === 0) this._initHandlers(this.ep);
|
if (this.eventHandlers.length === 0) this._initHandlers(this.ep);
|
||||||
await this._api(this.ep, [this.ep.uuid, 'connect']);
|
await this._api(this.ep, [this.ep.uuid, 'connect']);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({ err }, 'TtsStreamingBuffer:start Error connecting to TTS streaming');
|
this.logger.info({err}, 'TtsStreamingBuffer:start Error connecting to TTS streaming');
|
||||||
this._connectionStatus = TtsStreamingConnectionStatus.Failed;
|
this._connectionStatus = TtsStreamingConnectionStatus.Failed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,319 +67,204 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
this.removeCustomEventListeners();
|
this.removeCustomEventListeners();
|
||||||
if (this.ep) {
|
if (this.ep) {
|
||||||
this._api(this.ep, [this.ep.uuid, 'close'])
|
this._api(this.ep, [this.ep.uuid, 'close'])
|
||||||
.catch((err) =>
|
.catch((err) => this.logger.info({err}, 'TtsStreamingBuffer:kill Error closing TTS streaming'));
|
||||||
this.logger.info({ err }, 'TtsStreamingBuffer:stop Error closing TTS streaming')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
this.queue = [];
|
this.tokens = '';
|
||||||
this.bufferedLength = 0;
|
|
||||||
this._connectionStatus = TtsStreamingConnectionStatus.NotConnected;
|
this._connectionStatus = TtsStreamingConnectionStatus.NotConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buffer new text tokens.
|
* Add tokens to the buffer and start feeding them to the endpoint if necessary.
|
||||||
*/
|
*/
|
||||||
async bufferTokens(tokens) {
|
async bufferTokens(tokens) {
|
||||||
|
|
||||||
if (this._connectionStatus === TtsStreamingConnectionStatus.Failed) {
|
if (this._connectionStatus === TtsStreamingConnectionStatus.Failed) {
|
||||||
this.logger.info('TtsStreamingBuffer:bufferTokens TTS streaming connection failed, rejecting request');
|
this.logger.info('TtsStreamingBuffer:bufferTokens TTS streaming connection failed, rejecting request');
|
||||||
return { status: 'failed', reason: `connection to ${this.vendor} failed` };
|
return {status: 'failed', reason: `connection to ${this.vendor} failed`};
|
||||||
}
|
|
||||||
|
|
||||||
if (0 === this.bufferedLength && isWhitespace(tokens)) {
|
|
||||||
this.logger.debug({tokens}, 'TtsStreamingBuffer:bufferTokens discarded whitespace tokens');
|
|
||||||
return { status: 'ok' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayedTokens = tokens.length <= 40 ? tokens : tokens.substring(0, 40);
|
const displayedTokens = tokens.length <= 40 ? tokens : tokens.substring(0, 40);
|
||||||
const totalLength = tokens.length;
|
const totalLength = tokens.length;
|
||||||
|
|
||||||
if (this.bufferedLength + totalLength > HIGH_WATER_BUFFER_SIZE) {
|
/* if we crossed the high water mark, reject the request */
|
||||||
|
if (this.tokens.length + totalLength > HIGH_WATER_BUFFER_SIZE) {
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`TtsStreamingBuffer throttling: buffer is full, rejecting request to buffer ${totalLength} tokens`
|
`TtsStreamingBuffer throttling: buffer is full, rejecting request to buffer ${totalLength} tokens`);
|
||||||
);
|
|
||||||
if (!this._isFull) {
|
if (!this._isFull) {
|
||||||
this._isFull = true;
|
this._isFull = true;
|
||||||
this.emit(TtsStreamingEvents.Pause);
|
this.emit(TtsStreamingEvents.Pause);
|
||||||
}
|
}
|
||||||
return { status: 'failed', reason: 'full' };
|
return {status: 'failed', reason: 'full'};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`TtsStreamingBuffer:bufferTokens "${displayedTokens}" (length: ${totalLength})`
|
`TtsStreamingBuffer:bufferTokens "${displayedTokens}" (length: ${totalLength}), starting? ${this.isEmpty}`
|
||||||
);
|
);
|
||||||
this.queue.push({ type: 'text', value: tokens });
|
this.tokens += (tokens || '');
|
||||||
this.bufferedLength += totalLength;
|
|
||||||
// Update the last update time each time new text is buffered.
|
|
||||||
this.lastUpdateTime = Date.now();
|
|
||||||
|
|
||||||
await this._feedQueue();
|
await this._feedTokens();
|
||||||
return { status: 'ok' };
|
|
||||||
|
return {status: 'ok'};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert a flush command. If no text is queued, flush immediately.
|
|
||||||
* Otherwise, append a flush marker so that all text preceding it will be sent
|
|
||||||
* (regardless of sentence boundaries) before the flush is issued.
|
|
||||||
*/
|
|
||||||
flush() {
|
flush() {
|
||||||
|
this.logger.debug('TtsStreamingBuffer:flush');
|
||||||
if (this._connectionStatus === TtsStreamingConnectionStatus.Connecting) {
|
if (this._connectionStatus === TtsStreamingConnectionStatus.Connecting) {
|
||||||
this.logger.debug('TtsStreamingBuffer:flush TTS stream is not quite ready - wait for connect');
|
this.logger.debug('TtsStreamingBuffer:flush TTS stream is not quite ready - wait for connect');
|
||||||
if (this.queue.length === 0 || this.queue[this.queue.length - 1].type !== 'flush') {
|
this._flushPending = true;
|
||||||
this.queue.push({ type: 'flush' });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (this._connectionStatus === TtsStreamingConnectionStatus.Connected) {
|
else if (this._connectionStatus === TtsStreamingConnectionStatus.Connected) {
|
||||||
if (this.isEmpty) {
|
|
||||||
|
if (this.size === 0) {
|
||||||
this._doFlush();
|
this._doFlush();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (this.queue[this.queue.length - 1].type !== 'flush') {
|
/* we have tokens queued, so flush after they have been sent */
|
||||||
this.queue.push({ type: 'flush' });
|
this._pendingFlush = true;
|
||||||
this.logger.debug('TtsStreamingBuffer:flush added flush marker to queue');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
this.logger.debug(
|
|
||||||
`TtsStreamingBuffer:flush TTS stream is not connected, status: ${this._connectionStatus}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.logger.debug('TtsStreamingBuffer:clear');
|
this.logger.debug('TtsStreamingBuffer:clear');
|
||||||
|
|
||||||
if (this._connectionStatus !== TtsStreamingConnectionStatus.Connected) return;
|
if (this._connectionStatus !== TtsStreamingConnectionStatus.Connected) return;
|
||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
this._api(this.ep, [this.ep.uuid, 'clear']).catch((err) =>
|
this._api(this.ep, [this.ep.uuid, 'clear'])
|
||||||
this.logger.info({ err }, 'TtsStreamingBuffer:clear Error clearing TTS streaming')
|
.catch((err) => this.logger.info({err}, 'TtsStreamingBuffer:clear Error clearing TTS streaming'));
|
||||||
);
|
this.tokens = '';
|
||||||
this.queue = [];
|
|
||||||
this.bufferedLength = 0;
|
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
this._isFull = false;
|
this._isFull = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the queue in two phases.
|
* Send tokens to the TTS engine in sentence chunks for best playout
|
||||||
*
|
|
||||||
* Phase 1: Look for flush markers. When a flush marker is found (even if not at the very front),
|
|
||||||
* send all text tokens that came before it immediately (ignoring sentence boundaries)
|
|
||||||
* and then send the flush command. Repeat until there are no flush markers left.
|
|
||||||
*
|
|
||||||
* Phase 2: With the remaining queue (now containing only text items), accumulate text
|
|
||||||
* up to MAX_CHUNK_SIZE and use sentence-boundary logic to determine a chunk.
|
|
||||||
* Then, remove the exact tokens (or portions thereof) that were consumed.
|
|
||||||
*/
|
*/
|
||||||
async _feedQueue(handlingTimeout = false) {
|
async _feedTokens(handlingTimeout = false) {
|
||||||
this.logger.debug({ queue: this.queue }, 'TtsStreamingBuffer:_feedQueue');
|
this.logger.debug({tokens: this.tokens}, '_feedTokens');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.cs.isTtsStreamOpen || !this.ep) {
|
|
||||||
this.logger.debug('TtsStreamingBuffer:_feedQueue TTS stream is not open or no endpoint available');
|
/* are we in a state where we can feed tokens to the TTS? */
|
||||||
return;
|
if (!this.cs.isTtsStreamOpen || !this.ep || !this.tokens) {
|
||||||
|
this.logger.debug('TTS stream is not open or no tokens to send');
|
||||||
|
return this.tokens?.length || 0;
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
this._connectionStatus === TtsStreamingConnectionStatus.NotConnected ||
|
if (this._connectionStatus === TtsStreamingConnectionStatus.NotConnected ||
|
||||||
this._connectionStatus === TtsStreamingConnectionStatus.Failed
|
this._connectionStatus === TtsStreamingConnectionStatus.Failed) {
|
||||||
) {
|
this.logger.debug('TtsStreamingBuffer:_feedTokens TTS stream is not connected');
|
||||||
this.logger.debug('TtsStreamingBuffer:_feedQueue TTS stream is not connected');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Phase 1: Process flush markers ---
|
if (this._connectionStatus === TtsStreamingConnectionStatus.Connecting) {
|
||||||
// Process any flush marker that isn’t in the very first position.
|
this.logger.debug('TtsStreamingBuffer:_feedTokens TTS stream is not ready, waiting for connect');
|
||||||
let flushIndex = this.queue.findIndex((item, idx) => item.type === 'flush' && idx > 0);
|
|
||||||
while (flushIndex !== -1) {
|
|
||||||
let flushText = '';
|
|
||||||
// Accumulate all text tokens preceding the flush marker.
|
|
||||||
for (let i = 0; i < flushIndex; i++) {
|
|
||||||
if (this.queue[i].type === 'text') {
|
|
||||||
flushText += this.queue[i].value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove those text items.
|
|
||||||
for (let i = 0; i < flushIndex; i++) {
|
|
||||||
const item = this.queue.shift();
|
|
||||||
if (item.type === 'text') {
|
|
||||||
this.bufferedLength -= item.value.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove the flush marker (now at the front).
|
|
||||||
if (this.queue.length > 0 && this.queue[0].type === 'flush') {
|
|
||||||
this.queue.shift();
|
|
||||||
}
|
|
||||||
// Immediately send all accumulated text (ignoring sentence boundaries).
|
|
||||||
if (flushText.length > 0) {
|
|
||||||
const modifiedFlushText = flushText.replace(/\n\n/g, '\n \n');
|
|
||||||
try {
|
|
||||||
await this._api(this.ep, [this.ep.uuid, 'send', modifiedFlushText]);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.info({ err, flushText }, 'TtsStreamingBuffer:_feedQueue Error sending TTS chunk');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Send the flush command.
|
|
||||||
await this._doFlush();
|
|
||||||
|
|
||||||
flushIndex = this.queue.findIndex((item, idx) => item.type === 'flush' && idx > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a flush marker is at the very front, process it.
|
|
||||||
while (this.queue.length > 0 && this.queue[0].type === 'flush') {
|
|
||||||
this.queue.shift();
|
|
||||||
await this._doFlush();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Phase 2: Process remaining text tokens ---
|
|
||||||
if (this.queue.length === 0) {
|
|
||||||
this._removeTimer();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulate contiguous text tokens (from the front) up to MAX_CHUNK_SIZE.
|
/* must send at least one sentence */
|
||||||
let combinedText = '';
|
const limit = Math.min(MAX_CHUNK_SIZE, this.tokens.length);
|
||||||
for (const item of this.queue) {
|
let chunkEnd = findSentenceBoundary(this.tokens, limit);
|
||||||
if (item.type !== 'text') break;
|
|
||||||
combinedText += item.value;
|
|
||||||
if (combinedText.length >= MAX_CHUNK_SIZE) break;
|
|
||||||
}
|
|
||||||
if (combinedText.length === 0) {
|
|
||||||
this._removeTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const limit = Math.min(MAX_CHUNK_SIZE, combinedText.length);
|
|
||||||
let chunkEnd = findSentenceBoundary(combinedText, limit);
|
|
||||||
if (chunkEnd <= 0) {
|
if (chunkEnd <= 0) {
|
||||||
if (handlingTimeout) {
|
if (handlingTimeout) {
|
||||||
chunkEnd = findWordBoundary(combinedText, limit);
|
/* on a timeout we've left some tokens sitting around, so be more aggressive now in sending them */
|
||||||
|
chunkEnd = findWordBoundary(this.tokens, limit);
|
||||||
if (chunkEnd <= 0) {
|
if (chunkEnd <= 0) {
|
||||||
|
this.logger.debug('TtsStreamingBuffer:_feedTokens: no word boundary found');
|
||||||
this._setTimerIfNeeded();
|
this._setTimerIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
|
/* if we just received tokens, we wont send unless we have at least a full sentence */
|
||||||
|
this.logger.debug('TtsStreamingBuffer:_feedTokens: no sentence boundary found');
|
||||||
this._setTimerIfNeeded();
|
this._setTimerIfNeeded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const chunk = combinedText.slice(0, chunkEnd);
|
|
||||||
|
|
||||||
// Now we iterate over the queue items
|
const chunk = this.tokens.slice(0, chunkEnd);
|
||||||
// and deduct their lengths until we've accounted for chunkEnd characters.
|
this.tokens = this.tokens.slice(chunkEnd);
|
||||||
let remaining = chunkEnd;
|
|
||||||
let tokensProcessed = 0;
|
|
||||||
for (let i = 0; i < this.queue.length; i++) {
|
|
||||||
const token = this.queue[i];
|
|
||||||
if (token.type !== 'text') break;
|
|
||||||
if (remaining >= token.value.length) {
|
|
||||||
remaining -= token.value.length;
|
|
||||||
tokensProcessed = i + 1;
|
|
||||||
} else {
|
|
||||||
// Partially consumed token: update its value to remove the consumed part.
|
|
||||||
token.value = token.value.slice(remaining);
|
|
||||||
tokensProcessed = i;
|
|
||||||
remaining = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove the fully consumed tokens from the front of the queue.
|
|
||||||
this.queue.splice(0, tokensProcessed);
|
|
||||||
this.bufferedLength -= chunkEnd;
|
|
||||||
|
|
||||||
|
/* freeswitch looks for sequence of 2 newlines to determine end of message, so insert a space */
|
||||||
const modifiedChunk = chunk.replace(/\n\n/g, '\n \n');
|
const modifiedChunk = chunk.replace(/\n\n/g, '\n \n');
|
||||||
this.logger.debug(`TtsStreamingBuffer:_feedQueue sending chunk to tts: ${modifiedChunk}`);
|
await this._api(this.ep, [this.ep.uuid, 'send', modifiedChunk]);
|
||||||
|
this.logger.debug(`TtsStreamingBuffer:_feedTokens: sent ${chunk.length}, remaining: ${this.tokens.length}`);
|
||||||
|
|
||||||
try {
|
if (this._pendingFlush) {
|
||||||
await this._api(this.ep, [this.ep.uuid, 'send', modifiedChunk]);
|
this._doFlush();
|
||||||
} catch (err) {
|
this._pendingFlush = false;
|
||||||
this.logger.info({ err, chunk }, 'TtsStreamingBuffer:_feedQueue Error sending TTS chunk');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._isFull && this.bufferedLength <= LOW_WATER_BUFFER_SIZE) {
|
if (this.isFull && this.tokens.length <= LOW_WATER_BUFFER_SIZE) {
|
||||||
this.logger.info('TtsStreamingBuffer throttling: buffer is no longer full - resuming');
|
this.logger.info('TtsStreamingBuffer throttling: TTS streaming buffer is no longer full - resuming');
|
||||||
this._isFull = false;
|
this._isFull = false;
|
||||||
this.emit(TtsStreamingEvents.Resume);
|
this.emit(TtsStreamingEvents.Resume);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._feedQueue();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({ err }, 'TtsStreamingBuffer:_feedQueue Error sending TTS chunk');
|
this.logger.info({err}, 'TtsStreamingBuffer:_feedTokens Error sending TTS chunk');
|
||||||
this.queue = [];
|
this.tokens = '';
|
||||||
this.bufferedLength = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _api(ep, args) {
|
async _api(ep, args) {
|
||||||
const apiCmd = `uuid_${this.vendor.startsWith('custom:') ? 'custom' : this.vendor}_tts_streaming`;
|
const apiCmd = `uuid_${this.vendor.startsWith('custom:') ? 'custom' : this.vendor}_tts_streaming`;
|
||||||
const res = await ep.api(apiCmd, `^^|${args.join('|')}`);
|
const res = await ep.api(apiCmd, `^^|${args.join('|')}`);
|
||||||
if (!res.body?.startsWith('+OK')) {
|
if (!res.body?.startsWith('+OK')) {
|
||||||
this.logger.info({ args }, `Error calling ${apiCmd}: ${res.body}`);
|
this.logger.info({args}, `Error calling ${apiCmd}: ${res.body}`);
|
||||||
throw new Error(`Error calling ${apiCmd}: ${res.body}`);
|
throw new Error(`Error calling ${apiCmd}: ${res.body}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onConnectFailure(vendor) {
|
||||||
|
this.logger.info(`streaming tts connection failed to ${vendor}`);
|
||||||
|
this._connectionStatus = TtsStreamingConnectionStatus.Failed;
|
||||||
|
this.tokens = '';
|
||||||
|
this.emit(TtsStreamingEvents.ConnectFailure, {vendor});
|
||||||
|
}
|
||||||
|
|
||||||
_doFlush() {
|
_doFlush() {
|
||||||
return this._api(this.ep, [this.ep.uuid, 'flush'])
|
this._api(this.ep, [this.ep.uuid, 'flush'])
|
||||||
.then(() => this.logger.debug('TtsStreamingBuffer:_doFlush sent flush command'))
|
.catch((err) => this.logger.info({err},
|
||||||
.catch((err) =>
|
`TtsStreamingBuffer:_doFlush Error flushing TTS streaming: ${JSON.stringify(err)}`));
|
||||||
this.logger.info(
|
|
||||||
{ err },
|
|
||||||
`TtsStreamingBuffer:_doFlush Error flushing TTS streaming: ${JSON.stringify(err)}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onConnect(vendor) {
|
async _onConnect(vendor) {
|
||||||
this.logger.info(`TtsStreamingBuffer:_onConnect streaming tts connection made to ${vendor} successful`);
|
this.logger.info(`streaming tts connection made to ${vendor}`);
|
||||||
this._connectionStatus = TtsStreamingConnectionStatus.Connected;
|
this._connectionStatus = TtsStreamingConnectionStatus.Connected;
|
||||||
if (this.queue.length > 0) {
|
if (this.tokens.length > 0) {
|
||||||
await this._feedQueue();
|
await this._feedTokens();
|
||||||
|
}
|
||||||
|
if (this._flushPending) {
|
||||||
|
this.flush();
|
||||||
|
this._flushPending = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onConnectFailure(vendor) {
|
|
||||||
this.logger.info(`TtsStreamingBuffer:_onConnectFailure streaming tts connection failed to ${vendor}`);
|
|
||||||
this._connectionStatus = TtsStreamingConnectionStatus.Failed;
|
|
||||||
this.queue = [];
|
|
||||||
this.bufferedLength = 0;
|
|
||||||
this.emit(TtsStreamingEvents.ConnectFailure, { vendor });
|
|
||||||
}
|
|
||||||
|
|
||||||
_setTimerIfNeeded() {
|
_setTimerIfNeeded() {
|
||||||
if (this.bufferedLength > 0 && !this.timer) {
|
if (this.tokens.length > 0 && !this.timer) {
|
||||||
this.logger.debug({queue: this.queue},
|
|
||||||
`TtsStreamingBuffer:_setTimerIfNeeded setting timer because ${this.bufferedLength} buffered`);
|
|
||||||
this.timer = setTimeout(this._onTimeout.bind(this), TIMEOUT_RETRY_MSECS);
|
this.timer = setTimeout(this._onTimeout.bind(this), TIMEOUT_RETRY_MSECS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeTimer() {
|
|
||||||
if (this.timer) {
|
|
||||||
this.logger.debug('TtsStreamingBuffer:_removeTimer clearing timer');
|
|
||||||
clearTimeout(this.timer);
|
|
||||||
this.timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onTimeout() {
|
_onTimeout() {
|
||||||
this.logger.debug('TtsStreamingBuffer:_onTimeout Timeout waiting for sentence boundary');
|
this.logger.info('TtsStreamingBuffer:_onTimeout');
|
||||||
// Check if new text has been added since the timer was set.
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastUpdateTime < TIMEOUT_RETRY_MSECS) {
|
|
||||||
this.logger.debug('TtsStreamingBuffer:_onTimeout New text received recently; postponing flush.');
|
|
||||||
this._setTimerIfNeeded();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
this._feedQueue(true);
|
this._feedTokens(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onTtsEmpty(vendor) {
|
_onTtsEmpty(vendor) {
|
||||||
this.emit(TtsStreamingEvents.Empty, { vendor });
|
this.emit(TtsStreamingEvents.Empty, {vendor});
|
||||||
}
|
}
|
||||||
|
|
||||||
addCustomEventListener(ep, event, handler) {
|
addCustomEventListener(ep, event, handler) {
|
||||||
this.eventHandlers.push({ ep, event, handler });
|
this.eventHandlers.push({ep, event, handler});
|
||||||
ep.addCustomEventListener(event, handler);
|
ep.addCustomEventListener(event, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,6 +274,7 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
|
|
||||||
_initHandlers(ep) {
|
_initHandlers(ep) {
|
||||||
[
|
[
|
||||||
|
// DH: add other vendors here as modules are added
|
||||||
'deepgram',
|
'deepgram',
|
||||||
'cartesia',
|
'cartesia',
|
||||||
'elevenlabs',
|
'elevenlabs',
|
||||||
@@ -421,21 +293,23 @@ class TtsStreamingBuffer extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const findSentenceBoundary = (text, limit) => {
|
const findSentenceBoundary = (text, limit) => {
|
||||||
// Look for punctuation or double newline that signals sentence end.
|
// Match traditional sentence boundaries or double newlines
|
||||||
const sentenceEndRegex = /[.!?](?=\s|$)|\n\n/g;
|
const sentenceEndRegex = /[.!?](?=\s|$)|\n\n/g;
|
||||||
let lastSentenceBoundary = -1;
|
let lastSentenceBoundary = -1;
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = sentenceEndRegex.exec(text)) && match.index < limit) {
|
while ((match = sentenceEndRegex.exec(text)) && match.index < limit) {
|
||||||
const precedingText = text.slice(0, match.index).trim();
|
const precedingText = text.slice(0, match.index).trim(); // Extract text before the match and trim whitespace
|
||||||
if (precedingText.length > 0) {
|
if (precedingText.length > 0) { // Check if there's actual content
|
||||||
if (
|
if (
|
||||||
match[0] === '\n\n' ||
|
match[0] === '\n\n' || // It's a double newline
|
||||||
(match.index === 0 || !/\d$/.test(text[match.index - 1]))
|
(match.index === 0 || !/\d$/.test(text[match.index - 1])) // Standard punctuation rules
|
||||||
) {
|
) {
|
||||||
lastSentenceBoundary = match.index + (match[0] === '\n\n' ? 2 : 1);
|
lastSentenceBoundary = match.index + (match[0] === '\n\n' ? 2 : 1); // Include the boundary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastSentenceBoundary;
|
return lastSentenceBoundary;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -443,6 +317,7 @@ const findWordBoundary = (text, limit) => {
|
|||||||
const wordBoundaryRegex = /\s+/g;
|
const wordBoundaryRegex = /\s+/g;
|
||||||
let lastWordBoundary = -1;
|
let lastWordBoundary = -1;
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = wordBoundaryRegex.exec(text)) && match.index < limit) {
|
while ((match = wordBoundaryRegex.exec(text)) && match.index < limit) {
|
||||||
lastWordBoundary = match.index;
|
lastWordBoundary = match.index;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class WsRequestor extends BaseRequestor {
|
|||||||
|
|
||||||
/* prepare and send message */
|
/* prepare and send message */
|
||||||
let payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
let payload = params ? snakeCaseKeys(params, ['customerData', 'sip']) : null;
|
||||||
if (type === 'session:new' || type === 'session:adulting') this._sessionData = payload;
|
if (type === 'session:new') this._sessionData = payload;
|
||||||
if (type === 'session:reconnect') payload = this._sessionData;
|
if (type === 'session:reconnect') payload = this._sessionData;
|
||||||
assert.ok(url, 'WsRequestor:request url was not provided');
|
assert.ok(url, 'WsRequestor:request url was not provided');
|
||||||
|
|
||||||
@@ -146,9 +146,7 @@ class WsRequestor extends BaseRequestor {
|
|||||||
type,
|
type,
|
||||||
msgid,
|
msgid,
|
||||||
call_sid: this.call_sid,
|
call_sid: this.call_sid,
|
||||||
hook: [
|
hook: ['verb:hook', 'session:redirect', 'llm:event', 'llm:tool-call'].includes(type) ? url : undefined,
|
||||||
'verb:hook', 'dial:confirm', 'session:redirect', 'llm:event', 'llm:tool-call'
|
|
||||||
].includes(type) ? url : undefined,
|
|
||||||
data: {...payload},
|
data: {...payload},
|
||||||
...b3
|
...b3
|
||||||
};
|
};
|
||||||
@@ -433,21 +431,6 @@ class WsRequestor extends BaseRequestor {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.info({err, content}, 'WsRequestor:_onMessage - invalid incoming message');
|
this.logger.info({err, content}, 'WsRequestor:_onMessage - invalid incoming message');
|
||||||
const params = {
|
|
||||||
msg: 'InvalidMessage',
|
|
||||||
details: err.message,
|
|
||||||
content: Buffer.from(content).toString('utf-8')
|
|
||||||
};
|
|
||||||
const {writeAlerts, AlertType} = this.Alerter;
|
|
||||||
writeAlerts({
|
|
||||||
account_sid: this.account_sid,
|
|
||||||
alert_type: AlertType.INVALID_APP_PAYLOAD,
|
|
||||||
target_sid: this.call_sid,
|
|
||||||
message: err.message,
|
|
||||||
|
|
||||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for invalid message'));
|
|
||||||
this.request('jambonz:error', '/error', params)
|
|
||||||
.catch((err) => this.logger.debug({err}, 'WsRequestor:_onMessage - Error sending'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9810
package-lock.json
generated
9810
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -27,14 +27,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
"@aws-sdk/client-auto-scaling": "^3.549.0",
|
||||||
"@aws-sdk/client-sns": "^3.549.0",
|
"@aws-sdk/client-sns": "^3.549.0",
|
||||||
"@jambonz/db-helpers": "^0.9.11",
|
"@jambonz/db-helpers": "^0.9.6",
|
||||||
"@jambonz/http-health-check": "^0.0.1",
|
"@jambonz/http-health-check": "^0.0.1",
|
||||||
"@jambonz/mw-registrar": "^0.2.7",
|
"@jambonz/mw-registrar": "^0.2.7",
|
||||||
"@jambonz/realtimedb-helpers": "^0.8.13",
|
"@jambonz/realtimedb-helpers": "^0.8.8",
|
||||||
"@jambonz/speech-utils": "^0.2.3",
|
"@jambonz/speech-utils": "^0.2.1",
|
||||||
"@jambonz/stats-collector": "^0.1.10",
|
"@jambonz/stats-collector": "^0.1.10",
|
||||||
|
"@jambonz/verb-specifications": "^0.0.94",
|
||||||
"@jambonz/time-series": "^0.2.13",
|
"@jambonz/time-series": "^0.2.13",
|
||||||
"@jambonz/verb-specifications": "^0.0.102",
|
|
||||||
"@opentelemetry/api": "^1.8.0",
|
"@opentelemetry/api": "^1.8.0",
|
||||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
"bent": "^7.3.12",
|
"bent": "^7.3.12",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"deepcopy": "^2.1.0",
|
"deepcopy": "^2.1.0",
|
||||||
"drachtio-fsmrf": "^4.0.3",
|
"drachtio-fsmrf": "^4.0.1",
|
||||||
"drachtio-srf": "^5.0.5",
|
"drachtio-srf": "^5.0.1",
|
||||||
"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",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"short-uuid": "^5.1.0",
|
"short-uuid": "^5.1.0",
|
||||||
"sinon": "^17.0.1",
|
"sinon": "^17.0.1",
|
||||||
"to-snake-case": "^1.0.0",
|
"to-snake-case": "^1.0.0",
|
||||||
"undici": "^7.5.0",
|
"undici": "^6.20.0",
|
||||||
"uuid-random": "^1.3.2",
|
"uuid-random": "^1.3.2",
|
||||||
"verify-aws-sns-signature": "^0.1.0",
|
"verify-aws-sns-signature": "^0.1.0",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ const { sippUac } = require('./sipp')('test_fs');
|
|||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
const getJSON = bent('json')
|
const getJSON = bent('json')
|
||||||
const clearModule = require('clear-module');
|
const clearModule = require('clear-module');
|
||||||
const {provisionCallHook} = require('./utils');
|
const {provisionCallHook} = require('./utils')
|
||||||
const { sleepFor } = require('../lib/utils/helpers');
|
|
||||||
|
const sleepFor = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, p) => {
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const { sippUac } = require('./sipp')('test_fs');
|
|||||||
const clearModule = require('clear-module');
|
const clearModule = require('clear-module');
|
||||||
const {provisionCallHook, provisionActionHook, provisionAnyHook} = require('./utils');
|
const {provisionCallHook, provisionActionHook, provisionAnyHook} = require('./utils');
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
const { sleepFor } = require('../lib/utils/helpers');
|
|
||||||
const getJSON = bent('json');
|
const getJSON = bent('json');
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, p) => {
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
@@ -18,6 +17,8 @@ function connect(connectable) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sleepFor = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
||||||
|
|
||||||
test('\'enqueue-dequeue\' tests', async(t) => {
|
test('\'enqueue-dequeue\' tests', async(t) => {
|
||||||
|
|
||||||
clearModule.all();
|
clearModule.all();
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ const { sippUac } = require('./sipp')('test_fs');
|
|||||||
const clearModule = require('clear-module');
|
const clearModule = require('clear-module');
|
||||||
const {provisionCallHook, provisionCustomHook, provisionActionHook} = require('./utils')
|
const {provisionCallHook, provisionCustomHook, provisionActionHook} = require('./utils')
|
||||||
const bent = require('bent');
|
const bent = require('bent');
|
||||||
const { sleepFor } = require('../lib/utils/helpers');
|
|
||||||
const getJSON = bent('json')
|
const getJSON = bent('json')
|
||||||
|
|
||||||
|
const sleepFor = async(ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, p) => {
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user