Support ASR TTS fallback (#713)

* asr/tts fallback

* add notification

* add notification

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
This commit is contained in:
Hoan Luu Huu
2024-04-15 19:55:41 +07:00
committed by GitHub
parent eeec8c3099
commit 2184feb414
5 changed files with 133 additions and 59 deletions

View File

@@ -188,6 +188,24 @@ class CallSession extends Emitter {
this._synthesizer = synth; this._synthesizer = synth;
} }
/**
* ASR TTS fallback
*/
get hasFallbackAsr() {
return this._hasFallbackAsr || false;
}
set hasFallbackAsr(i) {
this._hasFallbackAsr = i;
}
get hasFallbackTts() {
return this._hasFallbackTts || false;
}
set hasFallbackTts(i) {
this._hasFallbackTts = i;
}
/** /**
* default vendor to use for speech synthesis if not provided in the app * default vendor to use for speech synthesis if not provided in the app
*/ */
@@ -1832,6 +1850,7 @@ Duration=${duration} `
/** /**
* called when the caller has hung up. Provided for subclasses to override * called when the caller has hung up. Provided for subclasses to override
* in order to apply logic at this point if needed. * in order to apply logic at this point if needed.
* return true if success fallback, return false if not
*/ */
_callerHungup() { _callerHungup() {
assert(false, 'subclass responsibility to override this method'); assert(false, 'subclass responsibility to override this method');

View File

@@ -174,12 +174,7 @@ class TaskGather extends SttTask {
this._startTranscribing(ep); this._startTranscribing(ep);
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
} catch (e) { } catch (e) {
if (this.fallbackVendor && this.isHandledByPrimaryProvider) { await this._startFallback(cs, ep, {error: e});
await this._fallback();
startListening(cs, ep);
} else {
this.logger.error({error: e}, 'error in initSpeech');
}
} }
} }
}; };
@@ -906,9 +901,9 @@ class TaskGather extends SttTask {
_onTranscriptionComplete(cs, ep) { _onTranscriptionComplete(cs, ep) {
this.logger.debug('TaskGather:_onTranscriptionComplete'); this.logger.debug('TaskGather:_onTranscriptionComplete');
} }
async _onJambonzError(cs, ep, evt) {
this.logger.info({evt}, 'TaskGather:_onJambonzError'); async _startFallback(cs, ep, evt) {
if (this.isHandledByPrimaryProvider && this.fallbackVendor) { if (this.canFallback) {
ep.stopTranscription({ ep.stopTranscription({
vendor: this.vendor, vendor: this.vendor,
bugname: this.bugname bugname: this.bugname
@@ -916,17 +911,31 @@ class TaskGather extends SttTask {
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`)); .catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
try { try {
await this._fallback(); this.logger.debug('gather:_startFallback');
await this._initSpeech(cs, ep); this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'in progress'});
await this._initFallback();
this._speechHandlersSet = false;
await this._setSpeechHandlers(cs, ep);
this._startTranscribing(ep); this._startTranscribing(ep);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
return; return true;
} catch (error) { } catch (error) {
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`); this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
} }
} else {
this.logger.debug('gather:_startFallback no condition for falling back');
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
}
return false;
} }
const {writeAlerts, AlertType} = cs.srf.locals;
async _onJambonzError(cs, ep, evt) {
this.logger.info({evt}, 'TaskGather:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') { if (this.vendor === 'nuance') {
const {code, error} = evt; const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout'); if (code === 404 && error === 'No speech') return this._resolve('timeout');
@@ -939,18 +948,24 @@ class TaskGather extends SttTask {
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`, message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor, vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`}); if (!(await this._startFallback(cs, ep, evt))) {
}
_onVendorConnectFailure(cs, _ep, evt) {
super._onVendorConnectFailure(cs, _ep, evt);
this.notifyTaskDone(); this.notifyTaskDone();
} }
}
_onVendorError(cs, _ep, evt) { async _onVendorConnectFailure(cs, _ep, evt) {
super._onVendorConnectFailure(cs, _ep, evt);
if (!(await this._startFallback(cs, _ep, evt))) {
this.notifyTaskDone();
}
}
async _onVendorError(cs, _ep, evt) {
super._onVendorError(cs, _ep, evt); super._onVendorError(cs, _ep, evt);
if (!(await this._startFallback(cs, _ep, evt))) {
this._resolve('stt-error', evt); this._resolve('stt-error', evt);
} }
}
_onVadDetected(cs, ep) { _onVadDetected(cs, ep) {
if (this.bargein && this.minBargeinWordCount === 0) { if (this.bargein && this.minBargeinWordCount === 0) {

View File

@@ -117,10 +117,6 @@ class TaskSay extends Task {
alert_type: AlertType.TTS_NOT_PROVISIONED, alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor vendor
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
this.notifyError({
msg: 'TTS error',
details:`No speech credentials provisioned for selected vendor ${vendor}`
});
throw new Error('no provisioned speech credentials for TTS'); throw new Error('no provisioned speech credentials for TTS');
} }
// synthesize all of the text elements // synthesize all of the text elements
@@ -192,7 +188,6 @@ class TaskSay extends Task {
vendor, vendor,
detail: err.message detail: err.message
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
this.notifyError({msg: 'TTS error', details: err.message || err});
throw err; throw err;
} }
}; };
@@ -215,16 +210,16 @@ class TaskSay extends Task {
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ? let vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
this.synthesizer.vendor : this.synthesizer.vendor :
cs.speechSynthesisVendor; cs.speechSynthesisVendor;
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ? let language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
this.synthesizer.language : this.synthesizer.language :
cs.speechSynthesisLanguage ; cs.speechSynthesisLanguage ;
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ? let voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
this.synthesizer.voice : this.synthesizer.voice :
cs.speechSynthesisVoice; cs.speechSynthesisVoice;
const label = this.synthesizer.label && this.synthesizer.label !== 'default' ? let label = this.synthesizer.label && this.synthesizer.label !== 'default' ?
this.synthesizer.label : this.synthesizer.label :
cs.speechSynthesisLabel; cs.speechSynthesisLabel;
@@ -241,12 +236,22 @@ class TaskSay extends Task {
this.synthesizer.fallbackLabel : this.synthesizer.fallbackLabel :
cs.fallbackSpeechSynthesisLabel; cs.fallbackSpeechSynthesisLabel;
if (cs.hasFallbackTts) {
vendor = fallbackVendor;
language = fallbackLanguage;
voice = fallbackVoice;
label = fallbackLabel;
}
let filepath; let filepath;
try { try {
filepath = await this._synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label}); filepath = await this._synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label});
} catch (error) { } catch (error) {
if (fallbackVendor && this.isHandledByPrimaryProvider) { if (fallbackVendor && this.isHandledByPrimaryProvider && !cs.hasFallbackTts) {
this.notifyError(
{ msg: 'TTS error', details:`TTS vendor ${vendor} error: ${error}`, failover: 'in progress'});
this.isHandledByPrimaryProvider = false; this.isHandledByPrimaryProvider = false;
cs.hasFallbackTts = true;
this.logger.info(`Synthesize error, fallback to ${fallbackVendor}`); this.logger.info(`Synthesize error, fallback to ${fallbackVendor}`);
filepath = await this._synthesizeWithSpecificVendor(cs, ep, filepath = await this._synthesizeWithSpecificVendor(cs, ep,
{ {
@@ -256,6 +261,8 @@ class TaskSay extends Task {
label: fallbackLabel label: fallbackLabel
}); });
} else { } else {
this.notifyError(
{ msg: 'TTS error', details:`TTS vendor ${vendor} error: ${error}`, failover: 'not available'});
throw error; throw error;
} }
} }

View File

@@ -102,6 +102,13 @@ class SttTask extends Task {
this.fallbackLabel = cs.fallbackSpeechRecognizerLabel; this.fallbackLabel = cs.fallbackSpeechRecognizerLabel;
if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel; if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel;
} }
// If call is already fallback to 2nd ASR vendor
// use that.
if (cs.hasFallbackAsr) {
this.vendor = this.fallbackVendor;
this.language = this.fallbackLanguage;
this.label = this.fallbackLabel;
}
if (!this.data.recognizer.vendor) { if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor; this.data.recognizer.vendor = this.vendor;
} }
@@ -119,9 +126,11 @@ class SttTask extends Task {
try { try {
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label); this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
} catch (error) { } catch (error) {
if (this.fallbackVendor && this.isHandledByPrimaryProvider) { if (this.canFallback) {
await this._fallback(); await this._initFallback();
this.notifyError({ msg: 'ASR error', details:`Invalid vendor ${this.vendor}`, failover: 'in progress'});
} else { } else {
this.notifyError({ msg: 'ASR error', details:`Invalid vendor ${this.vendor}`, failover: 'not available'});
throw error; throw error;
} }
} }
@@ -190,9 +199,14 @@ class SttTask extends Task {
return credentials; return credentials;
} }
async _fallback() { get canFallback() {
return this.fallbackVendor && this.isHandledByPrimaryProvider && !this.cs.hasFallbackAsr;
}
async _initFallback() {
assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration'); assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration');
this.isHandledByPrimaryProvider = false; this.isHandledByPrimaryProvider = false;
this.cs.hasFallbackAsr = true;
this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`); this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
this.vendor = this.fallbackVendor; this.vendor = this.fallbackVendor;
this.language = this.fallbackLanguage; this.language = this.fallbackLanguage;
@@ -201,6 +215,8 @@ class SttTask extends Task {
this.data.recognizer.language = this.language; this.data.recognizer.language = this.language;
this.data.recognizer.label = this.label; this.data.recognizer.label = this.label;
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label); this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
// cleanup previous listener from previous vendor
this.removeCustomEventListeners();
} }
async compileHintsForCobalt(ep, hostport, model, token, hints) { async compileHintsForCobalt(ep, hostport, model, token, hints) {
@@ -263,7 +279,6 @@ class SttTask extends Task {
detail: evt.error, detail: evt.error,
vendor: this.vendor, vendor: this.vendor,
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`)); }).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${evt.error}`});
} }
_onVendorConnectFailure(cs, _ep, evt) { _onVendorConnectFailure(cs, _ep, evt) {
@@ -276,7 +291,6 @@ class SttTask extends Task {
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`, message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
vendor: this.vendor, vendor: this.vendor,
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`)); }).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${reason}`});
} }
} }

View File

@@ -80,12 +80,15 @@ class TaskTranscribe extends SttTask {
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid) updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */}); .catch(() => {/*already logged error */});
await this.awaitTaskDone();
} catch (err) { } catch (err) {
if (!(await this._startFallback(cs, ep, {error: err}))) {
this.logger.info(err, 'TaskTranscribe:exec - error'); this.logger.info(err, 'TaskTranscribe:exec - error');
this.parentTask && this.parentTask.emit('error', err); this.parentTask && this.parentTask.emit('error', err);
this.removeCustomEventListeners();
return;
} }
}
await this.awaitTaskDone();
this.removeCustomEventListeners(); this.removeCustomEventListeners();
} }
@@ -512,10 +515,8 @@ class TaskTranscribe extends SttTask {
} }
} }
async _onJambonzError(cs, _ep, evt) { async _startFallback(cs, _ep, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError'); if (this.canFallback) {
if (this.paused) return;
if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
_ep.stopTranscription({ _ep.stopTranscription({
vendor: this.vendor, vendor: this.vendor,
bugname: this.bugname bugname: this.bugname
@@ -523,18 +524,33 @@ class TaskTranscribe extends SttTask {
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`)); .catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
try { try {
await this._fallback(); this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'in progress'});
await this._initFallback();
let channel = 1; let channel = 1;
if (this.ep !== _ep) { if (this.ep !== _ep) {
channel = 2; channel = 2;
} }
this[`_speechHandlersSet_${channel}`] = false;
this._startTranscribing(cs, _ep, channel); this._startTranscribing(cs, _ep, channel);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
return; return true;
} catch (error) { } catch (error) {
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`); this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
} }
} else { } else {
this.logger.debug('transcribe:_startFallback no condition for falling back');
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
}
return false;
}
async _onJambonzError(cs, _ep, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
if (this.paused) return;
const {writeAlerts, AlertType} = cs.srf.locals; const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') { if (this.vendor === 'nuance') {
@@ -549,11 +565,12 @@ class TaskTranscribe extends SttTask {
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`, message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor, vendor: this.vendor,
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`}); if (!(await this._startFallback(cs, _ep, evt))) {
this.notifyTaskDone();
} }
} }
_onVendorConnectFailure(cs, _ep, channel, evt) { async _onVendorConnectFailure(cs, _ep, channel, evt) {
super._onVendorConnectFailure(cs, _ep, evt); super._onVendorConnectFailure(cs, _ep, evt);
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) { if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({ this.childSpan[channel - 1].span.setAttributes({
@@ -562,8 +579,10 @@ class TaskTranscribe extends SttTask {
}); });
this.childSpan[channel - 1].span.end(); this.childSpan[channel - 1].span.end();
} }
if (!(await this._startFallback(cs, _ep, evt))) {
this.notifyTaskDone(); this.notifyTaskDone();
} }
}
_startAsrTimer(channel) { _startAsrTimer(channel) {
if (this.vendor === 'deepgram') return; // no need if (this.vendor === 'deepgram') return; // no need