mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-01-25 02:07:56 +00:00
Compare commits
27 Commits
fix/rest-d
...
fix/445
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71bb4707c1 | ||
|
|
2935574440 | ||
|
|
c10c561ba1 | ||
|
|
2ccd33e212 | ||
|
|
a03baa8461 | ||
|
|
90df33a15c | ||
|
|
a15479e6dc | ||
|
|
dd74cb2cc6 | ||
|
|
7a02c36bad | ||
|
|
78fd4549af | ||
|
|
b1ecf069bf | ||
|
|
6f0dbef433 | ||
|
|
32dcb2adfa | ||
|
|
ee514f7459 | ||
|
|
4cfea0707a | ||
|
|
f8c5abe9e9 | ||
|
|
ad722a55ee | ||
|
|
82939214a2 | ||
|
|
043a171f41 | ||
|
|
c8e9b34b53 | ||
|
|
d7dcdb1d0c | ||
|
|
fbd0782258 | ||
|
|
38f9329b12 | ||
|
|
d4bfdf0916 | ||
|
|
9203deef0f | ||
|
|
48b182c891 | ||
|
|
e8e987cb9d |
@@ -25,7 +25,7 @@ const JAMBONES_MYSQL_USER = process.env.JAMBONES_MYSQL_USER;
|
||||
const JAMBONES_MYSQL_PASSWORD = process.env.JAMBONES_MYSQL_PASSWORD;
|
||||
const JAMBONES_MYSQL_DATABASE = process.env.JAMBONES_MYSQL_DATABASE;
|
||||
const JAMBONES_MYSQL_PORT = parseInt(process.env.JAMBONES_MYSQL_PORT, 10) || 3306;
|
||||
const JAMBONES_MYSQL_REFRESH_TTL = process.env.JAMBONES_MYSQL_REFRESH_TTL;
|
||||
const JAMBONES_MYSQL_REFRESH_TTL = parseInt(process.env.JAMBONES_MYSQL_REFRESH_TTL, 10) || 0;
|
||||
const JAMBONES_MYSQL_CONNECTION_LIMIT = parseInt(process.env.JAMBONES_MYSQL_CONNECTION_LIMIT, 10) || 10;
|
||||
|
||||
/* redis */
|
||||
|
||||
@@ -47,6 +47,11 @@ router.post('/', async(req, res) => {
|
||||
const application = req.body.application_sid ? await lookupAppBySid(req.body.application_sid) : null;
|
||||
const record_all_calls = account.record_all_calls || (application && application.record_all_calls);
|
||||
const recordOutputFormat = account.record_format || 'mp3';
|
||||
const rootSpan = new RootSpan('rest-call', {
|
||||
callSid,
|
||||
accountSid,
|
||||
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid})
|
||||
});
|
||||
|
||||
opts.headers = {
|
||||
...opts.headers,
|
||||
@@ -54,6 +59,7 @@ router.post('/', async(req, res) => {
|
||||
'X-Jambonz-FS-UUID': srf.locals.fsUUID,
|
||||
'X-Call-Sid': callSid,
|
||||
'X-Account-Sid': accountSid,
|
||||
'X-Trace-ID': rootSpan.traceId,
|
||||
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
|
||||
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
|
||||
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat})
|
||||
@@ -194,7 +200,6 @@ router.post('/', async(req, res) => {
|
||||
/* ok our outbound INVITE is in flight */
|
||||
|
||||
const tasks = [restDial];
|
||||
const rootSpan = new RootSpan('rest-call', inviteReq);
|
||||
sipLogger = logger.child({
|
||||
callSid,
|
||||
callId: inviteReq.get('Call-ID'),
|
||||
@@ -258,6 +263,7 @@ router.post('/', async(req, res) => {
|
||||
sipStatus: err.status,
|
||||
sipReason: err.reason
|
||||
});
|
||||
cs.callGone = true;
|
||||
}
|
||||
else {
|
||||
if (cs) cs.emit('callStatusChange', {
|
||||
|
||||
@@ -9,25 +9,29 @@ const {CallStatus, CallDirection} = require('../../utils/constants');
|
||||
*/
|
||||
function retrieveCallSession(callSid, opts) {
|
||||
if (opts.call_status_hook && !opts.call_hook) {
|
||||
throw new DbErrorBadRequest('call_status_hook can be updated only when call_hook is also being updated');
|
||||
throw new DbErrorBadRequest(
|
||||
`call_status_hook can be updated only when call_hook is also being updated for call_sid ${callSid}`);
|
||||
}
|
||||
const cs = sessionTracker.get(callSid);
|
||||
if (!cs) {
|
||||
throw new DbErrorUnprocessableRequest('call session is gone');
|
||||
throw new DbErrorUnprocessableRequest(`call session is gone for call_sid ${callSid}`);
|
||||
}
|
||||
|
||||
if (opts.call_status === CallStatus.Completed && !cs.hasStableDialog) {
|
||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
||||
throw new DbErrorUnprocessableRequest(
|
||||
`current call state is incompatible with requested action for call_sid ${callSid}`);
|
||||
}
|
||||
else if (opts.call_status === CallStatus.NoAnswer) {
|
||||
if (cs.direction === CallDirection.Outbound) {
|
||||
if (!cs.isOutboundCallRinging) {
|
||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
||||
throw new DbErrorUnprocessableRequest(
|
||||
`current call state is incompatible with requested action for call_sid ${callSid}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (cs.isInboundCallAnswered) {
|
||||
throw new DbErrorUnprocessableRequest('current call state is incompatible with requested action');
|
||||
throw new DbErrorUnprocessableRequest(
|
||||
`current call state is incompatible with requested action for call_sid ${callSid}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,30 @@ class CallSession extends Emitter {
|
||||
set speechSynthesisVendor(vendor) {
|
||||
this.application.speech_synthesis_vendor = vendor;
|
||||
}
|
||||
|
||||
get fallbackSpeechSynthesisVendor() {
|
||||
return this.application.fallback_speech_synthesis_vendor;
|
||||
}
|
||||
set fallbackSpeechSynthesisVendor(vendor) {
|
||||
this.application.fallback_speech_synthesis_vendor = vendor;
|
||||
}
|
||||
|
||||
/**
|
||||
* default label to use for speech synthesis if not provided in the app
|
||||
*/
|
||||
get speechSynthesisLabel() {
|
||||
return this.application.speech_synthesis_label;
|
||||
}
|
||||
set speechSynthesisLabel(label) {
|
||||
this.application.speech_synthesis_label = label;
|
||||
}
|
||||
|
||||
get fallbackSpeechSynthesisLabel() {
|
||||
return this.application.fallback_speech_synthesis_label;
|
||||
}
|
||||
set fallbackSpeechSynthesisLabel(label) {
|
||||
this.application.fallback_speech_synthesis_label = label;
|
||||
}
|
||||
/**
|
||||
* default voice to use for speech synthesis if not provided in the app
|
||||
*/
|
||||
@@ -188,6 +212,13 @@ class CallSession extends Emitter {
|
||||
set speechSynthesisVoice(voice) {
|
||||
this.application.speech_synthesis_voice = voice;
|
||||
}
|
||||
|
||||
get fallbackSpeechSynthesisVoice() {
|
||||
return this.application.fallback_speech_synthesis_voice;
|
||||
}
|
||||
set fallbackSpeechSynthesisVoice(voice) {
|
||||
this.application.fallback_speech_synthesis_voice = voice;
|
||||
}
|
||||
/**
|
||||
* default language to use for speech synthesis if not provided in the app
|
||||
*/
|
||||
@@ -198,6 +229,13 @@ class CallSession extends Emitter {
|
||||
this.application.speech_synthesis_language = language;
|
||||
}
|
||||
|
||||
get fallbackSpeechSynthesisLanguage() {
|
||||
return this.application.fallback_speech_synthesis_language;
|
||||
}
|
||||
set fallbackSpeechSynthesisLanguage(language) {
|
||||
this.application.fallback_speech_synthesis_language = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* default vendor to use for speech recognition if not provided in the app
|
||||
*/
|
||||
@@ -207,6 +245,29 @@ class CallSession extends Emitter {
|
||||
set speechRecognizerVendor(vendor) {
|
||||
this.application.speech_recognizer_vendor = vendor;
|
||||
}
|
||||
|
||||
get fallbackSpeechRecognizerVendor() {
|
||||
return this.application.fallback_speech_recognizer_vendor;
|
||||
}
|
||||
set fallbackSpeechRecognizerVendor(vendor) {
|
||||
this.application.fallback_speech_recognizer_vendor = vendor;
|
||||
}
|
||||
/**
|
||||
* default vendor to use for speech recognition if not provided in the app
|
||||
*/
|
||||
get speechRecognizerLabel() {
|
||||
return this.application.speech_recognizer_label;
|
||||
}
|
||||
set speechRecognizerLabel(label) {
|
||||
this.application.speech_recognizer_label = label;
|
||||
}
|
||||
|
||||
get fallbackSpeechRecognizerLabel() {
|
||||
return this.application.fallback_speech_recognizer_label;
|
||||
}
|
||||
set fallbackSpeechRecognizerLabel(label) {
|
||||
this.application.fallback_speech_recognizer_label = label;
|
||||
}
|
||||
/**
|
||||
* default language to use for speech recognition if not provided in the app
|
||||
*/
|
||||
@@ -217,6 +278,13 @@ class CallSession extends Emitter {
|
||||
this.application.speech_recognizer_language = language;
|
||||
}
|
||||
|
||||
get fallbackSpeechRecognizerLanguage() {
|
||||
return this.application.fallback_speech_recognizer_language;
|
||||
}
|
||||
set fallbackSpeechRecognizerLanguage(language) {
|
||||
this.application.fallback_speech_recognizer_language = language;
|
||||
}
|
||||
|
||||
/**
|
||||
* indicates whether the call currently in progress
|
||||
*/
|
||||
@@ -338,6 +406,14 @@ class CallSession extends Emitter {
|
||||
return this._globalSttPunctuation;
|
||||
}
|
||||
|
||||
get onHoldMusic() {
|
||||
return this._onHoldMusic;
|
||||
}
|
||||
|
||||
set onHoldMusic(url) {
|
||||
this._onHoldMusic = url;
|
||||
}
|
||||
|
||||
hasGlobalSttPunctuation() {
|
||||
return this._globalSttPunctuation !== undefined;
|
||||
}
|
||||
@@ -640,15 +716,28 @@ class CallSession extends Emitter {
|
||||
* Check for speech credentials for the specified vendor
|
||||
* @param {*} vendor - google or aws
|
||||
*/
|
||||
getSpeechCredentials(vendor, type) {
|
||||
getSpeechCredentials(vendor, type, label = null) {
|
||||
const {writeAlerts, AlertType} = this.srf.locals;
|
||||
if (this.accountInfo.speech && this.accountInfo.speech.length > 0) {
|
||||
const credential = this.accountInfo.speech.find((s) => s.vendor === vendor);
|
||||
// firstly check if account level has expected credential
|
||||
let credential = this.accountInfo.speech.find((s) => s.vendor === vendor &&
|
||||
s.label === label && s.account_sid);
|
||||
if (!credential) {
|
||||
// check if SP level has expected credential
|
||||
credential = this.accountInfo.speech.find((s) => s.vendor === vendor &&
|
||||
s.label === label && !s.account_sid);
|
||||
}
|
||||
if (credential && (
|
||||
(type === 'tts' && credential.use_for_tts) ||
|
||||
(type === 'stt' && credential.use_for_stt)
|
||||
)) {
|
||||
this.logger.info(`Speech credential vendor: ${credential.vendor}
|
||||
${credential.label ? `, label: ${credential.label}` : ''} is chosen`);
|
||||
if ('google' === vendor) {
|
||||
if (type === 'tts' && !credential.tts_tested_ok ||
|
||||
type === 'stt' && !credential.stt_tested_ok) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const cred = JSON.parse(credential.service_key.replace(/\n/g, '\\n'));
|
||||
return {
|
||||
@@ -680,8 +769,10 @@ class CallSession extends Emitter {
|
||||
region: credential.region,
|
||||
use_custom_stt: credential.use_custom_stt,
|
||||
custom_stt_endpoint: credential.custom_stt_endpoint,
|
||||
custom_stt_endpoint_url: credential.custom_stt_endpoint_url,
|
||||
use_custom_tts: credential.use_custom_tts,
|
||||
custom_tts_endpoint: credential.custom_tts_endpoint
|
||||
custom_tts_endpoint: credential.custom_tts_endpoint,
|
||||
custom_tts_endpoint_url: credential.custom_tts_endpoint_url
|
||||
};
|
||||
}
|
||||
else if ('wellsaid' === vendor) {
|
||||
@@ -796,23 +887,15 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === this.tasks.length && this.requestor instanceof WsRequestor && !this.callGone) {
|
||||
//let span;
|
||||
if (0 === this.tasks.length &&
|
||||
this.requestor instanceof WsRequestor &&
|
||||
!this.requestor.closedGracefully &&
|
||||
!this.callGone
|
||||
) {
|
||||
try {
|
||||
//const {span} = this.rootSpan.startChildSpan('waiting for commands');
|
||||
//const {reason, queue, command} = await this._awaitCommandsOrHangup();
|
||||
/*
|
||||
span.setAttributes({
|
||||
'completion.reason': reason,
|
||||
'async.request.queue': queue,
|
||||
'async.request.command': command
|
||||
});
|
||||
span.end();
|
||||
*/
|
||||
await this._awaitCommandsOrHangup();
|
||||
if (this.callGone) break;
|
||||
} catch (err) {
|
||||
//span.end();
|
||||
this.logger.info(err, 'CallSession:exec - error waiting for new commands');
|
||||
break;
|
||||
}
|
||||
@@ -992,6 +1075,24 @@ class CallSession extends Emitter {
|
||||
listenTask.updateListen(opts.listen_status);
|
||||
}
|
||||
|
||||
/**
|
||||
* perform live call control -- change Transcribe status
|
||||
* @param {object} opts
|
||||
* @param {string} opts.transcribe_status - 'pause' or 'resume'
|
||||
*/
|
||||
async _lccTranscribeStatus(opts) {
|
||||
const task = this.currentTask;
|
||||
if (!task || ![TaskName.Dial, TaskName.Transcribe].includes(task.name)) {
|
||||
return this.logger.info(`CallSession:_lccTranscribeStatus - invalid transcribe_status in task ${task.name}`);
|
||||
}
|
||||
const transcribeTask = task.name === TaskName.Transcribe ? task : task.transcribeTask;
|
||||
if (!transcribeTask) {
|
||||
return this.logger
|
||||
.info('CallSession:_lccTranscribeStatus - invalid transcribe_status: Dial does not have a Transcribe');
|
||||
}
|
||||
transcribeTask.updateTranscribe(opts.transcribe_status);
|
||||
}
|
||||
|
||||
async _lccMuteStatus(callSid, mute) {
|
||||
// this whole thing requires us to be in a Dial or Conference verb
|
||||
const task = this.currentTask;
|
||||
@@ -1110,6 +1211,9 @@ class CallSession extends Emitter {
|
||||
if (opts.listen_status) {
|
||||
await this._lccListenStatus(opts);
|
||||
}
|
||||
if (opts.transcribe_status) {
|
||||
await this._lccTranscribeStatus(opts);
|
||||
}
|
||||
else if (opts.mute_status) {
|
||||
await this._lccMuteStatus(callSid, opts.mute_status === 'mute');
|
||||
}
|
||||
@@ -1286,6 +1390,10 @@ class CallSession extends Emitter {
|
||||
this._lccListenStatus(data);
|
||||
break;
|
||||
|
||||
case 'transcribe:status':
|
||||
this._lccTranscribeStatus(data);
|
||||
break;
|
||||
|
||||
case 'whisper':
|
||||
this._lccWhisper(data, call_sid);
|
||||
break;
|
||||
@@ -1378,6 +1486,8 @@ class CallSession extends Emitter {
|
||||
this.ep = ep;
|
||||
this.logger.debug(`allocated endpoint ${ep.uuid}`);
|
||||
|
||||
this._configMsEndpoint();
|
||||
|
||||
this.ep.on('destroy', () => {
|
||||
this.logger.debug(`endpoint was destroyed!! ${this.ep.uuid}`);
|
||||
});
|
||||
@@ -1448,6 +1558,7 @@ class CallSession extends Emitter {
|
||||
return;
|
||||
}
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
|
||||
this._configMsEndpoint();
|
||||
|
||||
await this.dlg.modify(this.ep.local.sdp);
|
||||
this.logger.debug('CallSession:replaceEndpoint completed');
|
||||
@@ -1541,9 +1652,14 @@ class CallSession extends Emitter {
|
||||
res.send(200, {body: this.ep.local.sdp});
|
||||
}
|
||||
else {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
|
||||
if (this.currentTask.name === TaskName.Dial && this.currentTask.isOnHold) {
|
||||
this.logger.info('onholdMusic reINVITE after media has been released');
|
||||
await this.currentTask.handleReinviteAfterMediaReleased(req, res);
|
||||
} else {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'handling reINVITE');
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (this.currentTask && this.currentTask.name === TaskName.Dial) {
|
||||
@@ -1590,6 +1706,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
if (!this.ep) {
|
||||
this.ep = await this.ms.createEndpoint({remoteSdp: this.req.body});
|
||||
this._configMsEndpoint();
|
||||
}
|
||||
return {ms: this.ms, ep: this.ep};
|
||||
}
|
||||
@@ -1744,6 +1861,7 @@ class CallSession extends Emitter {
|
||||
'X-Reason': 'anchor-media'
|
||||
}
|
||||
});
|
||||
this._configMsEndpoint();
|
||||
}
|
||||
|
||||
async handleReinviteAfterMediaReleased(req, res) {
|
||||
@@ -1769,7 +1887,8 @@ class CallSession extends Emitter {
|
||||
// nice, call is in progress, good time to enable record
|
||||
await this.enableRecordAllCall();
|
||||
} else if (callStatus == CallStatus.Completed && this.isBackGroundListen) {
|
||||
await this.stopBackgroundListen();
|
||||
this.stopBackgroundListen().catch((err) => this.logger.error(
|
||||
{err}, 'CallSession:_notifyCallStatusChange - error stopping background listen'));
|
||||
}
|
||||
|
||||
/* race condition: we hang up at the same time as the caller */
|
||||
@@ -1814,6 +1933,7 @@ class CallSession extends Emitter {
|
||||
username: JAMBONZ_RECORD_WS_USERNAME,
|
||||
password: JAMBONZ_RECORD_WS_PASSWORD
|
||||
},
|
||||
disableBidirectionalAudio: true,
|
||||
mixType : 'stereo',
|
||||
passDtmf: true
|
||||
};
|
||||
@@ -1823,6 +1943,12 @@ class CallSession extends Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
_configMsEndpoint() {
|
||||
if (this.onHoldMusic) {
|
||||
this.ep.set({hold_music: `shout://${this.onHoldMusic.replace(/^https?:\/\//, '')}`});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* notifyTaskError - only used when websocket connection is used instead of webhooks
|
||||
*/
|
||||
|
||||
@@ -49,7 +49,6 @@ class RestCallSession extends CallSession {
|
||||
*/
|
||||
_callerHungup() {
|
||||
if (this.restDialTask) {
|
||||
this.logger.info('RestCallSession: releasing AMD');
|
||||
this.restDialTask.turnOffAmd();
|
||||
}
|
||||
this.callInfo.callTerminationBy = 'caller';
|
||||
|
||||
@@ -48,7 +48,7 @@ class Conference extends Task {
|
||||
this.confName = this.data.name;
|
||||
[
|
||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
|
||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook'
|
||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook', 'endConferenceDuration'
|
||||
].forEach((attr) => this[attr] = this.data[attr]);
|
||||
this.record = this.data.record || {};
|
||||
this.statusEvents = [];
|
||||
@@ -344,9 +344,13 @@ class Conference extends Task {
|
||||
}
|
||||
|
||||
const opts = {};
|
||||
if (this.endConferenceOnExit) Object.assign(opts, {flags: {endconf: true}});
|
||||
if (this.startConferenceOnEnter) Object.assign(opts, {flags: {moderator: true}});
|
||||
if (this.joinMuted) Object.assign(opts, {flags: {mute: true}});
|
||||
if (this.endConferenceOnExit || this.startConferenceOnEnter || this.joinMuted) {
|
||||
Object.assign(opts, {flags: {
|
||||
...(this.endConferenceOnExit && {endconf: true}),
|
||||
...(this.startConferenceOnEnter && {moderator: true}),
|
||||
...(this.joinMuted && {joinMuted: true}),
|
||||
}});
|
||||
}
|
||||
|
||||
try {
|
||||
const {memberId, confUuid} = await this.ep.join(this.confName, opts);
|
||||
@@ -389,6 +393,11 @@ class Conference extends Task {
|
||||
this.ep.api('conference', `${this.confName} set max_members ${this.maxParticipants}`)
|
||||
.catch((err) => this.logger.error(err, `Error setting max participants to ${this.maxParticipants}`));
|
||||
}
|
||||
|
||||
if (typeof this.endConferenceDuration === 'number' && this.endConferenceDuration >= 0) {
|
||||
this.ep.api('conference', `${this.confName} set endconference_grace_time ${this.endConferenceDuration}`)
|
||||
.catch((err) => this.logger.error(err, `Error setting end conference time to ${this.endConferenceDuration}`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,6 +40,8 @@ class TaskConfig extends Task {
|
||||
this.preconditions = (this.bargeIn.enable || this.record?.action || this.listen?.url || this.data.amd) ?
|
||||
TaskPreconditions.Endpoint :
|
||||
TaskPreconditions.None;
|
||||
|
||||
this.onHoldMusic = this.data.onHoldMusic;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Config; }
|
||||
@@ -72,6 +74,7 @@ class TaskConfig extends Task {
|
||||
}
|
||||
if (this.data.amd) phrase.push('enable amd');
|
||||
if (this.notifyEvents) phrase.push(`event notification ${this.notifyEvents ? 'on' : 'off'}`);
|
||||
if (this.onHoldMusic) phrase.push(`onHoldMusic: ${this.onHoldMusic}`);
|
||||
return `${this.name}{${phrase.join(',')}}`;
|
||||
}
|
||||
|
||||
@@ -83,6 +86,10 @@ class TaskConfig extends Task {
|
||||
cs.notifyEvents = !!this.data.notifyEvents;
|
||||
}
|
||||
|
||||
if (this.onHoldMusic) {
|
||||
cs.onHoldMusic = this.onHoldMusic;
|
||||
}
|
||||
|
||||
if (this.data.amd) {
|
||||
this.startAmd = cs.startAmd;
|
||||
this.stopAmd = cs.stopAmd;
|
||||
@@ -105,21 +112,53 @@ class TaskConfig extends Task {
|
||||
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
|
||||
? this.synthesizer.vendor
|
||||
: cs.speechSynthesisVendor;
|
||||
cs.speechSynthesisLabel = this.synthesizer.label !== 'default'
|
||||
? this.synthesizer.label
|
||||
: cs.speechSynthesisLabel;
|
||||
cs.speechSynthesisLanguage = this.synthesizer.language !== 'default'
|
||||
? this.synthesizer.language
|
||||
: cs.speechSynthesisLanguage;
|
||||
cs.speechSynthesisVoice = this.synthesizer.voice !== 'default'
|
||||
? this.synthesizer.voice
|
||||
: cs.speechSynthesisVoice;
|
||||
|
||||
// fallback vendor
|
||||
cs.fallbackSpeechSynthesisVendor = this.synthesizer.fallbackVendor !== 'default'
|
||||
? this.synthesizer.fallbackVendor
|
||||
: cs.fallbackSpeechSynthesisVendor;
|
||||
cs.fallbackSpeechSynthesisLabel = this.synthesizer.fallbackLabel !== 'default'
|
||||
? this.synthesizer.fallbackLabel
|
||||
: cs.fallbackSpeechSynthesisLabel;
|
||||
cs.fallbackSpeechSynthesisLanguage = this.synthesizer.fallbackLanguage !== 'default'
|
||||
? this.synthesizer.fallbackLanguage
|
||||
: cs.fallbackSpeechSynthesisLanguage;
|
||||
cs.fallbackSpeechSynthesisVoice = this.synthesizer.fallbackVoice !== 'default'
|
||||
? this.synthesizer.fallbackVoice
|
||||
: cs.fallbackSpeechSynthesisVoice;
|
||||
this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer');
|
||||
}
|
||||
if (this.hasRecognizer) {
|
||||
cs.speechRecognizerVendor = this.recognizer.vendor !== 'default'
|
||||
? this.recognizer.vendor
|
||||
: cs.speechRecognizerVendor;
|
||||
cs.speechRecognizerLabel = this.recognizer.label !== 'default'
|
||||
? this.recognizer.label
|
||||
: cs.speechRecognizerLabel;
|
||||
cs.speechRecognizerLanguage = this.recognizer.language !== 'default'
|
||||
? this.recognizer.language
|
||||
: cs.speechRecognizerLanguage;
|
||||
|
||||
//fallback
|
||||
cs.fallbackSpeechRecognizerVendor = this.recognizer.fallbackVendor !== 'default'
|
||||
? this.recognizer.fallbackVendor
|
||||
: cs.fallbackSpeechRecognizerVendor;
|
||||
cs.fallbackSpeechRecognizerLabel = this.recognizer.fallbackLabel !== 'default'
|
||||
? this.recognizer.fallbackLabel
|
||||
: cs.fallbackSpeechRecognizerLabel;
|
||||
cs.fallbackSpeechRecognizerLanguage = this.recognizer.fallbackLanguage !== 'default'
|
||||
? this.recognizer.fallbackLanguage
|
||||
: cs.fallbackSpeechRecognizerLanguage;
|
||||
|
||||
cs.isContinuousAsr = typeof this.recognizer.asrTimeout === 'number' ? true : false;
|
||||
if (cs.isContinuousAsr) {
|
||||
cs.asrTimeout = this.recognizer.asrTimeout;
|
||||
|
||||
@@ -12,10 +12,13 @@ const assert = require('assert');
|
||||
const placeCall = require('../utils/place-outdial');
|
||||
const sessionTracker = require('../session/session-tracker');
|
||||
const DtmfCollector = require('../utils/dtmf-collector');
|
||||
const ConfirmCallSession = require('../session/confirm-call-session');
|
||||
const dbUtils = require('../utils/db-utils');
|
||||
const debug = require('debug')('jambonz:feature-server');
|
||||
const {parseUri} = require('drachtio-srf');
|
||||
const {ANCHOR_MEDIA_ALWAYS} = require('../config');
|
||||
const { isOnhold } = require('../utils/sdp-utils');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
|
||||
function parseDtmfOptions(logger, dtmfCapture) {
|
||||
let parentDtmfCollector, childDtmfCollector;
|
||||
@@ -135,6 +138,10 @@ class TaskDial extends Task {
|
||||
|
||||
get name() { return TaskName.Dial; }
|
||||
|
||||
get isOnHold() {
|
||||
return this.isIncomingLegHold || this.isOutgoingLegHold;
|
||||
}
|
||||
|
||||
get canReleaseMedia() {
|
||||
const keepAnchor = this.data.anchorMedia ||
|
||||
this.cs.isBackGroundListen ||
|
||||
@@ -225,7 +232,7 @@ class TaskDial extends Task {
|
||||
}
|
||||
this._removeDtmfDetection(cs.dlg);
|
||||
this._removeDtmfDetection(this.dlg);
|
||||
this._killOutdials();
|
||||
await this._killOutdials();
|
||||
if (this.sd) {
|
||||
this.sd.kill();
|
||||
this.sd.removeAllListeners();
|
||||
@@ -346,11 +353,16 @@ class TaskDial extends Task {
|
||||
sd.removeAllListeners('callCreateFail');
|
||||
}
|
||||
|
||||
_killOutdials() {
|
||||
async _killOutdials() {
|
||||
for (const [callSid, sd] of Array.from(this.dials)) {
|
||||
this.logger.debug(`Dial:_killOutdials killing callSid ${callSid}`);
|
||||
sd.kill().catch((err) => this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`));
|
||||
try {
|
||||
await sd.kill();
|
||||
} catch (err) {
|
||||
this.logger.info(err, `Dial:_killOutdials Error killing ${callSid}`);
|
||||
}
|
||||
this._removeHandlers(sd);
|
||||
this.logger.debug(`Dial:_killOutdials killed callSid ${callSid}`);
|
||||
}
|
||||
this.dials.clear();
|
||||
}
|
||||
@@ -442,10 +454,14 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
const ms = await cs.getMS();
|
||||
this.timerRing = setTimeout(() => {
|
||||
this.timerRing = setTimeout(async() => {
|
||||
this.logger.info(`Dial:_attemptCall: ring no answer timer ${this.timeout}s exceeded`);
|
||||
this.timerRing = null;
|
||||
this._killOutdials();
|
||||
try {
|
||||
await this._killOutdials();
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'Dial:_attemptCall - error killing outdials');
|
||||
}
|
||||
this.result = {
|
||||
dialCallStatus: CallStatus.NoAnswer,
|
||||
dialSipStatus: 487
|
||||
@@ -507,7 +523,8 @@ class TaskDial extends Task {
|
||||
callInfo: cs.callInfo,
|
||||
accountInfo: cs.accountInfo,
|
||||
rootSpan: cs.rootSpan,
|
||||
startSpan: this.startSpan.bind(this)
|
||||
startSpan: this.startSpan.bind(this),
|
||||
dialTask: this
|
||||
});
|
||||
this.dials.set(sd.callSid, sd);
|
||||
|
||||
@@ -523,7 +540,8 @@ class TaskDial extends Task {
|
||||
}
|
||||
})
|
||||
.on('callStatusChange', (obj) => {
|
||||
if (this.results.dialCallStatus !== CallStatus.Completed) {
|
||||
if (this.results.dialCallStatus !== CallStatus.Completed &&
|
||||
this.results.dialCallStatus !== CallStatus.NoAnswer) {
|
||||
Object.assign(this.results, {
|
||||
dialCallStatus: obj.callStatus,
|
||||
dialSipStatus: obj.sipStatus,
|
||||
@@ -576,11 +594,7 @@ class TaskDial extends Task {
|
||||
}
|
||||
})
|
||||
.on('reinvite', (req, res) => {
|
||||
try {
|
||||
cs.handleReinviteAfterMediaReleased(req, res);
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error in dial einvite from B leg');
|
||||
}
|
||||
this._onReinvite(req, res);
|
||||
})
|
||||
.on('refer', (callInfo, req, res) => {
|
||||
|
||||
@@ -616,6 +630,35 @@ class TaskDial extends Task {
|
||||
this._killOutdials(); // NB: order is important
|
||||
}
|
||||
|
||||
async _onReinvite(req, res) {
|
||||
try {
|
||||
let isHandled = false;
|
||||
if (this.cs.onHoldMusic) {
|
||||
if (isOnhold(req.body) && !this.epOther && !this.ep) {
|
||||
await this.cs.handleReinviteAfterMediaReleased(req, res);
|
||||
// Onhold but media is already released
|
||||
// reconnect A Leg and Response B leg
|
||||
await this.reAnchorMedia(this.cs, this.sd);
|
||||
this.isOutgoingLegHold = true;
|
||||
isHandled = true;
|
||||
this._onHoldHook();
|
||||
} else if (!isOnhold(req.body) && this.epOther && this.ep && this.isOutgoingLegHold && this.canReleaseMedia) {
|
||||
// Offhold, time to release media
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
await res.send(200, {body: newSdp});
|
||||
await this._releaseMedia(this.cs, this.sd);
|
||||
isHandled = true;
|
||||
this.isOutgoingLegHold = false;
|
||||
}
|
||||
}
|
||||
if (!isHandled) {
|
||||
this.cs.handleReinviteAfterMediaReleased(req, res);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'Error in dial einvite from B leg');
|
||||
}
|
||||
}
|
||||
|
||||
_onMaxCallDuration(cs) {
|
||||
this.logger.info(`Dial:_onMaxCallDuration tearing down call as it has reached ${this.timeLimit}s`);
|
||||
this.ep && this.ep.unbridge();
|
||||
@@ -720,9 +763,29 @@ class TaskDial extends Task {
|
||||
}
|
||||
|
||||
async handleReinviteAfterMediaReleased(req, res) {
|
||||
const sdp = await this.dlg.modify(req.body);
|
||||
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
|
||||
res.send(200, {body: sdp});
|
||||
let isHandled = false;
|
||||
if (isOnhold(req.body) && !this.epOther && !this.ep) {
|
||||
const sdp = await this.dlg.modify(req.body);
|
||||
res.send(200, {body: sdp});
|
||||
// Onhold but media is already released
|
||||
await this.reAnchorMedia(this.cs, this.sd);
|
||||
isHandled = true;
|
||||
this.isIncomingLegHold = true;
|
||||
this._onHoldHook();
|
||||
} else if (!isOnhold(req.body) && this.epOther && this.ep && this.isIncomingLegHold && this.canReleaseMedia) {
|
||||
// Offhold, time to release media
|
||||
const newSdp = await this.epOther.modify(req.body);
|
||||
await res.send(200, {body: newSdp});
|
||||
await this._releaseMedia(this.cs, this.sd);
|
||||
isHandled = true;
|
||||
this.isIncomingLegHold = false;
|
||||
}
|
||||
|
||||
if (!isHandled) {
|
||||
const sdp = await this.dlg.modify(req.body);
|
||||
this.logger.info({sdp}, 'Dial:handleReinviteAfterMediaReleased - sent reinvite to B leg');
|
||||
res.send(200, {body: sdp});
|
||||
}
|
||||
}
|
||||
|
||||
_onAmdEvent(cs, evt) {
|
||||
@@ -733,6 +796,48 @@ class TaskDial extends Task {
|
||||
this.logger.error({err}, 'Dial:_onAmdEvent - error calling actionHook');
|
||||
});
|
||||
}
|
||||
|
||||
async _onHoldHook(allowed = [TaskName.Play, TaskName.Say, TaskName.Pause]) {
|
||||
if (this.data.onHoldHook) {
|
||||
// send silence for keep Voice quality
|
||||
await this.epOther.play('silence_stream://500');
|
||||
let allowedTasks;
|
||||
do {
|
||||
try {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
const json = await this.cs.application.requestor.
|
||||
request('verb:hook', this.data.onHoldHook, this.cs.callInfo.toJSON(), httpHeaders);
|
||||
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
|
||||
allowedTasks = tasks.filter((t) => allowed.includes(t.name));
|
||||
if (tasks.length !== allowedTasks.length) {
|
||||
this.logger.debug({tasks, allowedTasks}, 'unsupported task');
|
||||
throw new Error(`unsupported verb in enqueue waitHook: only ${JSON.stringify(allowed)}`);
|
||||
}
|
||||
this.logger.debug(`DialTask:_onHoldHook: executing ${tasks.length} tasks`);
|
||||
if (tasks.length) {
|
||||
this._playSession = new ConfirmCallSession({
|
||||
logger: this.logger,
|
||||
application: this.cs.application,
|
||||
dlg: this.isIncomingLegHold ? this.dlg : this.cs.dlg,
|
||||
ep: this.isIncomingLegHold ? this.ep : this.cs.ep,
|
||||
callInfo: this.cs.callInfo,
|
||||
accountInfo: this.cs.accountInfo,
|
||||
tasks,
|
||||
rootSpan: this.cs.rootSpan
|
||||
});
|
||||
await this._playSession.exec();
|
||||
this._playSession = null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.info(error, 'DialTask:_onHoldHook: failed retrieving waitHook');
|
||||
this._playSession = null;
|
||||
break;
|
||||
}
|
||||
} while (allowedTasks && allowedTasks.length > 0 && !this.killed && this.isOnHold);
|
||||
this.logger.info('Finish onHoldHook');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskDial;
|
||||
|
||||
@@ -58,6 +58,13 @@ class Dialogflow extends Task {
|
||||
this.vendor = this.data.tts.vendor || 'default';
|
||||
this.language = this.data.tts.language || 'default';
|
||||
this.voice = this.data.tts.voice || 'default';
|
||||
this.speechSynthesisLabel = this.data.tts.label || 'default';
|
||||
|
||||
// fallback tts
|
||||
this.fallbackVendor = this.data.tts.fallbackVendor || 'default';
|
||||
this.fallbackLanguage = this.data.tts.fallbackLanguage || 'default';
|
||||
this.fallbackVoice = this.data.tts.fallbackLanguage || 'default';
|
||||
this.fallbackLabel = this.data.tts.fallbackLabel || 'default';
|
||||
}
|
||||
this.bargein = this.data.bargein;
|
||||
}
|
||||
@@ -118,8 +125,15 @@ class Dialogflow extends Task {
|
||||
this.vendor = cs.speechSynthesisVendor;
|
||||
this.language = cs.speechSynthesisLanguage;
|
||||
this.voice = cs.speechSynthesisVoice;
|
||||
this.speechSynthesisLabel = cs.speechSynthesisLabel;
|
||||
}
|
||||
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
|
||||
if (this.fallbackVendor === 'default') {
|
||||
this.fallbackVendor = cs.fallbackSpeechSynthesisVendor;
|
||||
this.fallbackLanguage = cs.fallbackSpeechSynthesisLanguage;
|
||||
this.fallbackVoice = cs.fallbackSpeechSynthesisVoice;
|
||||
this.fallbackLabel = cs.fallbackSpeechSynthesisLabel;
|
||||
}
|
||||
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts', this.speechSynthesisLabel);
|
||||
|
||||
this.ep.addCustomEventListener('dialogflow::intent', this._onIntent.bind(this, ep, cs));
|
||||
this.ep.addCustomEventListener('dialogflow::transcription', this._onTranscription.bind(this, ep, cs));
|
||||
@@ -221,17 +235,7 @@ class Dialogflow extends Task {
|
||||
}
|
||||
|
||||
try {
|
||||
const obj = {
|
||||
account_sid: cs.accountSid,
|
||||
text: intent.fulfillmentText,
|
||||
vendor: this.vendor,
|
||||
language: this.language,
|
||||
voice: this.voice,
|
||||
salt: cs.callSid,
|
||||
credentials: this.ttsCredentials
|
||||
};
|
||||
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via tts');
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, obj);
|
||||
const {filePath, servedFromCache} = await this._fallbackSynthAudio(cs, intent, stats, synthAudio);
|
||||
if (filePath) cs.trackTmpFile(filePath);
|
||||
if (!this.ttsCredentials && !servedFromCache) cs.billForTts(intent.fulfillmentText.length);
|
||||
|
||||
@@ -277,6 +281,46 @@ class Dialogflow extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
async _fallbackSynthAudio(cs, intent, stats, synthAudio) {
|
||||
try {
|
||||
const obj = {
|
||||
account_sid: cs.accountSid,
|
||||
text: intent.fulfillmentText,
|
||||
vendor: this.vendor,
|
||||
language: this.language,
|
||||
voice: this.voice,
|
||||
salt: cs.callSid,
|
||||
credentials: this.ttsCredentials
|
||||
};
|
||||
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via tts');
|
||||
|
||||
return await synthAudio(stats, obj);
|
||||
} catch (error) {
|
||||
this.logger.info({error}, 'Failed to synthesize audio from primary vendor');
|
||||
|
||||
try {
|
||||
if (this.fallbackVendor) {
|
||||
const credentials = cs.getSpeechCredentials(this.fallbackVendor, 'tts', this.fallbackLabel);
|
||||
const obj = {
|
||||
account_sid: cs.accountSid,
|
||||
text: intent.fulfillmentText,
|
||||
vendor: this.fallbackVendor,
|
||||
language: this.fallbackLanguage,
|
||||
voice: this.fallbackVoice,
|
||||
salt: cs.callSid,
|
||||
credentials
|
||||
};
|
||||
this.logger.debug({obj}, 'Dialogflow:_onIntent - playing message via fallback tts');
|
||||
return await synthAudio(stats, obj);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Failed to synthesize audio from falllback vendor');
|
||||
throw err;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A transcription - either interim or final - has been returned.
|
||||
* If we are doing barge-in based on hotword detection, check for the hotword or phrase.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
const Task = require('./task');
|
||||
const {
|
||||
TaskName,
|
||||
TaskPreconditions,
|
||||
GoogleTranscriptionEvents,
|
||||
NuanceTranscriptionEvents,
|
||||
AwsTranscriptionEvents,
|
||||
@@ -19,6 +17,7 @@ const {
|
||||
} = require('../config');
|
||||
const makeTask = require('./make_task');
|
||||
const assert = require('assert');
|
||||
const SttTask = require('./stt-task');
|
||||
|
||||
const compileTranscripts = (logger, evt, arr) => {
|
||||
if (!Array.isArray(arr) || arr.length === 0) return;
|
||||
@@ -30,23 +29,9 @@ const compileTranscripts = (logger, evt, arr) => {
|
||||
evt.alternatives[0].transcript = t.trim();
|
||||
};
|
||||
|
||||
class TaskGather extends Task {
|
||||
class TaskGather extends SttTask {
|
||||
constructor(logger, opts, parentTask) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
const {
|
||||
setChannelVarsForStt,
|
||||
normalizeTranscription,
|
||||
removeSpeechListeners,
|
||||
setSpeechCredentialsAtRuntime,
|
||||
compileSonioxTranscripts
|
||||
} = require('../utils/transcription-utils')(logger);
|
||||
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||
this.normalizeTranscription = normalizeTranscription;
|
||||
this.removeSpeechListeners = removeSpeechListeners;
|
||||
this.compileSonioxTranscripts = compileSonioxTranscripts;
|
||||
|
||||
super(logger, opts, parentTask);
|
||||
[
|
||||
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
|
||||
@@ -62,16 +47,10 @@ class TaskGather extends Task {
|
||||
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
this.language = recognizer.language;
|
||||
|
||||
/* let credentials be supplied in the recognizer object at runtime */
|
||||
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
||||
|
||||
/* continuous ASR (i.e. compile transcripts until a special timeout or dtmf key) */
|
||||
this.asrTimeout = typeof recognizer.asrTimeout === 'number' ? recognizer.asrTimeout * 1000 : 0;
|
||||
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = recognizer.asrDtmfTerminationDigit;
|
||||
this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ?
|
||||
this.data.recognizer.asrTimeout * 1000 : 0;
|
||||
if (this.asrTimeout > 0) this.asrDtmfTerminationDigit = this.data.recognizer.asrDtmfTerminationDigit;
|
||||
this.isContinuousAsr = this.asrTimeout > 0;
|
||||
|
||||
if (Array.isArray(this.data.recognizer.hints) &&
|
||||
@@ -79,10 +58,7 @@ class TaskGather extends Task {
|
||||
logger.debug('Gather: an empty hints array was supplied, so we will mask global hints');
|
||||
this.maskGlobalSttHints = true;
|
||||
}
|
||||
this.data.recognizer.hints = this.data.recognizer.hints || [];
|
||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || [];
|
||||
}
|
||||
else this.data.recognizer = {hints: [], altLanguages: []};
|
||||
|
||||
this.digitBuffer = '';
|
||||
this._earlyMedia = this.data.earlyMedia === true;
|
||||
@@ -97,11 +73,6 @@ class TaskGather extends Task {
|
||||
|
||||
/* buffer speech for continuous asr */
|
||||
this._bufferedTranscripts = [];
|
||||
|
||||
/* buffer for soniox transcripts */
|
||||
this._sonioxTranscripts = [];
|
||||
|
||||
this.parentTask = parentTask;
|
||||
this.partialTranscriptsCount = 0;
|
||||
}
|
||||
|
||||
@@ -137,7 +108,6 @@ class TaskGather extends Task {
|
||||
this.logger.debug({options: this.data}, 'Gather:exec');
|
||||
await super.exec(cs);
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
|
||||
|
||||
if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) {
|
||||
const {hints, hintsBoost} = cs.globalSttHints;
|
||||
@@ -151,7 +121,7 @@ class TaskGather extends Task {
|
||||
}
|
||||
if (cs.hasAltLanguages) {
|
||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages.concat(cs.altLanguages);
|
||||
this.logger.debug({altLanguages: this.altLanguages},
|
||||
this.logger.debug({altLanguages: this.data.recognizer?.altLanguages},
|
||||
'Gather:exec - applying altLanguages');
|
||||
}
|
||||
if (cs.hasGlobalSttPunctuation && !this.data.recognizer.punctuation) {
|
||||
@@ -184,57 +154,59 @@ class TaskGather extends Task {
|
||||
this.language = cs.speechRecognizerLanguage;
|
||||
if (this.data.recognizer) this.data.recognizer.language = this.language;
|
||||
}
|
||||
if ('default' === this.label || !this.label) {
|
||||
this.label = cs.speechRecognizerLabel;
|
||||
if (this.data.recognizer) this.data.recognizer.label = this.label;
|
||||
}
|
||||
// Fallback options
|
||||
if ('default' === this.fallbackVendor || !this.fallbackVendor) {
|
||||
this.fallbackVendor = cs.fallbackSpeechRecognizerVendor;
|
||||
if (this.data.recognizer) this.data.recognizer.fallbackVendor = this.fallbackVendor;
|
||||
}
|
||||
if ('default' === this.fallbackLanguage || !this.fallbackLanguage) {
|
||||
this.fallbackLanguage = cs.fallbackSpeechRecognizerLanguage;
|
||||
if (this.data.recognizer) this.data.recognizer.fallbackLanguage = this.fallbackLanguage;
|
||||
}
|
||||
if ('default' === this.fallbackLabel || !this.fallbackLabel) {
|
||||
this.fallbackLabel = cs.fallbackSpeechRecognizerLabel;
|
||||
if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel;
|
||||
}
|
||||
if (!this.data.recognizer.vendor) {
|
||||
this.data.recognizer.vendor = this.vendor;
|
||||
}
|
||||
if (this.needsStt && !this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
|
||||
if (this.needsStt && !this.sttCredentials) {
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||
vendor: this.vendor
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||
// Notify application that STT vender is wrong.
|
||||
this.notifyError({
|
||||
msg: 'ASR error',
|
||||
details: `No speech-to-text service credentials for ${this.vendor} have been configured`
|
||||
});
|
||||
this.notifyTaskDone();
|
||||
throw new Error(`No speech-to-text service credentials for ${this.vendor} have been configured`);
|
||||
try {
|
||||
this.sttCredentials = await this._initSpeechCredentials(cs, this.vendor, this.label);
|
||||
} catch (error) {
|
||||
if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
|
||||
await this._fallback();
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
|
||||
/* get nuance access token */
|
||||
const {client_id, secret} = this.sttCredentials;
|
||||
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
|
||||
this.logger.debug({client_id}, `Gather:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
this.sttCredentials = {...this.sttCredentials, access_token};
|
||||
}
|
||||
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
|
||||
/* get ibm access token */
|
||||
const {stt_api_key, stt_region} = this.sttCredentials;
|
||||
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
|
||||
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
|
||||
}
|
||||
const startListening = (cs, ep) => {
|
||||
const startListening = async(cs, ep) => {
|
||||
this._startTimer();
|
||||
if (this.isContinuousAsr && 0 === this.timeout) this._startAsrTimer();
|
||||
if (this.input.includes('speech') && !this.listenDuringPrompt) {
|
||||
this._initSpeech(cs, ep)
|
||||
.then(() => {
|
||||
if (this.killed) {
|
||||
this.logger.info('Gather:exec - task was quickly killed so do not transcribe');
|
||||
return;
|
||||
}
|
||||
this._startTranscribing(ep);
|
||||
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error({err}, 'error in initSpeech');
|
||||
});
|
||||
try {
|
||||
await this._initSpeech(cs, ep);
|
||||
if (this.killed) {
|
||||
this.logger.info('Gather:exec - task was quickly killed so do not transcribe');
|
||||
return;
|
||||
}
|
||||
this._startTranscribing(ep);
|
||||
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
|
||||
} catch (e) {
|
||||
if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
|
||||
await this._fallback();
|
||||
startListening(cs, ep);
|
||||
} else {
|
||||
this.logger.error({error: e}, 'error in initSpeech');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -704,8 +676,22 @@ class TaskGather extends Task {
|
||||
_onJambonzConnect(_cs, _ep) {
|
||||
this.logger.debug('TaskGather:_onJambonzConnect');
|
||||
}
|
||||
_onJambonzError(cs, _ep, evt) {
|
||||
async _onJambonzError(cs, _ep, evt) {
|
||||
this.logger.info({evt}, 'TaskGather:_onJambonzError');
|
||||
if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
|
||||
_ep.stopTranscription({vendor: this.vendor})
|
||||
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||
try {
|
||||
await this._fallback();
|
||||
await this._initSpeech(cs, _ep);
|
||||
this._startTranscribing(_ep);
|
||||
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
|
||||
}
|
||||
}
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
|
||||
if (this.vendor === 'nuance') {
|
||||
|
||||
@@ -25,6 +25,13 @@ class Lex extends Task {
|
||||
this.vendor = this.data.tts.vendor || 'default';
|
||||
this.language = this.data.tts.language || 'default';
|
||||
this.voice = this.data.tts.voice || 'default';
|
||||
this.speechCredentialLabel = this.data.tts.label || 'default';
|
||||
|
||||
// fallback tts
|
||||
this.fallbackVendor = this.data.tts.fallbackVendor || 'default';
|
||||
this.fallbackLanguage = this.data.tts.fallbackLanguage || 'default';
|
||||
this.fallbackVoice = this.data.tts.fallbackLanguage || 'default';
|
||||
this.fallbackLabel = this.data.tts.fallbackLabel || 'default';
|
||||
}
|
||||
|
||||
this.botName = `${this.bot}:${this.alias}:${this.region}`;
|
||||
@@ -102,8 +109,16 @@ class Lex extends Task {
|
||||
this.vendor = cs.speechSynthesisVendor;
|
||||
this.language = cs.speechSynthesisLanguage;
|
||||
this.voice = cs.speechSynthesisVoice;
|
||||
this.speechCredentialLabel = cs.speechSynthesisLabel;
|
||||
}
|
||||
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts');
|
||||
if (this.fallbackVendor === 'default') {
|
||||
this.fallbackVendor = cs.fallbackSpeechSynthesisVendor;
|
||||
this.fallbackLanguage = cs.fallbackSpeechSynthesisLanguage;
|
||||
this.fallbackVoice = cs.fallbackSpeechSynthesisVoice;
|
||||
this.fallbackLabel = cs.fallbackSpeechSynthesisLabel;
|
||||
}
|
||||
|
||||
this.ttsCredentials = cs.getSpeechCredentials(this.vendor, 'tts', this.speechCredentialLabel);
|
||||
|
||||
this.ep.addCustomEventListener('lex::intent', this._onIntent.bind(this, ep, cs));
|
||||
this.ep.addCustomEventListener('lex::transcription', this._onTranscription.bind(this, ep, cs));
|
||||
@@ -168,6 +183,41 @@ class Lex extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
async _fallbackSynthAudio(cs, msg, stats, synthAudio) {
|
||||
try {
|
||||
const {filePath} = await synthAudio(stats, {
|
||||
account_sid: cs.accountSid,
|
||||
text: msg,
|
||||
vendor: this.vendor,
|
||||
language: this.language,
|
||||
voice: this.voice,
|
||||
salt: cs.callSid,
|
||||
credentials: this.ttsCredentials
|
||||
});
|
||||
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
this.logger.info({error}, 'failed to synth audio from primary vendor');
|
||||
if (this.fallbackVendor) {
|
||||
try {
|
||||
const credential = cs.getSpeechCredentials(this.fallbackVendor, 'tts', this.fallbackLabel);
|
||||
const {filePath} = await synthAudio(stats, {
|
||||
account_sid: cs.accountSid,
|
||||
text: msg,
|
||||
vendor: this.fallbackVendor,
|
||||
language: this.fallbackLanguage,
|
||||
voice: this.fallbackVoice,
|
||||
salt: cs.callSid,
|
||||
credentials: credential
|
||||
});
|
||||
return filePath;
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'failed to synth audio from fallback vendor');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} evt - event data
|
||||
*/
|
||||
@@ -187,16 +237,7 @@ class Lex extends Task {
|
||||
|
||||
try {
|
||||
this.logger.debug(`tts with ${this.vendor} ${this.voice}`);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {filePath, servedFromCache} = await synthAudio(stats, {
|
||||
account_sid: cs.accountSid,
|
||||
text: msg,
|
||||
vendor: this.vendor,
|
||||
language: this.language,
|
||||
voice: this.voice,
|
||||
salt: cs.callSid,
|
||||
credentials: this.ttsCredentials
|
||||
});
|
||||
const filePath = await this._fallbackSynthAudio(cs, msg, stats, synthAudio);
|
||||
if (filePath) cs.trackTmpFile(filePath);
|
||||
|
||||
if (this.events.includes('start-play')) {
|
||||
|
||||
@@ -8,6 +8,7 @@ const DTMF_SPAN_NAME = 'dtmf';
|
||||
class TaskListen extends Task {
|
||||
constructor(logger, opts, parentTask) {
|
||||
super(logger, opts);
|
||||
this.disableBidirectionalAudio = opts.disableBidirectionalAudio;
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
@@ -154,7 +155,7 @@ class TaskListen extends Task {
|
||||
}
|
||||
|
||||
/* support bi-directional audio */
|
||||
if (!this.disableBiDirectionalAudio) {
|
||||
if (!this.disableBidirectionalAudio) {
|
||||
ep.addCustomEventListener(ListenEvents.PlayAudio, this._onPlayAudio.bind(this, ep));
|
||||
}
|
||||
ep.addCustomEventListener(ListenEvents.KillAudio, this._onKillAudio.bind(this, ep));
|
||||
|
||||
@@ -63,12 +63,13 @@ class TaskRestDial extends Task {
|
||||
this.canCancel = false;
|
||||
const cs = this.callSession;
|
||||
cs.setDialog(dlg);
|
||||
this.logger.debug('TaskRestDial:_onConnect - call connected');
|
||||
|
||||
try {
|
||||
const b3 = this.getTracingPropagation();
|
||||
const httpHeaders = b3 && {b3};
|
||||
const params = {
|
||||
...cs.callInfo,
|
||||
...(cs.callInfo.toJSON()),
|
||||
defaults: {
|
||||
synthesizer: {
|
||||
vendor: cs.speechSynthesisVendor,
|
||||
@@ -90,8 +91,10 @@ class TaskRestDial extends Task {
|
||||
}
|
||||
let tasks;
|
||||
if (this.app_json) {
|
||||
this.logger.debug('TaskRestDial: using app_json from task data');
|
||||
tasks = JSON.parse(this.app_json);
|
||||
} else {
|
||||
this.logger.debug({call_hook: this.call_hook}, 'TaskRestDial: retrieving application');
|
||||
tasks = await cs.requestor.request('session:new', this.call_hook, params, httpHeaders);
|
||||
}
|
||||
if (tasks && Array.isArray(tasks)) {
|
||||
@@ -125,7 +128,10 @@ class TaskRestDial extends Task {
|
||||
_onCallTimeout() {
|
||||
this.logger.debug('TaskRestDial: timeout expired without answer, killing task');
|
||||
this.timer = null;
|
||||
this.kill(this.cs);
|
||||
if (this.canCancel) {
|
||||
this.canCancel = false;
|
||||
this.cs?.req?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
_onAmdEvent(cs, evt) {
|
||||
|
||||
107
lib/tasks/say.js
107
lib/tasks/say.js
@@ -37,6 +37,7 @@ class TaskSay extends Task {
|
||||
this.synthesizer = this.data.synthesizer || {};
|
||||
this.disableTtsCache = this.data.disableTtsCache;
|
||||
this.options = this.synthesizer.options || {};
|
||||
this.isHandledByPrimaryProvider = true;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Say; }
|
||||
@@ -49,26 +50,15 @@ class TaskSay extends Task {
|
||||
return `${this.name}{${this.text[0]}}`;
|
||||
}
|
||||
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
|
||||
async _synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label}) {
|
||||
const {srf} = cs;
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
||||
const {writeAlerts, AlertType, stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
|
||||
this.synthesizer.vendor :
|
||||
cs.speechSynthesisVendor;
|
||||
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
|
||||
this.synthesizer.language :
|
||||
cs.speechSynthesisLanguage ;
|
||||
let voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
|
||||
this.synthesizer.voice :
|
||||
cs.speechSynthesisVoice;
|
||||
const engine = this.synthesizer.engine || 'standard';
|
||||
const salt = cs.callSid;
|
||||
let credentials = cs.getSpeechCredentials(vendor, 'tts');
|
||||
|
||||
let credentials = cs.getSpeechCredentials(vendor, 'tts', label);
|
||||
/* parse Nuance voices into name and model */
|
||||
let model;
|
||||
if (vendor === 'nuance' && voice) {
|
||||
@@ -90,7 +80,6 @@ class TaskSay extends Task {
|
||||
}
|
||||
|
||||
this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (!credentials) {
|
||||
writeAlerts({
|
||||
@@ -160,31 +149,83 @@ class TaskSay extends Task {
|
||||
detail: err.message
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
|
||||
this.notifyError({msg: 'TTS error', details: err.message || err});
|
||||
return;
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const arr = this.text.map((t) => generateAudio(t));
|
||||
const filepath = (await Promise.all(arr)).filter((fp) => fp && fp.length);
|
||||
this.notifyStatus({event: 'start-playback'});
|
||||
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
|
||||
let segment = 0;
|
||||
while (!this.killed && segment < filepath.length) {
|
||||
if (cs.isInConference) {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
|
||||
}
|
||||
else {
|
||||
this.logger.debug(`Say:exec sending command to play file ${filepath[segment]}`);
|
||||
await ep.play(filepath[segment]);
|
||||
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
||||
}
|
||||
segment++;
|
||||
}
|
||||
}
|
||||
return (await Promise.all(arr)).filter((fp) => fp && fp.length);
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'TaskSay:exec error');
|
||||
throw err;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async exec(cs, {ep}) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
|
||||
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
|
||||
this.synthesizer.vendor :
|
||||
cs.speechSynthesisVendor;
|
||||
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
|
||||
this.synthesizer.language :
|
||||
cs.speechSynthesisLanguage ;
|
||||
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
|
||||
this.synthesizer.voice :
|
||||
cs.speechSynthesisVoice;
|
||||
const label = this.synthesizer.label && this.synthesizer.label !== 'default' ?
|
||||
this.synthesizer.label :
|
||||
cs.speechSynthesisLabel;
|
||||
|
||||
const fallbackVendor = this.synthesizer.fallbackVendor && this.synthesizer.fallbackVendor !== 'default' ?
|
||||
this.synthesizer.fallbackVendor :
|
||||
cs.fallbackSpeechSynthesisVendor;
|
||||
const fallbackLanguage = this.synthesizer.fallbackLanguage && this.synthesizer.fallbackLanguage !== 'default' ?
|
||||
this.synthesizer.fallbackLanguage :
|
||||
cs.fallbackSpeechSynthesisLanguage ;
|
||||
const fallbackVoice = this.synthesizer.fallbackVoice && this.synthesizer.fallbackVoice !== 'default' ?
|
||||
this.synthesizer.fallbackVoice :
|
||||
cs.fallbackSpeechSynthesisVoice;
|
||||
const fallbackLabel = this.synthesizer.fallbackLabel && this.synthesizer.fallbackLabel !== 'default' ?
|
||||
this.synthesizer.fallbackLabel :
|
||||
cs.fallbackSpeechSynthesisLabel;
|
||||
|
||||
let filepath;
|
||||
try {
|
||||
filepath = await this._synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label});
|
||||
} catch (error) {
|
||||
if (fallbackVendor && this.isHandledByPrimaryProvider) {
|
||||
this.isHandledByPrimaryProvider = false;
|
||||
this.logger.info(`Synthesize error, fallback to ${fallbackVendor}`);
|
||||
filepath = await this._synthesizeWithSpecificVendor(cs, ep,
|
||||
{
|
||||
vendor: fallbackVendor,
|
||||
language: fallbackLanguage,
|
||||
voice: fallbackVoice,
|
||||
label: fallbackLabel
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
this.notifyStatus({event: 'start-playback'});
|
||||
|
||||
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
|
||||
let segment = 0;
|
||||
while (!this.killed && segment < filepath.length) {
|
||||
if (cs.isInConference) {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
|
||||
}
|
||||
else {
|
||||
this.logger.debug(`Say:exec sending command to play file ${filepath[segment]}`);
|
||||
await ep.play(filepath[segment]);
|
||||
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
||||
}
|
||||
segment++;
|
||||
}
|
||||
}
|
||||
this.emit('playDone');
|
||||
}
|
||||
|
||||
100
lib/tasks/stt-task.js
Normal file
100
lib/tasks/stt-task.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const Task = require('./task');
|
||||
const assert = require('assert');
|
||||
const { TaskPreconditions } = require('../utils/constants');
|
||||
|
||||
class SttTask extends Task {
|
||||
|
||||
constructor(logger, data, parentTask) {
|
||||
super(logger, data);
|
||||
this.parentTask = parentTask;
|
||||
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
const {
|
||||
setChannelVarsForStt,
|
||||
normalizeTranscription,
|
||||
removeSpeechListeners,
|
||||
setSpeechCredentialsAtRuntime,
|
||||
compileSonioxTranscripts
|
||||
} = require('../utils/transcription-utils')(logger);
|
||||
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||
this.normalizeTranscription = normalizeTranscription;
|
||||
this.removeSpeechListeners = removeSpeechListeners;
|
||||
this.compileSonioxTranscripts = compileSonioxTranscripts;
|
||||
|
||||
this.isHandledByPrimaryProvider = true;
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
this.language = recognizer.language;
|
||||
this.label = recognizer.label;
|
||||
|
||||
//fallback
|
||||
this.fallbackVendor = recognizer.fallbackVendor || 'default';
|
||||
this.fallbackLanguage = recognizer.fallbackLanguage || 'default';
|
||||
this.fallbackLabel = recognizer.fallbackLabel || 'default';
|
||||
|
||||
/* let credentials be supplied in the recognizer object at runtime */
|
||||
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
||||
} else {
|
||||
this.data.recognizer = {hints: [], altLanguages: []};
|
||||
}
|
||||
|
||||
/* buffer for soniox transcripts */
|
||||
this._sonioxTranscripts = [];
|
||||
|
||||
}
|
||||
|
||||
async _initSpeechCredentials(cs, vendor, label) {
|
||||
const {getNuanceAccessToken, getIbmAccessToken} = this.cs.srf.locals.dbHelpers;
|
||||
let credentials = cs.getSpeechCredentials(vendor, 'stt', label);
|
||||
|
||||
if (!credentials) {
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info(`ERROR stt using ${vendor} requested but creds not supplied`);
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||
vendor
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||
// Notify application that STT vender is wrong.
|
||||
this.notifyError({
|
||||
msg: 'ASR error',
|
||||
details: `No speech-to-text service credentials for ${vendor} have been configured`
|
||||
});
|
||||
this.notifyTaskDone();
|
||||
throw new Error(`No speech-to-text service credentials for ${vendor} have been configured`);
|
||||
}
|
||||
|
||||
if (vendor === 'nuance' && credentials.client_id) {
|
||||
/* get nuance access token */
|
||||
const {client_id, secret} = credentials;
|
||||
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
|
||||
this.logger.debug({client_id}, `got nuance access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
credentials = {...credentials, access_token};
|
||||
}
|
||||
else if (vendor == 'ibm' && credentials.stt_api_key) {
|
||||
/* get ibm access token */
|
||||
const {stt_api_key, stt_region} = credentials;
|
||||
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
|
||||
this.logger.debug({stt_api_key}, `got ibm access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
credentials = {...credentials, access_token, stt_region};
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
async _fallback() {
|
||||
assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration');
|
||||
this.isHandledByPrimaryProvider = false;
|
||||
this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
|
||||
this.vendor = this.fallbackVendor;
|
||||
this.language = this.fallbackLanguage;
|
||||
this.label = this.fallbackLabel;
|
||||
this.data.recognizer.vendor = this.vendor;
|
||||
this.data.recognizer.language = this.language;
|
||||
this.data.recognizer.label = this.label;
|
||||
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SttTask;
|
||||
@@ -1,7 +1,6 @@
|
||||
const Task = require('./task');
|
||||
const assert = require('assert');
|
||||
const {
|
||||
TaskName,
|
||||
TaskPreconditions,
|
||||
GoogleTranscriptionEvents,
|
||||
NuanceTranscriptionEvents,
|
||||
AwsTranscriptionEvents,
|
||||
@@ -10,52 +9,33 @@ const {
|
||||
SonioxTranscriptionEvents,
|
||||
IbmTranscriptionEvents,
|
||||
NvidiaTranscriptionEvents,
|
||||
JambonzTranscriptionEvents
|
||||
JambonzTranscriptionEvents,
|
||||
TranscribeStatus
|
||||
} = require('../utils/constants');
|
||||
const { normalizeJambones } = require('@jambonz/verb-specifications');
|
||||
const SttTask = require('./stt-task');
|
||||
|
||||
const STT_LISTEN_SPAN_NAME = 'stt-listen';
|
||||
|
||||
class TaskTranscribe extends Task {
|
||||
class TaskTranscribe extends SttTask {
|
||||
constructor(logger, opts, parentTask) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
this.parentTask = parentTask;
|
||||
|
||||
const {
|
||||
setChannelVarsForStt,
|
||||
normalizeTranscription,
|
||||
removeSpeechListeners,
|
||||
setSpeechCredentialsAtRuntime,
|
||||
compileSonioxTranscripts
|
||||
} = require('../utils/transcription-utils')(logger);
|
||||
this.setChannelVarsForStt = setChannelVarsForStt;
|
||||
this.normalizeTranscription = normalizeTranscription;
|
||||
this.removeSpeechListeners = removeSpeechListeners;
|
||||
this.compileSonioxTranscripts = compileSonioxTranscripts;
|
||||
super(logger, opts, parentTask);
|
||||
|
||||
this.transcriptionHook = this.data.transcriptionHook;
|
||||
this.earlyMedia = this.data.earlyMedia === true || (parentTask && parentTask.earlyMedia);
|
||||
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
this.language = recognizer.language;
|
||||
/* let credentials be supplied in the recognizer object at runtime */
|
||||
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
|
||||
|
||||
this.interim = !!recognizer.interim;
|
||||
this.separateRecognitionPerChannel = recognizer.separateRecognitionPerChannel;
|
||||
|
||||
this.data.recognizer.hints = this.data.recognizer.hints || [];
|
||||
this.data.recognizer.altLanguages = this.data.recognizer.altLanguages || [];
|
||||
this.interim = !!this.data.recognizer.interim;
|
||||
this.separateRecognitionPerChannel = this.data.recognizer.separateRecognitionPerChannel;
|
||||
}
|
||||
else this.data.recognizer = {hints: [], altLanguages: []};
|
||||
|
||||
/* buffer for soniox transcripts */
|
||||
this._sonioxTranscripts = [];
|
||||
|
||||
this.childSpan = [null, null];
|
||||
|
||||
// Continuos asr timeout
|
||||
this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ? this.data.recognizer.asrTimeout * 1000 : 0;
|
||||
this.isContinuousAsr = this.asrTimeout > 0;
|
||||
/* buffer speech for continuous asr */
|
||||
this._bufferedTranscripts = [];
|
||||
}
|
||||
|
||||
get name() { return TaskName.Transcribe; }
|
||||
@@ -63,7 +43,6 @@ class TaskTranscribe extends Task {
|
||||
async exec(cs, {ep, ep2}) {
|
||||
super.exec(cs);
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers;
|
||||
|
||||
if (cs.hasGlobalSttHints) {
|
||||
const {hints, hintsBoost} = cs.globalSttHints;
|
||||
@@ -91,38 +70,40 @@ class TaskTranscribe extends Task {
|
||||
this.language = cs.speechRecognizerLanguage;
|
||||
if (this.data.recognizer) this.data.recognizer.language = this.language;
|
||||
}
|
||||
if ('default' === this.label || !this.label) {
|
||||
this.label = cs.speechRecognizerLabel;
|
||||
if (this.data.recognizer) this.data.recognizer.label = this.label;
|
||||
}
|
||||
// Fallback options
|
||||
if ('default' === this.fallbackVendor || !this.fallbackVendor) {
|
||||
this.fallbackVendor = cs.fallbackSpeechRecognizerVendor;
|
||||
if (this.data.recognizer) this.data.recognizer.fallbackVendor = this.fallbackVendor;
|
||||
}
|
||||
if ('default' === this.fallbackLanguage || !this.fallbackLanguage) {
|
||||
this.fallbackLanguage = cs.fallbackSpeechRecognizerLanguage;
|
||||
if (this.data.recognizer) this.data.recognizer.fallbackLanguage = this.fallbackLanguage;
|
||||
}
|
||||
if ('default' === this.fallbackLabel || !this.fallbackLabel) {
|
||||
this.fallbackLabel = cs.fallbackSpeechRecognizerLabel;
|
||||
if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel;
|
||||
}
|
||||
if (!this.data.recognizer.vendor) {
|
||||
this.data.recognizer.vendor = this.vendor;
|
||||
}
|
||||
if (!this.sttCredentials) this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
|
||||
if (!this.sttCredentials) {
|
||||
try {
|
||||
this.sttCredentials = await this._initSpeechCredentials(cs, this.vendor, this.label);
|
||||
} catch (error) {
|
||||
if (this.fallbackVendor && this.isHandledByPrimaryProvider) {
|
||||
await this._fallback();
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.sttCredentials) {
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info(`TaskTranscribe:exec - ERROR stt using ${this.vendor} requested but creds not supplied`);
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_NOT_PROVISIONED,
|
||||
vendor: this.vendor
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
|
||||
throw new Error('no provisioned speech credentials for TTS');
|
||||
}
|
||||
|
||||
if (this.vendor === 'nuance' && this.sttCredentials.client_id) {
|
||||
/* get nuance access token */
|
||||
const {client_id, secret} = this.sttCredentials;
|
||||
const {access_token, servedFromCache} = await getNuanceAccessToken(client_id, secret, 'asr tts');
|
||||
this.logger.debug({client_id},
|
||||
`Transcribe:exec - got nuance access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
this.sttCredentials = {...this.sttCredentials, access_token};
|
||||
}
|
||||
else if (this.vendor == 'ibm' && this.sttCredentials.stt_api_key) {
|
||||
/* get ibm access token */
|
||||
const {stt_api_key, stt_region} = this.sttCredentials;
|
||||
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
|
||||
this.logger.debug({stt_api_key}, `Gather:exec - got ibm access token ${servedFromCache ? 'from cache' : ''}`);
|
||||
this.sttCredentials = {...this.sttCredentials, access_token, stt_region};
|
||||
}
|
||||
await this._startTranscribing(cs, ep, 1);
|
||||
if (this.separateRecognitionPerChannel && ep2) {
|
||||
await this._startTranscribing(cs, ep2, 2);
|
||||
@@ -139,8 +120,7 @@ class TaskTranscribe extends Task {
|
||||
this.removeSpeechListeners(ep);
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
async _stopTranscription() {
|
||||
let stopTranscription = false;
|
||||
if (this.ep?.connected) {
|
||||
stopTranscription = true;
|
||||
@@ -152,6 +132,13 @@ class TaskTranscribe extends Task {
|
||||
this.ep2.stopTranscription({vendor: this.vendor})
|
||||
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
||||
}
|
||||
|
||||
return stopTranscription;
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
const stopTranscription = this._stopTranscription();
|
||||
// hangup after 1 sec if we don't get a final transcription
|
||||
if (stopTranscription) this._timer = setTimeout(() => this.notifyTaskDone(), 1500);
|
||||
else this.notifyTaskDone();
|
||||
@@ -159,6 +146,23 @@ class TaskTranscribe extends Task {
|
||||
await this.awaitTaskDone();
|
||||
}
|
||||
|
||||
async updateTranscribe(status) {
|
||||
if (!this.killed && this.ep && this.ep.connected) {
|
||||
this.logger.info(`TaskTranscribe:updateTranscribe status ${status}`);
|
||||
switch (status) {
|
||||
case TranscribeStatus.Pause:
|
||||
await this._stopTranscription();
|
||||
break;
|
||||
case TranscribeStatus.Resume:
|
||||
await this._startTranscribing(this.cs, this.ep, 1);
|
||||
if (this.separateRecognitionPerChannel && this.ep2) {
|
||||
await this._startTranscribing(this.cs, this.ep2, 2);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _startTranscribing(cs, ep, channel) {
|
||||
const opts = this.setChannelVarsForStt(this, this.sttCredentials, this.data.recognizer);
|
||||
switch (this.vendor) {
|
||||
@@ -234,7 +238,19 @@ class TaskTranscribe extends Task {
|
||||
this._onVadDetected.bind(this, cs, ep));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid vendor ${this.vendor}`);
|
||||
if (this.vendor.startsWith('custom:')) {
|
||||
this.bugname = `${this.vendor}_transcribe`;
|
||||
ep.addCustomEventListener(JambonzTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(JambonzTranscriptionEvents.Connect, this._onJambonzConnect.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(JambonzTranscriptionEvents.ConnectFailure,
|
||||
this._onJambonzConnectFailure.bind(this, cs, ep));
|
||||
break;
|
||||
}
|
||||
else {
|
||||
this.notifyError({ msg: 'ASR error', details:`Invalid vendor ${this.vendor}`});
|
||||
this.notifyTaskDone();
|
||||
throw new Error(`Invalid vendor ${this.vendor}`);
|
||||
}
|
||||
}
|
||||
|
||||
/* common handler for all stt engine errors */
|
||||
@@ -296,6 +312,26 @@ class TaskTranscribe extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isContinuousAsr && evt.is_final) {
|
||||
this._bufferedTranscripts.push(evt);
|
||||
this._startAsrTimer(channel);
|
||||
} else {
|
||||
await this._resolve(channel, evt);
|
||||
}
|
||||
}
|
||||
|
||||
_compileTranscripts() {
|
||||
assert(this._bufferedTranscripts.length);
|
||||
const evt = this._bufferedTranscripts[0];
|
||||
let t = '';
|
||||
for (const a of this._bufferedTranscripts) {
|
||||
t += ` ${a.alternatives[0].transcript}`;
|
||||
}
|
||||
evt.alternatives[0].transcript = t.trim();
|
||||
return evt;
|
||||
}
|
||||
|
||||
async _resolve(channel, evt) {
|
||||
/* we've got a transcript, so end the otel child span for this channel */
|
||||
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
|
||||
this.childSpan[channel - 1].span.setAttributes({
|
||||
@@ -408,6 +444,24 @@ class TaskTranscribe extends Task {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_onJambonzConnect(_cs, _ep) {
|
||||
this.logger.debug('TaskTranscribe:_onJambonzConnect');
|
||||
}
|
||||
|
||||
_onJambonzConnectFailure(cs, _ep, evt) {
|
||||
const {reason} = evt;
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onJambonzConnectFailure');
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
|
||||
vendor: this.vendor,
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
|
||||
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${reason}`});
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_onIbmConnect(_cs, _ep) {
|
||||
this.logger.debug('TaskTranscribe:_onIbmConnect');
|
||||
}
|
||||
@@ -436,26 +490,59 @@ class TaskTranscribe extends Task {
|
||||
_onIbmError(cs, _ep, _channel, evt) {
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onIbmError');
|
||||
}
|
||||
_onJambonzError(cs, _ep, evt) {
|
||||
async _onJambonzError(cs, _ep, evt) {
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
|
||||
_ep.stopTranscription({vendor: this.vendor})
|
||||
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
|
||||
try {
|
||||
await this._fallback();
|
||||
let channel = 1;
|
||||
if (this.ep !== _ep) {
|
||||
channel = 2;
|
||||
}
|
||||
this._startTranscribing(cs, _ep, channel);
|
||||
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
|
||||
return;
|
||||
} catch (error) {
|
||||
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
|
||||
}
|
||||
} else {
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
|
||||
if (this.vendor === 'nuance') {
|
||||
const {code, error} = evt;
|
||||
if (code === 404 && error === 'No speech') return this._resolve('timeout');
|
||||
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
|
||||
if (this.vendor === 'nuance') {
|
||||
const {code, error} = evt;
|
||||
if (code === 404 && error === 'No speech') return this._resolve('timeout');
|
||||
if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
|
||||
}
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
|
||||
vendor: this.vendor,
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
|
||||
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
|
||||
}
|
||||
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
|
||||
writeAlerts({
|
||||
account_sid: cs.accountSid,
|
||||
alert_type: AlertType.STT_FAILURE,
|
||||
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
|
||||
vendor: this.vendor,
|
||||
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
|
||||
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`});
|
||||
}
|
||||
|
||||
_startAsrTimer(channel) {
|
||||
assert(this.isContinuousAsr);
|
||||
this._clearAsrTimer(channel);
|
||||
this._asrTimer = setTimeout(() => {
|
||||
this.logger.debug(`TaskTranscribe:_startAsrTimer - asr timer went off for channel: ${channel}`);
|
||||
const evt = this._compileTranscripts();
|
||||
this._bufferedTranscripts = [];
|
||||
this._resolve(channel, evt);
|
||||
}, this.asrTimeout);
|
||||
this.logger.debug(`TaskTranscribe:_startAsrTimer: set for ${this.asrTimeout}ms for channel ${channel}`);
|
||||
}
|
||||
|
||||
_clearAsrTimer(channel) {
|
||||
if (this._asrTimer) clearTimeout(this._asrTimer);
|
||||
this._asrTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskTranscribe;
|
||||
|
||||
@@ -54,7 +54,8 @@ class Amd extends Emitter {
|
||||
this.language = opts.recognizer?.language || cs.speechRecognizerLanguage;
|
||||
if ('default' === this.language) this.language = cs.speechRecognizerLanguage;
|
||||
|
||||
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt',
|
||||
opts.recognizer?.label || cs.speechRecognizerLabel);
|
||||
|
||||
if (!this.sttCredentials) throw new Error(`No speech credentials found for vendor ${this.vendor}`);
|
||||
|
||||
|
||||
@@ -2,17 +2,24 @@ const {context, trace} = require('@opentelemetry/api');
|
||||
const {Dialog} = require('drachtio-srf');
|
||||
class RootSpan {
|
||||
constructor(callType, req) {
|
||||
let tracer, callSid, linkedSpanId;
|
||||
const {srf} = require('../../');
|
||||
const tracer = srf.locals.otel.tracer;
|
||||
let callSid, accountSid, applicationSid, linkedSpanId;
|
||||
|
||||
if (req instanceof Dialog) {
|
||||
const dlg = req;
|
||||
tracer = dlg.srf.locals.otel.tracer;
|
||||
callSid = dlg.callSid;
|
||||
linkedSpanId = dlg.linkedSpanId;
|
||||
}
|
||||
else {
|
||||
tracer = req.srf.locals.otel.tracer;
|
||||
else if (req.srf) {
|
||||
callSid = req.locals.callSid;
|
||||
accountSid = req.get('X-Account-Sid'),
|
||||
applicationSid = req.locals.application_sid;
|
||||
}
|
||||
else {
|
||||
callSid = req.callSid;
|
||||
accountSid = req.accountSid;
|
||||
applicationSid = req.applicationSid;
|
||||
}
|
||||
this._span = tracer.startSpan(callType || 'incoming-call');
|
||||
if (req instanceof Dialog) {
|
||||
@@ -22,13 +29,20 @@ class RootSpan {
|
||||
callId: dlg.sip.callId
|
||||
});
|
||||
}
|
||||
else if (req.srf) {
|
||||
this._span.setAttributes({
|
||||
callSid,
|
||||
accountSid,
|
||||
applicationSid,
|
||||
callId: req.get('Call-ID'),
|
||||
externalCallId: req.get('X-CID')
|
||||
});
|
||||
}
|
||||
else {
|
||||
this._span.setAttributes({
|
||||
callSid,
|
||||
accountSid: req.get('X-Account-Sid'),
|
||||
applicationSid: req.locals.application_sid,
|
||||
callId: req.get('Call-ID'),
|
||||
externalCallId: req.get('X-CID')
|
||||
accountSid,
|
||||
applicationSid
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,11 @@
|
||||
"Silence": "silence",
|
||||
"Resume": "resume"
|
||||
},
|
||||
"TranscribeStatus": {
|
||||
"Pause": "pause",
|
||||
"Silence": "silence",
|
||||
"Resume": "resume"
|
||||
},
|
||||
"TaskPreconditions": {
|
||||
"None": "none",
|
||||
"Endpoint": "endpoint",
|
||||
|
||||
@@ -3,13 +3,10 @@ const {decrypt} = require('./encrypt-decrypt');
|
||||
const sqlAccountDetails = `SELECT *
|
||||
FROM accounts account
|
||||
WHERE account.account_sid = ?`;
|
||||
const sqlSpeechCredentials = `SELECT *
|
||||
const sqlSpeechCredentialsForAccount = `SELECT *
|
||||
FROM speech_credentials
|
||||
WHERE account_sid = ? `;
|
||||
const sqlSpeechCredentialsForSP = `SELECT *
|
||||
FROM speech_credentials
|
||||
WHERE service_provider_sid =
|
||||
(SELECT service_provider_sid from accounts where account_sid = ?)`;
|
||||
WHERE account_sid = ? OR (account_sid is NULL AND service_provider_sid =
|
||||
(SELECT service_provider_sid from accounts where account_sid = ?))`;
|
||||
const sqlQueryAccountCarrierByName = `SELECT voip_carrier_sid
|
||||
FROM voip_carriers vc
|
||||
WHERE vc.account_sid = ?
|
||||
@@ -49,8 +46,10 @@ const speechMapper = (cred) => {
|
||||
obj.region = o.region;
|
||||
obj.use_custom_stt = o.use_custom_stt;
|
||||
obj.custom_stt_endpoint = o.custom_stt_endpoint;
|
||||
obj.custom_stt_endpoint_url = o.custom_stt_endpoint_url;
|
||||
obj.use_custom_tts = o.use_custom_tts;
|
||||
obj.custom_tts_endpoint = o.custom_tts_endpoint;
|
||||
obj.custom_tts_endpoint_url = o.custom_tts_endpoint_url;
|
||||
}
|
||||
else if ('wellsaid' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
@@ -108,16 +107,9 @@ module.exports = (logger, srf) => {
|
||||
|
||||
const [r] = await pp.query({sql: sqlAccountDetails, nestTables: true}, [account_sid]);
|
||||
if (0 === r.length) throw new Error(`invalid accountSid: ${account_sid}`);
|
||||
const [r2] = await pp.query(sqlSpeechCredentials, [account_sid]);
|
||||
const [r2] = await pp.query(sqlSpeechCredentialsForAccount, [account_sid, account_sid]);
|
||||
const speech = r2.map(speechMapper);
|
||||
|
||||
/* add service provider creds unless we have that vendor at the account level */
|
||||
const [r3] = await pp.query(sqlSpeechCredentialsForSP, [account_sid]);
|
||||
r3.forEach((s) => {
|
||||
if (!speech.find((s2) => s2.vendor === s.vendor)) {
|
||||
speech.push(speechMapper(s));
|
||||
}
|
||||
});
|
||||
const account = r[0];
|
||||
bucketCredentialDecrypt(account);
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ const {PORT, HTTP_PORT_MAX} = require('../config');
|
||||
const doListen = (logger, app, port, resolve) => {
|
||||
const server = app.listen(port, () => {
|
||||
const {srf} = app.locals;
|
||||
logger.info(`listening for HTTP requests on port ${PORT}, serviceUrl is ${srf.locals.serviceUrl}`);
|
||||
srf.locals.serviceUrl = `http://${srf.locals.ipv4}:${port}`;
|
||||
logger.info(`listening for HTTP requests on port ${port}, serviceUrl is ${srf.locals.serviceUrl}`);
|
||||
resolve({server, app});
|
||||
});
|
||||
return server;
|
||||
|
||||
@@ -15,7 +15,7 @@ const RootSpan = require('./call-tracer');
|
||||
const uuidv4 = require('uuid-random');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan}) {
|
||||
constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask}) {
|
||||
super();
|
||||
assert(target.type);
|
||||
|
||||
@@ -37,6 +37,7 @@ class SingleDialer extends Emitter {
|
||||
this.callGone = false;
|
||||
|
||||
this.callSid = uuidv4();
|
||||
this.dialTask = dialTask;
|
||||
|
||||
this.on('callStatusChange', this._notifyCallStatusChange.bind(this));
|
||||
}
|
||||
@@ -247,9 +248,14 @@ class SingleDialer extends Emitter {
|
||||
.on('modify', async(req, res) => {
|
||||
try {
|
||||
if (this.ep) {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
||||
if (this.dialTask && this.dialTask.isOnHold) {
|
||||
this.logger.info('dial is onhold, emit event');
|
||||
this.emit('reinvite', req, res);
|
||||
} else {
|
||||
const newSdp = await this.ep.modify(req.body);
|
||||
res.send(200, {body: newSdp});
|
||||
this.logger.info({offer: req.body, answer: newSdp}, 'SingleDialer:exec: handling reINVITE');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.logger.info('SingleDialer:exec: handling reINVITE with released media, emit event');
|
||||
@@ -430,11 +436,11 @@ class SingleDialer extends Emitter {
|
||||
}
|
||||
|
||||
function placeOutdial({
|
||||
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan
|
||||
logger, srf, ms, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask
|
||||
}) {
|
||||
const myOpts = deepcopy(opts);
|
||||
const sd = new SingleDialer({
|
||||
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan
|
||||
logger, sbcAddress, target, myOpts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask
|
||||
});
|
||||
sd.exec(srf, ms, myOpts);
|
||||
return sd;
|
||||
|
||||
7
lib/utils/sdp-utils.js
Normal file
7
lib/utils/sdp-utils.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const isOnhold = (sdp) => {
|
||||
return sdp && (sdp.includes('a=sendonly') || sdp.includes('a=inactive'));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
isOnhold
|
||||
};
|
||||
@@ -225,12 +225,17 @@ const normalizeGoogle = (evt, channel, language) => {
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeCustom = (evt, channel, language) => {
|
||||
const normalizeCustom = (evt, channel, language, vendor) => {
|
||||
const copy = JSON.parse(JSON.stringify(evt));
|
||||
return {
|
||||
language_code: language,
|
||||
channel_tag: channel,
|
||||
is_final: evt.is_final,
|
||||
alternatives: [evt.alternatives[0]]
|
||||
alternatives: [evt.alternatives[0]],
|
||||
vendor: {
|
||||
name: vendor,
|
||||
evt: copy
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -314,7 +319,7 @@ module.exports = (logger) => {
|
||||
return normalizeSoniox(evt, channel, language);
|
||||
default:
|
||||
if (vendor.startsWith('custom:')) {
|
||||
return normalizeCustom(evt, channel, language);
|
||||
return normalizeCustom(evt, channel, language, vendor);
|
||||
}
|
||||
logger.error(`Unknown vendor ${vendor}`);
|
||||
return evt;
|
||||
@@ -356,12 +361,12 @@ module.exports = (logger) => {
|
||||
...(rOpts.punctuation === false && {GOOGLE_SPEECH_ENABLE_AUTOMATIC_PUNCTUATION: 0}),
|
||||
...(rOpts.words == false && {GOOGLE_SPEECH_ENABLE_WORD_TIME_OFFSETS: 0}),
|
||||
...(rOpts.diarization === false && {GOOGLE_SPEECH_SPEAKER_DIARIZATION: 0}),
|
||||
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'string' &&
|
||||
{GOOGLE_SPEECH_HINTS: rOpts.hints.join(',')}),
|
||||
...(rOpts.hints.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
|
||||
{GOOGLE_SPEECH_HINTS: JSON.stringify(rOpts.hints)}),
|
||||
...(typeof rOpts.hintsBoost === 'number' && {GOOGLE_SPEECH_HINTS_BOOST: rOpts.hintsBoost}),
|
||||
...(rOpts.altLanguages.length > 0 &&
|
||||
...(rOpts.altLanguages?.length > 0 &&
|
||||
{GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
|
||||
...(rOpts.interactionType &&
|
||||
{GOOGLE_SPEECH_METADATA_INTERACTION_TYPE: rOpts.interactionType}),
|
||||
@@ -394,6 +399,8 @@ module.exports = (logger) => {
|
||||
{AZURE_SPEECH_ALTERNATIVE_LANGUAGE_CODES: [...new Set(rOpts.altLanguages)].join(',')}),
|
||||
...(rOpts.requestSnr && {AZURE_REQUEST_SNR: 1}),
|
||||
...(rOpts.profanityOption && {AZURE_PROFANITY_OPTION: rOpts.profanityOption}),
|
||||
...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint_url &&
|
||||
{AZURE_SERVICE_ENDPOINT: sttCredentials.custom_stt_endpoint_url}),
|
||||
...(rOpts.azureServiceEndpoint && {AZURE_SERVICE_ENDPOINT: rOpts.azureServiceEndpoint}),
|
||||
...(rOpts.initialSpeechTimeoutMs > 0 &&
|
||||
{AZURE_INITIAL_SPEECH_TIMEOUT_MS: rOpts.initialSpeechTimeoutMs}),
|
||||
@@ -401,11 +408,11 @@ module.exports = (logger) => {
|
||||
...(rOpts.audioLogging && {AZURE_AUDIO_LOGGING: 1}),
|
||||
...{AZURE_USE_OUTPUT_FORMAT_DETAILED: 1},
|
||||
...(sttCredentials && {
|
||||
AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key,
|
||||
AZURE_REGION: sttCredentials.region,
|
||||
...(sttCredentials.api_key && {AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key}),
|
||||
...(sttCredentials.region && {AZURE_REGION: sttCredentials.region}),
|
||||
}),
|
||||
...(sttCredentials.use_custom_stt && sttCredentials.custom_stt_endpoint &&
|
||||
{AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint})
|
||||
{AZURE_SERVICE_ENDPOINT_ID: sttCredentials.custom_stt_endpoint}),
|
||||
};
|
||||
}
|
||||
else if ('nuance' === vendor) {
|
||||
|
||||
@@ -43,6 +43,7 @@ class WsRequestor extends BaseRequestor {
|
||||
async request(type, hook, params, httpHeaders = {}) {
|
||||
assert(HookMsgTypes.includes(type));
|
||||
const url = hook.url || hook;
|
||||
const wantsAck = !['call:status', 'verb:status', 'jambonz:error'].includes(type);
|
||||
|
||||
if (this.maliciousClient) {
|
||||
this.logger.info({url: this.url}, 'WsRequestor:request - discarding msg to malicious client');
|
||||
@@ -73,11 +74,19 @@ class WsRequestor extends BaseRequestor {
|
||||
if (this.connectInProgress) {
|
||||
this.logger.debug(
|
||||
`WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`);
|
||||
this.queuedMsg.push({type, hook, params, httpHeaders});
|
||||
if (wantsAck) {
|
||||
const p = new Promise((resolve, reject) => {
|
||||
this.queuedMsg.push({type, hook, params, httpHeaders, promise: {resolve, reject}});
|
||||
});
|
||||
return p;
|
||||
}
|
||||
else {
|
||||
this.queuedMsg.push({type, hook, params, httpHeaders});
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.connectInProgress = true;
|
||||
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection`);
|
||||
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection for ${type}`);
|
||||
if (this.connections >= MAX_RECONNECTS) {
|
||||
return Promise.reject(`max attempts connecting to ${this.url}`);
|
||||
}
|
||||
@@ -116,9 +125,14 @@ class WsRequestor extends BaseRequestor {
|
||||
|
||||
const sendQueuedMsgs = () => {
|
||||
if (this.queuedMsg.length > 0) {
|
||||
for (const {type, hook, params, httpHeaders} of this.queuedMsg) {
|
||||
for (const {type, hook, params, httpHeaders, promise} of this.queuedMsg) {
|
||||
this.logger.debug(`WsRequestor:request - preparing queued ${type} for sending`);
|
||||
setImmediate(this.request.bind(this, type, hook, params, httpHeaders));
|
||||
if (promise) {
|
||||
this.request(type, hook, params, httpHeaders)
|
||||
.then((res) => promise.resolve(res))
|
||||
.catch((err) => promise.reject(err));
|
||||
}
|
||||
else setImmediate(this.request.bind(this, type, hook, params, httpHeaders));
|
||||
}
|
||||
this.queuedMsg.length = 0;
|
||||
}
|
||||
@@ -137,7 +151,7 @@ class WsRequestor extends BaseRequestor {
|
||||
}
|
||||
|
||||
/* simple notifications */
|
||||
if (['call:status', 'verb:status', 'jambonz:error'].includes(type) || reconnectingWithoutAck) {
|
||||
if (!wantsAck || reconnectingWithoutAck) {
|
||||
this.ws?.send(JSON.stringify(obj), () => {
|
||||
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
|
||||
sendQueuedMsgs();
|
||||
|
||||
86
package-lock.json
generated
86
package-lock.json
generated
@@ -14,10 +14,10 @@
|
||||
"@jambonz/db-helpers": "^0.9.1",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.6",
|
||||
"@jambonz/speech-utils": "^0.0.18",
|
||||
"@jambonz/stats-collector": "^0.1.8",
|
||||
"@jambonz/speech-utils": "^0.0.20",
|
||||
"@jambonz/stats-collector": "^0.1.9",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.26",
|
||||
"@jambonz/verb-specifications": "^0.0.30",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.35.0",
|
||||
@@ -30,7 +30,7 @@
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.23",
|
||||
"drachtio-fsmrf": "^3.0.24",
|
||||
"drachtio-srf": "^4.5.26",
|
||||
"express": "^4.18.2",
|
||||
"ip": "^1.1.8",
|
||||
@@ -47,7 +47,7 @@
|
||||
"uuid-random": "^1.3.2",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
"ws": "^8.9.0",
|
||||
"xml2js": "^0.5.0"
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clear-module": "^4.1.2",
|
||||
@@ -2983,9 +2983,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/speech-utils": {
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.0.18.tgz",
|
||||
"integrity": "sha512-UoX1sPV/kDUBp4uiivbFftpn9nx6G9rmspneszrksnSWERp0SP09yL6rvEFFTDMvFmK2ivP6TIMRywlExO4OQg==",
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.0.20.tgz",
|
||||
"integrity": "sha512-vyMgfCAdv5+zOVisyJKhVQ1+lY1i1Ygv5amgw3JgyUy+lDbWmzANctLZXpJpoUGbTw8ZOwippEnk0F4XWFmtlg==",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-polly": "^3.359.0",
|
||||
"@google-cloud/text-to-speech": "^4.2.1",
|
||||
@@ -2996,14 +2996,14 @@
|
||||
"google-protobuf": "^3.21.2",
|
||||
"ibm-watson": "^8.0.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.26.0",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.31.0",
|
||||
"undici": "^5.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/stats-collector": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/stats-collector/-/stats-collector-0.1.8.tgz",
|
||||
"integrity": "sha512-PadBKTmcphU7bZI7pVr4awhoIi+DMLw7An6AGE0GGsLCn2x0IGnxr9k47gI4Bk3OaSF6N7RyVLEAMkJfr24l8Q==",
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/stats-collector/-/stats-collector-0.1.9.tgz",
|
||||
"integrity": "sha512-JNRBaHQ47pWsXydj4gUp7zc64/0pM89a6E9pA8uQ15l1KxPGYYTrNRdone5aJqLTFOoPl3tYeF1kXj+3nU1nEA==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.2",
|
||||
"hot-shots": "^8.5.0"
|
||||
@@ -3019,9 +3019,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jambonz/verb-specifications": {
|
||||
"version": "0.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.26.tgz",
|
||||
"integrity": "sha512-C/2KpX7dLPrEOFbcpyjJ3FkR8EEp+QbNmJoWbCcfYoZEyLiOcawWVwPRvz8hNPVa/Hf2Scth9OvjKeGuny33gQ==",
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.30.tgz",
|
||||
"integrity": "sha512-IbusgtBBo2V5Tc1FvDJvkWogHOhR2tNZN6Iyb2PjUomMI48BsWKmHW/wegppKTDpBBeN3ABY+L96pvX4N0+mCw==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"pino": "^8.8.0"
|
||||
@@ -5152,9 +5152,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/drachtio-fsmrf": {
|
||||
"version": "3.0.23",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.23.tgz",
|
||||
"integrity": "sha512-ElruNKuPzFiMOUH06PUd3dR9tElEGRhbP/gXxai58qhrqRQNLJxzCRJkbgbjrZdYWFQPHOAzy4ZQb7+qq0AUPw==",
|
||||
"version": "3.0.24",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.24.tgz",
|
||||
"integrity": "sha512-MsTwHDOOg5yrKCcQ2buNTVSEwoMZzTcVeU5/BZ2Km0TPxBI1lErW6IiyZxA5pyoLfSdBtbqSh9ZwXrtxWTp6kA==",
|
||||
"dependencies": {
|
||||
"camel-case": "^4.1.2",
|
||||
"debug": "^2.6.9",
|
||||
@@ -7807,9 +7807,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/microsoft-cognitiveservices-speech-sdk": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.29.0.tgz",
|
||||
"integrity": "sha512-/NaDni70OR5x0FPG6LD/aOkmfLIsOwqpw0UsijcHbH+U0q6FPAX1VZVlv6ZMvkObw3k/FJv0N9CFNsN1P4M7kA==",
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.31.0.tgz",
|
||||
"integrity": "sha512-wmNi0XoGtQwRoI2To6QSrGHVW0d8WfhJwXtE2nk48l4YkBiDqdPV2tdSXFHRrdv3uwr/+THip45H91Fllpm8qA==",
|
||||
"dependencies": {
|
||||
"agent-base": "^6.0.1",
|
||||
"bent": "^7.3.12",
|
||||
@@ -10445,9 +10445,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
@@ -12949,9 +12949,9 @@
|
||||
}
|
||||
},
|
||||
"@jambonz/speech-utils": {
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.0.18.tgz",
|
||||
"integrity": "sha512-UoX1sPV/kDUBp4uiivbFftpn9nx6G9rmspneszrksnSWERp0SP09yL6rvEFFTDMvFmK2ivP6TIMRywlExO4OQg==",
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/speech-utils/-/speech-utils-0.0.20.tgz",
|
||||
"integrity": "sha512-vyMgfCAdv5+zOVisyJKhVQ1+lY1i1Ygv5amgw3JgyUy+lDbWmzANctLZXpJpoUGbTw8ZOwippEnk0F4XWFmtlg==",
|
||||
"requires": {
|
||||
"@aws-sdk/client-polly": "^3.359.0",
|
||||
"@google-cloud/text-to-speech": "^4.2.1",
|
||||
@@ -12962,14 +12962,14 @@
|
||||
"google-protobuf": "^3.21.2",
|
||||
"ibm-watson": "^8.0.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.26.0",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.31.0",
|
||||
"undici": "^5.21.0"
|
||||
}
|
||||
},
|
||||
"@jambonz/stats-collector": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/stats-collector/-/stats-collector-0.1.8.tgz",
|
||||
"integrity": "sha512-PadBKTmcphU7bZI7pVr4awhoIi+DMLw7An6AGE0GGsLCn2x0IGnxr9k47gI4Bk3OaSF6N7RyVLEAMkJfr24l8Q==",
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/stats-collector/-/stats-collector-0.1.9.tgz",
|
||||
"integrity": "sha512-JNRBaHQ47pWsXydj4gUp7zc64/0pM89a6E9pA8uQ15l1KxPGYYTrNRdone5aJqLTFOoPl3tYeF1kXj+3nU1nEA==",
|
||||
"requires": {
|
||||
"debug": "^4.3.2",
|
||||
"hot-shots": "^8.5.0"
|
||||
@@ -12985,9 +12985,9 @@
|
||||
}
|
||||
},
|
||||
"@jambonz/verb-specifications": {
|
||||
"version": "0.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.26.tgz",
|
||||
"integrity": "sha512-C/2KpX7dLPrEOFbcpyjJ3FkR8EEp+QbNmJoWbCcfYoZEyLiOcawWVwPRvz8hNPVa/Hf2Scth9OvjKeGuny33gQ==",
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.30.tgz",
|
||||
"integrity": "sha512-IbusgtBBo2V5Tc1FvDJvkWogHOhR2tNZN6Iyb2PjUomMI48BsWKmHW/wegppKTDpBBeN3ABY+L96pvX4N0+mCw==",
|
||||
"requires": {
|
||||
"debug": "^4.3.4",
|
||||
"pino": "^8.8.0"
|
||||
@@ -14610,9 +14610,9 @@
|
||||
}
|
||||
},
|
||||
"drachtio-fsmrf": {
|
||||
"version": "3.0.23",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.23.tgz",
|
||||
"integrity": "sha512-ElruNKuPzFiMOUH06PUd3dR9tElEGRhbP/gXxai58qhrqRQNLJxzCRJkbgbjrZdYWFQPHOAzy4ZQb7+qq0AUPw==",
|
||||
"version": "3.0.24",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-3.0.24.tgz",
|
||||
"integrity": "sha512-MsTwHDOOg5yrKCcQ2buNTVSEwoMZzTcVeU5/BZ2Km0TPxBI1lErW6IiyZxA5pyoLfSdBtbqSh9ZwXrtxWTp6kA==",
|
||||
"requires": {
|
||||
"camel-case": "^4.1.2",
|
||||
"debug": "^2.6.9",
|
||||
@@ -16661,9 +16661,9 @@
|
||||
}
|
||||
},
|
||||
"microsoft-cognitiveservices-speech-sdk": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.29.0.tgz",
|
||||
"integrity": "sha512-/NaDni70OR5x0FPG6LD/aOkmfLIsOwqpw0UsijcHbH+U0q6FPAX1VZVlv6ZMvkObw3k/FJv0N9CFNsN1P4M7kA==",
|
||||
"version": "1.31.0",
|
||||
"resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.31.0.tgz",
|
||||
"integrity": "sha512-wmNi0XoGtQwRoI2To6QSrGHVW0d8WfhJwXtE2nk48l4YkBiDqdPV2tdSXFHRrdv3uwr/+THip45H91Fllpm8qA==",
|
||||
"requires": {
|
||||
"agent-base": "^6.0.1",
|
||||
"bent": "^7.3.12",
|
||||
@@ -18637,9 +18637,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
|
||||
14
package.json
14
package.json
@@ -25,13 +25,15 @@
|
||||
"jslint:fix": "eslint app.js tracer.js lib --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-auto-scaling": "^3.360.0",
|
||||
"@aws-sdk/client-sns": "^3.360.0",
|
||||
"@jambonz/db-helpers": "^0.9.1",
|
||||
"@jambonz/http-health-check": "^0.0.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.8.6",
|
||||
"@jambonz/speech-utils": "^0.0.18",
|
||||
"@jambonz/stats-collector": "^0.1.8",
|
||||
"@jambonz/speech-utils": "^0.0.20",
|
||||
"@jambonz/stats-collector": "^0.1.9",
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.26",
|
||||
"@jambonz/verb-specifications": "^0.0.30",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.35.0",
|
||||
@@ -41,12 +43,10 @@
|
||||
"@opentelemetry/sdk-trace-base": "^1.9.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.9.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.9.0",
|
||||
"@aws-sdk/client-sns": "^3.360.0",
|
||||
"@aws-sdk/client-auto-scaling": "^3.360.0",
|
||||
"bent": "^7.3.12",
|
||||
"debug": "^4.3.4",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^3.0.23",
|
||||
"drachtio-fsmrf": "^3.0.24",
|
||||
"drachtio-srf": "^4.5.26",
|
||||
"express": "^4.18.2",
|
||||
"ip": "^1.1.8",
|
||||
@@ -63,7 +63,7 @@
|
||||
"uuid-random": "^1.3.2",
|
||||
"verify-aws-sns-signature": "^0.1.0",
|
||||
"ws": "^8.9.0",
|
||||
"xml2js": "^0.5.0"
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clear-module": "^4.1.2",
|
||||
|
||||
@@ -37,7 +37,7 @@ test('test create-call timeout', async(t) => {
|
||||
'account_sid':account_sid,
|
||||
'timeout': 1,
|
||||
"call_hook": {
|
||||
"url": "https://public-apps.jambonz.us/hello-world",
|
||||
"url": "https://public-apps.jambonz.cloud/hello-world",
|
||||
"method": "POST"
|
||||
},
|
||||
"from": "15083718299",
|
||||
@@ -88,8 +88,8 @@ test('test create-call call-hook basic authentication', async(t) => {
|
||||
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "say",
|
||||
"text": "hello"
|
||||
"verb": "pause",
|
||||
"length": 1
|
||||
}
|
||||
];
|
||||
await provisionCallHook(from, verbs);
|
||||
|
||||
@@ -896,7 +896,7 @@ CREATE TABLE `service_providers` (
|
||||
|
||||
LOCK TABLES `service_providers` WRITE;
|
||||
/*!40000 ALTER TABLE `service_providers` DISABLE KEYS */;
|
||||
INSERT INTO `service_providers` VALUES ('2708b1b3-2736-40ea-b502-c53d8396247f','jambonz.us','jambonz.us service provider','yakeeda.com',NULL,NULL);
|
||||
INSERT INTO `service_providers` VALUES ('2708b1b3-2736-40ea-b502-c53d8396247f','jambonz.cloud','jambonz.cloud service provider','yakeeda.com',NULL,NULL);
|
||||
/*!40000 ALTER TABLE `service_providers` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
@@ -1045,6 +1045,7 @@ CREATE TABLE `speech_credentials` (
|
||||
`tts_tested_ok` tinyint(1) DEFAULT NULL,
|
||||
`stt_tested_ok` tinyint(1) DEFAULT NULL,
|
||||
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`label` VARCHAR(64),
|
||||
PRIMARY KEY (`speech_credential_sid`),
|
||||
UNIQUE KEY `speech_credential_sid` (`speech_credential_sid`),
|
||||
UNIQUE KEY `speech_credentials_idx_1` (`vendor`,`account_sid`),
|
||||
@@ -1063,7 +1064,7 @@ CREATE TABLE `speech_credentials` (
|
||||
|
||||
LOCK TABLES `speech_credentials` WRITE;
|
||||
/*!40000 ALTER TABLE `speech_credentials` DISABLE KEYS */;
|
||||
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1,'2023-05-31 03:44:21'),('2add347f-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','microsoft','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1,'2023-05-31 03:44:21'),('84154212-5c99-4c94-8993-bc2a46288daa',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',1,1,NULL,NULL,NULL,NULL,'2023-05-31 03:44:21');
|
||||
INSERT INTO `speech_credentials` VALUES ('2add163c-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1,'2023-05-31 03:44:21', NULL),('2add347f-34f2-45c6-a016-f955d218ffb6',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','microsoft','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1,'2023-05-31 03:44:21', NULL),('84154212-5c99-4c94-8993-bc2a46288daa',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',1,1,NULL,NULL,1,1,'2023-05-31 03:44:21', NULL);
|
||||
/*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
@@ -1253,7 +1254,7 @@ CREATE TABLE `webhooks` (
|
||||
|
||||
LOCK TABLES `webhooks` WRITE;
|
||||
/*!40000 ALTER TABLE `webhooks` DISABLE KEYS */;
|
||||
INSERT INTO `webhooks` VALUES ('10692465-a511-4277-9807-b7157e4f81e1','http://127.0.0.1:3102/','POST',NULL,NULL),('293904c1-351b-4bca-8d58-1a29b853c7db','http://127.0.0.1:3100/callStatus','POST',NULL,NULL),('54ab0976-a6c0-45d8-89a4-d90d45bf9d96','http://127.0.0.1:3101/','POST',NULL,NULL),('6ac36aeb-6bd0-428a-80a1-aed95640a296','https://flows.jambonz.us/callStatus','POST',NULL,NULL),('c71e79db-24f2-4866-a3ee-febb0f97b341','http://127.0.0.1:3100/','POST',NULL,NULL),('c9113e7a-741f-48b9-96c1-f2f78176eeb3','http://127.0.0.1:3104/','POST',NULL,NULL),('d9c205c6-a129-443e-a9c0-d1bb437d4bb7','https://flows.jambonz.us/testCall','POST',NULL,NULL),('ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','http://127.0.0.1:3103/','POST',NULL,NULL);
|
||||
INSERT INTO `webhooks` VALUES ('10692465-a511-4277-9807-b7157e4f81e1','http://127.0.0.1:3102/','POST',NULL,NULL),('293904c1-351b-4bca-8d58-1a29b853c7db','http://127.0.0.1:3100/callStatus','POST',NULL,NULL),('54ab0976-a6c0-45d8-89a4-d90d45bf9d96','http://127.0.0.1:3101/','POST',NULL,NULL),('6ac36aeb-6bd0-428a-80a1-aed95640a296','https://flows.jambonz.cloud/callStatus','POST',NULL,NULL),('c71e79db-24f2-4866-a3ee-febb0f97b341','http://127.0.0.1:3100/','POST',NULL,NULL),('c9113e7a-741f-48b9-96c1-f2f78176eeb3','http://127.0.0.1:3104/','POST',NULL,NULL),('d9c205c6-a129-443e-a9c0-d1bb437d4bb7','https://flows.jambonz.cloud/testCall','POST',NULL,NULL),('ecb67a8f-f7ce-4919-abf0-bbc69c1001e5','http://127.0.0.1:3103/','POST',NULL,NULL);
|
||||
/*!40000 ALTER TABLE `webhooks` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
@@ -13,6 +13,8 @@ DROP TABLE IF EXISTS beta_invite_codes;
|
||||
|
||||
DROP TABLE IF EXISTS call_routes;
|
||||
|
||||
DROP TABLE IF EXISTS clients;
|
||||
|
||||
DROP TABLE IF EXISTS dns_records;
|
||||
|
||||
DROP TABLE IF EXISTS lcr;
|
||||
@@ -127,6 +129,16 @@ application_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (call_route_sid)
|
||||
) COMMENT='a regex-based pattern match for call routing';
|
||||
|
||||
CREATE TABLE clients
|
||||
(
|
||||
client_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
account_sid CHAR(36) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
username VARCHAR(64),
|
||||
password VARCHAR(1024),
|
||||
PRIMARY KEY (client_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE dns_records
|
||||
(
|
||||
dns_record_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -322,6 +334,7 @@ last_tested DATETIME,
|
||||
tts_tested_ok BOOLEAN,
|
||||
stt_tested_ok BOOLEAN,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
label VARCHAR(64),
|
||||
PRIMARY KEY (speech_credential_sid)
|
||||
);
|
||||
|
||||
@@ -411,7 +424,7 @@ PRIMARY KEY (smpp_gateway_sid)
|
||||
CREATE TABLE phone_numbers
|
||||
(
|
||||
phone_number_sid CHAR(36) UNIQUE ,
|
||||
number VARCHAR(132) NOT NULL UNIQUE ,
|
||||
number VARCHAR(132) NOT NULL,
|
||||
voip_carrier_sid CHAR(36),
|
||||
account_sid CHAR(36),
|
||||
application_sid CHAR(36),
|
||||
@@ -466,9 +479,12 @@ app_json TEXT,
|
||||
speech_synthesis_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US',
|
||||
speech_synthesis_voice VARCHAR(64),
|
||||
speech_synthesis_label VARCHAR(64),
|
||||
speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google',
|
||||
speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US',
|
||||
speech_recognizer_label VARCHAR(64),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (application_sid)
|
||||
) COMMENT='A defined set of behaviors to be applied to phone calls ';
|
||||
|
||||
@@ -506,6 +522,9 @@ subspace_client_secret VARCHAR(255),
|
||||
subspace_sip_teleport_id VARCHAR(255),
|
||||
subspace_sip_teleport_destinations VARCHAR(255),
|
||||
siprec_hook_sid CHAR(36),
|
||||
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||
record_format VARCHAR(16) NOT NULL DEFAULT 'mp3',
|
||||
bucket_credential VARCHAR(8192) COMMENT 'credential used to authenticate with storage service',
|
||||
PRIMARY KEY (account_sid)
|
||||
) COMMENT='An enterprise that uses the platform for comm services';
|
||||
|
||||
@@ -526,6 +545,9 @@ ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERE
|
||||
|
||||
ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
CREATE INDEX client_sid_idx ON clients (client_sid);
|
||||
ALTER TABLE clients ADD CONSTRAINT account_sid_idxfk_13 FOREIGN KEY account_sid_idxfk_13 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid);
|
||||
ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid);
|
||||
|
||||
@@ -590,8 +612,6 @@ CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid);
|
||||
ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
|
||||
CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid);
|
||||
|
||||
CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid);
|
||||
CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid);
|
||||
ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid);
|
||||
@@ -628,6 +648,8 @@ CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid);
|
||||
CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid);
|
||||
ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
CREATE UNIQUE INDEX phone_numbers_unique_idx_voip_carrier_number ON phone_numbers (number,voip_carrier_sid);
|
||||
|
||||
CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid);
|
||||
CREATE INDEX number_idx ON phone_numbers (number);
|
||||
CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid);
|
||||
@@ -682,5 +704,4 @@ ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hoo
|
||||
ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid);
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
@@ -210,6 +210,44 @@ test('\'transcribe\' test - soniox', async(t) => {
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'transcribe: succeeds when using soniox credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
disconnect();
|
||||
t.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
test('\'transcribe\' test - google with asrTimeout', async(t) => {
|
||||
if (!GCP_JSON_KEY) {
|
||||
t.pass('skipping google tests');
|
||||
return t.end();
|
||||
}
|
||||
clearModule.all();
|
||||
const {srf, disconnect} = require('../app');
|
||||
|
||||
try {
|
||||
await connect(srf);
|
||||
// GIVEN
|
||||
let verbs = [
|
||||
{
|
||||
"verb": "transcribe",
|
||||
"recognizer": {
|
||||
"vendor": "google",
|
||||
"hints": ["customer support", "sales", "human resources", "HR"],
|
||||
"asrTimeout": 4
|
||||
},
|
||||
"transcriptionHook": "/transcriptionHook"
|
||||
}
|
||||
];
|
||||
let from = "gather_success";
|
||||
await provisionCallHook(from, verbs);
|
||||
// THEN
|
||||
await sippUac('uac-gather-account-creds-success.xml', '172.38.0.10', from);
|
||||
let obj = await getJSON(`http://127.0.0.1:3100/lastRequest/${from}_actionHook`);
|
||||
t.ok(obj.body.speech.alternatives[0].transcript.toLowerCase().startsWith('i\'d like to speak to customer support'),
|
||||
'transcribe: succeeds when using google credentials');
|
||||
|
||||
disconnect();
|
||||
} catch (err) {
|
||||
console.log(`error received: ${err}`);
|
||||
|
||||
Reference in New Issue
Block a user