mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-19 04:17:44 +00:00
wip: implemented listen, transcribe, play
This commit is contained in:
@@ -9,6 +9,7 @@ const makeTask = require('./tasks/make_task');
|
||||
const resourcesMixin = require('./utils/resources');
|
||||
const moment = require('moment');
|
||||
const assert = require('assert');
|
||||
const Dialog = require('drachtio-srf').Dialog;
|
||||
const BADPRECONDITIONS = 'preconditions not met';
|
||||
|
||||
class CallSession extends Emitter {
|
||||
@@ -34,27 +35,34 @@ class CallSession extends Emitter {
|
||||
|
||||
this.hooks = notifiers(this.logger, this.callAttributes);
|
||||
|
||||
req.on('cancel', this._onCallerHangup.bind(this));
|
||||
this.callGone = false;
|
||||
|
||||
req.on('cancel', this._onCallerHangup.bind(this, req));
|
||||
this.on('callStatusChange', this._onCallStatusChange.bind(this));
|
||||
}
|
||||
|
||||
get callSid() { return this.callAttributes.CallSid; }
|
||||
get parentCallSid() { return this.callAttributes.CallSid; }
|
||||
get actionHook() { return this.hooks.actionHook; }
|
||||
get callingNumber() { return this.req.callingNumber; }
|
||||
get calledNumber() { return this.req.calledNumber; }
|
||||
|
||||
async exec() {
|
||||
let idx = 0;
|
||||
while (this._executionStack.length) {
|
||||
const taskList = this.currentTaskList = this._executionStack.shift();
|
||||
this.logger.debug(`CallSession:exec starting task list with ${taskList.tasks.length} tasks`);
|
||||
while (taskList.length) {
|
||||
while (taskList.length && !this.callGone) {
|
||||
const {task, callSid} = taskList.shift();
|
||||
this.logger.debug(`CallSession:exec starting task #${++idx}: ${task.name}`);
|
||||
try {
|
||||
const resources = await this._evaluatePreconditions(task, callSid);
|
||||
this.currentTask = task;
|
||||
await task.exec(this, resources);
|
||||
this.currentTask = null;
|
||||
this.logger.debug(`CallSession:exec completed task #${idx}: ${task.name}`);
|
||||
} catch (err) {
|
||||
this.currentTask = null;
|
||||
if (err.message.includes(BADPRECONDITIONS)) {
|
||||
this.logger.info(`CallSession:exec task #${idx}: ${task.name}: ${err.message}`);
|
||||
}
|
||||
@@ -92,6 +100,7 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
async _evalEndpointPrecondition(task, callSid) {
|
||||
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
const resources = this.calls.get(callSid);
|
||||
if (!resources) throw new Error(`task ${task.name} attempting to operate on unknown CallSid ${callSid}`);
|
||||
if (resources.ep) return resources.ep;
|
||||
@@ -105,6 +114,7 @@ class CallSession extends Emitter {
|
||||
this.addResource('ms', ms);
|
||||
}
|
||||
const ep = await ms.createEndpoint({remoteSdp: this.req.body});
|
||||
ep.cs = this;
|
||||
resources.ep = ep;
|
||||
if (task.earlyMedia && callSid === this.parentCallSid && this.req && !this.req.finalResponseSent) {
|
||||
this.res.send(183, {body: ep.local.sdp});
|
||||
@@ -112,7 +122,10 @@ class CallSession extends Emitter {
|
||||
return ep;
|
||||
}
|
||||
const uas = await this.srf.createUAS(this.req, this.res, {localSdp: ep.local.sdp});
|
||||
uas.on('destroy', this._onCallerHangup.bind(this, uas));
|
||||
uas.callSid = callSid;
|
||||
resources.dlg = uas;
|
||||
this.logger.debug(`CallSession:_evalEndpointPrecondition - call was answered with callSid ${callSid}`);
|
||||
this.calls.set(callSid, resources);
|
||||
return ep;
|
||||
} catch (err) {
|
||||
@@ -122,13 +135,15 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
_evalStableCallPrecondition(task, callSid) {
|
||||
if (this.callGone) throw new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
const resources = this.calls.get(callSid);
|
||||
if (!resources) throw new Error(`task ${task.name} attempting to operate on unknown callSid ${callSid}`);
|
||||
if (resources.dlg) throw new Error(`${BADPRECONDITIONS}: call was not answered - callSid ${callSid}`);
|
||||
if (!resources.dlg) throw new Error(`${BADPRECONDITIONS}: call was not answered - callSid ${callSid}`);
|
||||
return resources.dlg;
|
||||
}
|
||||
|
||||
_evalUnansweredCallPrecondition(task, callSid) {
|
||||
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
|
||||
if (callSid !== this.parentCallSid || !this.req) {
|
||||
throw new Error(`${BADPRECONDITIONS}: no inbound call - callSid ${callSid}`);
|
||||
}
|
||||
@@ -152,6 +167,11 @@ class CallSession extends Emitter {
|
||||
this.calls.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* These below methods are needed mainly by the dial verb, which
|
||||
* deals with a variety of scenarios that can't simply be
|
||||
* described by the precondition concept as other verbs can
|
||||
*/
|
||||
|
||||
/**
|
||||
* retrieve the media server and endpoint for this call, allocate them if needed
|
||||
@@ -208,6 +228,7 @@ class CallSession extends Emitter {
|
||||
return {ep, ms, res: this.res};
|
||||
}
|
||||
const dlg = await this.srf.createUAS(this.req, this.res, {localSdp: ep.local.sdp});
|
||||
this.logger.debug(`CallSession:connectInboundCallToIvr - answered callSid ${this.parentCallSid}`);
|
||||
this.calls.set(this.parentCallSid, {ep, dlg});
|
||||
return {ep, ms, dlg};
|
||||
} catch (err) {
|
||||
@@ -265,10 +286,25 @@ class CallSession extends Emitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* got CANCEL from inbound leg
|
||||
* got CANCEL or BYE from inbound leg
|
||||
*/
|
||||
_onCallerHangup(evt) {
|
||||
this.logger.debug('CallSession: caller hung before connection');
|
||||
_onCallerHangup(obj, evt) {
|
||||
this.callGone = true;
|
||||
if (obj instanceof Dialog) {
|
||||
this.logger.debug('CallSession: caller hung up');
|
||||
/* cant destroy endpoint as current task may need to get final transcription
|
||||
const resources = this.calls.get(obj.callSid);
|
||||
if (resources.ep && resources.ep.connected) {
|
||||
resources.ep.destroy();
|
||||
resources.ep = null;
|
||||
this.calls.set(obj.callSid, resources);
|
||||
}
|
||||
*/
|
||||
}
|
||||
else {
|
||||
this.logger.debug('CallSession: caller hung before answer');
|
||||
}
|
||||
if (this.currentTask) this.currentTask.kill();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ const request = require('request');
|
||||
//require('request-debug')(request);
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const makeTask = require('./tasks/make_task');
|
||||
const normalizeJamones = require('./utils/normalize-jamones');
|
||||
const {CallStatus, CallDirection} = require('./utils/constants');
|
||||
|
||||
module.exports = function(srf, logger) {
|
||||
@@ -68,19 +69,21 @@ module.exports = function(srf, logger) {
|
||||
const call_sid = uuidv4();
|
||||
const method = (app.hook_http_method || 'POST').toUpperCase();
|
||||
const from = req.getParsedHeader('From');
|
||||
const qs = req.locals.callAttributes = {
|
||||
req.locals.callAttributes = {
|
||||
CallSid: call_sid,
|
||||
AccountSid: app.account_sid,
|
||||
From: req.callingNumber,
|
||||
To: req.calledNumber,
|
||||
CallStatus: CallStatus.Trying,
|
||||
SipStatus: 100,
|
||||
Direction: CallDirection.Inbound,
|
||||
CallerName: from.name || req.callingNumber,
|
||||
SipCallID: req.get('Call-ID'),
|
||||
SipCallID: req.get('Call-ID')
|
||||
};
|
||||
const qs = Object.assign({}, req.locals.callAttributes, {
|
||||
CallStatus: CallStatus.Trying,
|
||||
SipStatus: 100,
|
||||
RequestorIP: req.get('X-Forwarded-For'),
|
||||
RequestorName: req.get('X-Originating-Carrier')
|
||||
};
|
||||
});
|
||||
const opts = {
|
||||
url: app.call_hook,
|
||||
method,
|
||||
@@ -88,6 +91,7 @@ module.exports = function(srf, logger) {
|
||||
qs
|
||||
};
|
||||
if (app.hook_basic_auth_user && app.hook_basic_auth_password) {
|
||||
logger.debug(`using basic auth with ${app.hook_basic_auth_user}:${app.hook_basic_auth_password}`);
|
||||
Object.assign(opts, {auth: {user: app.hook_basic_auth_user, password: app.hook_basic_auth_password}});
|
||||
}
|
||||
if (method === 'POST') Object.assign(opts, {body: req.msg});
|
||||
@@ -95,26 +99,16 @@ module.exports = function(srf, logger) {
|
||||
request(opts, (err, response, body) => {
|
||||
if (err) {
|
||||
logger.error(err, `Error invoking callback ${app.call_hook}`);
|
||||
return res.send(603, 'Bad webhook');
|
||||
return res.send(500, 'Webhook Failure');
|
||||
}
|
||||
logger.debug(body, 'application payload');
|
||||
const taskData = Array.isArray(body) ? body : [body];
|
||||
app.tasks = [];
|
||||
for (const t in taskData) {
|
||||
try {
|
||||
const task = makeTask(logger, taskData[t]);
|
||||
app.tasks.push(task);
|
||||
} catch (err) {
|
||||
logger.error({err, data: taskData[t]}, `invalid web callback payload: ${err.message}`);
|
||||
res.send(500, 'Application Error', {
|
||||
headers: {
|
||||
'X-Reason': err.message
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
logger.debug(body, `application payload: ${body}`);
|
||||
try {
|
||||
app.tasks = normalizeJamones(logger, body).map((tdata) => makeTask(logger, tdata));
|
||||
next();
|
||||
} catch (err) {
|
||||
logger.error(err, 'Invalid Webhook Response');
|
||||
res.send(500);
|
||||
}
|
||||
if (!res.finalResponseSent) next();
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err, 'Error invoking web callback');
|
||||
|
||||
@@ -28,10 +28,10 @@ class TaskDial extends Task {
|
||||
this.timeLimit = opts.timeLimit;
|
||||
|
||||
if (opts.listen) {
|
||||
this.listenTask = makeTask(logger, {'listen': opts.transcribe});
|
||||
this.listenTask = makeTask(logger, {'listen': opts.listen});
|
||||
}
|
||||
if (opts.transcribe) {
|
||||
this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe});
|
||||
this.transcribeTask = makeTask(logger, {'transcribe' : opts.transcribe});
|
||||
}
|
||||
|
||||
this.canceled = false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const {TaskName, TaskPreconditions, TranscriptionEvents} = require('../utils/constants');
|
||||
const makeTask = require('./make_task');
|
||||
const assert = require('assert');
|
||||
|
||||
@@ -19,6 +19,7 @@ class TaskGather extends Task {
|
||||
this.timeout = (this.timeout || 5) * 1000;
|
||||
this.language = this.language || 'en-US';
|
||||
this.digitBuffer = '';
|
||||
//this._earlyMedia = this.data.earlyMedia === true;
|
||||
|
||||
if (this.say) {
|
||||
this.sayTask = makeTask(this.logger, {say: this.say});
|
||||
@@ -27,6 +28,11 @@ class TaskGather extends Task {
|
||||
|
||||
get name() { return TaskName.Gather; }
|
||||
|
||||
get earlyMedia() {
|
||||
return (this.sayTask && this.sayTask.earlyMedia) ||
|
||||
(this.playTask && this.playTask.earlyMedia);
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
this.ep = ep;
|
||||
this.actionHook = cs.actionHook;
|
||||
@@ -35,33 +41,15 @@ class TaskGather extends Task {
|
||||
try {
|
||||
if (this.sayTask) {
|
||||
this.sayTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.sayTask.on('playDone', this._onPlayDone.bind(this, ep));
|
||||
this.sayTask.on('playDone', (err) => {
|
||||
if (this.taskInProgress) this._startTimer();
|
||||
});
|
||||
}
|
||||
else this._startTimer();
|
||||
|
||||
if (this.input.includes('speech')) {
|
||||
const opts = {
|
||||
GOOGLE_SPEECH_USE_ENHANCED: true,
|
||||
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
|
||||
GOOGLE_SPEECH_MODEL: 'phone_call'
|
||||
};
|
||||
if (this.hints) {
|
||||
Object.assign(opts, {'GOOGLE_SPEECH_HINTS': this.hints.join(',')});
|
||||
}
|
||||
if (this.profanityFilter === true) {
|
||||
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
|
||||
}
|
||||
this.logger.debug(`setting freeswitch vars ${JSON.stringify(opts)}`);
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error set'));
|
||||
ep.addCustomEventListener('google_transcribe::transcription', this._onTranscription.bind(this, ep));
|
||||
ep.addCustomEventListener('google_transcribe::no_audio_detected', this._onNoAudioDetected.bind(this, ep));
|
||||
ep.addCustomEventListener('google_transcribe::max_duration_exceeded', this._onMaxDuration.bind(this, ep));
|
||||
this.logger.debug('starting transcription');
|
||||
ep.startTranscription({
|
||||
interim: this.partialResultCallback ? true : false,
|
||||
language: this.language
|
||||
}).catch((err) => this.logger.error(err, 'TaskGather:exec error starting transcription'));
|
||||
await this._initSpeech(ep);
|
||||
this._startTranscribing(ep);
|
||||
}
|
||||
|
||||
if (this.input.includes('dtmf')) {
|
||||
@@ -73,10 +61,12 @@ class TaskGather extends Task {
|
||||
this.logger.error(err, 'TaskGather:exec error');
|
||||
}
|
||||
this.taskInProgress = false;
|
||||
ep.removeAllListeners();
|
||||
ep.removeCustomEventListener(TranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(TranscriptionEvents.EndOfUtterance);
|
||||
}
|
||||
|
||||
kill() {
|
||||
super.kill();
|
||||
this._killAudio();
|
||||
this._resolve('killed');
|
||||
}
|
||||
@@ -85,12 +75,6 @@ class TaskGather extends Task {
|
||||
return new Promise((resolve) => this.resolver = resolve);
|
||||
}
|
||||
|
||||
_onPlayDone(ep, err, evt) {
|
||||
if (err || !this.taskInProgress) return;
|
||||
this.logger.debug(evt, 'TaskGather:_onPlayDone, starting input timer');
|
||||
this._startTimer();
|
||||
}
|
||||
|
||||
_onDtmf(ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
||||
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
|
||||
@@ -101,6 +85,32 @@ class TaskGather extends Task {
|
||||
this._killAudio();
|
||||
}
|
||||
|
||||
async _initSpeech(ep) {
|
||||
const opts = {
|
||||
GOOGLE_SPEECH_USE_ENHANCED: true,
|
||||
GOOGLE_SPEECH_SINGLE_UTTERANCE: true,
|
||||
GOOGLE_SPEECH_MODEL: 'phone_call'
|
||||
};
|
||||
if (this.hints) {
|
||||
Object.assign(opts, {'GOOGLE_SPEECH_HINTS': this.hints.join(',')});
|
||||
}
|
||||
if (this.profanityFilter === true) {
|
||||
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
|
||||
}
|
||||
this.logger.debug(`setting freeswitch vars ${JSON.stringify(opts)}`);
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'Error set'));
|
||||
ep.addCustomEventListener(TranscriptionEvents.Transcription, this._onTranscription.bind(this, ep));
|
||||
ep.addCustomEventListener(TranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, ep));
|
||||
}
|
||||
|
||||
_startTranscribing(ep) {
|
||||
ep.startTranscription({
|
||||
interim: this.partialResultCallback ? true : false,
|
||||
language: this.language
|
||||
}).catch((err) => this.logger.error(err, 'TaskGather:_startTranscribing error'));
|
||||
}
|
||||
|
||||
_startTimer() {
|
||||
assert(!this._timeoutTimer);
|
||||
this._timeoutTimer = setTimeout(() => this._resolve('timeout'), this.timeout);
|
||||
@@ -123,6 +133,8 @@ class TaskGather extends Task {
|
||||
_onTranscription(ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||
if (evt.is_final) {
|
||||
ep.removeCustomEventListener(TranscriptionEvents.Transcription);
|
||||
ep.removeCustomEventListener(TranscriptionEvents.EndOfUtterance);
|
||||
this._resolve('speech', evt);
|
||||
}
|
||||
else if (this.partialResultCallback) {
|
||||
@@ -131,11 +143,9 @@ class TaskGather extends Task {
|
||||
});
|
||||
}
|
||||
}
|
||||
_onNoAudioDetected(ep, evt) {
|
||||
this.logger.info(evt, 'TaskGather:_onNoAudioDetected');
|
||||
}
|
||||
_onMaxDuration(ep, evt) {
|
||||
this.logger.info(evt, 'TaskGather:_onMaxDuration');
|
||||
_onEndOfUtterance(ep, evt) {
|
||||
this.logger.info(evt, 'TaskGather:_onEndOfUtterance');
|
||||
this._startTranscribing(ep);
|
||||
}
|
||||
|
||||
_resolve(reason, evt) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName} = require('../utils/constants');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
|
||||
class TaskHangup extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.headers = this.data.headers || {};
|
||||
|
||||
this.preconditions = TaskPreconditions.StableCall;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Hangup; }
|
||||
@@ -16,7 +18,7 @@ class TaskHangup extends Task {
|
||||
try {
|
||||
await dlg.destroy({headers: this.headers});
|
||||
} catch (err) {
|
||||
this.logger.error(err, `TaskHangup:exec - Error hanging up call with sip call id ${dlg.sip.callId}`);
|
||||
this.logger.error(err, 'TaskHangup:exec - Error hanging up call');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
131
lib/tasks/listen.js
Normal file
131
lib/tasks/listen.js
Normal file
@@ -0,0 +1,131 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions, ListenEvents} = require('../utils/constants');
|
||||
const makeTask = require('./make_task');
|
||||
const assert = require('assert');
|
||||
|
||||
class TaskListen extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
'url', 'finishOnKey', 'maxLength', 'metadata', 'mixType', 'passDtmf', 'playBeep',
|
||||
'sampleRate', 'timeout', 'transcribe'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
|
||||
this.mixType = this.mixType || 'mono';
|
||||
this.sampleRate = this.sampleRate || 8000;
|
||||
this.earlyMedia = this.data.earlyMedia === true;
|
||||
|
||||
if (this.transcribe) {
|
||||
this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe});
|
||||
}
|
||||
|
||||
this._dtmfHandler = this._onDtmf.bind(this);
|
||||
|
||||
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
||||
}
|
||||
|
||||
get name() { return TaskName.Listen; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (this.playBeep) await this._playBeep(ep);
|
||||
if (this.transcribeTask) {
|
||||
this.logger.debug('TaskListen:exec - starting nested transcribe task');
|
||||
this.transcribeTask.exec(cs, ep, this); // kicked off, _not_ waiting for it to complete
|
||||
}
|
||||
await this._startListening(ep);
|
||||
await this._completionPromise;
|
||||
} catch (err) {
|
||||
this.logger.info(err, `TaskListen:exec - error ${this.url}`);
|
||||
}
|
||||
if (this.transcribeTask) this.transcribeTask.kill();
|
||||
this._removeListeners(ep);
|
||||
this.listenComplete = true;
|
||||
this.emit('listenDone');
|
||||
}
|
||||
|
||||
async kill() {
|
||||
super.kill();
|
||||
this._clearTimer();
|
||||
if (this.ep.connected && !this.listenComplete) {
|
||||
this.listenComplete = true;
|
||||
if (this.transcribeTask) {
|
||||
await this.transcribeTask.kill();
|
||||
this.transcribeTask = null;
|
||||
}
|
||||
await this.ep.forkAudioStop()
|
||||
.catch((err) => this.logger.info(err, 'TaskListen:kill'));
|
||||
}
|
||||
this._completionResolver();
|
||||
}
|
||||
|
||||
async _playBeep(ep) {
|
||||
await ep.play('tone_stream://L=1;%(500, 0, 1500)')
|
||||
.catch((err) => this.logger.info(err, 'TaskListen:_playBeep Error playing beep'));
|
||||
}
|
||||
|
||||
async _startListening(ep) {
|
||||
this._initListeners(ep);
|
||||
await ep.forkAudioStart({
|
||||
wsUrl: this.url,
|
||||
mixType: this.mixType,
|
||||
sampling: this.sampleRate,
|
||||
metadata: this.metadata
|
||||
});
|
||||
if (this.timeout) {
|
||||
this._timer = setTimeout(() => {
|
||||
this.logger.debug(`TaskListen:_startListening terminating task due to timeout of ${this.timeout} reached`);
|
||||
this.kill();
|
||||
}, this.timeout * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
_initListeners(ep) {
|
||||
ep.addCustomEventListener(ListenEvents.Connect, this._onConnect.bind(this, ep));
|
||||
ep.addCustomEventListener(ListenEvents.ConnectFailure, this._onConnectFailure.bind(this, ep));
|
||||
ep.addCustomEventListener(ListenEvents.Error, this._onError.bind(this, ep));
|
||||
if (this.finishOnKey || this.passDtmf) {
|
||||
ep.on('dtmf', this._dtmfHandler);
|
||||
}
|
||||
}
|
||||
|
||||
_removeListeners(ep) {
|
||||
ep.removeCustomEventListener(ListenEvents.Connect);
|
||||
ep.removeCustomEventListener(ListenEvents.ConnectFailure);
|
||||
ep.removeCustomEventListener(ListenEvents.Error);
|
||||
if (this.finishOnKey || this.passDtmf) {
|
||||
ep.removeListener('dtmf', this._dtmfHandler);
|
||||
}
|
||||
}
|
||||
|
||||
_onDtmf(evt) {
|
||||
if (evt.dtmf === this.finishOnKey) {
|
||||
this.logger.info(`TaskListen:_onDtmf terminating task due to dtmf ${evt.dtmf}`);
|
||||
this.kill();
|
||||
}
|
||||
}
|
||||
|
||||
_clearTimer() {
|
||||
if (this._timer) {
|
||||
clearTimeout(this._timer);
|
||||
this._timer = null;
|
||||
}
|
||||
}
|
||||
_onConnect(ep) {
|
||||
this.logger.debug('TaskListen:_onConnect');
|
||||
}
|
||||
_onConnectFailure(ep, evt) {
|
||||
this.logger.info(evt, 'TaskListen:_onConnectFailure');
|
||||
this._completionResolver();
|
||||
}
|
||||
_onError(ep, evt) {
|
||||
this.logger.info(evt, 'TaskListen:_onError');
|
||||
this._completionResolver();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = TaskListen;
|
||||
@@ -2,13 +2,15 @@ const Task = require('./task');
|
||||
const {TaskName} = require('../utils/constants');
|
||||
const errBadInstruction = new Error('invalid instruction payload');
|
||||
|
||||
function makeTask(logger, opts) {
|
||||
logger.debug(opts, 'makeTask');
|
||||
if (typeof opts !== 'object' || Array.isArray(opts)) throw errBadInstruction;
|
||||
const keys = Object.keys(opts);
|
||||
if (keys.length !== 1) throw errBadInstruction;
|
||||
function makeTask(logger, obj) {
|
||||
const keys = Object.keys(obj);
|
||||
if (!keys || keys.length !== 1) {
|
||||
throw errBadInstruction;
|
||||
}
|
||||
const name = keys[0];
|
||||
const data = opts[name];
|
||||
const data = obj[name];
|
||||
logger.debug(data, `makeTask: ${name}`);
|
||||
if (typeof data !== 'object') throw errBadInstruction;
|
||||
Task.validate(name, data);
|
||||
switch (name) {
|
||||
case TaskName.SipDecline:
|
||||
@@ -26,6 +28,12 @@ function makeTask(logger, opts) {
|
||||
case TaskName.Gather:
|
||||
const TaskGather = require('./gather');
|
||||
return new TaskGather(logger, data);
|
||||
case TaskName.Transcribe:
|
||||
const TaskTranscribe = require('./transcribe');
|
||||
return new TaskTranscribe(logger, data);
|
||||
case TaskName.Listen:
|
||||
const TaskListen = require('./listen');
|
||||
return new TaskListen(logger, data);
|
||||
}
|
||||
|
||||
// should never reach
|
||||
|
||||
40
lib/tasks/play.js
Normal file
40
lib/tasks/play.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
|
||||
class TaskPlay extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.url = this.data.url;
|
||||
this.loop = this.data.loop || 1;
|
||||
this.earlyMedia = this.data.earlyMedia === true;
|
||||
this.playComplete = false;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Play; }
|
||||
|
||||
async exec(cs, ep) {
|
||||
this.ep = ep;
|
||||
try {
|
||||
while (!this.playComplete && this.loop--) {
|
||||
await ep.play(this.url);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info(err, `TaskPlay:exec - error playing ${this.url}`);
|
||||
}
|
||||
this.playComplete = true;
|
||||
this.emit('playDone');
|
||||
}
|
||||
|
||||
kill() {
|
||||
super.kill();
|
||||
if (this.ep.connected && !this.playComplete) {
|
||||
this.logger.debug('TaskPlay:kill - killing audio');
|
||||
this.playComplete = true;
|
||||
this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskPlay;
|
||||
@@ -31,7 +31,7 @@ class TaskSay extends Task {
|
||||
text: this.text
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.message !== 'hangup') this.logger.info(err, 'TaskSay:exec error');
|
||||
this.logger.info(err, 'TaskSay:exec error');
|
||||
}
|
||||
this.emit('playDone');
|
||||
this.sayComplete = true;
|
||||
|
||||
@@ -16,11 +16,22 @@
|
||||
"required": [
|
||||
]
|
||||
},
|
||||
"play": {
|
||||
"properties": {
|
||||
"url": "string",
|
||||
"loop": "number",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"say": {
|
||||
"properties": {
|
||||
"text": "string",
|
||||
"loop": "number",
|
||||
"synthesizer": "#synthesizer"
|
||||
"synthesizer": "#synthesizer",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"text",
|
||||
@@ -31,14 +42,12 @@
|
||||
"properties": {
|
||||
"action": "string",
|
||||
"finishOnKey": "string",
|
||||
"hints": "array",
|
||||
"input": "array",
|
||||
"language": "string",
|
||||
"numDigits": "number",
|
||||
"partialResultCallback": "string",
|
||||
"profanityFilter": "boolean",
|
||||
"speechTimeout": "number",
|
||||
"timeout": "number",
|
||||
"recognizer": "#recognizer",
|
||||
"say": "#say"
|
||||
},
|
||||
"required": [
|
||||
@@ -73,37 +82,34 @@
|
||||
},
|
||||
"listen": {
|
||||
"properties": {
|
||||
"finishOnKey": "string",
|
||||
"maxLength": "number",
|
||||
"metadata": "object",
|
||||
"mixType": {
|
||||
"type": "string",
|
||||
"enum": ["mono", "stereo", "mixed"]
|
||||
},
|
||||
"passDtmf": "boolean",
|
||||
"playBeep": "boolean",
|
||||
"sampleRate": "number",
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["parent", "child", "both"]
|
||||
},
|
||||
"wsUrl": "string"
|
||||
"timeout": "number",
|
||||
"transcribe": "#transcribe",
|
||||
"url": "string",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"wsUrl",
|
||||
"sampleRate"
|
||||
"url"
|
||||
]
|
||||
},
|
||||
"transcribe": {
|
||||
"properties": {
|
||||
"action": "string",
|
||||
"interim": "boolean",
|
||||
"jsonKey": "string",
|
||||
"language": "string",
|
||||
"source": "string",
|
||||
"vendor": "string"
|
||||
"recognizer": "#recognizer",
|
||||
"earlyMedia": "boolean"
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"jsonKey",
|
||||
"language"
|
||||
"recognizer"
|
||||
]
|
||||
},
|
||||
"target": {
|
||||
@@ -143,6 +149,28 @@
|
||||
"enum": ["google"]
|
||||
},
|
||||
"voice": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
]
|
||||
},
|
||||
"recognizer": {
|
||||
"properties": {
|
||||
"vendor": {
|
||||
"type": "string",
|
||||
"enum": ["google"]
|
||||
},
|
||||
"language": "string",
|
||||
"hints": "array",
|
||||
"profanityFilter": "boolean",
|
||||
"interim": "boolean",
|
||||
"mixType": {
|
||||
"type": "string",
|
||||
"enum": ["mono", "stereo", "mixed"]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"vendor"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ class Task extends Emitter {
|
||||
this.preconditions = TaskPreconditions.None;
|
||||
this.logger = logger;
|
||||
this.data = data;
|
||||
|
||||
this._killInProgress = false;
|
||||
}
|
||||
|
||||
get killed() {
|
||||
return this._killInProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,6 +26,8 @@ class Task extends Emitter {
|
||||
* what to do is up to each type of task
|
||||
*/
|
||||
kill() {
|
||||
this.logger.debug(`${this.name} is being killed`);
|
||||
this._killInProgress = true;
|
||||
// no-op
|
||||
}
|
||||
|
||||
|
||||
106
lib/tasks/transcribe.js
Normal file
106
lib/tasks/transcribe.js
Normal file
@@ -0,0 +1,106 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions, TranscriptionEvents} = require('../utils/constants');
|
||||
const assert = require('assert');
|
||||
|
||||
class TaskTranscribe extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.action = this.data.action;
|
||||
this.language = this.data.language || 'en-US';
|
||||
this.vendor = this.data.vendor;
|
||||
this.interim = this.data.interim === true;
|
||||
this.mixType = this.data.mixType;
|
||||
this.earlyMedia = this.data.earlyMedia === true;
|
||||
|
||||
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
|
||||
}
|
||||
|
||||
get name() { return TaskName.Transcribe; }
|
||||
|
||||
async exec(cs, ep, parentTask) {
|
||||
this.ep = ep;
|
||||
this.actionHook = ep.cs.actionHook;
|
||||
this.transcribeInProgress = true;
|
||||
try {
|
||||
await this._initSpeech(ep);
|
||||
await this._startTranscribing(ep);
|
||||
await this._completionPromise;
|
||||
} catch (err) {
|
||||
this.logger.info(err, 'TaskTranscribe:exec - error');
|
||||
}
|
||||
this.transcribeInProgress = true;
|
||||
ep.removeCustomEventListener(TranscriptionEvents.Transcription);
|
||||
}
|
||||
|
||||
async kill() {
|
||||
super.kill();
|
||||
if (this.ep.connected && this.transcribeInProgress) {
|
||||
this.ep.stopTranscription().catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
|
||||
|
||||
// hangup after 1 sec if we don't get a final transcription
|
||||
this._timer = setTimeout(() => this._completionResolver(), 1000);
|
||||
}
|
||||
else {
|
||||
this._completionResolver();
|
||||
}
|
||||
await this._completionPromise;
|
||||
}
|
||||
|
||||
async _initSpeech(ep) {
|
||||
const opts = {
|
||||
GOOGLE_SPEECH_USE_ENHANCED: true,
|
||||
GOOGLE_SPEECH_MODEL: 'phone_call'
|
||||
};
|
||||
if (this.hints) {
|
||||
Object.assign(opts, {'GOOGLE_SPEECH_HINTS': this.hints.join(',')});
|
||||
}
|
||||
if (this.profanityFilter === true) {
|
||||
Object.assign(opts, {'GOOGLE_SPEECH_PROFANITY_FILTER': true});
|
||||
}
|
||||
await ep.set(opts)
|
||||
.catch((err) => this.logger.info(err, 'TaskTranscribe:_initSpeech error setting fs vars'));
|
||||
ep.addCustomEventListener(TranscriptionEvents.Transcription, this._onTranscription.bind(this, ep));
|
||||
ep.addCustomEventListener(TranscriptionEvents.NoAudioDetected, this._onNoAudio.bind(this, ep));
|
||||
ep.addCustomEventListener(TranscriptionEvents.MaxDurationExceeded, this._onMaxDurationExceeded.bind(this, ep));
|
||||
}
|
||||
|
||||
async _startTranscribing(ep) {
|
||||
await ep.startTranscription({
|
||||
interim: this.interim ? true : false,
|
||||
language: this.language
|
||||
});
|
||||
}
|
||||
|
||||
_onTranscription(ep, evt) {
|
||||
this.logger.debug(evt, 'TaskTranscribe:_onTranscription');
|
||||
this.actionHook(this.action, 'POST', {
|
||||
Speech: evt
|
||||
});
|
||||
if (this.killed) {
|
||||
this.logger.debug('TaskTranscribe:_onTranscription exiting after receiving final transcription');
|
||||
this._clearTimer();
|
||||
this._completionResolver();
|
||||
}
|
||||
}
|
||||
|
||||
_onNoAudio(ep) {
|
||||
this.logger.debug('TaskTranscribe:_onNoAudio restarting transcription');
|
||||
this._startTranscribing(ep);
|
||||
}
|
||||
|
||||
_onMaxDurationExceeded(ep) {
|
||||
this.logger.debug('TaskTranscribe:_onMaxDurationExceeded restarting transcription');
|
||||
this._startTranscribing(ep);
|
||||
}
|
||||
|
||||
_clearTimer() {
|
||||
if (this._timer) {
|
||||
clearTimeout(this._timer);
|
||||
this._timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaskTranscribe;
|
||||
@@ -32,5 +32,23 @@
|
||||
"Endpoint": "endpoint",
|
||||
"StableCall": "stable-call",
|
||||
"UnansweredCall": "unanswered-call"
|
||||
},
|
||||
"TranscriptionEvents": {
|
||||
"Transcription": "google_transcribe::transcription",
|
||||
"EndOfUtterance": "google_transcribe::end_of_utterance",
|
||||
"NoAudioDetected": "google_transcribe::no_audio_detected",
|
||||
"MaxDurationExceeded": "google_transcribe::max_duration_exceeded"
|
||||
},
|
||||
"ListenEvents": {
|
||||
"Connect": "mod_audio_fork::connect",
|
||||
"ConnectFailure": "mod_audio_fork::connect_failed",
|
||||
"Transcription": "mod_audio_fork::transcription",
|
||||
"Transfer": "mod_audio_fork::transcription",
|
||||
"PlayAudio": "mod_audio_fork::play_audio",
|
||||
"KillAudio": "mod_audio_fork::kill_audio",
|
||||
"Disconnect": "mod_audio_fork::disconnect",
|
||||
"Error": "mod_audio_fork::error",
|
||||
"BufferOverrun": "mod_audio_fork::buffer_overrun",
|
||||
"JsonMessage": "mod_audio_fork::json"
|
||||
}
|
||||
}
|
||||
|
||||
33
lib/utils/normalize-jamones.js
Normal file
33
lib/utils/normalize-jamones.js
Normal file
@@ -0,0 +1,33 @@
|
||||
function normalizeJambones(logger, obj) {
|
||||
logger.debug(`normalizeJambones: ${JSON.stringify(obj)}`);
|
||||
if (!Array.isArray(obj)) throw new Error('invalid JSON: jambones docs must be array');
|
||||
const document = [];
|
||||
for (const tdata of obj) {
|
||||
if (typeof tdata !== 'object') throw new Error('invalid JSON: jambones docs must be array of objects');
|
||||
if (Object.keys(tdata).length === 1) {
|
||||
// {'say': {..}}
|
||||
logger.debug(`pushing ${JSON.stringify(tdata)}`);
|
||||
document.push(tdata);
|
||||
}
|
||||
else if ('verb' in tdata) {
|
||||
// {verb: 'say', text: 'foo..bar'..}
|
||||
const name = tdata.verb;
|
||||
const o = {};
|
||||
Object.keys(tdata)
|
||||
.filter((k) => k !== 'verb')
|
||||
.forEach((k) => o[k] = tdata[k]);
|
||||
const o2 = {};
|
||||
o2[name] = o;
|
||||
document.push(o2);
|
||||
}
|
||||
else {
|
||||
logger.info(tdata, `invalid JSON: invalid verb form, numkeys ${Object.keys(tdata).length}`);
|
||||
throw new Error('invalid JSON: invalid verb form');
|
||||
}
|
||||
}
|
||||
logger.debug(`returning document with ${document.length} tasks`);
|
||||
return document;
|
||||
}
|
||||
|
||||
module.exports = normalizeJambones;
|
||||
|
||||
@@ -11,8 +11,8 @@ function hooks(logger, callAttributes) {
|
||||
url,
|
||||
method,
|
||||
json: true,
|
||||
qs: 'GET' === method ? params : null,
|
||||
body: 'POST' === method ? params : null
|
||||
qs: 'GET' === method ? params : callAttributes,
|
||||
body: 'POST' === method ? opts : null
|
||||
};
|
||||
logger.debug(`${method} ${url} sending ${JSON.stringify(obj)}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
55
lib/utils/place-outdial.js
Normal file
55
lib/utils/place-outdial.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const Emitter = require('events');
|
||||
const {CallStatus} = require('./constants');
|
||||
|
||||
class SingleDialer extends Emitter {
|
||||
constructor(logger, opts) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
this.cs = opts.cs;
|
||||
this.ms = opts.ms;
|
||||
}
|
||||
|
||||
get callState() {
|
||||
return this._callState;
|
||||
}
|
||||
|
||||
/**
|
||||
* launch the outdial
|
||||
*/
|
||||
exec() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* kill the call in progress, or stable dialog, whichever
|
||||
*/
|
||||
async kill() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* execute a jambones application on this call / endpoint
|
||||
* @param {*} jambones document
|
||||
*/
|
||||
async runApp(document) {
|
||||
|
||||
}
|
||||
|
||||
async _createEndpoint() {
|
||||
|
||||
}
|
||||
|
||||
async _outdial() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function placeOutdial(logger, opts) {
|
||||
const singleDialer = new SingleDialer(logger, opts);
|
||||
singleDialer.exec();
|
||||
return singleDialer;
|
||||
}
|
||||
|
||||
module.exports = placeOutdial;
|
||||
|
||||
73
package-lock.json
generated
73
package-lock.json
generated
@@ -706,6 +706,50 @@
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fn-b2b-sugar/-/drachtio-fn-b2b-sugar-0.0.12.tgz",
|
||||
"integrity": "sha512-FKPAcEMJTYKDrd9DJUCc4VHnY/c65HOO9k8XqVNognF9T02hKEjGuBCM4Da9ipyfiHmVRuECwj0XNvZ361mkVQ=="
|
||||
},
|
||||
"drachtio-fsmrf": {
|
||||
"version": "1.5.11",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-fsmrf/-/drachtio-fsmrf-1.5.11.tgz",
|
||||
"integrity": "sha512-4DQ5N0jCQIHYVn06zPw0lH4sq+nCP11NiC0Y680eC2biFeK2UREFzqJg+/tFKeK4yuj89sshMcVx+0cG+x1E8Q==",
|
||||
"requires": {
|
||||
"async": "^1.4.2",
|
||||
"debug": "^2.2.0",
|
||||
"delegates": "^0.1.0",
|
||||
"drachtio-modesl": "^1.2.0",
|
||||
"drachtio-srf": "^4.4.15",
|
||||
"lodash": "^4.17.15",
|
||||
"minimist": "^1.2.0",
|
||||
"moment": "^2.13.0",
|
||||
"only": "0.0.2",
|
||||
"sdp-transform": "^2.7.0",
|
||||
"uuid": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
}
|
||||
}
|
||||
},
|
||||
"drachtio-modesl": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-modesl/-/drachtio-modesl-1.2.0.tgz",
|
||||
"integrity": "sha512-nkua3RfYnT32OvglERO2zWzJZAfQooZIarZVVAye+iGqTwYJ69X7bU7du5SBHz/jBl+LgeWITMP2fMe2TelxmA==",
|
||||
"requires": {
|
||||
"debug": "^4.1.1",
|
||||
"eventemitter2": "^4.1",
|
||||
"uuid": "^3.1.0",
|
||||
"xml2js": "^0.4.19"
|
||||
}
|
||||
},
|
||||
"drachtio-mw-registration-parser": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/drachtio-mw-registration-parser/-/drachtio-mw-registration-parser-0.0.2.tgz",
|
||||
@@ -959,6 +1003,11 @@
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"dev": true
|
||||
},
|
||||
"eventemitter2": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-4.1.2.tgz",
|
||||
"integrity": "sha1-DhqEd6+CGm7zmVsxG/dMI6UkfxU="
|
||||
},
|
||||
"events-to-array": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz",
|
||||
@@ -2762,6 +2811,16 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"sdp-transform": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.13.0.tgz",
|
||||
"integrity": "sha512-3zT7pcjR090E0WCV9eOtFX06iojoNKsyMXqXs7clOs8sy+RoegR0cebmCuCrTKdY2jw1XhT9jkraygJrqAUwzA=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
@@ -4887,6 +4946,20 @@
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
"config": "^3.2.4",
|
||||
"debug": "^4.1.1",
|
||||
"drachtio-fn-b2b-sugar": "0.0.12",
|
||||
"drachtio-fsmrf": "1.5.10",
|
||||
"drachtio-fsmrf": "^1.5.11",
|
||||
"drachtio-srf": "^4.4.27",
|
||||
"jambonz-db-helpers": "^0.1.6",
|
||||
"jambonz-db-helpers": "^0.1.7",
|
||||
"moment": "^2.24.0",
|
||||
"pino": "^5.14.0",
|
||||
"request": "^2.88.0",
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"sip:decline": {
|
||||
"status": "hello"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
test/data/good/alternate-syntax.json
Normal file
31
test/data/good/alternate-syntax.json
Normal file
@@ -0,0 +1,31 @@
|
||||
[
|
||||
{
|
||||
"verb": "gather",
|
||||
"action": "https://00dd977a.ngrok.io/gather",
|
||||
"input": ["speech"],
|
||||
"timeout": 12,
|
||||
"recognizer": {
|
||||
"vendor": "google",
|
||||
"language": "en-US",
|
||||
"hints": ["sales", "support", "engineering", "human resources", "HR", "operator", "agent"]
|
||||
},
|
||||
"say": {
|
||||
"text": "Please say the name of the department that you would like to speak with. To speak to an operator, just say operator.",
|
||||
"synthesizer": {
|
||||
"vendor": "google",
|
||||
"voice": "en-US-Wavenet-C"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"verb": "say",
|
||||
"text": "I'm sorry, I did not hear a response. Goodbye.",
|
||||
"synthesizer": {
|
||||
"vendor": "google",
|
||||
"voice": "en-US-Wavenet-C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"verb": "hangup"
|
||||
}
|
||||
]
|
||||
@@ -9,11 +9,10 @@
|
||||
}
|
||||
],
|
||||
"listen": {
|
||||
"wsUrl": "wss://myrecorder.example.com:4433",
|
||||
"url": "wss://myrecorder.example.com:4433",
|
||||
"mixType" : "stereo",
|
||||
"sampleRate": 8000,
|
||||
"passDtmf": true,
|
||||
"source": "parent",
|
||||
"metadata": {
|
||||
"clientId": "12udih"
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
],
|
||||
"transcribe": {
|
||||
"action": "http://example.com/transcribe",
|
||||
"language" : "en-US",
|
||||
"source" : "both",
|
||||
"interim": true,
|
||||
"vendor": "google",
|
||||
"jsonKey": "--json service key--"
|
||||
"recognizer": {
|
||||
"vendor": "google",
|
||||
"language" : "en-US",
|
||||
"mixType" : "stereo",
|
||||
"interim": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,13 @@ test('app payload parsing tests', (t) => {
|
||||
task = makeTask(logger, require('./data/good/dial-listen'));
|
||||
t.ok(task.name === 'dial', 'parsed dial w/ listen');
|
||||
|
||||
const alt = require('./data/good/alternate-syntax');
|
||||
const normalize = require('../lib/utils/normalize-jamones');
|
||||
normalize(logger, alt).forEach((t) => {
|
||||
const task = makeTask(logger, t);
|
||||
});
|
||||
t.pass('alternate syntax works');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user