mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2025-12-20 08:40:38 +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 resourcesMixin = require('./utils/resources');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const Dialog = require('drachtio-srf').Dialog;
|
||||||
const BADPRECONDITIONS = 'preconditions not met';
|
const BADPRECONDITIONS = 'preconditions not met';
|
||||||
|
|
||||||
class CallSession extends Emitter {
|
class CallSession extends Emitter {
|
||||||
@@ -34,27 +35,34 @@ class CallSession extends Emitter {
|
|||||||
|
|
||||||
this.hooks = notifiers(this.logger, this.callAttributes);
|
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));
|
this.on('callStatusChange', this._onCallStatusChange.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
get callSid() { return this.callAttributes.CallSid; }
|
get callSid() { return this.callAttributes.CallSid; }
|
||||||
get parentCallSid() { return this.callAttributes.CallSid; }
|
get parentCallSid() { return this.callAttributes.CallSid; }
|
||||||
get actionHook() { return this.hooks.actionHook; }
|
get actionHook() { return this.hooks.actionHook; }
|
||||||
|
get callingNumber() { return this.req.callingNumber; }
|
||||||
|
get calledNumber() { return this.req.calledNumber; }
|
||||||
|
|
||||||
async exec() {
|
async exec() {
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
while (this._executionStack.length) {
|
while (this._executionStack.length) {
|
||||||
const taskList = this.currentTaskList = this._executionStack.shift();
|
const taskList = this.currentTaskList = this._executionStack.shift();
|
||||||
this.logger.debug(`CallSession:exec starting task list with ${taskList.tasks.length} tasks`);
|
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();
|
const {task, callSid} = taskList.shift();
|
||||||
this.logger.debug(`CallSession:exec starting task #${++idx}: ${task.name}`);
|
this.logger.debug(`CallSession:exec starting task #${++idx}: ${task.name}`);
|
||||||
try {
|
try {
|
||||||
const resources = await this._evaluatePreconditions(task, callSid);
|
const resources = await this._evaluatePreconditions(task, callSid);
|
||||||
|
this.currentTask = task;
|
||||||
await task.exec(this, resources);
|
await task.exec(this, resources);
|
||||||
|
this.currentTask = null;
|
||||||
this.logger.debug(`CallSession:exec completed task #${idx}: ${task.name}`);
|
this.logger.debug(`CallSession:exec completed task #${idx}: ${task.name}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
this.currentTask = null;
|
||||||
if (err.message.includes(BADPRECONDITIONS)) {
|
if (err.message.includes(BADPRECONDITIONS)) {
|
||||||
this.logger.info(`CallSession:exec task #${idx}: ${task.name}: ${err.message}`);
|
this.logger.info(`CallSession:exec task #${idx}: ${task.name}: ${err.message}`);
|
||||||
}
|
}
|
||||||
@@ -92,6 +100,7 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _evalEndpointPrecondition(task, callSid) {
|
async _evalEndpointPrecondition(task, callSid) {
|
||||||
|
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
|
||||||
const resources = this.calls.get(callSid);
|
const resources = this.calls.get(callSid);
|
||||||
if (!resources) throw new Error(`task ${task.name} attempting to operate on unknown CallSid ${callSid}`);
|
if (!resources) throw new Error(`task ${task.name} attempting to operate on unknown CallSid ${callSid}`);
|
||||||
if (resources.ep) return resources.ep;
|
if (resources.ep) return resources.ep;
|
||||||
@@ -105,6 +114,7 @@ class CallSession extends Emitter {
|
|||||||
this.addResource('ms', ms);
|
this.addResource('ms', ms);
|
||||||
}
|
}
|
||||||
const ep = await ms.createEndpoint({remoteSdp: this.req.body});
|
const ep = await ms.createEndpoint({remoteSdp: this.req.body});
|
||||||
|
ep.cs = this;
|
||||||
resources.ep = ep;
|
resources.ep = ep;
|
||||||
if (task.earlyMedia && callSid === this.parentCallSid && this.req && !this.req.finalResponseSent) {
|
if (task.earlyMedia && callSid === this.parentCallSid && this.req && !this.req.finalResponseSent) {
|
||||||
this.res.send(183, {body: ep.local.sdp});
|
this.res.send(183, {body: ep.local.sdp});
|
||||||
@@ -112,7 +122,10 @@ class CallSession extends Emitter {
|
|||||||
return ep;
|
return ep;
|
||||||
}
|
}
|
||||||
const uas = await this.srf.createUAS(this.req, this.res, {localSdp: ep.local.sdp});
|
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;
|
resources.dlg = uas;
|
||||||
|
this.logger.debug(`CallSession:_evalEndpointPrecondition - call was answered with callSid ${callSid}`);
|
||||||
this.calls.set(callSid, resources);
|
this.calls.set(callSid, resources);
|
||||||
return ep;
|
return ep;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -122,13 +135,15 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_evalStableCallPrecondition(task, callSid) {
|
_evalStableCallPrecondition(task, callSid) {
|
||||||
|
if (this.callGone) throw new Error(`${BADPRECONDITIONS}: call gone`);
|
||||||
const resources = this.calls.get(callSid);
|
const resources = this.calls.get(callSid);
|
||||||
if (!resources) throw new Error(`task ${task.name} attempting to operate on unknown callSid ${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;
|
return resources.dlg;
|
||||||
}
|
}
|
||||||
|
|
||||||
_evalUnansweredCallPrecondition(task, callSid) {
|
_evalUnansweredCallPrecondition(task, callSid) {
|
||||||
|
if (this.callGone) new Error(`${BADPRECONDITIONS}: call gone`);
|
||||||
if (callSid !== this.parentCallSid || !this.req) {
|
if (callSid !== this.parentCallSid || !this.req) {
|
||||||
throw new Error(`${BADPRECONDITIONS}: no inbound call - callSid ${callSid}`);
|
throw new Error(`${BADPRECONDITIONS}: no inbound call - callSid ${callSid}`);
|
||||||
}
|
}
|
||||||
@@ -152,6 +167,11 @@ class CallSession extends Emitter {
|
|||||||
this.calls.clear();
|
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
|
* 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};
|
return {ep, ms, res: this.res};
|
||||||
}
|
}
|
||||||
const dlg = await this.srf.createUAS(this.req, this.res, {localSdp: ep.local.sdp});
|
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});
|
this.calls.set(this.parentCallSid, {ep, dlg});
|
||||||
return {ep, ms, dlg};
|
return {ep, ms, dlg};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -265,10 +286,25 @@ class CallSession extends Emitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* got CANCEL from inbound leg
|
* got CANCEL or BYE from inbound leg
|
||||||
*/
|
*/
|
||||||
_onCallerHangup(evt) {
|
_onCallerHangup(obj, evt) {
|
||||||
this.logger.debug('CallSession: caller hung before connection');
|
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);
|
//require('request-debug')(request);
|
||||||
const uuidv4 = require('uuid/v4');
|
const uuidv4 = require('uuid/v4');
|
||||||
const makeTask = require('./tasks/make_task');
|
const makeTask = require('./tasks/make_task');
|
||||||
|
const normalizeJamones = require('./utils/normalize-jamones');
|
||||||
const {CallStatus, CallDirection} = require('./utils/constants');
|
const {CallStatus, CallDirection} = require('./utils/constants');
|
||||||
|
|
||||||
module.exports = function(srf, logger) {
|
module.exports = function(srf, logger) {
|
||||||
@@ -68,19 +69,21 @@ module.exports = function(srf, logger) {
|
|||||||
const call_sid = uuidv4();
|
const call_sid = uuidv4();
|
||||||
const method = (app.hook_http_method || 'POST').toUpperCase();
|
const method = (app.hook_http_method || 'POST').toUpperCase();
|
||||||
const from = req.getParsedHeader('From');
|
const from = req.getParsedHeader('From');
|
||||||
const qs = req.locals.callAttributes = {
|
req.locals.callAttributes = {
|
||||||
CallSid: call_sid,
|
CallSid: call_sid,
|
||||||
AccountSid: app.account_sid,
|
AccountSid: app.account_sid,
|
||||||
From: req.callingNumber,
|
From: req.callingNumber,
|
||||||
To: req.calledNumber,
|
To: req.calledNumber,
|
||||||
CallStatus: CallStatus.Trying,
|
|
||||||
SipStatus: 100,
|
|
||||||
Direction: CallDirection.Inbound,
|
Direction: CallDirection.Inbound,
|
||||||
CallerName: from.name || req.callingNumber,
|
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'),
|
RequestorIP: req.get('X-Forwarded-For'),
|
||||||
RequestorName: req.get('X-Originating-Carrier')
|
RequestorName: req.get('X-Originating-Carrier')
|
||||||
};
|
});
|
||||||
const opts = {
|
const opts = {
|
||||||
url: app.call_hook,
|
url: app.call_hook,
|
||||||
method,
|
method,
|
||||||
@@ -88,6 +91,7 @@ module.exports = function(srf, logger) {
|
|||||||
qs
|
qs
|
||||||
};
|
};
|
||||||
if (app.hook_basic_auth_user && app.hook_basic_auth_password) {
|
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}});
|
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});
|
if (method === 'POST') Object.assign(opts, {body: req.msg});
|
||||||
@@ -95,26 +99,16 @@ module.exports = function(srf, logger) {
|
|||||||
request(opts, (err, response, body) => {
|
request(opts, (err, response, body) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error(err, `Error invoking callback ${app.call_hook}`);
|
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');
|
logger.debug(body, `application payload: ${body}`);
|
||||||
const taskData = Array.isArray(body) ? body : [body];
|
try {
|
||||||
app.tasks = [];
|
app.tasks = normalizeJamones(logger, body).map((tdata) => makeTask(logger, tdata));
|
||||||
for (const t in taskData) {
|
next();
|
||||||
try {
|
} catch (err) {
|
||||||
const task = makeTask(logger, taskData[t]);
|
logger.error(err, 'Invalid Webhook Response');
|
||||||
app.tasks.push(task);
|
res.send(500);
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!res.finalResponseSent) next();
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err, 'Error invoking web callback');
|
logger.error(err, 'Error invoking web callback');
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ class TaskDial extends Task {
|
|||||||
this.timeLimit = opts.timeLimit;
|
this.timeLimit = opts.timeLimit;
|
||||||
|
|
||||||
if (opts.listen) {
|
if (opts.listen) {
|
||||||
this.listenTask = makeTask(logger, {'listen': opts.transcribe});
|
this.listenTask = makeTask(logger, {'listen': opts.listen});
|
||||||
}
|
}
|
||||||
if (opts.transcribe) {
|
if (opts.transcribe) {
|
||||||
this.transcribeTask = makeTask(logger, {'transcribe': opts.transcribe});
|
this.transcribeTask = makeTask(logger, {'transcribe' : opts.transcribe});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canceled = false;
|
this.canceled = false;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
const {TaskName, TaskPreconditions, TranscriptionEvents} = require('../utils/constants');
|
||||||
const makeTask = require('./make_task');
|
const makeTask = require('./make_task');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ class TaskGather extends Task {
|
|||||||
this.timeout = (this.timeout || 5) * 1000;
|
this.timeout = (this.timeout || 5) * 1000;
|
||||||
this.language = this.language || 'en-US';
|
this.language = this.language || 'en-US';
|
||||||
this.digitBuffer = '';
|
this.digitBuffer = '';
|
||||||
|
//this._earlyMedia = this.data.earlyMedia === true;
|
||||||
|
|
||||||
if (this.say) {
|
if (this.say) {
|
||||||
this.sayTask = makeTask(this.logger, {say: this.say});
|
this.sayTask = makeTask(this.logger, {say: this.say});
|
||||||
@@ -27,6 +28,11 @@ class TaskGather extends Task {
|
|||||||
|
|
||||||
get name() { return TaskName.Gather; }
|
get name() { return TaskName.Gather; }
|
||||||
|
|
||||||
|
get earlyMedia() {
|
||||||
|
return (this.sayTask && this.sayTask.earlyMedia) ||
|
||||||
|
(this.playTask && this.playTask.earlyMedia);
|
||||||
|
}
|
||||||
|
|
||||||
async exec(cs, ep) {
|
async exec(cs, ep) {
|
||||||
this.ep = ep;
|
this.ep = ep;
|
||||||
this.actionHook = cs.actionHook;
|
this.actionHook = cs.actionHook;
|
||||||
@@ -35,33 +41,15 @@ class TaskGather extends Task {
|
|||||||
try {
|
try {
|
||||||
if (this.sayTask) {
|
if (this.sayTask) {
|
||||||
this.sayTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
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();
|
else this._startTimer();
|
||||||
|
|
||||||
if (this.input.includes('speech')) {
|
if (this.input.includes('speech')) {
|
||||||
const opts = {
|
await this._initSpeech(ep);
|
||||||
GOOGLE_SPEECH_USE_ENHANCED: true,
|
this._startTranscribing(ep);
|
||||||
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'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.input.includes('dtmf')) {
|
if (this.input.includes('dtmf')) {
|
||||||
@@ -73,10 +61,12 @@ class TaskGather extends Task {
|
|||||||
this.logger.error(err, 'TaskGather:exec error');
|
this.logger.error(err, 'TaskGather:exec error');
|
||||||
}
|
}
|
||||||
this.taskInProgress = false;
|
this.taskInProgress = false;
|
||||||
ep.removeAllListeners();
|
ep.removeCustomEventListener(TranscriptionEvents.Transcription);
|
||||||
|
ep.removeCustomEventListener(TranscriptionEvents.EndOfUtterance);
|
||||||
}
|
}
|
||||||
|
|
||||||
kill() {
|
kill() {
|
||||||
|
super.kill();
|
||||||
this._killAudio();
|
this._killAudio();
|
||||||
this._resolve('killed');
|
this._resolve('killed');
|
||||||
}
|
}
|
||||||
@@ -85,12 +75,6 @@ class TaskGather extends Task {
|
|||||||
return new Promise((resolve) => this.resolver = resolve);
|
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) {
|
_onDtmf(ep, evt) {
|
||||||
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
||||||
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
|
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
|
||||||
@@ -101,6 +85,32 @@ class TaskGather extends Task {
|
|||||||
this._killAudio();
|
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() {
|
_startTimer() {
|
||||||
assert(!this._timeoutTimer);
|
assert(!this._timeoutTimer);
|
||||||
this._timeoutTimer = setTimeout(() => this._resolve('timeout'), this.timeout);
|
this._timeoutTimer = setTimeout(() => this._resolve('timeout'), this.timeout);
|
||||||
@@ -123,6 +133,8 @@ class TaskGather extends Task {
|
|||||||
_onTranscription(ep, evt) {
|
_onTranscription(ep, evt) {
|
||||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||||
if (evt.is_final) {
|
if (evt.is_final) {
|
||||||
|
ep.removeCustomEventListener(TranscriptionEvents.Transcription);
|
||||||
|
ep.removeCustomEventListener(TranscriptionEvents.EndOfUtterance);
|
||||||
this._resolve('speech', evt);
|
this._resolve('speech', evt);
|
||||||
}
|
}
|
||||||
else if (this.partialResultCallback) {
|
else if (this.partialResultCallback) {
|
||||||
@@ -131,11 +143,9 @@ class TaskGather extends Task {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_onNoAudioDetected(ep, evt) {
|
_onEndOfUtterance(ep, evt) {
|
||||||
this.logger.info(evt, 'TaskGather:_onNoAudioDetected');
|
this.logger.info(evt, 'TaskGather:_onEndOfUtterance');
|
||||||
}
|
this._startTranscribing(ep);
|
||||||
_onMaxDuration(ep, evt) {
|
|
||||||
this.logger.info(evt, 'TaskGather:_onMaxDuration');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_resolve(reason, evt) {
|
_resolve(reason, evt) {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
const Task = require('./task');
|
const Task = require('./task');
|
||||||
const {TaskName} = require('../utils/constants');
|
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||||
|
|
||||||
class TaskHangup extends Task {
|
class TaskHangup extends Task {
|
||||||
constructor(logger, opts) {
|
constructor(logger, opts) {
|
||||||
super(logger, opts);
|
super(logger, opts);
|
||||||
this.headers = this.data.headers || {};
|
this.headers = this.data.headers || {};
|
||||||
|
|
||||||
|
this.preconditions = TaskPreconditions.StableCall;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() { return TaskName.Hangup; }
|
get name() { return TaskName.Hangup; }
|
||||||
@@ -16,7 +18,7 @@ class TaskHangup extends Task {
|
|||||||
try {
|
try {
|
||||||
await dlg.destroy({headers: this.headers});
|
await dlg.destroy({headers: this.headers});
|
||||||
} catch (err) {
|
} 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 {TaskName} = require('../utils/constants');
|
||||||
const errBadInstruction = new Error('invalid instruction payload');
|
const errBadInstruction = new Error('invalid instruction payload');
|
||||||
|
|
||||||
function makeTask(logger, opts) {
|
function makeTask(logger, obj) {
|
||||||
logger.debug(opts, 'makeTask');
|
const keys = Object.keys(obj);
|
||||||
if (typeof opts !== 'object' || Array.isArray(opts)) throw errBadInstruction;
|
if (!keys || keys.length !== 1) {
|
||||||
const keys = Object.keys(opts);
|
throw errBadInstruction;
|
||||||
if (keys.length !== 1) throw errBadInstruction;
|
}
|
||||||
const name = keys[0];
|
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);
|
Task.validate(name, data);
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case TaskName.SipDecline:
|
case TaskName.SipDecline:
|
||||||
@@ -26,6 +28,12 @@ function makeTask(logger, opts) {
|
|||||||
case TaskName.Gather:
|
case TaskName.Gather:
|
||||||
const TaskGather = require('./gather');
|
const TaskGather = require('./gather');
|
||||||
return new TaskGather(logger, data);
|
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
|
// 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
|
text: this.text
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message !== 'hangup') this.logger.info(err, 'TaskSay:exec error');
|
this.logger.info(err, 'TaskSay:exec error');
|
||||||
}
|
}
|
||||||
this.emit('playDone');
|
this.emit('playDone');
|
||||||
this.sayComplete = true;
|
this.sayComplete = true;
|
||||||
|
|||||||
@@ -16,11 +16,22 @@
|
|||||||
"required": [
|
"required": [
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"play": {
|
||||||
|
"properties": {
|
||||||
|
"url": "string",
|
||||||
|
"loop": "number",
|
||||||
|
"earlyMedia": "boolean"
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"url"
|
||||||
|
]
|
||||||
|
},
|
||||||
"say": {
|
"say": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"text": "string",
|
"text": "string",
|
||||||
"loop": "number",
|
"loop": "number",
|
||||||
"synthesizer": "#synthesizer"
|
"synthesizer": "#synthesizer",
|
||||||
|
"earlyMedia": "boolean"
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"text",
|
"text",
|
||||||
@@ -31,14 +42,12 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"action": "string",
|
"action": "string",
|
||||||
"finishOnKey": "string",
|
"finishOnKey": "string",
|
||||||
"hints": "array",
|
|
||||||
"input": "array",
|
"input": "array",
|
||||||
"language": "string",
|
|
||||||
"numDigits": "number",
|
"numDigits": "number",
|
||||||
"partialResultCallback": "string",
|
"partialResultCallback": "string",
|
||||||
"profanityFilter": "boolean",
|
|
||||||
"speechTimeout": "number",
|
"speechTimeout": "number",
|
||||||
"timeout": "number",
|
"timeout": "number",
|
||||||
|
"recognizer": "#recognizer",
|
||||||
"say": "#say"
|
"say": "#say"
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -73,37 +82,34 @@
|
|||||||
},
|
},
|
||||||
"listen": {
|
"listen": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"finishOnKey": "string",
|
||||||
|
"maxLength": "number",
|
||||||
"metadata": "object",
|
"metadata": "object",
|
||||||
"mixType": {
|
"mixType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["mono", "stereo", "mixed"]
|
"enum": ["mono", "stereo", "mixed"]
|
||||||
},
|
},
|
||||||
"passDtmf": "boolean",
|
"passDtmf": "boolean",
|
||||||
|
"playBeep": "boolean",
|
||||||
"sampleRate": "number",
|
"sampleRate": "number",
|
||||||
"source": {
|
"timeout": "number",
|
||||||
"type": "string",
|
"transcribe": "#transcribe",
|
||||||
"enum": ["parent", "child", "both"]
|
"url": "string",
|
||||||
},
|
"earlyMedia": "boolean"
|
||||||
"wsUrl": "string"
|
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"wsUrl",
|
"url"
|
||||||
"sampleRate"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"transcribe": {
|
"transcribe": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"action": "string",
|
"action": "string",
|
||||||
"interim": "boolean",
|
"recognizer": "#recognizer",
|
||||||
"jsonKey": "string",
|
"earlyMedia": "boolean"
|
||||||
"language": "string",
|
|
||||||
"source": "string",
|
|
||||||
"vendor": "string"
|
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"action",
|
"action",
|
||||||
"jsonKey",
|
"recognizer"
|
||||||
"language"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"target": {
|
"target": {
|
||||||
@@ -143,6 +149,28 @@
|
|||||||
"enum": ["google"]
|
"enum": ["google"]
|
||||||
},
|
},
|
||||||
"voice": "string"
|
"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.preconditions = TaskPreconditions.None;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.data = data;
|
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
|
* what to do is up to each type of task
|
||||||
*/
|
*/
|
||||||
kill() {
|
kill() {
|
||||||
|
this.logger.debug(`${this.name} is being killed`);
|
||||||
|
this._killInProgress = true;
|
||||||
// no-op
|
// 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",
|
"Endpoint": "endpoint",
|
||||||
"StableCall": "stable-call",
|
"StableCall": "stable-call",
|
||||||
"UnansweredCall": "unanswered-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,
|
url,
|
||||||
method,
|
method,
|
||||||
json: true,
|
json: true,
|
||||||
qs: 'GET' === method ? params : null,
|
qs: 'GET' === method ? params : callAttributes,
|
||||||
body: 'POST' === method ? params : null
|
body: 'POST' === method ? opts : null
|
||||||
};
|
};
|
||||||
logger.debug(`${method} ${url} sending ${JSON.stringify(obj)}`);
|
logger.debug(`${method} ${url} sending ${JSON.stringify(obj)}`);
|
||||||
return new Promise((resolve, reject) => {
|
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",
|
"resolved": "https://registry.npmjs.org/drachtio-fn-b2b-sugar/-/drachtio-fn-b2b-sugar-0.0.12.tgz",
|
||||||
"integrity": "sha512-FKPAcEMJTYKDrd9DJUCc4VHnY/c65HOO9k8XqVNognF9T02hKEjGuBCM4Da9ipyfiHmVRuECwj0XNvZ361mkVQ=="
|
"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": {
|
"drachtio-mw-registration-parser": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/drachtio-mw-registration-parser/-/drachtio-mw-registration-parser-0.0.2.tgz",
|
"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==",
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
"dev": true
|
"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": {
|
"events-to-array": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"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": {
|
"semver": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
@@ -4887,6 +4946,20 @@
|
|||||||
"signal-exit": "^3.0.2"
|
"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": {
|
"xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -28,9 +28,9 @@
|
|||||||
"config": "^3.2.4",
|
"config": "^3.2.4",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"drachtio-fn-b2b-sugar": "0.0.12",
|
"drachtio-fn-b2b-sugar": "0.0.12",
|
||||||
"drachtio-fsmrf": "1.5.10",
|
"drachtio-fsmrf": "^1.5.11",
|
||||||
"drachtio-srf": "^4.4.27",
|
"drachtio-srf": "^4.4.27",
|
||||||
"jambonz-db-helpers": "^0.1.6",
|
"jambonz-db-helpers": "^0.1.7",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"pino": "^5.14.0",
|
"pino": "^5.14.0",
|
||||||
"request": "^2.88.0",
|
"request": "^2.88.0",
|
||||||
|
|||||||
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": {
|
"listen": {
|
||||||
"wsUrl": "wss://myrecorder.example.com:4433",
|
"url": "wss://myrecorder.example.com:4433",
|
||||||
"mixType" : "stereo",
|
"mixType" : "stereo",
|
||||||
"sampleRate": 8000,
|
"sampleRate": 8000,
|
||||||
"passDtmf": true,
|
"passDtmf": true,
|
||||||
"source": "parent",
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"clientId": "12udih"
|
"clientId": "12udih"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,12 @@
|
|||||||
],
|
],
|
||||||
"transcribe": {
|
"transcribe": {
|
||||||
"action": "http://example.com/transcribe",
|
"action": "http://example.com/transcribe",
|
||||||
"language" : "en-US",
|
"recognizer": {
|
||||||
"source" : "both",
|
"vendor": "google",
|
||||||
"interim": true,
|
"language" : "en-US",
|
||||||
"vendor": "google",
|
"mixType" : "stereo",
|
||||||
"jsonKey": "--json service key--"
|
"interim": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,13 @@ test('app payload parsing tests', (t) => {
|
|||||||
task = makeTask(logger, require('./data/good/dial-listen'));
|
task = makeTask(logger, require('./data/good/dial-listen'));
|
||||||
t.ok(task.name === 'dial', 'parsed dial w/ 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();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user