mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-02-10 08:21:33 +00:00
Compare commits
11 Commits
v0.6.7-rc5
...
v0.6.7-rc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e93973419 | ||
|
|
fe1778e9ae | ||
|
|
af15449451 | ||
|
|
12c34de15c | ||
|
|
7c77bedd15 | ||
|
|
0c5150cb30 | ||
|
|
2262973f43 | ||
|
|
db78ffffed | ||
|
|
2930cd6aaf | ||
|
|
2a013377cc | ||
|
|
dcf27ba5d3 |
10
Dockerfile
10
Dockerfile
@@ -1,16 +1,10 @@
|
||||
FROM node:alpine as builder
|
||||
RUN apk update && apk add --no-cache python make g++
|
||||
FROM node:16
|
||||
WORKDIR /opt/app/
|
||||
COPY package.json ./
|
||||
RUN npm install
|
||||
RUN npm prune
|
||||
|
||||
FROM node:alpine as app
|
||||
WORKDIR /opt/app
|
||||
COPY . /opt/app
|
||||
COPY --from=builder /opt/app/node_modules ./node_modules
|
||||
|
||||
ARG NODE_ENV
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
CMD [ "npm", "start" ]
|
||||
@@ -137,7 +137,9 @@ module.exports = function(srf, logger) {
|
||||
const obj = Object.assign({}, app);
|
||||
delete obj.requestor;
|
||||
delete obj.notifier;
|
||||
logger.info({app: obj}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {call_hook, call_status_hook, ...appInfo} = obj; // mask sensitive data like user/pass on webhook
|
||||
logger.info({app: appInfo}, `retrieved application for incoming call to ${req.locals.calledNumber}`);
|
||||
req.locals.callInfo = new CallInfo({req, app, direction: CallDirection.Inbound});
|
||||
next();
|
||||
} catch (err) {
|
||||
|
||||
@@ -257,6 +257,13 @@ class CallSession extends Emitter {
|
||||
region: process.env.AWS_REGION || credential.aws_region
|
||||
};
|
||||
}
|
||||
else if ('microsoft' === vendor) {
|
||||
return {
|
||||
speech_credential_sid: credential.speech_credential_sid,
|
||||
api_key: credential.api_key,
|
||||
region: credential.region
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
writeAlerts({
|
||||
|
||||
@@ -10,6 +10,7 @@ const WAIT = 'wait';
|
||||
const JOIN = 'join';
|
||||
const START = 'start';
|
||||
|
||||
|
||||
function confNoMatch(str) {
|
||||
return str.match(/^No active conferences/) || str.match(/Conference.*not found/);
|
||||
}
|
||||
@@ -49,7 +50,7 @@ class Conference extends Task {
|
||||
'beep', 'startConferenceOnEnter', 'endConferenceOnExit', 'joinMuted',
|
||||
'maxParticipants', 'waitHook', 'statusHook', 'endHook', 'enterHook'
|
||||
].forEach((attr) => this[attr] = this.data[attr]);
|
||||
|
||||
this.record = this.data.record || {};
|
||||
this.statusEvents = [];
|
||||
if (this.statusHook) {
|
||||
['start', 'end', 'join', 'leave', 'start-talking', 'stop-talking'].forEach((e) => {
|
||||
@@ -68,6 +69,9 @@ class Conference extends Task {
|
||||
|
||||
get name() { return TaskName.Conference; }
|
||||
|
||||
get shouldRecord() { return this.record.path; }
|
||||
get isRecording() { return this.recordingInProgress; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
this.ep = ep;
|
||||
@@ -346,6 +350,16 @@ class Conference extends Task {
|
||||
if (response.body && /\d+/.test(response.body)) this.participantCount = parseInt(response.body);
|
||||
this._notifyConferenceEvent(cs, 'join');
|
||||
|
||||
// start recording if requested and we just started the conference
|
||||
if (startConf && this.shouldRecord) {
|
||||
this.logger.info(`recording conference to ${this.record.path}`);
|
||||
try {
|
||||
await this.ep.api(`conference ${this.confName} record ${this.record.path}`);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Conference:_joinConference - failed to start recording');
|
||||
}
|
||||
}
|
||||
|
||||
// listen for conference events
|
||||
this.ep.filter('Conference-Unique-ID', this.confUuid);
|
||||
this.ep.conn.on('esl::event::CUSTOM::*', this.__onConferenceEvent.bind(this, cs)) ;
|
||||
|
||||
@@ -161,6 +161,10 @@ class TaskDial extends Task {
|
||||
async kill(cs, reason) {
|
||||
super.kill(cs);
|
||||
this.killReason = reason || KillReason.Hangup;
|
||||
if (this.timerMaxCallDuration) {
|
||||
clearTimeout(this.timerMaxCallDuration);
|
||||
this.timerMaxCallDuration = null;
|
||||
}
|
||||
this._removeDtmfDetection(cs.dlg);
|
||||
this._removeDtmfDetection(this.dlg);
|
||||
this._killOutdials();
|
||||
@@ -171,7 +175,6 @@ class TaskDial extends Task {
|
||||
if (this.callSid) sessionTracker.remove(this.callSid);
|
||||
if (this.listenTask) await this.listenTask.kill(cs);
|
||||
if (this.transcribeTask) await this.transcribeTask.kill(cs);
|
||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
@@ -458,7 +461,7 @@ class TaskDial extends Task {
|
||||
if (this.timeLimit) {
|
||||
this.timerMaxCallDuration = setTimeout(() => {
|
||||
this.logger.info(`Dial:_selectSingleDial tearing down call as it has reached ${this.timeLimit}s`);
|
||||
this.ep.unbridge();
|
||||
this.ep && this.ep.unbridge();
|
||||
this.kill(cs);
|
||||
}, this.timeLimit * 1000);
|
||||
}
|
||||
@@ -468,7 +471,10 @@ class TaskDial extends Task {
|
||||
if (this.dlg) {
|
||||
this.logger.debug('Dial:_selectSingleDial called party hungup, ending dial operation');
|
||||
sessionTracker.remove(this.callSid);
|
||||
if (this.timerMaxCallDuration) clearTimeout(this.timerMaxCallDuration);
|
||||
if (this.timerMaxCallDuration) {
|
||||
clearTimeout(this.timerMaxCallDuration);
|
||||
this.timerMaxCallDuration = null;
|
||||
}
|
||||
this.ep && this.ep.unbridge();
|
||||
this.kill(cs);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ const {
|
||||
TaskName,
|
||||
TaskPreconditions,
|
||||
GoogleTranscriptionEvents,
|
||||
AwsTranscriptionEvents
|
||||
AwsTranscriptionEvents,
|
||||
AzureTranscriptionEvents
|
||||
} = require('../utils/constants');
|
||||
|
||||
const makeTask = require('./make_task');
|
||||
@@ -33,6 +34,12 @@ class TaskGather extends Task {
|
||||
this.vocabularyName = recognizer.vocabularyName;
|
||||
this.vocabularyFilterName = recognizer.vocabularyFilterName;
|
||||
this.filterMethod = recognizer.filterMethod;
|
||||
|
||||
/* microsoft options */
|
||||
this.outputFormat = recognizer.outputFormat || 'simple';
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
}
|
||||
|
||||
this.digitBuffer = '';
|
||||
@@ -46,6 +53,8 @@ class TaskGather extends Task {
|
||||
|
||||
get name() { return TaskName.Gather; }
|
||||
|
||||
get needsStt() { return this.input.includes('speech'); }
|
||||
|
||||
get earlyMedia() {
|
||||
return (this.sayTask && this.sayTask.earlyMedia) ||
|
||||
(this.playTask && this.playTask.earlyMedia);
|
||||
@@ -59,9 +68,9 @@ class TaskGather extends Task {
|
||||
if ('default' === this.vendor || !this.vendor) this.vendor = cs.speechRecognizerVendor;
|
||||
if ('default' === this.language || !this.language) this.language = cs.speechRecognizerLanguage;
|
||||
this.sttCredentials = cs.getSpeechCredentials(this.vendor, 'stt');
|
||||
if (!this.sttCredentials) {
|
||||
if (this.needsStt && !this.sttCredentials) {
|
||||
const {writeAlerts, AlertType} = cs.srf.locals;
|
||||
this.logger.info(`TaskGather:exec - ERROR stt using ${this.vendor} requested but not creds supplied`);
|
||||
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,
|
||||
@@ -104,6 +113,8 @@ class TaskGather extends Task {
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
|
||||
}
|
||||
|
||||
kill(cs) {
|
||||
@@ -133,7 +144,9 @@ class TaskGather extends Task {
|
||||
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
|
||||
GOOGLE_SPEECH_MODEL: 'command_and_search'
|
||||
});
|
||||
if (this.hints && this.hints.length > 1) opts.GOOGLE_SPEECH_HINTS = this.hints.join(',');
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.GOOGLE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.altLanguages && this.altLanguages.length > 1) {
|
||||
opts.GOOGLE_SPEECH_ALTERNATIVE_LANGUAGE_CODES = this.altLanguages.join(',');
|
||||
}
|
||||
@@ -143,22 +156,41 @@ class TaskGather extends Task {
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
|
||||
}
|
||||
else {
|
||||
else if (['aws', 'polly'].includes(this.vendor)) {
|
||||
if (this.vocabularyName) opts.AWS_VOCABULARY_NAME = this.vocabularyName;
|
||||
if (this.vocabularyFilterName) {
|
||||
opts.AWS_VOCABULARY_NAME = this.vocabularyFilterName;
|
||||
opts.AWS_VOCABULARY_FILTER_METHOD = this.filterMethod || 'mask';
|
||||
}
|
||||
Object.assign(opts, {
|
||||
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
|
||||
AWS_REGION: this.sttCredentials.region
|
||||
});
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
AWS_ACCESS_KEY_ID: this.sttCredentials.accessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: this.sttCredentials.secretAccessKey,
|
||||
AWS_REGION: this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
}
|
||||
else if ('microsoft' === this.vendor) {
|
||||
if (this.sttCredentials) {
|
||||
Object.assign(opts, {
|
||||
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
|
||||
'AZURE_REGION': this.sttCredentials.region
|
||||
});
|
||||
}
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
//if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
||||
//if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
|
||||
opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoSpeechDetected.bind(this, cs, ep));
|
||||
}
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error setting channel variables'));
|
||||
|
||||
}
|
||||
|
||||
_startTranscribing(ep) {
|
||||
@@ -206,11 +238,21 @@ class TaskGather extends Task {
|
||||
|
||||
_onTranscription(cs, ep, evt) {
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||
const final = evt.is_final;
|
||||
if (final) {
|
||||
this._resolve('speech', evt);
|
||||
if ('microsoft' === this.vendor) {
|
||||
const nbest = evt.NBest;
|
||||
const newEvent = {
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
alternatives: [
|
||||
{
|
||||
confidence: nbest[0].Confidence,
|
||||
transcript: nbest[0].Display
|
||||
}
|
||||
]
|
||||
};
|
||||
evt = newEvent;
|
||||
}
|
||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||
if (evt.is_final) this._resolve('speech', evt);
|
||||
else if (this.partialResultHook) {
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'GatherTask:_onTranscription error'));
|
||||
@@ -223,6 +265,10 @@ class TaskGather extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
_onNoSpeechDetected(cs, ep) {
|
||||
this._resolve('timeout');
|
||||
}
|
||||
|
||||
async _resolve(reason, evt) {
|
||||
if (this.resolved) return;
|
||||
this.resolved = true;
|
||||
|
||||
@@ -17,8 +17,10 @@ function makeTask(logger, obj, parent) {
|
||||
case TaskName.SipDecline:
|
||||
const TaskSipDecline = require('./sip_decline');
|
||||
return new TaskSipDecline(logger, data, parent);
|
||||
case TaskName.SipRefer:
|
||||
const TaskSipRefer = require('./sip_refer');
|
||||
return new TaskSipRefer(logger, data, parent);
|
||||
case TaskName.Conference:
|
||||
logger.debug({data}, 'Conference verb');
|
||||
const TaskConference = require('./conference');
|
||||
return new TaskConference(logger, data, parent);
|
||||
case TaskName.Dial:
|
||||
|
||||
@@ -21,19 +21,15 @@ class TaskSay extends Task {
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
||||
const {writeAlerts, AlertType, stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
const vendor = ('default' === this.synthesizer.vendor || !this.synthesizer.vendor) ?
|
||||
cs.speechSynthesisVendor :
|
||||
this.synthesizer.vendor;
|
||||
const language = ('default' === this.synthesizer.language || !this.synthesizer.language) ?
|
||||
cs.speechSynthesisLanguage :
|
||||
this.synthesizer.language;
|
||||
const voice = ('default' === this.synthesizer.voice || !this.synthesizer.voice) ?
|
||||
cs.speechSynthesisVoice :
|
||||
this.synthesizer.voice;
|
||||
const hasVerbLevelTts = this.synthesizer.vendor && this.synthesizer.vendor !== 'default';
|
||||
const vendor = hasVerbLevelTts ? this.synthesizer.vendor : cs.speechSynthesisVendor ;
|
||||
const language = hasVerbLevelTts ? this.synthesizer.language : cs.speechSynthesisLanguage ;
|
||||
const voice = hasVerbLevelTts ? this.synthesizer.voice : cs.speechSynthesisVoice ;
|
||||
|
||||
const salt = cs.callSid;
|
||||
const credentials = cs.getSpeechCredentials(vendor, 'tts');
|
||||
|
||||
this.logger.info({vendor, credentials}, 'Task:say - using vendor');
|
||||
this.logger.info({language, voice}, `Task:say - using vendor: ${vendor}`);
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (!credentials) {
|
||||
|
||||
101
lib/tasks/sip_refer.js
Normal file
101
lib/tasks/sip_refer.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const {parseUri} = require('drachtio-srf');
|
||||
|
||||
/**
|
||||
* sends a sip REFER to transfer the existing call
|
||||
*/
|
||||
class TaskSipRefer extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.StableCall;
|
||||
|
||||
this.referTo = this.data.referTo;
|
||||
this.referredBy = this.data.referredBy;
|
||||
this.headers = this.data.headers || {};
|
||||
this.eventHook = this.data.eventHook;
|
||||
}
|
||||
|
||||
get name() { return TaskName.SipRefer; }
|
||||
|
||||
async exec(cs) {
|
||||
super.exec(cs);
|
||||
const {dlg} = cs;
|
||||
const {referTo, referredBy} = this._normalizeReferHeaders(cs, dlg);
|
||||
|
||||
try {
|
||||
this.notifyHandler = this._handleNotify.bind(this, cs, dlg);
|
||||
dlg.on('notify', this.notifyHandler);
|
||||
const response = await dlg.request({
|
||||
method: 'REFER',
|
||||
headers: {
|
||||
...this.headers,
|
||||
'Refer-To': referTo,
|
||||
'Referred-By': referredBy
|
||||
}
|
||||
});
|
||||
this.referStatus = response.status;
|
||||
this.logger.info(`TaskSipRefer:exec - received ${this.referStatus} to REFER`);
|
||||
|
||||
/* if we fail, fall through to next verb. If success, we should get BYE from far end */
|
||||
if (this.referStatus === 202) {
|
||||
await this.awaitTaskDone();
|
||||
}
|
||||
else await this.performAction({refer_status: this.referStatus});
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'TaskSipRefer:exec - error sending REFER');
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
const {dlg} = cs;
|
||||
dlg.off('notify', this.notifyHandler);
|
||||
}
|
||||
|
||||
async _handleNotify(cs, dlg, req, res) {
|
||||
res.send(200);
|
||||
|
||||
const contentType = req.get('Content-Type');
|
||||
this.logger.debug({body: req.body}, `TaskSipRefer:_handleNotify got ${contentType}`);
|
||||
|
||||
if (contentType === 'message/sipfrag') {
|
||||
const arr = /SIP\/2\.0\s+(\d+)/.exec(req.body);
|
||||
if (arr) {
|
||||
const status = arr[1];
|
||||
this.logger.debug(`TaskSipRefer:_handleNotify: call got status ${status}`);
|
||||
if (this.eventHook) {
|
||||
await cs.requestor.request(this.eventHook, {event: 'transfer-status', call_status: status});
|
||||
}
|
||||
if (status >= 200) {
|
||||
await this.performAction({refer_status: 202, final_referred_call_status: status});
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_normalizeReferHeaders(cs, dlg) {
|
||||
let {referTo, referredBy} = this;
|
||||
|
||||
/* get IP address of the SBC to use as hostname if needed */
|
||||
const {host} = parseUri(dlg.remote.uri);
|
||||
|
||||
if (!referTo.startsWith('<') && !referTo.startsWith('sip') && !referTo.startsWith('"')) {
|
||||
/* they may have only provided a phone number/user */
|
||||
referTo = `sip:${referTo}@${host}`;
|
||||
}
|
||||
if (!referredBy) {
|
||||
/* default */
|
||||
referredBy = cs.req?.callingNumber || dlg.local.uri;
|
||||
this.logger.info({referredBy}, 'setting referredby');
|
||||
}
|
||||
if (!referredBy.startsWith('<') && !referredBy.startsWith('sip') && !referredBy.startsWith('"')) {
|
||||
/* they may have only provided a phone number/user */
|
||||
referredBy = `sip:${referredBy}@${host}`;
|
||||
}
|
||||
return {referTo, referredBy};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskSipRefer;
|
||||
@@ -9,6 +9,18 @@
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"sip:refer": {
|
||||
"properties": {
|
||||
"referTo": "string",
|
||||
"referredBy": "string",
|
||||
"headers": "object",
|
||||
"actionHook": "object|string",
|
||||
"eventHook": "object|string"
|
||||
},
|
||||
"required": [
|
||||
"referTo"
|
||||
]
|
||||
},
|
||||
"dequeue": {
|
||||
"properties": {
|
||||
"name": "string",
|
||||
@@ -92,7 +104,8 @@
|
||||
"waitHook": "object|string",
|
||||
"statusEvents": "array",
|
||||
"statusHook": "object|string",
|
||||
"enterHook": "object|string"
|
||||
"enterHook": "object|string",
|
||||
"record": "#record"
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
@@ -241,6 +254,14 @@
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"record": {
|
||||
"properties": {
|
||||
"path": "string"
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"redirect": {
|
||||
"properties": {
|
||||
"actionHook": "object|string"
|
||||
@@ -327,7 +348,7 @@
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google", "aws", "polly", "default"]
|
||||
"enum": ["google", "aws", "polly", "microsoft", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"voice": "string",
|
||||
@@ -344,7 +365,7 @@
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google", "aws", "default"]
|
||||
"enum": ["google", "aws", "microsoft", "default"]
|
||||
},
|
||||
"language": "string",
|
||||
"hints": "array",
|
||||
@@ -384,7 +405,24 @@
|
||||
"mask",
|
||||
"tag"
|
||||
]
|
||||
}
|
||||
},
|
||||
"outputFormat": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"simple",
|
||||
"detailed"
|
||||
]
|
||||
},
|
||||
"profanityOption": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"masked",
|
||||
"removed",
|
||||
"raw"
|
||||
]
|
||||
},
|
||||
"requestSnr": "boolean",
|
||||
"initialSpeechTimeoutMs": "number"
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
|
||||
@@ -3,6 +3,7 @@ const {
|
||||
TaskName,
|
||||
TaskPreconditions,
|
||||
GoogleTranscriptionEvents,
|
||||
AzureTranscriptionEvents,
|
||||
AwsTranscriptionEvents
|
||||
} = require('../utils/constants');
|
||||
|
||||
@@ -38,6 +39,12 @@ class TaskTranscribe extends Task {
|
||||
this.vocabularyName = recognizer.vocabularyName;
|
||||
this.vocabularyFilterName = recognizer.vocabularyFilterName;
|
||||
this.filterMethod = recognizer.filterMethod;
|
||||
|
||||
/* microsoft options */
|
||||
this.outputFormat = recognizer.outputFormat || 'simple';
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Transcribe; }
|
||||
@@ -53,7 +60,13 @@ class TaskTranscribe extends Task {
|
||||
|
||||
try {
|
||||
if (!this.sttCredentials) {
|
||||
// TODO: generate alert (actually should be done by cs.getSpeechCredentials)
|
||||
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');
|
||||
}
|
||||
await this._startTranscribing(cs, ep);
|
||||
@@ -70,6 +83,8 @@ class TaskTranscribe extends Task {
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.NoAudioDetected);
|
||||
ep.removeCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected);
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
@@ -96,6 +111,8 @@ class TaskTranscribe extends Task {
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AwsTranscriptionEvents.MaxDurationExceeded,
|
||||
this._onMaxDurationExceeded.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
|
||||
ep.addCustomEventListener(AzureTranscriptionEvents.NoSpeechDetected, this._onNoAudio.bind(this, cs, ep));
|
||||
|
||||
if (this.vendor === 'google') {
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
@@ -164,6 +181,22 @@ class TaskTranscribe extends Task {
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with aws'));
|
||||
}
|
||||
else if (this.vendor === 'microsoft') {
|
||||
Object.assign(opts, {
|
||||
'AZURE_SUBSCRIPTION_KEY': this.sttCredentials.api_key,
|
||||
'AZURE_REGION': this.sttCredentials.region
|
||||
});
|
||||
if (this.hints && this.hints.length > 1) {
|
||||
opts.AZURE_SPEECH_HINTS = this.hints.map((h) => h.trim()).join(',');
|
||||
}
|
||||
if (this.requestSnr) opts.AZURE_REQUEST_SNR = 1;
|
||||
if (this.profanityOption !== 'raw') opts.AZURE_PROFANITY_OPTION = this.profanityOption;
|
||||
if (this.initialSpeechTimeoutMs > 0) opts.AZURE_INITIAL_SPEECH_TIMEOUT_MS = this.initialSpeechTimeoutMs;
|
||||
if (this.outputFormat !== 'simple') opts.AZURE_USE_OUTPUT_FORMAT_DETAILED = 1;
|
||||
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_startTranscribing with azure'));
|
||||
}
|
||||
await this._transcribe(ep);
|
||||
}
|
||||
|
||||
@@ -178,6 +211,20 @@ class TaskTranscribe extends Task {
|
||||
|
||||
_onTranscription(cs, ep, evt) {
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === this.vendor) {
|
||||
const nbest = evt.NBest;
|
||||
const alternatives = nbest.map((n) => {
|
||||
return {
|
||||
confidence: n.Confidence,
|
||||
transcript: n.Display
|
||||
};
|
||||
});
|
||||
const newEvent = {
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
alternatives
|
||||
};
|
||||
evt = newEvent;
|
||||
}
|
||||
this.logger.debug(evt, 'TaskTranscribe:_onTranscription');
|
||||
|
||||
this.cs.requestor.request(this.transcriptionHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"Redirect": "redirect",
|
||||
"RestDial": "rest:dial",
|
||||
"SipDecline": "sip:decline",
|
||||
"SipRefer": "sip:refer",
|
||||
"SipNotify": "sip:notify",
|
||||
"SipRedirect": "sip:redirect",
|
||||
"Say": "say",
|
||||
@@ -64,6 +65,12 @@
|
||||
"NoAudioDetected": "aws_transcribe::no_audio_detected",
|
||||
"MaxDurationExceeded": "aws_transcribe::max_duration_exceeded"
|
||||
},
|
||||
"AzureTranscriptionEvents": {
|
||||
"Transcription": "azure_transcribe::transcription",
|
||||
"StartOfUtterance": "azure_transcribe::start_of_utterance",
|
||||
"EndOfUtterance": "azure_transcribe::end_of_utterance",
|
||||
"NoSpeechDetected": "azure_transcribe::no_speech_detected"
|
||||
},
|
||||
"ListenEvents": {
|
||||
"Connect": "mod_audio_fork::connect",
|
||||
"ConnectFailure": "mod_audio_fork::connect_failed",
|
||||
|
||||
@@ -21,6 +21,11 @@ const speechMapper = (cred) => {
|
||||
obj.access_key_id = o.access_key_id;
|
||||
obj.secret_access_key = o.secret_access_key;
|
||||
}
|
||||
else if ('microsoft' === obj.vendor) {
|
||||
const o = JSON.parse(decrypt(credential));
|
||||
obj.api_key = o.api_key;
|
||||
obj.region = o.region;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
@@ -37,7 +42,8 @@ module.exports = (logger, srf) => {
|
||||
/* search at the service provider level if we don't find it at the account level */
|
||||
const haveGoogle = speech.find((s) => s.vendor === 'google');
|
||||
const haveAws = speech.find((s) => s.vendor === 'aws');
|
||||
if (!haveGoogle || !haveAws) {
|
||||
const haveMicrosoft = speech.find((s) => s.vendor === 'microsoft');
|
||||
if (!haveGoogle || !haveAws || !haveMicrosoft) {
|
||||
const [r3] = await pp.query(sqlSpeechCredentialsForSP, account_sid);
|
||||
if (r3.length) {
|
||||
if (!haveGoogle) {
|
||||
@@ -48,6 +54,10 @@ module.exports = (logger, srf) => {
|
||||
const aws = r3.find((s) => s.vendor === 'aws');
|
||||
if (aws) speech.push(speechMapper(aws));
|
||||
}
|
||||
if (!haveMicrosoft) {
|
||||
const ms = r3.find((s) => s.vendor === 'microsoft');
|
||||
if (ms) speech.push(speechMapper(ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const crypto = require('crypto');
|
||||
const algorithm = 'aes-256-ctr';
|
||||
const algorithm = 'aes-256-cbc';
|
||||
const iv = crypto.randomBytes(16);
|
||||
const secretKey = crypto.createHash('sha256')
|
||||
.update(String(process.env.JWT_SECRET))
|
||||
|
||||
@@ -100,15 +100,15 @@ class Requestor {
|
||||
|
||||
assert.ok(url, 'Requestor:request url was not provided');
|
||||
assert.ok, (['GET', 'POST'].includes(method), `Requestor:request method must be 'GET' or 'POST' not ${method}`);
|
||||
|
||||
this.logger.debug({hook, payload}, `Requestor:request ${method} ${url}`);
|
||||
const {url: urlInfo = hook, method: methodInfo = 'POST'} = hook; // mask user/pass
|
||||
this.logger.debug({url: urlInfo, method: methodInfo, payload}, `Requestor:request ${method} ${url}`);
|
||||
const startAt = process.hrtime();
|
||||
|
||||
let buf;
|
||||
try {
|
||||
const sigHeader = generateSigHeader(payload, this.secret);
|
||||
const headers = {...sigHeader, ...this.authHeader};
|
||||
this.logger.info({url, headers}, 'send webhook');
|
||||
//this.logger.info({url, headers}, 'send webhook');
|
||||
buf = isRelativeUrl(url) ?
|
||||
await this.post(url, payload, headers) :
|
||||
await bent(method, 'buffer', 200, 201, 202)(url, payload, headers);
|
||||
|
||||
1114
package-lock.json
generated
1114
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,9 @@
|
||||
"jslint": "eslint app.js lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jambonz/db-helpers": "^0.6.13",
|
||||
"@jambonz/db-helpers": "^0.6.14",
|
||||
"@jambonz/mw-registrar": "^0.2.1",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.8",
|
||||
"@jambonz/realtimedb-helpers": "^0.4.9",
|
||||
"@jambonz/stats-collector": "^0.1.5",
|
||||
"@jambonz/time-series": "^0.1.5",
|
||||
"aws-sdk": "^2.846.0",
|
||||
@@ -36,7 +36,7 @@
|
||||
"cidr-matcher": "^2.1.1",
|
||||
"debug": "^4.3.1",
|
||||
"deepcopy": "^2.1.0",
|
||||
"drachtio-fsmrf": "^2.0.7",
|
||||
"drachtio-fsmrf": "^2.0.11",
|
||||
"drachtio-srf": "^4.4.55",
|
||||
"express": "^4.17.1",
|
||||
"ip": "^1.1.5",
|
||||
|
||||
@@ -587,7 +587,8 @@ DROP TABLE IF EXISTS `speech_credentials`;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `speech_credentials` (
|
||||
`speech_credential_sid` char(36) NOT NULL,
|
||||
`account_sid` char(36) NOT NULL,
|
||||
`service_provider_sid` CHAR(36),
|
||||
`account_sid` char(36) NOT NULL,
|
||||
`vendor` varchar(255) NOT NULL,
|
||||
`credential` VARCHAR(8192) NOT NULL,
|
||||
`use_for_tts` tinyint(1) DEFAULT '1',
|
||||
@@ -611,7 +612,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','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','google','credential-goes-here',1,1,NULL,'2021-04-03 15:42:10',1,1),('84154212-5c99-4c94-8993-bc2a46288daa','bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',0,0,NULL,NULL,NULL,NULL);
|
||||
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),('84154212-5c99-4c94-8993-bc2a46288daa',NULL,'bb845d4b-83a9-4cde-a6e9-50f3743bab3f','aws','credential-goes-here',0,0,NULL,NULL,NULL,NULL);
|
||||
/*!40000 ALTER TABLE `speech_credentials` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
|
||||
@@ -20,12 +20,16 @@ DROP TABLE IF EXISTS lcr_routes;
|
||||
|
||||
DROP TABLE IF EXISTS predefined_sip_gateways;
|
||||
|
||||
DROP TABLE IF EXISTS predefined_smpp_gateways;
|
||||
|
||||
DROP TABLE IF EXISTS predefined_carriers;
|
||||
|
||||
DROP TABLE IF EXISTS account_offers;
|
||||
|
||||
DROP TABLE IF EXISTS products;
|
||||
|
||||
DROP TABLE IF EXISTS schema_version;
|
||||
|
||||
DROP TABLE IF EXISTS api_keys;
|
||||
|
||||
DROP TABLE IF EXISTS sbc_addresses;
|
||||
@@ -148,6 +152,20 @@ predefined_carrier_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (predefined_sip_gateway_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE predefined_smpp_gateways
|
||||
(
|
||||
predefined_smpp_gateway_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. ',
|
||||
port INTEGER NOT NULL DEFAULT 2775 COMMENT 'smpp signaling port',
|
||||
inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound SMS from the gateway',
|
||||
outbound BOOLEAN NOT NULL COMMENT 'i',
|
||||
netmask INTEGER NOT NULL DEFAULT 32,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT 1,
|
||||
use_tls BOOLEAN DEFAULT 0,
|
||||
predefined_carrier_sid CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (predefined_smpp_gateway_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE products
|
||||
(
|
||||
product_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -174,6 +192,11 @@ stripe_product_id VARCHAR(56) NOT NULL,
|
||||
PRIMARY KEY (account_offer_sid)
|
||||
);
|
||||
|
||||
CREATE TABLE schema_version
|
||||
(
|
||||
version VARCHAR(16)
|
||||
);
|
||||
|
||||
CREATE TABLE api_keys
|
||||
(
|
||||
api_key_sid CHAR(36) NOT NULL UNIQUE ,
|
||||
@@ -420,6 +443,10 @@ CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefin
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid);
|
||||
ALTER TABLE predefined_sip_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
|
||||
|
||||
CREATE INDEX predefined_smpp_gateway_sid_idx ON predefined_smpp_gateways (predefined_smpp_gateway_sid);
|
||||
CREATE INDEX predefined_carrier_sid_idx ON predefined_smpp_gateways (predefined_carrier_sid);
|
||||
ALTER TABLE predefined_smpp_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk_1 (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid);
|
||||
|
||||
CREATE INDEX product_sid_idx ON products (product_sid);
|
||||
CREATE INDEX account_product_sid_idx ON account_products (account_product_sid);
|
||||
CREATE INDEX account_subscription_sid_idx ON account_products (account_subscription_sid);
|
||||
@@ -545,4 +572,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);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
Reference in New Issue
Block a user