@onereach/step-voice
Version:
Onereach.ai Voice Steps
221 lines (220 loc) • 9.55 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 getPhrasesForSpeechRec = (choices) => {
const texts = lodash_1.default.flatMap(choices, (choice) => choice.options.map((opt) => opt.text));
return texts;
};
const getAsrSettings = (choices, recognitionModel) => {
// this step works only with the Google engine
// hardcoded en-US lang will be changed to dynamic after step testing and design approval
return {
enabled: true,
engine: 'google',
language: 'en-US',
config: {
speechContexts: [
{
phrases: getPhrasesForSpeechRec(choices)
}
],
recognitionModel
}
};
};
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)));
};
const alterRecognitionPhrases = (phrases, interpretation) => {
if (interpretation.length !== 1)
return;
switch (interpretation[0]) {
case '$POSTALCODE':
phrases.forEach(p => { p.lexical = p.lexical.replace(/\s+/g, ''); });
break;
case '$PERCENT':
phrases.forEach(p => { p.lexical = p.lexical.replace(/\s+(?=%)/, ''); });
break;
}
};
class CustomVoiceInput extends voice_1.default {
async runStep() {
const call = await this.fetchData();
const { textType, tts, sensitiveData, noReplyDelay, usePromptsTriggers, recognitionModel, phraseList } = this.data;
const exitExists = (exitId) => {
return lodash_1.default.some(choices, (choice) => choice.exitId === exitId);
};
const mainLeg = {
exitId: '5886d1ee-db6d-45ca-a2ce-d586862c65ff',
dtmf: null,
grxml: '',
text: '.+',
grammar: {}
};
const choices = this.buildChoices({ choices: [...this.data.choices, mainLeg] });
const ttsSettings = tts.getSettings(call.tts);
const asrSettings = getAsrSettings(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: 'double-asr',
params: {
grammar,
sections: [],
resultInterpretation: this.data.resultInterpretation,
tokenPhrases: phraseList || []
}
};
// 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) {
// digit recognition removed
case 'recognition': {
const params = event.params;
const exitId = params.exitId;
alterRecognitionPhrases(params.phrases, this.data.resultInterpretation);
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));
}
else if (this.rptsHasMore({ repromptsList }) &&
(usePromptsTriggers ? isRepromptTrigger(phrases, this.data.promptsTriggers) : true)) {
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));
}
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', {});
}
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 = CustomVoiceInput;
// --------------------------------------------------------------------