diff --git a/lib/tasks/gather.js b/lib/tasks/gather.js index 8a381b73..03ff4de0 100644 --- a/lib/tasks/gather.js +++ b/lib/tasks/gather.js @@ -9,6 +9,7 @@ const { const makeTask = require('./make_task'); const assert = require('assert'); +const GATHER_STABILITY_THRESHOLD = Number(process.env.JAMBONZ_GATHER_STABILITY_THRESHOLD || 0.7); class TaskGather extends Task { constructor(logger, opts, parentTask) { @@ -16,11 +17,14 @@ class TaskGather extends Task { this.preconditions = TaskPreconditions.Endpoint; [ - 'finishOnKey', 'hints', 'input', 'numDigits', - 'partialResultHook', 'bargein', + 'finishOnKey', 'hints', 'input', 'numDigits', 'minDigits', 'maxDigits', + 'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein', 'speechTimeout', 'timeout', 'say', 'play' ].forEach((k) => this[k] = this.data[k]); + /* when collecting dtmf, bargein on dtmf is true unless explicitly set to false */ + if (this.dtmfBargein !== false && this.input.includes('digits')) this.dtmfBargein = true; + this.timeout = (this.timeout || 15) * 1000; this.interim = this.partialResultCallback; if (this.data.recognizer) { @@ -123,7 +127,7 @@ class TaskGather extends Task { .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)); } @@ -142,17 +146,33 @@ class TaskGather extends Task { super.kill(cs); this._killAudio(cs); this.ep.removeAllListeners('dtmf'); + clearTimeout(this.interDigitTimer); this._resolve('killed'); } _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 && this.input.includes('digits')) { + 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) { @@ -292,19 +312,20 @@ class TaskGather extends Task { } if (evt.is_final) this._resolve('speech', evt); else { - if (this.bargein && evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) { - this.logger.debug('Gather:_onTranscription - killing audio due to bargein'); + /* google has a measure of stability: + https://cloud.google.com/speech-to-text/docs/basics#streaming_responses + others do not. + */ + const isStableEnough = typeof evt.stability === 'undefined' || evt.stability > GATHER_STABILITY_THRESHOLD; + + if (this.bargein && isStableEnough && + evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) { + this.logger.debug('Gather:_onTranscription - killing audio due to speech bargein'); this._killAudio(cs); } - else { - if (this.bargein && evt.alternatives[0].transcript.split(' ').length >= this.minBargeinWordCount) { - this.logger.debug('Gather:_onTranscription - killing audio due to bargein'); - this._killAudio(cs); - } - 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')); - } + 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')); } } } @@ -323,6 +344,7 @@ class TaskGather extends Task { if (this.resolved) return; this.resolved = true; this.logger.debug(`TaskGather:resolve with reason ${reason}`); + clearTimeout(this.interDigitTimer); if (this.ep && this.ep.connected) { this.ep.stopTranscription({vendor: this.vendor}) diff --git a/lib/tasks/say.js b/lib/tasks/say.js index 2cf3331f..d6b6626a 100644 --- a/lib/tasks/say.js +++ b/lib/tasks/say.js @@ -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({vendor, language, voice}, 'TaskSay:exec'); this.ep = ep; try { if (!credentials) { diff --git a/lib/tasks/specs.json b/lib/tasks/specs.json index 6878491c..93ba81f7 100644 --- a/lib/tasks/specs.json +++ b/lib/tasks/specs.json @@ -98,9 +98,13 @@ "finishOnKey": "string", "input": "array", "numDigits": "number", + "minDigits": "number", + "maxDigits": "number", + "interDigitTimeout": "number", "partialResultHook": "object|string", "speechTimeout": "number", "listenDuringPrompt": "boolean", + "dtmfBargein": "boolean", "bargein": "boolean", "minBargeinWordCount": "number", "timeout": "number",