mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-11 00:39:56 +00:00
Compare commits
26 Commits
v0.9.5-3
...
fix/whatsa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e05abc37f7 | ||
|
|
3142dbf58e | ||
|
|
d3f031b841 | ||
|
|
961c2589ac | ||
|
|
e4ec0025c3 | ||
|
|
ba275ef547 | ||
|
|
83a8cf6d25 | ||
|
|
09220872ae | ||
|
|
fdce05fa40 | ||
|
|
3bd1dd6323 | ||
|
|
54dc172ebd | ||
|
|
e007e0e2d3 | ||
|
|
c5cd488fdf | ||
|
|
57982335e0 | ||
|
|
5cea91e18a | ||
|
|
e396b6aa98 | ||
|
|
9104ebb603 | ||
|
|
1ad0261336 | ||
|
|
7802822773 | ||
|
|
edb4d21ce1 | ||
|
|
8048e9cf88 | ||
|
|
451feafed4 | ||
|
|
7f1543a0f3 | ||
|
|
83955ba972 | ||
|
|
a5fa5fce5b | ||
|
|
cc1751f500 |
@@ -119,7 +119,7 @@ const ENCRYPTION_SECRET = process.env.ENCRYPTION_SECRET;
|
||||
const HTTP_POOL = process.env.HTTP_POOL && parseInt(process.env.HTTP_POOL);
|
||||
const HTTP_POOLSIZE = parseInt(process.env.HTTP_POOLSIZE, 10) || 10;
|
||||
const HTTP_PIPELINING = parseInt(process.env.HTTP_PIPELINING, 10) || 1;
|
||||
const HTTP_TIMEOUT = 10000;
|
||||
const HTTP_TIMEOUT = parseInt(process.env.JAMBONES_HTTP_TIMEOUT, 10) || 10000;
|
||||
const HTTP_PROXY_IP = process.env.JAMBONES_HTTP_PROXY_IP;
|
||||
const HTTP_PROXY_PORT = process.env.JAMBONES_HTTP_PROXY_PORT;
|
||||
const HTTP_PROXY_PROTOCOL = process.env.JAMBONES_HTTP_PROXY_PROTOCOL || 'http';
|
||||
@@ -139,6 +139,11 @@ 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_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_WEBHOOK_ERROR_RETURN = parseInt(process.env.JAMBONES_WEBHOOK_ERROR_RETURN, 10) || 480;
|
||||
|
||||
/* say / tts */
|
||||
const JAMBONES_SAY_CHUNK_SIZE = parseInt(process.env.JAMBONES_SAY_CHUNK_SIZE, 10) || 900;
|
||||
|
||||
// jambonz
|
||||
const JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS =
|
||||
process.env.JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS;
|
||||
@@ -231,5 +236,7 @@ module.exports = {
|
||||
JAMBONES_DIAL_SBC_FOR_REGISTERED_USER,
|
||||
JAMBONES_MEDIA_TIMEOUT_MS,
|
||||
JAMBONES_MEDIA_HOLD_TIMEOUT_MS,
|
||||
JAMBONES_SAY_CHUNK_SIZE,
|
||||
JAMBONES_TRANSCRIBE_EP_DESTROY_DELAY_MS,
|
||||
JAMBONES_WEBHOOK_ERROR_RETURN
|
||||
};
|
||||
|
||||
@@ -291,7 +291,7 @@ router.post('/',
|
||||
}, {
|
||||
...(account.enable_debug_log && {level: 'debug'})
|
||||
});
|
||||
app.requestor.logger = app.notifier.logger = sipLogger;
|
||||
app.requestor.logger = app.notifier.logger = restDial.logger = sipLogger;
|
||||
const callInfo = new CallInfo({
|
||||
direction: CallDirection.Outbound,
|
||||
req: inviteReq,
|
||||
|
||||
@@ -12,7 +12,8 @@ const RootSpan = require('./utils/call-tracer');
|
||||
const listTaskNames = require('./utils/summarize-tasks');
|
||||
const {
|
||||
JAMBONES_MYSQL_REFRESH_TTL,
|
||||
JAMBONES_DISABLE_DIRECT_P2P_CALL
|
||||
JAMBONES_DISABLE_DIRECT_P2P_CALL,
|
||||
JAMBONES_WEBHOOK_ERROR_RETURN
|
||||
} = require('./config');
|
||||
const { createJambonzApp } = require('./dynamic-apps');
|
||||
const { decrypt } = require('./utils/encrypt-decrypt');
|
||||
@@ -480,7 +481,7 @@ module.exports = function(srf, logger) {
|
||||
message: `${err?.message}`.trim()
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for parsing application'));
|
||||
logger.info({err}, `Error retrieving or parsing application: ${err?.message}`);
|
||||
res.send(480, {headers: {'X-Reason': err?.message || 'unknown'}});
|
||||
res.send(JAMBONES_WEBHOOK_ERROR_RETURN, {headers: {'X-Reason': err?.message || 'unknown'}});
|
||||
app.requestor.close(WS_CLOSE_CODES.GoingAway);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class CallInfo {
|
||||
let srf;
|
||||
this.direction = opts.direction;
|
||||
this.traceId = opts.traceId;
|
||||
this.hasRecording = false;
|
||||
this.callTerminationBy = undefined;
|
||||
if (opts.req) {
|
||||
const u = opts.req.getParsedHeader('from');
|
||||
|
||||
@@ -504,7 +504,12 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
get isTtsStreamEnabled() {
|
||||
return this.backgroundTaskManager.isTaskRunning('ttsStream');
|
||||
// 1st background tts stream
|
||||
return this.backgroundTaskManager.isTaskRunning('ttsStream') ||
|
||||
// 2nd current task streaming tts
|
||||
TaskName.Say === this.currentTask?.name && this.currentTask?.isStreamingTts ||
|
||||
// 3rd nested verb is streaming tts
|
||||
TaskName.Gather === this.currentTask?.name && this.currentTask.sayTask?.isStreamingTts;
|
||||
}
|
||||
|
||||
get isListenEnabled() {
|
||||
@@ -751,69 +756,101 @@ class CallSession extends Emitter {
|
||||
return this._fillerNoise;
|
||||
}
|
||||
|
||||
async pauseOrResumeBackgroundListenIfRequired(action, silence = false) {
|
||||
if ((action == 'pauseCallRecording' || action == 'resumeCallRecording') &&
|
||||
this.backgroundTaskManager.isTaskRunning('record')) {
|
||||
this.logger.debug({action, silence}, 'CallSession:pauseOrResumeBackgroundListenIfRequired');
|
||||
const backgroundListenTask = this.backgroundTaskManager.getTask('record');
|
||||
const status = action === 'pauseCallRecording' ? ListenStatus.Pause : ListenStatus.Resume;
|
||||
backgroundListenTask.updateListen(
|
||||
status,
|
||||
silence
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async notifyRecordOptions(opts) {
|
||||
const {action, silence} = opts;
|
||||
const {action, silence = false, type = 'siprec'} = opts;
|
||||
this.logger.debug({opts}, 'CallSession:notifyRecordOptions');
|
||||
|
||||
this.pauseOrResumeBackgroundListenIfRequired(action, silence);
|
||||
|
||||
/* if we have not answered yet, just save the details for later */
|
||||
if (!this.dlg) {
|
||||
if (action === 'startCallRecording') {
|
||||
this.recordOptions = opts;
|
||||
return true;
|
||||
if (type == 'cloud') {
|
||||
switch (action) {
|
||||
case 'pauseCallRecording':
|
||||
if (this.backgroundTaskManager.isTaskRunning('record')) {
|
||||
this.logger.debug({action, silence, type}, 'CallSession:cloudRecording');
|
||||
const backgroundListenTask = this.backgroundTaskManager.getTask('record');
|
||||
backgroundListenTask.updateListen(
|
||||
ListenStatus.Pause,
|
||||
silence
|
||||
);
|
||||
return true;
|
||||
} else { return false; }
|
||||
case 'resumeCallRecording':
|
||||
if (this.backgroundTaskManager.isTaskRunning('record')) {
|
||||
this.logger.debug({action, silence, type}, 'CallSession:cloudRecording');
|
||||
const backgroundListenTask = this.backgroundTaskManager.getTask('record');
|
||||
backgroundListenTask.updateListen(
|
||||
ListenStatus.Resume,
|
||||
silence
|
||||
);
|
||||
return true;
|
||||
} else { return false; }
|
||||
case 'startCallRecording':
|
||||
if (!this.backgroundTaskManager.isTaskRunning('record')) {
|
||||
this.logger.debug({action, silence, type}, 'CallSession:cloudRecording');
|
||||
this.callInfo.hasRecording = true;
|
||||
this.updateCallStatus(Object.assign({}, this.callInfo.toJSON()), this.serviceUrl)
|
||||
.catch((err) => this.logger.error(err, 'redis error'));
|
||||
if (!this.dlg) {
|
||||
// Call not yet answered so set flag to record on status change
|
||||
this.application.record_all_calls = true;
|
||||
} else {
|
||||
this.backgroundTaskManager.newTask('record');
|
||||
}
|
||||
return true;
|
||||
} else { return false; }
|
||||
case 'stopCallRecording':
|
||||
if (this.backgroundTaskManager.isTaskRunning('record')) {
|
||||
this.logger.debug({action, silence, type}, 'CallSession:cloudRecording');
|
||||
this.backgroundTaskManager.stop('record');
|
||||
return true;
|
||||
} else { return false; }
|
||||
}
|
||||
} else {
|
||||
// SIPREC
|
||||
/* if we have not answered yet, just save the details for later */
|
||||
if (!this.dlg) {
|
||||
if (action === 'startCallRecording') {
|
||||
this.recordOptions = opts;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* check validity of request */
|
||||
if (action == 'startCallRecording' && this.recordState !== RecordState.RecordingOff) {
|
||||
this.logger.info({recordState: this.recordState},
|
||||
'CallSession:notifyRecordOptions: recording is already started, ignoring request');
|
||||
return false;
|
||||
}
|
||||
if (action == 'stopCallRecording' && this.recordState === RecordState.RecordingOff) {
|
||||
this.logger.info({recordState: this.recordState},
|
||||
'CallSession:notifyRecordOptions: recording is already stopped, ignoring request');
|
||||
return false;
|
||||
}
|
||||
if (action == 'pauseCallRecording' && this.recordState !== RecordState.RecordingOn) {
|
||||
this.logger.info({recordState: this.recordState},
|
||||
'CallSession:notifyRecordOptions: cannot pause recording, ignoring request ');
|
||||
return false;
|
||||
}
|
||||
if (action == 'resumeCallRecording' && this.recordState !== RecordState.RecordingPaused) {
|
||||
this.logger.info({recordState: this.recordState},
|
||||
'CallSession:notifyRecordOptions: cannot resume recording, ignoring request ');
|
||||
return false;
|
||||
}
|
||||
/* check validity of request */
|
||||
if (action == 'startCallRecording' && this.recordState !== RecordState.RecordingOff) {
|
||||
this.logger.info({recordState: this.recordState},
|
||||
'CallSession:notifyRecordOptions: recording is already started, ignoring request');
|
||||
return false;
|
||||
}
|
||||
if (action == 'stopCallRecording' && this.recordState === RecordState.RecordingOff) {
|
||||
this.logger.info({recordState: this.recordState},
|
||||
'CallSession:notifyRecordOptions: recording is already stopped, ignoring request');
|
||||
return false;
|
||||
}
|
||||
if (action == 'pauseCallRecording' && this.recordState !== RecordState.RecordingOn) {
|
||||
this.logger.info({recordState: this.recordState},
|
||||
'CallSession:notifyRecordOptions: cannot pause recording, ignoring request ');
|
||||
return false;
|
||||
}
|
||||
if (action == 'resumeCallRecording' && this.recordState !== RecordState.RecordingPaused) {
|
||||
this.logger.info({recordState: this.recordState},
|
||||
'CallSession:notifyRecordOptions: cannot resume recording, ignoring request ');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.recordOptions = opts;
|
||||
this.recordOptions = opts;
|
||||
|
||||
switch (action) {
|
||||
case 'startCallRecording':
|
||||
return await this.startRecording();
|
||||
case 'stopCallRecording':
|
||||
return await this.stopRecording();
|
||||
case 'pauseCallRecording':
|
||||
return await this.pauseRecording();
|
||||
case 'resumeCallRecording':
|
||||
return await this.resumeRecording();
|
||||
default:
|
||||
throw new Error(`invalid record action ${action}`);
|
||||
switch (action) {
|
||||
case 'startCallRecording':
|
||||
return await this.startRecording();
|
||||
case 'stopCallRecording':
|
||||
return await this.stopRecording();
|
||||
case 'pauseCallRecording':
|
||||
return await this.pauseRecording();
|
||||
case 'resumeCallRecording':
|
||||
return await this.resumeRecording();
|
||||
default:
|
||||
throw new Error(`invalid record action ${action}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,7 +964,7 @@ class CallSession extends Emitter {
|
||||
this.logger.debug('CallSession:enableBackgroundTtsStream - ttsStream enabled');
|
||||
} else {
|
||||
this.logger.debug(
|
||||
'CallSession:enableBackgroundTtsStream - ignoring request as call does not have required conditions');
|
||||
'CallSession:enableBackgroundTtsStream - ignoring request; conditions not met (probably not using ws api)');
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err, say}, 'CallSession:enableBackgroundTtsStream - Error creating background tts stream task');
|
||||
@@ -941,9 +978,11 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
clearTtsStream() {
|
||||
this.requestor?.request('tts:streaming-event', '/streaming-event', {event_type: 'user_interruption'})
|
||||
.catch((err) => this.logger.info({err}, 'CallSession:clearTtsStream - Error sending user_interruption'));
|
||||
this.ttsStreamingBuffer?.clear();
|
||||
if (this.isTtsStreamEnabled) {
|
||||
this.requestor?.request('tts:streaming-event', '/streaming-event', {event_type: 'user_interruption'})
|
||||
.catch((err) => this.logger.info({err}, 'CallSession:clearTtsStream - Error sending user_interruption'));
|
||||
this.ttsStreamingBuffer?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
startTtsStream() {
|
||||
@@ -951,7 +990,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
stopTtsStream() {
|
||||
if (this.appIsUsingWebsockets) {
|
||||
if (this.isTtsStreamEnabled) {
|
||||
this.requestor?.request('tts:streaming-event', '/streaming-event', {event_type: 'stream_closed'})
|
||||
.catch((err) => this.logger.info({err}, 'CallSession:clearTtsStream - Error sending user_interruption'));
|
||||
this.ttsStreamingBuffer?.stop();
|
||||
@@ -1195,7 +1234,8 @@ class CallSession extends Emitter {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
client_id: credential.client_id,
|
||||
client_key: credential.client_key,
|
||||
user_id: credential.user_id
|
||||
user_id: credential.user_id,
|
||||
houndify_server_uri: credential.houndify_server_uri
|
||||
};
|
||||
}
|
||||
else if ('deepgramflux' === vendor) {
|
||||
@@ -1247,9 +1287,10 @@ class CallSession extends Emitter {
|
||||
}
|
||||
else {
|
||||
writeAlerts({
|
||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||
alert_type: type === 'tts' ? AlertType.TTS_NOT_PROVISIONED : AlertType.STT_NOT_PROVISIONED,
|
||||
account_sid: this.accountSid,
|
||||
vendor,
|
||||
label,
|
||||
target_sid: this.callSid
|
||||
}).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
|
||||
}
|
||||
@@ -2459,6 +2500,36 @@ Duration=${duration} `
|
||||
}
|
||||
else {
|
||||
this.logger.error(err, `Error attempting to allocate endpoint for for task ${task.name}`);
|
||||
// Check for SipError type (e.g., 488 codec incompatibility)
|
||||
const isSipError = err.name === 'SipError';
|
||||
if (isSipError && err.status) {
|
||||
// Extract Reason header from SIP response if available (e.g., Q.850;cause=88;text="INCOMPATIBLE_DESTINATION")
|
||||
const sipReasonHeader = err.res?.msg?.headers?.reason;
|
||||
this._endpointAllocationError = {
|
||||
status: err.status,
|
||||
reason: err.reason || 'Endpoint Allocation Failed',
|
||||
sipReasonHeader
|
||||
};
|
||||
this.logger.info({endpointAllocationError: this._endpointAllocationError},
|
||||
'Captured SipError for propagation to SBC');
|
||||
|
||||
// Send SIP error response immediately for inbound calls
|
||||
if (this.res && !this.res.finalResponseSent) {
|
||||
this.logger.info(`Sending ${err.status} response to SBC due to SipError`);
|
||||
this.res.send(err.status, {
|
||||
headers: {
|
||||
'X-Reason': `endpoint allocation failure: ${err.reason || 'Endpoint Allocation Failed'}`,
|
||||
...(sipReasonHeader && {'Reason': sipReasonHeader})
|
||||
}
|
||||
});
|
||||
this._notifyCallStatusChange({
|
||||
callStatus: CallStatus.Failed,
|
||||
sipStatus: err.status,
|
||||
sipReason: err.reason || 'Endpoint Allocation Failed'
|
||||
});
|
||||
this._callReleased();
|
||||
}
|
||||
}
|
||||
throw new Error(`${BADPRECONDITIONS}: unable to allocate endpoint`);
|
||||
}
|
||||
}
|
||||
@@ -2656,8 +2727,12 @@ Duration=${duration} `
|
||||
tidyUp();
|
||||
}
|
||||
else {
|
||||
this.logger.debug('CallSession:propagateAnswer - call already answered - re-anchor media with a reinvite');
|
||||
await this.dlg.modify(this.ep.local.sdp);
|
||||
if (this.currentTask?.name === TaskName.Dial && !this.currentTask.canReleaseMedia) {
|
||||
this.logger.debug('CallSession:propagateAnswer - call already answered and anchored');
|
||||
} else {
|
||||
this.logger.debug('CallSession:propagateAnswer - call already answered - re-anchor media with a reinvite');
|
||||
await this.dlg.modify(this.ep.local.sdp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2971,8 +3046,7 @@ Duration=${duration} `
|
||||
|
||||
// manage record all call.
|
||||
if (callStatus === CallStatus.InProgress) {
|
||||
if (this.accountInfo.account.record_all_calls ||
|
||||
this.application.record_all_calls) {
|
||||
if (this.accountInfo.account.record_all_calls || this.application.record_all_calls) {
|
||||
this.backgroundTaskManager.newTask('record');
|
||||
}
|
||||
} else if (callStatus == CallStatus.Completed) {
|
||||
|
||||
@@ -60,6 +60,19 @@ class InboundCallSession extends CallSession {
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (this._endpointAllocationError) {
|
||||
// Propagate SIP error from endpoint allocation failure back to the client
|
||||
const {status, reason, sipReasonHeader} = this._endpointAllocationError;
|
||||
this.rootSpan.setAttributes({'call.termination': `endpoint allocation SIP error ${status}`});
|
||||
this.logger.info({endpointAllocationError: this._endpointAllocationError},
|
||||
`InboundCallSession:_onTasksDone generating ${status} due to endpoint allocation failure`);
|
||||
this.res.send(status, {
|
||||
headers: {
|
||||
'X-Reason': `endpoint allocation failure: ${reason}`,
|
||||
...(sipReasonHeader && {'Reason': sipReasonHeader})
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.rootSpan.setAttributes({'call.termination': 'tasks completed without answering call'});
|
||||
this.logger.info('InboundCallSession:_onTasksDone auto-generating non-success response to invite');
|
||||
|
||||
@@ -158,7 +158,7 @@ class TaskDial extends Task {
|
||||
|
||||
get canReleaseMedia() {
|
||||
const keepAnchor = this.data.anchorMedia ||
|
||||
this.weAreTranscoding ||
|
||||
this.isTranscoding ||
|
||||
this.cs.isBackGroundListen ||
|
||||
this.cs.onHoldMusic ||
|
||||
ANCHOR_MEDIA_ALWAYS ||
|
||||
@@ -576,7 +576,7 @@ class TaskDial extends Task {
|
||||
proxy: `sip:${sbcAddress}`,
|
||||
callingNumber: this.callerId || fromUri.user,
|
||||
...(this.callerName && {callingName: this.callerName}),
|
||||
opusFirst: isOpusFirst(this.cs.ep.remote.sdp),
|
||||
opusFirst: isOpusFirst(this.cs.ep.local.sdp),
|
||||
isVideoCall: this.cs.ep.remote.sdp.includes('m=video')
|
||||
};
|
||||
|
||||
@@ -773,6 +773,15 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
async _connectSingleDial(cs, sd) {
|
||||
// start connect with dialed leg, this is the soonest we can identify transcoding
|
||||
if (this.epOther && sd.ep) {
|
||||
const codecA = getLeadingCodec(this.epOther.local.sdp);
|
||||
const codecB = getLeadingCodec(sd.ep.remote.sdp);
|
||||
this.isTranscoding = (codecA !== codecB);
|
||||
if (this.isTranscoding) {
|
||||
this.logger.info(`Dial:_connectSingleDial - transcoding from ${codecA} (A leg) to ${codecB} (B leg)`);
|
||||
}
|
||||
}
|
||||
if (!this.bridged && !this.canReleaseMedia) {
|
||||
this.logger.debug('Dial:_connectSingleDial bridging endpoints');
|
||||
if (this.epOther) {
|
||||
@@ -930,13 +939,6 @@ class TaskDial extends Task {
|
||||
this.logger.info({err}, 'Dial:_selectSingleDial - Error boosting audio signal');
|
||||
}
|
||||
}
|
||||
/* basic determination to see if call is being transcoded */
|
||||
const codecA = getLeadingCodec(this.epOther.local.sdp);
|
||||
const codecB = getLeadingCodec(this.ep.remote.sdp);
|
||||
this.weAreTranscoding = (codecA !== codecB);
|
||||
if (this.weAreTranscoding) {
|
||||
this.logger.info(`Dial:_selectSingleDial - transcoding from ${codecA} (A leg) to ${codecB} (B leg)`);
|
||||
}
|
||||
/* if we can release the media back to the SBC, do so now */
|
||||
if (this.canReleaseMedia || this.shouldExitMediaPathEntirely) {
|
||||
setTimeout(this._releaseMedia.bind(this, cs, sd, this.shouldExitMediaPathEntirely), 200);
|
||||
|
||||
@@ -258,7 +258,7 @@ class TaskGather extends SttTask {
|
||||
startDtmfListener();
|
||||
}
|
||||
this._stopVad();
|
||||
if (!this.killed) {
|
||||
if (!this.killed && !this.resolved) {
|
||||
startListening(cs, ep);
|
||||
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
|
||||
this.logger.debug('Gather:exec - starting transcription timers after say completes');
|
||||
@@ -296,7 +296,7 @@ class TaskGather extends SttTask {
|
||||
startDtmfListener();
|
||||
}
|
||||
this._stopVad();
|
||||
if (!this.killed) {
|
||||
if (!this.killed && !this.resolved) {
|
||||
startListening(cs, ep);
|
||||
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
|
||||
this.logger.debug('Gather:exec - starting transcription timers after play completes');
|
||||
@@ -500,6 +500,10 @@ class TaskGather extends SttTask {
|
||||
this.addCustomEventListener(ep, GladiaTranscriptionEvents.ConnectFailure,
|
||||
this._onVendorConnectFailure.bind(this, cs, ep));
|
||||
this.addCustomEventListener(ep, GladiaTranscriptionEvents.Error, this._onVendorError.bind(this, cs, ep));
|
||||
// gladia require unique url for each session
|
||||
const {host, path} = await this.createGladiaLiveSession();
|
||||
opts.GLADIA_SPEECH_HOST = host;
|
||||
opts.GLADIA_SPEECH_PATH = path;
|
||||
break;
|
||||
|
||||
case 'soniox':
|
||||
@@ -881,7 +885,7 @@ class TaskGather extends SttTask {
|
||||
this._fillerNoiseOn = false; // in a race, if we just started audio it may sneak through here
|
||||
this.ep.api('uuid_break', this.ep.uuid)
|
||||
.catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
cs.clearTtsStream();
|
||||
if (cs.isTtsStreamEnabled) cs.clearTtsStream();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1161,7 +1165,7 @@ class TaskGather extends SttTask {
|
||||
}
|
||||
|
||||
async _startFallback(cs, ep, evt) {
|
||||
if (this.canFallback) {
|
||||
if (this.canFallback()) {
|
||||
this._stopTranscribing(ep);
|
||||
try {
|
||||
this.logger.debug('gather:_startFallback');
|
||||
@@ -1318,6 +1322,8 @@ class TaskGather extends SttTask {
|
||||
}
|
||||
|
||||
this.resolved = true;
|
||||
// gather is resolved, prevent any further transcription events while resolve in progress
|
||||
this.removeCustomEventListeners();
|
||||
// If bargin is false and ws application return ack to verb:hook
|
||||
// the gather should not play any audio
|
||||
this._killAudio(this.cs);
|
||||
|
||||
@@ -146,8 +146,9 @@ class TaskLlmUltravox_S2S extends Task {
|
||||
return data;
|
||||
}
|
||||
|
||||
_unregisterHandlers() {
|
||||
_unregisterHandlers(ep) {
|
||||
this.removeCustomEventListeners();
|
||||
ep.removeAllListeners('dtmf');
|
||||
}
|
||||
|
||||
_registerHandlers(ep) {
|
||||
@@ -155,6 +156,7 @@ class TaskLlmUltravox_S2S extends Task {
|
||||
this.addCustomEventListener(ep, LlmEvents_Ultravox.ConnectFailure, this._onConnectFailure.bind(this, ep));
|
||||
this.addCustomEventListener(ep, LlmEvents_Ultravox.Disconnect, this._onDisconnect.bind(this, ep));
|
||||
this.addCustomEventListener(ep, LlmEvents_Ultravox.ServerEvent, this._onServerEvent.bind(this, ep));
|
||||
ep.on('dtmf', this._onDtmf.bind(this, ep));
|
||||
}
|
||||
|
||||
async _startListening(cs, ep) {
|
||||
@@ -189,7 +191,7 @@ class TaskLlmUltravox_S2S extends Task {
|
||||
/* note: the parent llm verb started the span, which is why this is necessary */
|
||||
await this.parent.performAction(this.results);
|
||||
|
||||
this._unregisterHandlers();
|
||||
this._unregisterHandlers(ep);
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
@@ -346,6 +348,18 @@ class TaskLlmUltravox_S2S extends Task {
|
||||
excludeEvents: this.excludeEvents
|
||||
}, 'TaskLlmUltravox_S2S:_populateEvents');
|
||||
}
|
||||
|
||||
_onDtmf(ep, evt) {
|
||||
this.logger.info({evt}, 'TaskLlmUltravox_S2S:_onDtmf - DTMF received');
|
||||
const {dtmf} = evt;
|
||||
const data = {
|
||||
type: 'user_text_message',
|
||||
text: `DTMF received: ${dtmf}`,
|
||||
urgency: 'immediate'
|
||||
};
|
||||
this._api(ep, [ep.uuid, ClientEvent, JSON.stringify(data)])
|
||||
.catch((err) => this.logger.info({err, evt}, 'TaskLlmUltravox_S2S:_onDtmf - Error sending DTMF as text message'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskLlmUltravox_S2S;
|
||||
|
||||
@@ -33,7 +33,7 @@ class TaskRedirect extends Task {
|
||||
}
|
||||
else {
|
||||
const baseUrl = this.cs.application.requestor.baseUrl;
|
||||
const newUrl = URL.parse(this.actionHook);
|
||||
const newUrl = new URL(this.actionHook);
|
||||
const newBaseUrl = newUrl.protocol + '//' + newUrl.host;
|
||||
if (baseUrl != newBaseUrl) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const assert = require('assert');
|
||||
const TtsTask = require('./tts-task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const {JAMBONES_SAY_CHUNK_SIZE} = require('../config');
|
||||
const pollySSMLSplit = require('polly-ssml-split');
|
||||
const { SpeechCredentialError, NonFatalTaskError } = require('../utils/error');
|
||||
const { sleepFor } = require('../utils/helpers');
|
||||
@@ -31,7 +32,7 @@ const isMatchingEvent = (logger, filename, playbackId, evt) => {
|
||||
const breakLengthyTextIfNeeded = (logger, text) => {
|
||||
// As The text can be used for tts streaming, we need to break lengthy text into smaller chunks
|
||||
// HIGH_WATER_BUFFER_SIZE defined in tts-streaming-buffer.js
|
||||
const chunkSize = 900;
|
||||
const chunkSize = JAMBONES_SAY_CHUNK_SIZE;
|
||||
const isSSML = text.startsWith('<speak>');
|
||||
const options = {
|
||||
softLimit: 100,
|
||||
|
||||
@@ -171,7 +171,7 @@ class SttTask extends Task {
|
||||
try {
|
||||
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
|
||||
} catch (error) {
|
||||
if (this.canFallback) {
|
||||
if (this.canFallback()) {
|
||||
this.notifyError(
|
||||
{
|
||||
msg: 'ASR error', details:`Invalid vendor ${this.vendor}, Error: ${error}`,
|
||||
@@ -203,26 +203,14 @@ class SttTask extends Task {
|
||||
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
||||
this.data.recognizer.punctuation = cs.globalSttPunctuation;
|
||||
}
|
||||
if (this.vendor === 'gladia') {
|
||||
const { api_key, region } = this.sttCredentials;
|
||||
const {url} = await this.createGladiaLiveSession({
|
||||
api_key, region,
|
||||
model: this.data.recognizer.model || 'solaria-1',
|
||||
options: this.data.recognizer.gladiaOptions || {}
|
||||
});
|
||||
const {host, pathname, search} = new URL(url);
|
||||
this.sttCredentials.host = host;
|
||||
this.sttCredentials.path = `${pathname}${search}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async createGladiaLiveSession({
|
||||
api_key,
|
||||
region = 'us-west',
|
||||
model = 'solaria-1',
|
||||
options = {},
|
||||
}) {
|
||||
async createGladiaLiveSession() {
|
||||
const { api_key, region = 'us-west' } = this.sttCredentials;
|
||||
const model = this.data.recognizer.model || 'solaria-1';
|
||||
const options = this.data.recognizer.gladiaOptions || {};
|
||||
|
||||
const url = `https://api.gladia.io/v2/live?region=${region}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -252,7 +240,9 @@ class SttTask extends Task {
|
||||
|
||||
const data = await response.json();
|
||||
this.logger.debug({url: data.url}, 'Gladia Call registered');
|
||||
return data;
|
||||
|
||||
const {host, pathname, search} = new URL(data.url);
|
||||
return {host, path: `${pathname}${search}`};
|
||||
}
|
||||
|
||||
addCustomEventListener(ep, event, handler) {
|
||||
@@ -260,8 +250,19 @@ class SttTask extends Task {
|
||||
ep.addCustomEventListener(event, handler);
|
||||
}
|
||||
|
||||
removeCustomEventListeners() {
|
||||
this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler));
|
||||
removeCustomEventListeners(ep) {
|
||||
if (ep) {
|
||||
// for specific endpoint
|
||||
this.eventHandlers.filter((h) => h.ep === ep).forEach((h) => {
|
||||
h.ep.removeCustomEventListener(h.event, h.handler);
|
||||
});
|
||||
this.eventHandlers = this.eventHandlers.filter((h) => h.ep !== ep);
|
||||
return;
|
||||
} else {
|
||||
// for all endpoints
|
||||
this.eventHandlers.forEach((h) => h.ep.removeCustomEventListener(h.event, h.handler));
|
||||
this.eventHandlers = [];
|
||||
}
|
||||
}
|
||||
|
||||
async _initSpeechCredentials(cs, vendor, label) {
|
||||
@@ -275,6 +276,7 @@ class SttTask extends Task {
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||
vendor,
|
||||
label,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||
// the ASR might have fallback configuration, should not done task here.
|
||||
@@ -329,11 +331,13 @@ class SttTask extends Task {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
get canFallback() {
|
||||
canFallback() {
|
||||
return this.fallbackVendor && this.isHandledByPrimaryProvider && !this.cs.hasFallbackAsr;
|
||||
}
|
||||
|
||||
async _initFallback() {
|
||||
// ep is optional for gather or any verb that have single ep,
|
||||
// but transcribe does need as it might has 2 eps
|
||||
async _initFallback(ep) {
|
||||
assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration');
|
||||
this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
|
||||
this.isHandledByPrimaryProvider = false;
|
||||
@@ -346,7 +350,7 @@ class SttTask extends Task {
|
||||
this.data.recognizer.label = this.label;
|
||||
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
|
||||
// cleanup previous listener from previous vendor
|
||||
this.removeCustomEventListeners();
|
||||
this.removeCustomEventListeners(ep);
|
||||
}
|
||||
|
||||
async compileHintsForCobalt(ep, hostport, model, token, hints) {
|
||||
@@ -473,6 +477,7 @@ class SttTask extends Task {
|
||||
message: 'STT failure reported by vendor',
|
||||
detail: evt.error,
|
||||
vendor: this.vendor,
|
||||
label: this.label,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
|
||||
}
|
||||
@@ -486,6 +491,7 @@ class SttTask extends Task {
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
|
||||
vendor: this.vendor,
|
||||
label: this.label,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
|
||||
}
|
||||
|
||||
@@ -70,6 +70,9 @@ class TaskTranscribe extends SttTask {
|
||||
this._bufferedTranscripts = [ [], [] ]; // for channel 1 and 2
|
||||
this.bugname_prefix = 'transcribe_';
|
||||
this.paused = false;
|
||||
// fallback flags
|
||||
this.isHandledByPrimaryProviderForEp1 = true;
|
||||
this.isHandledByPrimaryProviderForEp2 = true;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Transcribe; }
|
||||
@@ -456,6 +459,14 @@ class TaskTranscribe extends SttTask {
|
||||
else if (this.data.recognizer?.hints?.length > 0) {
|
||||
prompt = this.data.recognizer?.hints.join(', ');
|
||||
}
|
||||
} else if (this.vendor === 'gladia') {
|
||||
// gladia require unique url for each session
|
||||
const {host, path} = await this.createGladiaLiveSession();
|
||||
await ep.set({
|
||||
GLADIA_SPEECH_HOST: host,
|
||||
GLADIA_SPEECH_PATH: path,
|
||||
})
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
}
|
||||
|
||||
await ep.startTranscription({
|
||||
@@ -776,7 +787,7 @@ class TaskTranscribe extends SttTask {
|
||||
}
|
||||
|
||||
async _startFallback(cs, _ep, evt) {
|
||||
if (this.canFallback) {
|
||||
if (this.canFallback(_ep)) {
|
||||
_ep.stopTranscription({
|
||||
vendor: this.vendor,
|
||||
bugname: this.bugname,
|
||||
@@ -786,7 +797,7 @@ class TaskTranscribe extends SttTask {
|
||||
try {
|
||||
this.notifyError({ msg: 'ASR error',
|
||||
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'in progress'});
|
||||
await this._initFallback();
|
||||
await this._initFallback(_ep);
|
||||
let channel = 1;
|
||||
if (this.ep !== _ep) {
|
||||
channel = 2;
|
||||
@@ -895,6 +906,41 @@ class TaskTranscribe extends SttTask {
|
||||
if (this._asrTimer) clearTimeout(this._asrTimer);
|
||||
this._asrTimer = null;
|
||||
}
|
||||
|
||||
// We need to keep track the fallback is happened for each endpoint
|
||||
// override the canFallback and _initFallback methods to make sure that
|
||||
// we only fallback once per endpoint
|
||||
// we want to keep track this on task level instead of endpoint level
|
||||
// because the endpoint instance is used across multiple tasks.
|
||||
canFallback(ep) {
|
||||
let isHandledByPrimaryProvider = this.isHandledByPrimaryProvider;
|
||||
if (ep === this.ep) {
|
||||
isHandledByPrimaryProvider = this.isHandledByPrimaryProviderForEp1;
|
||||
} else if (ep === this.ep2) {
|
||||
isHandledByPrimaryProvider = this.isHandledByPrimaryProviderForEp2;
|
||||
}
|
||||
|
||||
const isOneOfEndpointAlreadyFallenBack = !!this.ep && !!this.ep2 &&
|
||||
this.isHandledByPrimaryProviderForEp1 !== this.isHandledByPrimaryProviderForEp2;
|
||||
|
||||
// fallback is configured
|
||||
return this.fallbackVendor &&
|
||||
// has this endpoint already fallen back
|
||||
isHandledByPrimaryProvider &&
|
||||
// in global level, is there any fallback is already happened
|
||||
// one fallen endpoint will mark cs.hasFallbackAsr to true,
|
||||
// so if one endpoint was fallen, the other endpoint would be able to fallback.
|
||||
(isOneOfEndpointAlreadyFallenBack || !this.cs.hasFallbackAsr);
|
||||
}
|
||||
|
||||
_initFallback(ep) {
|
||||
if (ep === this.ep) {
|
||||
this.isHandledByPrimaryProviderForEp1 = false;
|
||||
} else if (ep === this.ep2) {
|
||||
this.isHandledByPrimaryProviderForEp2 = false;
|
||||
}
|
||||
return super._initFallback(ep);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskTranscribe;
|
||||
|
||||
@@ -89,8 +89,9 @@ class TtsTask extends Task {
|
||||
// api_key, model_id, api_uri, custom_tts_streaming_url, and auth_token are encoded in the credentials
|
||||
// allow them to be overriden via config, using options
|
||||
// give preference to options passed in via config
|
||||
const local_options = {...JSON.parse(options), ...this.options};
|
||||
const local_voice_settings = {...JSON.parse(options).voice_settings, ...this.options.voice_settings};
|
||||
const parsed_options = options ? JSON.parse(options) : {};
|
||||
const local_options = {...parsed_options, ...this.options};
|
||||
const local_voice_settings = {...(parsed_options.voice_settings || {}), ...(this.options.voice_settings || {})};
|
||||
const local_api_key = local_options.api_key ?? api_key;
|
||||
const local_model_id = local_options.model_id ?? model_id;
|
||||
const local_api_uri = local_options.api_uri ?? api_uri;
|
||||
@@ -273,6 +274,7 @@ class TtsTask extends Task {
|
||||
account_sid,
|
||||
alert_type: AlertType.TTS_NOT_PROVISIONED,
|
||||
vendor,
|
||||
label,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
|
||||
throw new SpeechCredentialError('no provisioned speech credentials for TTS');
|
||||
@@ -359,6 +361,7 @@ class TtsTask extends Task {
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.TTS_FAILURE,
|
||||
vendor,
|
||||
label,
|
||||
detail: err.message,
|
||||
target_sid: cs.callSid
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||
|
||||
@@ -405,19 +405,21 @@ module.exports = (logger) => {
|
||||
if (ep.amd) {
|
||||
vendor = ep.amd.vendor;
|
||||
ep.amd.stopAllTimers();
|
||||
|
||||
ep.removeListener(GoogleTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(GoogleTranscriptionEvents.EndOfUtterance, ep.amd.EndOfUtteranceHandler);
|
||||
ep.removeListener(AwsTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(AzureTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(AzureTranscriptionEvents.NoSpeechDetected, ep.amd.noSpeechHandler);
|
||||
ep.removeListener(NuanceTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(DeepgramTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(SonioxTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(IbmTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(NvidiaTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(JambonzTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
|
||||
try {
|
||||
ep.removeListener(GoogleTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(GoogleTranscriptionEvents.EndOfUtterance, ep.amd.EndOfUtteranceHandler);
|
||||
ep.removeListener(AwsTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(AzureTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(AzureTranscriptionEvents.NoSpeechDetected, ep.amd.noSpeechHandler);
|
||||
ep.removeListener(NuanceTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(DeepgramTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(SonioxTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(IbmTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(NvidiaTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
ep.removeListener(JambonzTranscriptionEvents.Transcription, ep.amd.transcriptionHandler);
|
||||
} catch (error) {
|
||||
logger.error('Unable to Remove AMD Listener', error);
|
||||
}
|
||||
ep.amd = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -135,26 +135,24 @@ class BackgroundTaskManager extends Emitter {
|
||||
|
||||
// Initiate Record
|
||||
async _initRecord() {
|
||||
if (this.cs.accountInfo.account.record_all_calls || this.cs.application.record_all_calls) {
|
||||
if (!JAMBONZ_RECORD_WS_BASE_URL || !this.cs.accountInfo.account.bucket_credential) {
|
||||
this.logger.error('_initRecord: invalid cfg - missing JAMBONZ_RECORD_WS_BASE_URL or bucket config');
|
||||
return undefined;
|
||||
}
|
||||
const listenOpts = {
|
||||
url: `${JAMBONZ_RECORD_WS_BASE_URL}/record/${this.cs.accountInfo.account.bucket_credential.vendor}`,
|
||||
disableBidirectionalAudio: true,
|
||||
mixType : 'stereo',
|
||||
passDtmf: true
|
||||
};
|
||||
if (JAMBONZ_RECORD_WS_USERNAME && JAMBONZ_RECORD_WS_PASSWORD) {
|
||||
listenOpts.wsAuth = {
|
||||
username: JAMBONZ_RECORD_WS_USERNAME,
|
||||
password: JAMBONZ_RECORD_WS_PASSWORD
|
||||
};
|
||||
}
|
||||
this.logger.debug({listenOpts}, '_initRecord: enabling listen');
|
||||
return await this._initListen({verb: 'listen', ...listenOpts}, 'jambonz-session-record', true, 'record');
|
||||
if (!JAMBONZ_RECORD_WS_BASE_URL || !this.cs.accountInfo.account.bucket_credential) {
|
||||
this.logger.error('_initRecord: invalid cfg - missing JAMBONZ_RECORD_WS_BASE_URL or bucket config');
|
||||
return undefined;
|
||||
}
|
||||
const listenOpts = {
|
||||
url: `${JAMBONZ_RECORD_WS_BASE_URL}/record/${this.cs.accountInfo.account.bucket_credential.vendor}`,
|
||||
disableBidirectionalAudio: true,
|
||||
mixType : 'stereo',
|
||||
passDtmf: true
|
||||
};
|
||||
if (JAMBONZ_RECORD_WS_USERNAME && JAMBONZ_RECORD_WS_PASSWORD) {
|
||||
listenOpts.wsAuth = {
|
||||
username: JAMBONZ_RECORD_WS_USERNAME,
|
||||
password: JAMBONZ_RECORD_WS_PASSWORD
|
||||
};
|
||||
}
|
||||
this.logger.debug({listenOpts}, '_initRecord: enabling listen');
|
||||
return await this._initListen({verb: 'listen', ...listenOpts}, 'jambonz-session-record', true, 'record');
|
||||
}
|
||||
|
||||
// Initiate Transcribe
|
||||
|
||||
@@ -152,6 +152,7 @@ const speechMapper = (cred) => {
|
||||
obj.client_id = o.client_id;
|
||||
obj.client_key = o.client_key;
|
||||
obj.user_id = o.user_id;
|
||||
obj.houndify_server_uri = o.houndify_server_uri;
|
||||
}
|
||||
else if ('voxist' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
|
||||
@@ -191,7 +191,7 @@ class HttpRequestor extends BaseRequestor {
|
||||
method,
|
||||
headers: hdrs,
|
||||
...('POST' === method && {body: JSON.stringify(payload)}),
|
||||
timeout: HTTP_TIMEOUT,
|
||||
headersTimeout: HTTP_TIMEOUT,
|
||||
followRedirects: false
|
||||
};
|
||||
|
||||
|
||||
@@ -100,6 +100,30 @@ module.exports = (logger) => {
|
||||
else if (K8S) {
|
||||
lifecycleEmitter.scaleIn = () => process.exit(0);
|
||||
}
|
||||
else {
|
||||
process.on('SIGUSR1', () => {
|
||||
logger.info('received SIGUSR1: begin drying up calls for scale-in');
|
||||
dryUpCalls = true;
|
||||
|
||||
const {srf} = require('../..');
|
||||
const {writeSystemAlerts} = srf.locals;
|
||||
if (writeSystemAlerts) {
|
||||
const {SystemState, FEATURE_SERVER} = require('./constants');
|
||||
writeSystemAlerts({
|
||||
system_component: FEATURE_SERVER,
|
||||
state : SystemState.GracefulShutdownInProgress,
|
||||
fields : {
|
||||
detail: `feature-server with process_id ${process.pid} shutdown in progress`,
|
||||
host: srf.locals?.ipv4
|
||||
}
|
||||
});
|
||||
}
|
||||
pingProxies(srf);
|
||||
|
||||
// Note: in response to SIGUSR1 we start drying up but do not exit when calls reach zero.
|
||||
// This is to allow external scripts that sent the signal to manage the lifecycle.
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function pingProxies(srf) {
|
||||
|
||||
@@ -127,7 +127,6 @@ class SttLatencyCalculator extends Emitter {
|
||||
|
||||
calculateLatency() {
|
||||
if (!this.isRunning) {
|
||||
this.logger.debug('Latency calculator is not running, cannot calculate latency, returning default values');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -920,7 +920,7 @@ module.exports = (logger) => {
|
||||
...(rOpts.initialSpeechTimeoutMs > 0 &&
|
||||
{AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}),
|
||||
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
|
||||
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
|
||||
...(azureOptions.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
|
||||
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
|
||||
...(azureOptions.speechSegmentationSilenceTimeoutMs &&
|
||||
{AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS: azureOptions.speechSegmentationSilenceTimeoutMs}),
|
||||
@@ -1085,13 +1085,6 @@ module.exports = (logger) => {
|
||||
...(keyterms && keyterms.length > 0 && {DEEPGRAMFLUX_SPEECH_KEYTERMS: keyterms.join(',')}),
|
||||
};
|
||||
}
|
||||
else if ('gladia' === vendor) {
|
||||
const {host, path} = sttCredentials;
|
||||
opts = {
|
||||
GLADIA_SPEECH_HOST: host,
|
||||
GLADIA_SPEECH_PATH: path,
|
||||
};
|
||||
}
|
||||
else if ('soniox' === vendor) {
|
||||
const {sonioxOptions = {}} = rOpts;
|
||||
const {storage = {}} = sonioxOptions;
|
||||
@@ -1226,8 +1219,10 @@ module.exports = (logger) => {
|
||||
audioFormat, enableNoiseReduction, enableProfanityFilter, enablePunctuation,
|
||||
enableCapitalization, confidenceThreshold, enableDisfluencyFilter,
|
||||
maxResults, enableWordTimestamps, maxAlternatives, partialTranscriptInterval,
|
||||
sessionTimeout, connectionTimeout, customVocabulary, languageModel
|
||||
sessionTimeout, connectionTimeout, customVocabulary, languageModel,
|
||||
requestInfo, sampleRate
|
||||
} = rOpts.houndifyOptions || {};
|
||||
const audioEndpointUri = audioEndpoint || sttCredentials.houndify_server_uri;
|
||||
|
||||
opts = {
|
||||
...opts,
|
||||
@@ -1263,10 +1258,12 @@ module.exports = (logger) => {
|
||||
...(country && {HOUNDIFY_COUNTRY: country}),
|
||||
...(timeZone && {HOUNDIFY_TIMEZONE: timeZone}),
|
||||
...(domain && {HOUNDIFY_DOMAIN: domain}),
|
||||
...(audioEndpoint && {HOUNDIFY_AUDIO_ENDPOINT: audioEndpoint}),
|
||||
...(audioEndpointUri && {HOUNDIFY_AUDIO_ENDPOINT: audioEndpointUri}),
|
||||
...(customVocabulary && {HOUNDIFY_CUSTOM_VOCABULARY:
|
||||
Array.isArray(customVocabulary) ? customVocabulary.join(',') : customVocabulary}),
|
||||
...(languageModel && {HOUNDIFY_LANGUAGE_MODEL: languageModel}),
|
||||
...(requestInfo && {HOUNDIFY_REQUEST_INFO: JSON.stringify(requestInfo)}),
|
||||
...(sampleRate && {HOUNDIFY_SAMPLING_RATE: sampleRate}),
|
||||
};
|
||||
}
|
||||
else if ('voxist' === vendor) {
|
||||
|
||||
@@ -163,7 +163,6 @@ class TtsStreamingBuffer extends Emitter {
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.logger.debug('TtsStreamingBuffer:clear');
|
||||
if (this._connectionStatus !== TtsStreamingConnectionStatus.Connected) return;
|
||||
clearTimeout(this.timer);
|
||||
this._api(this.ep, [this.ep.uuid, 'clear']).catch((err) =>
|
||||
@@ -437,7 +436,15 @@ class TtsStreamingBuffer extends Emitter {
|
||||
|
||||
const findSentenceBoundary = (text, limit) => {
|
||||
// Look for punctuation or double newline that signals sentence end.
|
||||
const sentenceEndRegex = /[.!?](?=\s|$)|\n\n/g;
|
||||
// Includes:
|
||||
// - ASCII: . ! ?
|
||||
// - Arabic: ؟ (question mark), ۔ (full stop)
|
||||
// - Japanese: 。 (full stop), !, ? (full-width exclamation/question)
|
||||
//
|
||||
// For languages that use spaces between sentences, we still require
|
||||
// whitespace or end-of-string after the mark. For Japanese (no spaces),
|
||||
// we treat the punctuation itself as a boundary regardless of following char.
|
||||
const sentenceEndRegex = /[.!?؟۔](?=\s|$)|[。!?]|\n\n/g;
|
||||
let lastSentenceBoundary = -1;
|
||||
let match;
|
||||
while ((match = sentenceEndRegex.exec(text)) && match.index < limit) {
|
||||
|
||||
5116
package-lock.json
generated
5116
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,8 +33,8 @@
|
||||
"@jambonz/realtimedb-helpers": "^0.8.15",
|
||||
"@jambonz/speech-utils": "^0.2.26",
|
||||
"@jambonz/stats-collector": "^0.1.10",
|
||||
"@jambonz/time-series": "^0.2.14",
|
||||
"@jambonz/verb-specifications": "^0.0.119",
|
||||
"@jambonz/time-series": "^0.2.15",
|
||||
"@jambonz/verb-specifications": "^0.0.123",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@opentelemetry/api": "^1.8.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.23.0",
|
||||
|
||||
Reference in New Issue
Block a user