@onereach/step-voice
Version:
Onereach.ai Voice Steps
276 lines (275 loc) • 12 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const voice_1 = tslib_1.__importDefault(require("./voice"));
const digitWords = [
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine'
];
const containsOnlyDigits = (stringsList) => {
let isOnlyDigits = true;
stringsList.forEach((str) => {
if (digitWords.includes(String(str).toLowerCase())) {
// Do nothing
}
else if (isNaN(str)) {
isOnlyDigits = false;
}
});
return isOnlyDigits;
};
const getPhrasesForSpeechRec = (choices) => {
const texts = lodash_1.default.flatMap(choices, (choice) => choice.options.map((opt) => opt.text));
if (containsOnlyDigits(texts)) {
return ['$OOV_CLASS_DIGIT_SEQUENCE'];
}
return texts;
};
const getAsrSettings = (settings, choices, recognitionModel) => {
if (settings.engine === 'google' || settings.engine === 'google_beta') {
return {
...settings,
config: {
speechContexts: [
{
phrases: getPhrasesForSpeechRec(choices)
}
],
recognitionModel
}
};
}
return settings;
};
const isRepromptTrigger = (recogResult, promptsTriggers) => {
const phrases = recogResult.map((r) => {
return r.lexical;
});
const triggers = promptsTriggers.flatMap((trigger) => {
if (lodash_1.default.isEmpty(trigger.grammar)) {
return trigger.text.replace(/`/g, '');
}
else {
return trigger.grammar.value.map((v) => {
return v.replace(/`/g, '');
});
}
});
return !lodash_1.default.isEmpty(phrases.filter((e) => triggers.includes(e)));
};
class Choice extends voice_1.default {
async runStep() {
const call = await this.fetchData();
const { textType, asr, tts, sensitiveData, noReplyDelay, usePromptsTriggers, recognitionModel, useInterspeechTimeout, interSpeechTimeout, longRecognition, usePromptsForUnrecognized } = this.data;
const exitExists = (exitId) => {
return lodash_1.default.some(choices, (choice) => choice.exitId === exitId);
};
const choices = this.buildChoices({ choices: this.data.choices });
const ttsSettings = tts.getSettings(call.tts);
const asrSettings = getAsrSettings(asr.getSettings(call.asr), choices, recognitionModel);
const repromptsList = this.buildReprompts({ prompts: this.data.prompts });
const speechSections = this.buildSections({ sections: this.data.audio, textType, ttsSettings });
const grammar = {
id: this.currentStepId,
choices,
asr: asrSettings
};
const command = {
name: 'speak',
params: {
grammar,
dictation: longRecognition
? 'continuous'
: useInterspeechTimeout,
interSpeechTimeout: interSpeechTimeout * 1000,
sections: []
}
};
// There's a specific need to do so. There might be ${variable} section
this.triggers.local(`in/voice/${call.id}`, async (event) => {
const reportingSettingsKey = this.rptsStarted ? 'transcriptRepromptResponse' : 'transcriptResponse';
await this.handleInterruption({
call,
event,
speechSections,
repromptsList,
reportingSettingsKey: this.rptsStarted ? 'transcriptReprompt' : 'transcriptPrompt'
});
switch (event.params.type) {
case 'digit':
case 'digits': {
const params = event.params;
const digit = params.digit ?? params.digits;
const exitId = params.exitId;
// On bargeIn, we should stop playback
if (exitExists(exitId)) {
const exitStepLabel = this.getExitStepLabel(exitId) ?? '';
const message = lodash_1.default.replace(exitStepLabel, /^(\d+\. )?(~ )?/, '');
await this.transcript(call, {
keyPress: digit,
message: message,
reportingSettingsKey,
action: 'Call DTMF',
actionFromBot: false
});
await this.resumeRecording(call, sensitiveData);
return this.exitStep(exitId, this.exitChoiceData('dtmf', params), longRecognition);
}
else if ((lodash_1.default.isUndefined(usePromptsForUnrecognized) || usePromptsForUnrecognized) && this.rptsHasMore({ repromptsList })) {
await this.transcript(call, {
message: 'Unrecognized',
keyPress: digit,
reportingSettingsKey,
action: 'Call DTMF',
actionFromBot: false
});
await this.rptsSend(call, {
command,
repromptsList,
noReplyDelay,
speechSections,
textType,
ttsSettings,
sensitiveData
});
return this.exitFlow();
}
await this.transcript(call, {
message: 'Not Recognized',
keyPress: digit,
reportingSettingsKey,
action: 'Call DTMF',
actionFromBot: false
});
await this.resumeRecording(call, sensitiveData);
return this.exitStep('unrecognized', this.exitChoiceData('dtmf', { digit }), longRecognition);
}
case 'recognition': {
const params = event.params;
const exitId = params.exitId;
const phrases = params.phrases;
if (lodash_1.default.isEmpty(phrases)) {
await this.resumeRecording(call, sensitiveData);
return this.exitStep('unrecognized', {});
}
const voiceProcessResult = lodash_1.default.chain(phrases)
.map((p) => p.lexical)
.join(' | ')
.value();
// On bargeIn, we should stop playback
if (exitExists(exitId)) {
await this.transcript(call, {
message: lodash_1.default.replace(this.getExitStepLabel(exitId) ?? '', /^(\d+\. )?(~ )?/, ''),
voiceProcessResult,
reportingSettingsKey,
action: 'Call Recognition',
actionFromBot: false
});
await this.resumeRecording(call, sensitiveData);
// There might be hooks after this step which we will try to avoid
return this.exitStep(exitId, this.exitChoiceData('voice', params), longRecognition);
}
else if (((lodash_1.default.isUndefined(usePromptsForUnrecognized) || usePromptsForUnrecognized) ||
(usePromptsTriggers ? isRepromptTrigger(phrases, this.data.promptsTriggers) : false)) &&
this.rptsHasMore({ repromptsList })) {
await this.transcript(call, {
message: 'Unrecognized',
voiceProcessResult,
reportingSettingsKey,
action: 'Call Recognition',
actionFromBot: false
});
await this.rptsSend(call, {
command,
repromptsList,
noReplyDelay,
speechSections,
textType,
ttsSettings,
sensitiveData
});
return this.exitFlow();
}
// There's no more reprompts, should move on to unrecognized direction
await this.transcript(call, {
message: 'Not Recognized',
voiceProcessResult,
reportingSettingsKey,
action: 'Call Recognition',
actionFromBot: false
});
await this.resumeRecording(call, sensitiveData);
// We might end up in same session
return this.exitStep('unrecognized', this.exitChoiceData('voice', params), longRecognition);
}
case 'timeout': {
if (this.rptsHasMore({ repromptsList })) {
await this.transcript(call, {
message: 'No Reply',
reportingSettingsKey,
action: 'Call Prompt',
actionFromBot: false
});
await this.rptsSend(call, {
command,
repromptsList,
noReplyDelay,
speechSections,
textType,
ttsSettings,
sensitiveData
});
return this.exitFlow();
}
await this.transcript(call, {
message: 'Not Replied',
reportingSettingsKey,
action: 'Call Prompt',
actionFromBot: false
});
await this.resumeRecording(call, sensitiveData);
// We might end up in same session
return this.exitStep('no reply', {}, longRecognition);
}
case 'hangup': {
await this.handleHangup(call);
return await this.waitConvEnd();
}
case 'cancel': {
return this.handleCancel();
}
case 'error':
return this.throwError(event.params.error);
default:
return this.exitFlow();
}
});
this.triggers.otherwise(async () => {
const eventId = await this.transcript(call, {
sections: speechSections,
reprompt: {
maxAttempts: repromptsList.length,
attempt: 0
},
reportingSettingsKey: 'transcriptPrompt',
action: 'Call Prompt',
actionFromBot: true
});
command.params.reporterTranscriptEventId = eventId;
command.params.sections = speechSections;
command.params.timeout = this.rptsTimeout({ noReplyDelay, repromptsList, initial: true });
await this.pauseRecording(call, command, sensitiveData);
return this.exitFlow();
});
}
}
exports.default = Choice;