mirror of
https://github.com/jambonz/jambonz-feature-server.git
synced 2026-01-25 02:07:56 +00:00
Compare commits
103 Commits
v0.9.5-10
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4855fec4f5 | ||
|
|
a11822609f | ||
|
|
8dd9bfbb74 | ||
|
|
cee21c3dbc | ||
|
|
e2753ca8a3 | ||
|
|
df302320ca | ||
|
|
83a3bb61fe | ||
|
|
c36b55303a | ||
|
|
60d7f0f31c | ||
|
|
b089bd4663 | ||
|
|
5c8f91c1c1 | ||
|
|
50f926bce4 | ||
|
|
2a61d21bff | ||
|
|
418baa20df | ||
|
|
eb418a42e9 | ||
|
|
57ba79f908 | ||
|
|
3a791a67b5 | ||
|
|
91108fa3ef | ||
|
|
36c723d8f0 | ||
|
|
bc59fc80c9 | ||
|
|
6960466afc | ||
|
|
09c2608114 | ||
|
|
f1c17e537d | ||
|
|
22dad4eed6 | ||
|
|
017bc39103 | ||
|
|
8b982f20d6 | ||
|
|
b6d0d4cb0e | ||
|
|
4636b487b4 | ||
|
|
fce40a47ce | ||
|
|
1fd94dce94 | ||
|
|
efba631282 | ||
|
|
2fc3febcf6 | ||
|
|
cc67132dfa | ||
|
|
f8e88f085f | ||
|
|
1050eb47cd | ||
|
|
4e2feda7f3 | ||
|
|
1685262658 | ||
|
|
6fc9a3567e | ||
|
|
fb33574861 | ||
|
|
d2732c9be6 | ||
|
|
294d38dcd1 | ||
|
|
7bdac328bf | ||
|
|
2e0f4b94dc | ||
|
|
53e5360ab3 | ||
|
|
8163c33462 | ||
|
|
7e9b6498c5 | ||
|
|
a1c59a5f25 | ||
|
|
52a2ce8a86 | ||
|
|
2738299524 | ||
|
|
e872d314b3 | ||
|
|
b1683bc294 | ||
|
|
78869e4bb8 | ||
|
|
ba994af012 | ||
|
|
e579514321 | ||
|
|
baed1b0eac | ||
|
|
b9dfecceff | ||
|
|
c7c99f45a4 | ||
|
|
67d18b26ff | ||
|
|
53cfbc1f56 | ||
|
|
da35449c16 | ||
|
|
83c015c839 | ||
|
|
f1f21fb23b | ||
|
|
c88ead7f71 | ||
|
|
b41c1ffb91 | ||
|
|
cc98f40d44 | ||
|
|
11035264ec | ||
|
|
5389083107 | ||
|
|
9657017669 | ||
|
|
b532a49e45 | ||
|
|
97d7a60994 | ||
|
|
6dbbbf8c9e | ||
|
|
4ea9707e4e | ||
|
|
e20c472bd2 | ||
|
|
f2421bb3dd | ||
|
|
18a141edc6 | ||
|
|
30a4e0e6a3 | ||
|
|
924627a50c | ||
|
|
d76a5e6efb | ||
|
|
cdfb1fff1e | ||
|
|
5c1501b6c7 | ||
|
|
161e67ece5 | ||
|
|
65b04d7cbf | ||
|
|
8a641f1d9a | ||
|
|
95188f59ec | ||
|
|
4e76077dc9 | ||
|
|
3c75d5a489 | ||
|
|
3164e1ea4e | ||
|
|
c9e3e97d53 | ||
|
|
ed157c6aee | ||
|
|
c395109966 | ||
|
|
7228594f79 | ||
|
|
28cde62d5d | ||
|
|
db6f56a068 | ||
|
|
9f757b439f | ||
|
|
35114b22d8 | ||
|
|
68b2ad526a | ||
|
|
b8ef1dba73 | ||
|
|
2ce902c00d | ||
|
|
8c00c89882 | ||
|
|
dcd6ddcbca | ||
|
|
9a71350875 | ||
|
|
1bca165fc1 | ||
|
|
b94605127e |
@@ -1,248 +0,0 @@
|
||||
const Task = require('./task');
|
||||
const {TaskName, TaskPreconditions} = require('../utils/constants');
|
||||
const makeTask = require('./make_task');
|
||||
const { SocketClient } = require('@cognigy/socket-client');
|
||||
|
||||
const parseGallery = (obj = {}) => {
|
||||
const {_default} = obj;
|
||||
if (_default) {
|
||||
const {_gallery} = _default;
|
||||
if (_gallery) return _gallery.fallbackText;
|
||||
}
|
||||
};
|
||||
|
||||
const parseQuickReplies = (obj) => {
|
||||
const {_default} = obj;
|
||||
if (_default) {
|
||||
const {_quickReplies} = _default;
|
||||
if (_quickReplies) return _quickReplies.text || _quickReplies.fallbackText;
|
||||
}
|
||||
};
|
||||
|
||||
const parseBotText = (evt) => {
|
||||
const {text, data} = evt;
|
||||
if (text) return text;
|
||||
|
||||
switch (data?.type) {
|
||||
case 'quickReplies':
|
||||
return parseQuickReplies(data?._cognigy);
|
||||
case 'gallery':
|
||||
return parseGallery(data?._cognigy);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
class Cognigy extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.url = this.data.url;
|
||||
this.token = this.data.token;
|
||||
this.prompt = this.data.prompt;
|
||||
this.eventHook = this.data?.eventHook;
|
||||
this.actionHook = this.data?.actionHook;
|
||||
this.data = this.data.data || {};
|
||||
this.prompts = [];
|
||||
}
|
||||
|
||||
get name() { return TaskName.Cognigy; }
|
||||
|
||||
get hasReportedFinalAction() {
|
||||
return this.reportedFinalAction || this.isReplacingApplication;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
|
||||
this.ep = ep;
|
||||
try {
|
||||
/* set event handlers and start transcribing */
|
||||
this.on('transcription', this._onTranscription.bind(this, cs, ep));
|
||||
this.on('error', this._onError.bind(this, cs, ep));
|
||||
|
||||
this.transcribeTask = this._makeTranscribeTask();
|
||||
this.transcribeTask.exec(cs, ep, this)
|
||||
.catch((err) => {
|
||||
this.logger.info({err}, 'Cognigy transcribe task returned error');
|
||||
this.notifyTaskDone();
|
||||
});
|
||||
if (this.prompt) {
|
||||
this.sayTask = this._makeSayTask(this.prompt);
|
||||
this.sayTask.exec(cs, ep, this)
|
||||
.catch((err) => {
|
||||
this.logger.info({err}, 'Cognigy say task returned error');
|
||||
this.notifyTaskDone();
|
||||
});
|
||||
}
|
||||
|
||||
/* connect to the bot and send initial data */
|
||||
this.client = new SocketClient(
|
||||
this.url,
|
||||
this.token,
|
||||
{
|
||||
sessionId: cs.callSid,
|
||||
channel: 'jambonz',
|
||||
forceWebsockets: true,
|
||||
reconnection: true,
|
||||
settings: {
|
||||
enableTypingIndicator: false
|
||||
}
|
||||
}
|
||||
);
|
||||
this.client.on('output', this._onBotUtterance.bind(this, cs, ep));
|
||||
this.client.on('typingStatus', this._onBotTypingStatus.bind(this, cs, ep));
|
||||
this.client.on('error', this._onBotError.bind(this, cs, ep));
|
||||
this.client.on('finalPing', this._onBotFinalPing.bind(this, cs, ep));
|
||||
await this.client.connect();
|
||||
this.client.sendMessage('', {...this.data, ...cs.callInfo});
|
||||
|
||||
await this.awaitTaskDone();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Cognigy error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
this.logger.debug('Cognigy:kill');
|
||||
|
||||
this.removeAllListeners();
|
||||
this.transcribeTask && this.transcribeTask.kill();
|
||||
|
||||
this.client.removeAllListeners();
|
||||
if (this.client && this.client.connected) this.client.disconnect();
|
||||
|
||||
if (!this.hasReportedFinalAction) {
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({cognigyResult: 'caller hungup'})
|
||||
.catch((err) => this.logger.info({err}, 'cognigy - error w/ action webook'));
|
||||
}
|
||||
|
||||
if (this.ep.connected) {
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_makeTranscribeTask() {
|
||||
const opts = {
|
||||
recognizer: this.data.recognizer || {
|
||||
vendor: 'default',
|
||||
language: 'default',
|
||||
outputFormat: 'detailed'
|
||||
}
|
||||
};
|
||||
this.logger.debug({opts}, 'constructing a nested transcribe object');
|
||||
const transcribe = makeTask(this.logger, {transcribe: opts}, this);
|
||||
return transcribe;
|
||||
}
|
||||
|
||||
_makeSayTask(text) {
|
||||
const opts = {
|
||||
text,
|
||||
synthesizer: this.data.synthesizer ||
|
||||
{
|
||||
vendor: 'default',
|
||||
language: 'default',
|
||||
voice: 'default'
|
||||
}
|
||||
};
|
||||
this.logger.debug({opts}, 'constructing a nested say object');
|
||||
const say = makeTask(this.logger, {say: opts}, this);
|
||||
return say;
|
||||
}
|
||||
|
||||
async _onBotError(cs, ep, evt) {
|
||||
this.logger.info({evt}, 'Cognigy:_onBotError');
|
||||
this.performAction({cognigyResult: 'botError', message: evt.message });
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _onBotTypingStatus(cs, ep, evt) {
|
||||
this.logger.info({evt}, 'Cognigy:_onBotTypingStatus');
|
||||
}
|
||||
async _onBotFinalPing(cs, ep) {
|
||||
this.logger.info('Cognigy:_onBotFinalPing');
|
||||
if (this.prompts.length) {
|
||||
const text = this.prompts.join('.');
|
||||
this.prompts = [];
|
||||
if (text && !this.killed) {
|
||||
this.sayTask = this._makeSayTask(text);
|
||||
this.sayTask.exec(cs, ep, this)
|
||||
.catch((err) => {
|
||||
this.logger.info({err}, 'Cognigy say task returned error');
|
||||
this.notifyTaskDone();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _onBotUtterance(cs, ep, evt) {
|
||||
this.logger.debug({evt}, 'Cognigy:_onBotUtterance');
|
||||
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'botMessage', message: evt})
|
||||
.then((redirected) => {
|
||||
if (redirected) {
|
||||
this.logger.info('Cognigy_onTranscription: event handler for bot message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({cognigyResult: 'redirect'}, false);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Cognigy_onTranscription: error sending event hook');
|
||||
});
|
||||
}
|
||||
const text = parseBotText(evt);
|
||||
this.prompts.push(text);
|
||||
}
|
||||
|
||||
async _onTranscription(cs, ep, evt) {
|
||||
this.logger.debug({evt}, `Cognigy: got transcription for callSid ${cs.callSid}`);
|
||||
const utterance = evt.alternatives[0].transcript;
|
||||
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'userMessage', message: utterance})
|
||||
.then((redirected) => {
|
||||
if (redirected) {
|
||||
this.logger.info('Cognigy_onTranscription: event handler for user message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({cognigyResult: 'redirect'}, false);
|
||||
if (this.transcribeTask) this.transcribeTask.kill(cs);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Cognigy_onTranscription: error sending event hook');
|
||||
});
|
||||
}
|
||||
|
||||
/* send the user utterance to the bot */
|
||||
try {
|
||||
if (this.client && this.client.connected) {
|
||||
this.client.sendMessage(utterance);
|
||||
}
|
||||
else {
|
||||
this.logger.info('Cognigy_onTranscription - not sending user utterance as bot is disconnected');
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Cognigy_onTranscription: Error sending user utterance to Cognigy - ending task');
|
||||
this.performAction({cognigyResult: 'socketError'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
_onError(cs, ep, err) {
|
||||
this.logger.debug({err}, 'Cognigy: got error');
|
||||
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'error', err});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Cognigy;
|
||||
470
lib/tasks/cognigy/index.js
Normal file
470
lib/tasks/cognigy/index.js
Normal file
@@ -0,0 +1,470 @@
|
||||
const Task = require('../task');
|
||||
const {TaskName, TaskPreconditions} = require('../../utils/constants');
|
||||
const makeTask = require('../make_task');
|
||||
const { SocketClient } = require('@cognigy/socket-client');
|
||||
const SpeechConfig = require('./speech-config');
|
||||
const queue = require('queue');
|
||||
|
||||
const parseGallery = (obj = {}) => {
|
||||
const {_default} = obj;
|
||||
if (_default) {
|
||||
const {_gallery} = _default;
|
||||
if (_gallery) return _gallery.fallbackText;
|
||||
}
|
||||
};
|
||||
|
||||
const parseQuickReplies = (obj) => {
|
||||
const {_default} = obj;
|
||||
if (_default) {
|
||||
const {_quickReplies} = _default;
|
||||
if (_quickReplies) return _quickReplies.text || _quickReplies.fallbackText;
|
||||
}
|
||||
};
|
||||
|
||||
const parseBotText = (evt) => {
|
||||
const {text, data} = evt;
|
||||
if (text !== undefined) return String(text);
|
||||
|
||||
switch (data?.type) {
|
||||
case 'quickReplies':
|
||||
return parseQuickReplies(data?._cognigy);
|
||||
case 'gallery':
|
||||
return parseGallery(data?._cognigy);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
class Cognigy extends Task {
|
||||
constructor(logger, opts) {
|
||||
super(logger, opts);
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
this.url = this.data.url;
|
||||
this.token = this.data.token;
|
||||
this.prompt = this.data.prompt;
|
||||
this.eventHook = this.data?.eventHook;
|
||||
this.actionHook = this.data?.actionHook;
|
||||
this.data = this.data.data || {};
|
||||
this.prompts = [];
|
||||
this.retry = {};
|
||||
this.timeoutCount = 0;
|
||||
// create a task queue so we can execute our taskss subsequently
|
||||
// also executing tasks whenever they come in
|
||||
|
||||
this.taskQueue = queue({concurrency: 1, autostart: 1});
|
||||
this.changeConfigTasks = [];
|
||||
|
||||
// keep track of turns so we only do gather once per turn
|
||||
this.turn = 0;
|
||||
this.gatherTurn = 0;
|
||||
}
|
||||
|
||||
get name() { return TaskName.Cognigy; }
|
||||
|
||||
get hasReportedFinalAction() {
|
||||
return this.reportedFinalAction || this.isReplacingApplication;
|
||||
}
|
||||
|
||||
async _enqueueTask(task) {
|
||||
let resolver;
|
||||
let rejector;
|
||||
|
||||
const boundTask = task.bind(this);
|
||||
const taskPromise = new Promise(async(resolve, reject) => {
|
||||
resolver = resolve;
|
||||
rejector = reject;
|
||||
});
|
||||
taskPromise.resolve = resolver;
|
||||
this.taskQueue.push(async(cb) => {
|
||||
this.logger.debug('executing task from queue');
|
||||
try {
|
||||
|
||||
const result = await boundTask();
|
||||
// if this is a config task, remove it from the config task storage,
|
||||
// as we have now executed it
|
||||
if(task.isConfigTask){
|
||||
this.changeConfigTasks.shift();
|
||||
}
|
||||
resolver(result);
|
||||
cb(result);
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'could not execute task in task queue');
|
||||
rejector(err);
|
||||
cb(err);
|
||||
}
|
||||
this.logger.debug('say task executed from queue');
|
||||
});
|
||||
|
||||
// if this is a config task, lets also push the config
|
||||
if(task.isConfigTask){
|
||||
this.changeConfigTasks.push(task);
|
||||
}
|
||||
if (this.taskQueue.lastPromise) {
|
||||
// resolve the previous promise for cleanup
|
||||
this.taskQueue.lastPromise.resolve({});
|
||||
}
|
||||
this.taskQueue.lastPromise = taskPromise;
|
||||
return taskPromise;
|
||||
}
|
||||
|
||||
async exec(cs, ep) {
|
||||
await super.exec(cs);
|
||||
|
||||
const opts = {
|
||||
synthesizer: this.data.synthesizer || {
|
||||
vendor: 'default',
|
||||
language: 'default',
|
||||
voice: 'default'
|
||||
},
|
||||
recognizer: this.data.recognizer || {
|
||||
vendor: 'default',
|
||||
language: 'default'
|
||||
},
|
||||
bargein: this.data.bargein || {},
|
||||
bot: this.data.bot || {},
|
||||
user: this.data.user || {},
|
||||
dtmf: this.data.dtmf || {}
|
||||
};
|
||||
this.config = new SpeechConfig({logger: this.logger, ep, opts});
|
||||
this.ep = ep;
|
||||
try {
|
||||
|
||||
/* set event handlers and start transcribing */
|
||||
this.on('transcription', this._onTranscription.bind(this, cs, ep));
|
||||
this.on('dtmf-collected', this._onDtmf.bind(this, cs, ep));
|
||||
this.on('timeout', this._onTimeout.bind(this, cs, ep));
|
||||
this.on('error', this._onError.bind(this, cs, ep));
|
||||
|
||||
/* connect to the bot and send initial data */
|
||||
this.client = new SocketClient(
|
||||
this.url,
|
||||
this.token,
|
||||
{
|
||||
sessionId: cs.callSid,
|
||||
channel: 'jambonz',
|
||||
forceWebsockets: true,
|
||||
reconnection: true,
|
||||
settings: {
|
||||
enableTypingIndicator: false
|
||||
}
|
||||
}
|
||||
);
|
||||
this.client.on('output', this._onBotUtterance.bind(this, cs, ep));
|
||||
this.client.on('error', this._onBotError.bind(this, cs, ep));
|
||||
this.client.on('finalPing', this._onBotFinalPing.bind(this, cs, ep));
|
||||
await this.client.connect();
|
||||
// todo make welcome message configurable (enable or disable it when
|
||||
// we start a conversation (should be enabled by defaul))
|
||||
this.client.sendMessage('Welcome Message', {...this.data, ...cs.callInfo});
|
||||
|
||||
await this.awaitTaskDone();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Cognigy error');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async kill(cs) {
|
||||
super.kill(cs);
|
||||
this.logger.debug('Cognigy:kill');
|
||||
|
||||
|
||||
this.removeAllListeners();
|
||||
this.transcribeTask && this.transcribeTask.kill();
|
||||
|
||||
this.client.removeAllListeners();
|
||||
if (this.client && this.client.connected) this.client.disconnect();
|
||||
|
||||
try {
|
||||
// end the task queue AFTER we have removed all listeneres since now we cannot get new stuff inside the queue
|
||||
this.taskQueue.end();
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'could not end tasks queue!!');
|
||||
}
|
||||
|
||||
|
||||
if (!this.hasReportedFinalAction) {
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({cognigyResult: 'caller hungup'})
|
||||
.catch((err) => this.logger.info({err}, 'cognigy - error w/ action webook'));
|
||||
}
|
||||
|
||||
if (this.ep.connected) {
|
||||
await this.ep.api('uuid_break', this.ep.uuid).catch((err) => this.logger.info(err, 'Error killing audio'));
|
||||
}
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a promt which will be sent to the consumer. We will create a say task if bargein is disabled
|
||||
* for session and nextTurn, else create a gather task.
|
||||
*/
|
||||
_createPromtTask({text, url, turnConfig, listenAfterSpeech} = {}) {
|
||||
const bargeInOnNextTurn = turnConfig?.bargein?.enable?.length > 0;
|
||||
const bargeInSession = this.config.bargeInEnabled;
|
||||
if (bargeInOnNextTurn || bargeInSession) {
|
||||
return this._makeGatherTask({textPrompt: text, url, turnConfig, listenAfterSpeech});
|
||||
}
|
||||
return this._makeSayTask({text, turnConfig});
|
||||
}
|
||||
|
||||
_makeGatherTask({textPrompt, urlPrompt, turnConfig, listenAfterSpeech} = {}) {
|
||||
this.logger.debug({textPrompt, urlPrompt, turnConfig}, '_makeGatherTask');
|
||||
const config = this.config.makeGatherTaskConfig({textPrompt, urlPrompt, turnConfig, listenAfterSpeech});
|
||||
const {retry, ...rest} = config;
|
||||
this.retry = retry;
|
||||
const gather = makeTask(this.logger, {gather: rest}, this);
|
||||
return gather;
|
||||
}
|
||||
|
||||
_makeSayTask({ text, turnConfig } = {}) {
|
||||
this.logger.debug({text, turnConfig}, '_makeSayTask');
|
||||
const config = this.config.makeSayTaskConfig({text, turnConfig });
|
||||
this.logger.debug({config}, 'created say task config');
|
||||
const say = makeTask(this.logger, { say: config }, this);
|
||||
return say;
|
||||
}
|
||||
|
||||
_makeReferTask(referTo) {
|
||||
return makeTask(this.logger, {'sip:refer': {
|
||||
referTo
|
||||
}}
|
||||
);
|
||||
}
|
||||
|
||||
_makeHangupTask(reason) {
|
||||
return makeTask(this.logger, {
|
||||
hangup: {
|
||||
headers: {
|
||||
'X-Reason': reason
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
_makePlayTask(url, loop) {
|
||||
return makeTask(this.logger, {
|
||||
play: {
|
||||
url,
|
||||
loop
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* if we need to interrupt the currently-running say task(s), call this */
|
||||
_killSayTasks(ep) {
|
||||
if (ep && ep.connected) {
|
||||
ep.api('uuid_break', this.ep.uuid)
|
||||
.catch((err) => this.logger.info({err}, 'Cognigy:_killSayTasks - error killing audio for current say task'));
|
||||
}
|
||||
}
|
||||
|
||||
async _onBotError(cs, ep, evt) {
|
||||
this.logger.info({evt}, 'Cognigy:_onBotError');
|
||||
this.performAction({cognigyResult: 'botError', message: evt.message });
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
async _onBotFinalPing(cs, ep) {
|
||||
this.logger.info({prompts: this.prompts}, 'Cognigy:_onBotFinalPing');
|
||||
try {
|
||||
// lets wait until we have finished processing the speech before
|
||||
// starting a gather...
|
||||
this.logger.debug('enqueued bot final ping gather');
|
||||
this._enqueueTask(async() => {
|
||||
this.logger.debug('executing bot final ping gather');
|
||||
try {
|
||||
const gatherTask = this._makeGatherTask();
|
||||
await gatherTask.exec(cs, ep, this);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Cognigy final ping gather task returned error');
|
||||
}
|
||||
this.logger.debug('executed bot final ping gather');
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Cognigy gather task returned error');
|
||||
}
|
||||
}
|
||||
|
||||
async _onBotUtterance(cs, ep, evt) {
|
||||
this.logger.debug({evt}, 'Cognigy:_onBotUtterance');
|
||||
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'botMessage', message: evt})
|
||||
.then((redirected) => {
|
||||
if (redirected) {
|
||||
this.logger.info('Cognigy_onBotUtterance: event handler for bot message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({cognigyResult: 'redirect'}, false);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Cognigy_onBotUtterance: error sending event hook');
|
||||
});
|
||||
}
|
||||
|
||||
const text = parseBotText(evt);
|
||||
|
||||
// only add say task if its a normal cognigy node and not a "gather task"
|
||||
if (text && (evt?.data?.type !== 'promt')) {
|
||||
this.logger.info({text}, 'received text');
|
||||
this._enqueueTask(async() => {
|
||||
// todo inject the session config into the say task
|
||||
const promtTask = this._createPromtTask({ text, listenAfterSpeech: false });
|
||||
await promtTask.exec(cs, ep, this);
|
||||
this.logger.debug({text}, 'executed say task');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
switch (evt?.data?.type) {
|
||||
case 'hangup':
|
||||
this._enqueueTask(async() => {
|
||||
this.performAction({cognigyResult: 'hangup Succeeded'});
|
||||
this.reportedFinalAction = true;
|
||||
cs.replaceApplication([this._makeHangupTask(evt.data.reason)]);
|
||||
this.taskQueue.end();
|
||||
});
|
||||
return;
|
||||
case 'refer':
|
||||
this._enqueueTask(async() => {
|
||||
this.performAction({cognigyResult: 'refer succeeded'});
|
||||
this.reportedFinalAction = true;
|
||||
cs.replaceApplication([this._makeReferTask(evt.data.referTo)]);
|
||||
});
|
||||
return;
|
||||
case 'promt':
|
||||
this._enqueueTask(async() => {
|
||||
const sayTask = this._createPromtTask({
|
||||
text: evt.data.text,
|
||||
turnConfig: evt?.data?.config?.nextTurn,
|
||||
url: evt.data.url
|
||||
});
|
||||
try {
|
||||
await sayTask.exec(cs, ep, this);
|
||||
} catch (err) {
|
||||
this.logger.info({err}, 'Cognigy sayTask task returned error');
|
||||
}
|
||||
});
|
||||
return;
|
||||
case 'setSessionConfig':
|
||||
// change session params in the order they come in with the say tasks
|
||||
// so we are consistent with the flow logic executed within cognigy
|
||||
const updateConfigTask = () => {
|
||||
if (evt?.data?.config?.session) this.config.update(evt.data.config.session);
|
||||
};
|
||||
updateConfigTask.isConfigTask = true;
|
||||
this._enqueueTask(updateConfigTask);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.info({err, evtData: evt.data}, 'encountered error parsing cognigy response data');
|
||||
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'error', err});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
|
||||
async _onTranscription(cs, ep, evt) {
|
||||
this.logger.debug({evt}, `Cognigy: got transcription for callSid ${cs.callSid}`);
|
||||
const utterance = evt.alternatives[0].transcript;
|
||||
|
||||
//if we have barge in enabled AND we enabled skipping until next question
|
||||
//then stop execution of currently queues bot output before sending the
|
||||
//response to waiting bot since otherwise we could stop upcoming bot output
|
||||
|
||||
if (this.config.bargeInEnabled && this.config.skipToBotOutputEnd !== false) {
|
||||
// clear task queue, resolve the last promise and cleanup;
|
||||
this._killSayTasks();
|
||||
this.taskQueue.lastPromise.resolve();
|
||||
this.taskQueue.end();
|
||||
while(this.changeConfigTasks.length > 0){
|
||||
// apply all the config tasks FIFO
|
||||
const changeConfigTask = this.changeConfigTasks.shift();
|
||||
changeConfigTask();
|
||||
}
|
||||
this.taskQueue.autostart = true;
|
||||
}
|
||||
|
||||
if (this.eventHook) {
|
||||
this.performHook(cs, this.eventHook, {event: 'userMessage', message: utterance})
|
||||
.then((redirected) => {
|
||||
if (redirected) {
|
||||
this.logger.info('Cognigy_onTranscription: event handler for user message redirected us to new webhook');
|
||||
this.reportedFinalAction = true;
|
||||
this.performAction({cognigyResult: 'redirect'}, false);
|
||||
if (this.transcribeTask) this.transcribeTask.kill(cs);
|
||||
}
|
||||
return;
|
||||
})
|
||||
.catch(({err}) => {
|
||||
this.logger.info({err}, 'Cognigy_onTranscription: error sending event hook');
|
||||
});
|
||||
}
|
||||
|
||||
/* send the user utterance to the bot */
|
||||
try {
|
||||
if (this.client && this.client.connected) {
|
||||
this.client.sendMessage(utterance);
|
||||
}
|
||||
else {
|
||||
// if the bot is not connected, should we maybe throw an error here?
|
||||
this.logger.info('Cognigy_onTranscription - not sending user utterance as bot is disconnected');
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({err}, 'Cognigy_onTranscription: Error sending user utterance to Cognigy - ending task');
|
||||
this.performAction({cognigyResult: 'socketError'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
this.logger.info({evt}, 'got dtmf');
|
||||
|
||||
/* send dtmf to bot */
|
||||
try {
|
||||
if (this.client && this.client.connected) {
|
||||
this.client.sendMessage(String(evt.digits));
|
||||
}
|
||||
else {
|
||||
// if the bot is not connected, should we maybe throw an error here?
|
||||
this.logger.info('Cognigy_onTranscription - not sending user dtmf as bot is disconnected');
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({err}, '_onDtmf: Error sending user dtmf to Cognigy - ending task');
|
||||
this.performAction({cognigyResult: 'socketError'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
_onError(cs, ep, err) {
|
||||
this.logger.info({err}, 'Cognigy: got error');
|
||||
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'error', err});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
|
||||
_onTimeout(cs, ep, evt) {
|
||||
const {noInputRetries, noInputSpeech, noInputUrl} = this.retry;
|
||||
this.logger.debug({evt, retry: this.retry}, 'Cognigy: got timeout');
|
||||
if (noInputRetries && this.timeoutCount++ < noInputRetries) {
|
||||
const gatherTask = this._makeGatherTask({textPrompt: noInputSpeech, urlPrompt: noInputUrl});
|
||||
gatherTask.exec(cs, ep, this)
|
||||
.catch((err) => this.logger.info({err}, 'Cognigy gather task returned error'));
|
||||
}
|
||||
else {
|
||||
if (!this.hasReportedFinalAction) this.performAction({cognigyResult: 'timeout'});
|
||||
this.reportedFinalAction = true;
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Cognigy;
|
||||
148
lib/tasks/cognigy/speech-config.js
Normal file
148
lib/tasks/cognigy/speech-config.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const obj = require('drachtio-fsmrf/lib/utils');
|
||||
const Emitter = require('events');
|
||||
const { isArray } = require('lodash');
|
||||
const lodash = require('lodash');
|
||||
const hasKeys = (obj) => typeof obj === 'object' && Object.keys(obj) > 0;
|
||||
|
||||
const stripNulls = (obj) => {
|
||||
Object.keys(obj).forEach((k) => (obj[k] === null || typeof obj[k] === 'undefined') && delete obj[k]);
|
||||
return obj;
|
||||
};
|
||||
|
||||
class SpeechConfig extends Emitter {
|
||||
constructor({logger, ep, opts = {}}) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
this.ep = ep;
|
||||
this.sessionConfig = opts.session || {};
|
||||
this.update(opts);
|
||||
}
|
||||
|
||||
_mergeConfig(changedConfig = {}) {
|
||||
const merged = lodash.mergeWith(
|
||||
this.sessionConfig,
|
||||
changedConfig,
|
||||
(objValue, sourceValue) => {
|
||||
if (Array.isArray(objValue)) {
|
||||
if (Array.isArray(sourceValue)) {
|
||||
return sourceValue;
|
||||
}
|
||||
return objValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
this.logger.debug({merged, sessionConfig: this.sessionConfig, changedConfig}, 'merged config');
|
||||
// should we override hints with empty array or leave it as it is once saved?
|
||||
// merged.recognizer.hints = changedConfig.recognizer?.hints
|
||||
return merged;
|
||||
}
|
||||
|
||||
update(session) {
|
||||
// TODO validation of session params?
|
||||
if (session) {
|
||||
this.sessionConfig = this._mergeConfig(session);
|
||||
}
|
||||
this.logger.debug({sessionLevel: this.sessionConfig}, 'SpeechConfig updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* check if we should skip all nodes until next bot input
|
||||
*/
|
||||
get skipUntilBotInput() {
|
||||
return !this.sessionConfig.bargein?.skipUntilBotInput;
|
||||
}
|
||||
/**
|
||||
* Check if barge is enabled on session level
|
||||
*/
|
||||
get bargeInEnabled() {
|
||||
return this.sessionConfig.bargein?.enable?.length > 0;
|
||||
}
|
||||
|
||||
makeSayTaskConfig({text, turnConfig = {}} = {}) {
|
||||
const synthesizer = lodash.merge({}, this.sessionConfig.synthesizer, turnConfig.synthesizer);
|
||||
return {
|
||||
text,
|
||||
synthesizer
|
||||
};
|
||||
}
|
||||
|
||||
makeGatherTaskConfig({textPrompt, urlPrompt, turnConfig = {}, listenAfterSpeech} = {}) {
|
||||
// we merge from top to bottom deeply so we wil have
|
||||
// defaults from session config and then will override them via turn config
|
||||
const opts = this._mergeConfig(turnConfig);
|
||||
|
||||
this.logger.debug({
|
||||
opts,
|
||||
sessionConfig: this.sessionConfig,
|
||||
turnConfig
|
||||
}, 'Congigy SpeechConfig:_makeGatherTask current options');
|
||||
|
||||
/* input type: speech and/or dtmf entry */
|
||||
const input = [];
|
||||
if (opts.recognizer) input.push('speech');
|
||||
if (hasKeys(opts.dtmf)) input.push('digits');
|
||||
|
||||
if (opts.synthesizer) {
|
||||
// todo remove this once we add support for disabling tts cache
|
||||
delete opts.synthesizer.disableTtsCache;
|
||||
}
|
||||
|
||||
/* bargein settings */
|
||||
const bargein = opts.bargein || {};
|
||||
const speechBargein = Array.isArray(bargein.enable) && bargein.enable.includes('speech');
|
||||
const dtmfBargein = Array.isArray(bargein.enable) && bargein.enable.includes('dtmf');
|
||||
const minBargeinWordCount = speechBargein ? (bargein.minWordCount || 1) : 0;
|
||||
const {interDigitTimeout = 0, maxDigits, minDigits = 1, submitDigit} = (opts.dtmf || {});
|
||||
const {noInputTimeout, noInputRetries, noInputSpeech, noInputUrl} = (opts.user || {});
|
||||
|
||||
let sayConfig;
|
||||
let playConfig;
|
||||
|
||||
if (textPrompt) {
|
||||
sayConfig = {
|
||||
text: textPrompt,
|
||||
synthesizer: opts.synthesizer
|
||||
};
|
||||
}
|
||||
|
||||
// todo what is the logic here if we put both? play over say or say over play?
|
||||
if (urlPrompt) {
|
||||
playConfig = {
|
||||
url: urlPrompt
|
||||
};
|
||||
}
|
||||
|
||||
const config = {
|
||||
input,
|
||||
listenDuringPrompt: speechBargein,
|
||||
bargein: speechBargein,
|
||||
minBargeinWordCount,
|
||||
dtmfBargein,
|
||||
minDigits,
|
||||
maxDigits,
|
||||
interDigitTimeout,
|
||||
finishOnKey: submitDigit,
|
||||
recognizer: opts?.recognizer,
|
||||
timeout: noInputTimeout,
|
||||
retry : {
|
||||
noInputRetries,
|
||||
noInputSpeech,
|
||||
noInputUrl
|
||||
},
|
||||
listenAfterSpeech
|
||||
};
|
||||
|
||||
const final = stripNulls(config);
|
||||
|
||||
const finalConfig = final;
|
||||
if (sayConfig) {
|
||||
finalConfig.say = sayConfig;
|
||||
} else if (playConfig) {
|
||||
finalConfig.play = playConfig;
|
||||
}
|
||||
this.logger.info({finalConfig}, 'created gather config');
|
||||
return finalConfig;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SpeechConfig;
|
||||
@@ -16,13 +16,17 @@ class TaskGather extends Task {
|
||||
this.preconditions = TaskPreconditions.Endpoint;
|
||||
|
||||
[
|
||||
'finishOnKey', 'hints', 'input', 'numDigits',
|
||||
'partialResultHook',
|
||||
'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits',
|
||||
'interDigitTimeout', 'submitDigit', 'partialResultHook', 'bargein', 'dtmfBargein',
|
||||
'retries', 'retryPromptTts', 'retryPromptUrl',
|
||||
'speechTimeout', 'timeout', 'say', 'play'
|
||||
].forEach((k) => this[k] = this.data[k]);
|
||||
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
|
||||
this.minBargeinWordCount = this.data.minBargeinWordCount || 1;
|
||||
|
||||
this.timeout = (this.timeout || 5) * 1000;
|
||||
this.interim = this.partialResultCallback;
|
||||
this.logger.debug({opts}, 'created gather task');
|
||||
this.timeout = (this.timeout || 15) * 1000;
|
||||
this.interim = this.partialResultCallback || this.bargein;
|
||||
if (this.data.recognizer) {
|
||||
const recognizer = this.data.recognizer;
|
||||
this.vendor = recognizer.vendor;
|
||||
@@ -40,6 +44,10 @@ class TaskGather extends Task {
|
||||
this.profanityOption = recognizer.profanityOption || 'raw';
|
||||
this.requestSnr = recognizer.requestSnr || false;
|
||||
this.initialSpeechTimeoutMs = recognizer.initialSpeechTimeoutMs || 0;
|
||||
|
||||
/* vad: if provided, we dont connect to recognizer until voice activity is detected */
|
||||
const {enable, voiceMs = 0, mode = -1} = recognizer.vad || {};
|
||||
this.vad = {enable, voiceMs, mode};
|
||||
}
|
||||
|
||||
this.digitBuffer = '';
|
||||
@@ -48,6 +56,12 @@ class TaskGather extends Task {
|
||||
if (this.say) this.sayTask = makeTask(this.logger, {say: this.say}, this);
|
||||
if (this.play) this.playTask = makeTask(this.logger, {play: this.play}, this);
|
||||
|
||||
if(this.sayTask || this.playTask){
|
||||
// this is specially for barge in where we want to make a bargebale promt
|
||||
// to a user without listening after the say task has finished
|
||||
this.listenAfterSpeech = typeof this.data.listenAfterSpeech === "boolean" ? this.data.listenAfterSpeech : true;
|
||||
}
|
||||
|
||||
this.parentTask = parentTask;
|
||||
}
|
||||
|
||||
@@ -80,33 +94,67 @@ class TaskGather extends Task {
|
||||
throw new Error(`no speech-to-text service credentials for ${this.vendor} have been configured`);
|
||||
}
|
||||
|
||||
const startListening = (cs, ep) => {
|
||||
this._startTimer();
|
||||
if (this.input.includes('speech') && !this.listenDuringPrompt) {
|
||||
this.logger.debug('listening after speech 1');
|
||||
this._initSpeech(cs, ep)
|
||||
.then(() => {
|
||||
this._startTranscribing(ep);
|
||||
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.sayTask) {
|
||||
this.sayTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.sayTask.on('playDone', (err) => {
|
||||
if (!this.killed) this._startTimer();
|
||||
this.logger.debug('Gather: kicking off say task');
|
||||
this.sayTask.exec(cs, ep);
|
||||
this.sayTask.on('playDone', async(err) => {
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing tts');
|
||||
this.logger.debug('Gather: say task completed');
|
||||
if (!this.killed) {
|
||||
if (this.listenAfterSpeech === true) {
|
||||
this.logger.debug('listening after speech 2');
|
||||
startListening(cs, ep);
|
||||
} else {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (this.playTask) {
|
||||
this.playTask.exec(cs, ep); // kicked off, _not_ waiting for it to complete
|
||||
this.playTask.on('playDone', (err) => {
|
||||
if (!this.killed) this._startTimer();
|
||||
});
|
||||
this.playTask.on('playDone', async(err) => {
|
||||
if (err) return this.logger.error({err}, 'Gather:exec Error playing url');
|
||||
if (!this.killed) {
|
||||
if (this.listenAfterSpeech === true) {
|
||||
this.logger.debug('listening after speech 3');
|
||||
startListening(cs, ep);
|
||||
} else {
|
||||
this.notifyTaskDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else this._startTimer();
|
||||
else startListening(cs, ep);
|
||||
|
||||
if (this.input.includes('speech')) {
|
||||
if (this.input.includes('speech') && this.listenDuringPrompt) {
|
||||
this.logger.debug('listening after speech 4');
|
||||
await this._initSpeech(cs, ep);
|
||||
this._startTranscribing(ep);
|
||||
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
|
||||
.catch(() => {/*already logged error */});
|
||||
}
|
||||
|
||||
if (this.input.includes('digits')) {
|
||||
if (this.input.includes('digits') || this.dtmfBargein) {
|
||||
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
|
||||
}
|
||||
|
||||
await this.awaitTaskDone();
|
||||
this.logger.debug('Gather:exec task has completed');
|
||||
} catch (err) {
|
||||
this.logger.error(err, 'TaskGather:exec error');
|
||||
}
|
||||
@@ -118,6 +166,7 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
kill(cs) {
|
||||
this.logger.debug('Gather:kill');
|
||||
super.kill(cs);
|
||||
this._killAudio(cs);
|
||||
this.ep.removeAllListeners('dtmf');
|
||||
@@ -126,17 +175,37 @@ class TaskGather extends Task {
|
||||
|
||||
_onDtmf(cs, ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onDtmf');
|
||||
if (evt.dtmf === this.finishOnKey) this._resolve('dtmf-terminator-key');
|
||||
clearTimeout(this.interDigitTimer);
|
||||
let resolved = false;
|
||||
if (this.dtmfBargein) this._killAudio(cs);
|
||||
if (evt.dtmf === this.finishOnKey) {
|
||||
resolved = true;
|
||||
this._resolve('dtmf-terminator-key');
|
||||
}
|
||||
else {
|
||||
this.digitBuffer += evt.dtmf;
|
||||
if (this.digitBuffer.length === this.numDigits) this._resolve('dtmf-num-digits');
|
||||
const len = this.digitBuffer.length;
|
||||
if (len === this.numDigits || len === this.maxDigits) {
|
||||
resolved = true;
|
||||
this._resolve('dtmf-num-digits');
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved && this.interDigitTimeout > 0 && this.digitBuffer.length >= this.minDigits) {
|
||||
/* start interDigitTimer */
|
||||
const ms = this.interDigitTimeout * 1000;
|
||||
this.logger.debug(`starting interdigit timer of ${ms}`);
|
||||
this.interDigitTimer = setTimeout(() => this._resolve('dtmf-interdigit-timeout'), ms);
|
||||
}
|
||||
this._killAudio(cs);
|
||||
}
|
||||
|
||||
async _initSpeech(cs, ep) {
|
||||
const opts = {};
|
||||
|
||||
if (this.vad.enable) {
|
||||
opts.START_RECOGNIZING_ON_VAD = 1;
|
||||
if (this.vad.voiceMs) opts.RECOGNIZER_VAD_VOICE_MS = this.vad.voiceMs;
|
||||
if (this.vad.mode >= 0 && this.vad.mode <= 3) opts.RECOGNIZER_VAD_MODE = this.vad.mode;
|
||||
}
|
||||
if ('google' === this.vendor) {
|
||||
if (this.sttCredentials) opts.GOOGLE_APPLICATION_CREDENTIALS = JSON.stringify(this.sttCredentials.credentials);
|
||||
Object.assign(opts, {
|
||||
@@ -197,7 +266,7 @@ class TaskGather extends Task {
|
||||
ep.startTranscription({
|
||||
vendor: this.vendor,
|
||||
locale: this.language,
|
||||
interim: this.partialResultCallback ? true : false,
|
||||
interim: this.interim,
|
||||
}).catch((err) => {
|
||||
const {writeAlerts, AlertType} = this.cs.srf.locals;
|
||||
this.logger.error(err, 'TaskGather:_startTranscribing error');
|
||||
@@ -237,25 +306,46 @@ class TaskGather extends Task {
|
||||
}
|
||||
|
||||
_onTranscription(cs, ep, evt) {
|
||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||
if ('aws' === this.vendor && Array.isArray(evt) && evt.length > 0) evt = evt[0];
|
||||
if ('microsoft' === this.vendor) {
|
||||
const nbest = evt.NBest;
|
||||
const newEvent = {
|
||||
is_final: evt.RecognitionStatus === 'Success',
|
||||
alternatives: [
|
||||
{
|
||||
confidence: nbest[0].Confidence,
|
||||
transcript: nbest[0].Display
|
||||
}
|
||||
]
|
||||
};
|
||||
evt = newEvent;
|
||||
const final = evt.RecognitionStatus === 'Success';
|
||||
if (final) {
|
||||
const nbest = evt.NBest;
|
||||
evt = {
|
||||
is_final: true,
|
||||
alternatives: [
|
||||
{
|
||||
confidence: nbest[0].Confidence,
|
||||
transcript: nbest[0].Display
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
else {
|
||||
evt = {
|
||||
is_final: false,
|
||||
alternatives: [
|
||||
{
|
||||
transcript: evt.Text
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
this.logger.debug(evt, 'TaskGather:_onTranscription');
|
||||
if (evt.is_final) this._resolve('speech', evt);
|
||||
else if (this.partialResultHook) {
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'GatherTask:_onTranscription error'));
|
||||
else {
|
||||
if (evt.stability > 0.70 &&
|
||||
this.bargein &&
|
||||
evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) {
|
||||
this.logger.debug('Gather:_onTranscription - killing audio due to bargein');
|
||||
this._killAudio(cs);
|
||||
this._resolve('speech', evt);
|
||||
}
|
||||
if (this.partialResultHook) {
|
||||
this.cs.requestor.request(this.partialResultHook, Object.assign({speech: evt}, this.cs.callInfo))
|
||||
.catch((err) => this.logger.info(err, 'GatherTask:_onTranscription error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
_onEndOfUtterance(cs, ep) {
|
||||
@@ -281,7 +371,8 @@ class TaskGather extends Task {
|
||||
|
||||
this._clearTimer();
|
||||
if (reason.startsWith('dtmf')) {
|
||||
await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
|
||||
if (this.parentTask) this.parentTask.emit('dtmf-collected', {reason, digits: this.digitBuffer});
|
||||
else await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
|
||||
}
|
||||
else if (reason.startsWith('speech')) {
|
||||
if (this.parentTask) this.parentTask.emit('transcription', evt);
|
||||
|
||||
@@ -21,15 +21,20 @@ class TaskSay extends Task {
|
||||
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
|
||||
const {writeAlerts, AlertType, stats} = srf.locals;
|
||||
const {synthAudio} = srf.locals.dbHelpers;
|
||||
const hasVerbLevelTts = this.synthesizer.vendor && this.synthesizer.vendor !== 'default';
|
||||
const vendor = hasVerbLevelTts ? this.synthesizer.vendor : cs.speechSynthesisVendor ;
|
||||
const language = hasVerbLevelTts ? this.synthesizer.language : cs.speechSynthesisLanguage ;
|
||||
const voice = hasVerbLevelTts ? this.synthesizer.voice : cs.speechSynthesisVoice ;
|
||||
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ? this.synthesizer.vendor : cs.speechSynthesisVendor;
|
||||
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ? this.synthesizer.language : cs.speechSynthesisLanguage ;
|
||||
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ? this.synthesizer.voice : cs.speechSynthesisVoice;
|
||||
const engine = this.synthesizer.engine || 'standard';
|
||||
const salt = cs.callSid;
|
||||
const credentials = cs.getSpeechCredentials(vendor, 'tts');
|
||||
|
||||
this.logger.info({language, voice}, `Task:say - using vendor: ${vendor}`);
|
||||
this.logger.info({language,
|
||||
voice,
|
||||
localSynthesizer: this.synthesizer,
|
||||
speechSynthesisVendor: cs.speechSynthesisVendor,
|
||||
speechSynthesisLanguage: cs.speechSynthesisLanguage,
|
||||
speechSynthesisVoice: cs.speechSynthesisVoice
|
||||
}, `Task:say - using vendor: ${vendor}`);
|
||||
this.ep = ep;
|
||||
try {
|
||||
if (!credentials) {
|
||||
@@ -79,7 +84,11 @@ class TaskSay extends Task {
|
||||
const {memberId, confName, confUuid} = cs;
|
||||
await this.playToConfMember(this.ep, memberId, confName, confUuid, filepath[segment]);
|
||||
}
|
||||
else await ep.play(filepath[segment]);
|
||||
else {
|
||||
this.logger.debug(`Say:exec sending command to play file ${filepath[segment]}`);
|
||||
await ep.play(filepath[segment]);
|
||||
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
|
||||
}
|
||||
} while (!this.killed && ++segment < filepath.length);
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -100,7 +100,15 @@
|
||||
"numDigits": "number",
|
||||
"partialResultHook": "object|string",
|
||||
"speechTimeout": "number",
|
||||
"listenDuringPrompt": "boolean",
|
||||
"bargein": "boolean",
|
||||
"minBargeinWordCount": "number",
|
||||
"dtmfBargein": "boolean",
|
||||
"minDigits": "number",
|
||||
"maxDigits": "number",
|
||||
"interDigitTimeout": "number",
|
||||
"timeout": "number",
|
||||
"listenAfterSpeech": "boolean",
|
||||
"recognizer": "#recognizer",
|
||||
"play": "#play",
|
||||
"say": "#say"
|
||||
|
||||
57
package-lock.json
generated
57
package-lock.json
generated
@@ -26,9 +26,11 @@
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^5.0.2",
|
||||
"ip": "^1.1.5",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"parse-url": "^5.0.7",
|
||||
"pino": "^6.13.4",
|
||||
"pino": "^6.13.2",
|
||||
"queue": "^6.0.2",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"verify-aws-sns-signature": "^0.0.6",
|
||||
@@ -2251,6 +2253,11 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz",
|
||||
"integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig=="
|
||||
},
|
||||
"node_modules/fastify-warning": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz",
|
||||
"integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw=="
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
|
||||
@@ -4042,15 +4049,15 @@
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "6.13.4",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.4.tgz",
|
||||
"integrity": "sha512-g4tHSISmQJYUEKEMVdaZ+ZokWwFnTwZL5JPn+lnBVZ1BuBbrSchrXwQINknkM5+Q4fF6U9NjiI8PWwwMDHt9zA==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz",
|
||||
"integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==",
|
||||
"dependencies": {
|
||||
"fast-redact": "^3.0.0",
|
||||
"fast-safe-stringify": "^2.0.8",
|
||||
"fastify-warning": "^0.2.0",
|
||||
"flatstr": "^1.0.12",
|
||||
"pino-std-serializers": "^3.1.0",
|
||||
"process-warning": "^1.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"sonic-boom": "^1.0.2"
|
||||
},
|
||||
@@ -4095,11 +4102,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz",
|
||||
"integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -4193,6 +4195,15 @@
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"node_modules/queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://cognigy.pkgs.visualstudio.com/_packaging/cognigy-feed/npm/registry/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha1-uRUlKD4jFcdVPS76GNg+dkMv7WU=",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz",
|
||||
@@ -7267,6 +7278,11 @@
|
||||
"resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz",
|
||||
"integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig=="
|
||||
},
|
||||
"fastify-warning": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz",
|
||||
"integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw=="
|
||||
},
|
||||
"file-entry-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz",
|
||||
@@ -8627,15 +8643,15 @@
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"pino": {
|
||||
"version": "6.13.4",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.4.tgz",
|
||||
"integrity": "sha512-g4tHSISmQJYUEKEMVdaZ+ZokWwFnTwZL5JPn+lnBVZ1BuBbrSchrXwQINknkM5+Q4fF6U9NjiI8PWwwMDHt9zA==",
|
||||
"version": "6.13.3",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz",
|
||||
"integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==",
|
||||
"requires": {
|
||||
"fast-redact": "^3.0.0",
|
||||
"fast-safe-stringify": "^2.0.8",
|
||||
"fastify-warning": "^0.2.0",
|
||||
"flatstr": "^1.0.12",
|
||||
"pino-std-serializers": "^3.1.0",
|
||||
"process-warning": "^1.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"sonic-boom": "^1.0.2"
|
||||
}
|
||||
@@ -8668,11 +8684,6 @@
|
||||
"fromentries": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"process-warning": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz",
|
||||
"integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q=="
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -8745,6 +8756,14 @@
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="
|
||||
},
|
||||
"queue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://cognigy.pkgs.visualstudio.com/_packaging/cognigy-feed/npm/registry/queue/-/queue-6.0.2.tgz",
|
||||
"integrity": "sha1-uRUlKD4jFcdVPS76GNg+dkMv7WU=",
|
||||
"requires": {
|
||||
"inherits": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"quick-format-unescaped": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.3.tgz",
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"start": "node app",
|
||||
"test": "NODE_ENV=test JAMBONES_HOSTING=1 DRACHTIO_HOST=127.0.0.1 DRACHTIO_PORT=9060 DRACHTIO_SECRET=cymru JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=127.0.0.1 JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug ENABLE_METRICS=0 HTTP_PORT=3000 JAMBONES_SBCS=172.38.0.10 JAMBONES_FREESWITCH=127.0.0.1:8022:ClueCon:docker-host JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_NETWORK_CIDR=172.38.0.0/16 node test/ ",
|
||||
"coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test",
|
||||
"jslint": "eslint app.js lib"
|
||||
"jslint": "eslint app.js lib",
|
||||
"jslint:fix": "eslint app.js lib --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cognigy/socket-client": "^4.5.5",
|
||||
@@ -43,9 +44,11 @@
|
||||
"express": "^4.17.1",
|
||||
"helmet": "^5.0.2",
|
||||
"ip": "^1.1.5",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"parse-url": "^5.0.7",
|
||||
"pino": "^6.13.4",
|
||||
"pino": "^6.13.2",
|
||||
"queue": "^6.0.2",
|
||||
"to-snake-case": "^1.0.0",
|
||||
"uuid": "^8.3.2",
|
||||
"verify-aws-sns-signature": "^0.0.6",
|
||||
|
||||
Reference in New Issue
Block a user