fix(voice): fix silent STT failure — use AAC/M4A, store onError, guard NaN amplitude
Three bugs causing silent STT failure on Android: 1. AudioEncoder.opus produces an OGG container on Android but the file was named .webm — faster-whisper rejected it due to format mismatch. Changed to AudioEncoder.aacLc + .m4a (reliable on all Android versions). Updated voice_api.dart to send audio/mp4 MIME type accordingly. 2. onError callback was never stored, so errors in _startListening() and _handleSilence() were silently swallowed. Now stored as _onError and called before exitVoiceMode() on any failure. 3. Amplitude stream can emit NaN or ±Infinity on some Android devices during recorder initialisation. NaN < -40.0 is false, so silence was never detected. Now treated explicitly as silence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -100,6 +100,7 @@ class VoiceNotifier extends Notifier<VoiceState> {
|
||||
|
||||
// Voice mode callbacks
|
||||
Future<void> Function(String transcript)? _onTranscript;
|
||||
void Function(String message)? _onError;
|
||||
bool _enableTts = false;
|
||||
|
||||
// Streaming TTS state
|
||||
@@ -165,6 +166,7 @@ class VoiceNotifier extends Notifier<VoiceState> {
|
||||
}
|
||||
|
||||
_onTranscript = onTranscript;
|
||||
_onError = onError;
|
||||
_enableTts = enableTts;
|
||||
_tempDir = await getTemporaryDirectory();
|
||||
|
||||
@@ -184,6 +186,7 @@ class VoiceNotifier extends Notifier<VoiceState> {
|
||||
_lastSeenLength = 0;
|
||||
_streamComplete = false;
|
||||
_onTranscript = null;
|
||||
_onError = null;
|
||||
state = const VoiceState();
|
||||
}
|
||||
|
||||
@@ -217,18 +220,25 @@ class VoiceNotifier extends Notifier<VoiceState> {
|
||||
state = state.copyWith(mode: VoiceMode.recording);
|
||||
|
||||
final dir = _tempDir ?? await getTemporaryDirectory();
|
||||
// AAC/M4A is reliably supported on Android (unlike WebM/Opus which can
|
||||
// produce OGG bytes in a .webm file, confusing server-side decoders).
|
||||
final path =
|
||||
'${dir.path}/voice_rec_${DateTime.now().millisecondsSinceEpoch}.webm';
|
||||
'${dir.path}/voice_rec_${DateTime.now().millisecondsSinceEpoch}.m4a';
|
||||
|
||||
await _recorder!.start(
|
||||
const RecordConfig(encoder: AudioEncoder.opus, sampleRate: 16000),
|
||||
path: path,
|
||||
);
|
||||
try {
|
||||
await _recorder!.start(
|
||||
const RecordConfig(encoder: AudioEncoder.aacLc, sampleRate: 16000),
|
||||
path: path,
|
||||
);
|
||||
|
||||
_amplitudeSubscription?.cancel();
|
||||
_amplitudeSubscription = _recorder!
|
||||
.onAmplitudeChanged(const Duration(milliseconds: 200))
|
||||
.listen(_onAmplitude);
|
||||
_amplitudeSubscription?.cancel();
|
||||
_amplitudeSubscription = _recorder!
|
||||
.onAmplitudeChanged(const Duration(milliseconds: 200))
|
||||
.listen(_onAmplitude);
|
||||
} catch (e) {
|
||||
_onError?.call('Microphone error: could not start recording');
|
||||
exitVoiceMode();
|
||||
}
|
||||
}
|
||||
|
||||
void _onAmplitude(Amplitude event) {
|
||||
@@ -238,7 +248,12 @@ class VoiceNotifier extends Notifier<VoiceState> {
|
||||
DateTime.now().millisecondsSinceEpoch - _recordingStartMs;
|
||||
if (elapsed < _minRecordingMs) return;
|
||||
|
||||
if (event.current < _silenceThresholdDb) {
|
||||
final db = event.current;
|
||||
// Guard against NaN / ±Infinity which can arrive on some Android devices
|
||||
// when the recorder is initialising. Treat invalid readings as silence.
|
||||
final isSilent = db.isNaN || db.isInfinite || db < _silenceThresholdDb;
|
||||
|
||||
if (isSilent) {
|
||||
_silenceMs += 200;
|
||||
if (_silenceMs >= _silenceDurationMs) {
|
||||
_amplitudeSubscription?.cancel();
|
||||
@@ -289,8 +304,8 @@ class VoiceNotifier extends Notifier<VoiceState> {
|
||||
if (!_enableTts && state.voiceModeActive) {
|
||||
await _startListening();
|
||||
}
|
||||
} catch (_) {
|
||||
// Network/API error — exit voice mode
|
||||
} catch (e) {
|
||||
_onError?.call('Voice error: transcription failed');
|
||||
exitVoiceMode();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user