Fix Android voice bugs - STT restart and token retry

Critical Android fixes:

BUG 1 - STT not restarting after TTS:
- Problem: isSpeaking delay (300ms iOS visual) blocked Android STT
- Android audio focus conflict: STT cannot start while isSpeaking=true
- Fix: Platform-specific isSpeaking timing
  - iOS: 300ms delay (smooth visual indicator)
  - Android: immediate (allows STT to restart)

BUG 2 - Session expired loop:
- Problem: 401 error → token reset → no retry → user hears error
- Fix: Automatic token refresh and retry on 401
- Flow: 401 → clear token → get new token → retry request
- User never hears "Session expired" unless retry also fails

contexts/VoiceContext.tsx:12-23,387-360
This commit is contained in:
Sergei 2026-01-28 20:43:42 -08:00
parent 29fb3c1026
commit 8c0e36cae3

View File

@ -17,6 +17,7 @@ import React, {
useRef,
ReactNode,
} from 'react';
import { Platform } from 'react-native';
import * as Speech from 'expo-speech';
import { api } from '@/services/api';
import { useVoiceTranscript } from './VoiceTranscriptContext';
@ -322,10 +323,42 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
return responseText;
} else {
// Token might be expired
// Token might be expired - retry with new token
if (data.status === '401 Unauthorized') {
console.log('[VoiceContext] Token expired, retrying with new token...');
apiTokenRef.current = null;
throw new Error('Session expired, please try again');
// Get new token and retry request
const newToken = await getWellNuoToken();
const retryRequestParams: Record<string, string> = {
function: voiceApiType,
clientId: 'MA_001',
user_name: WELLNUO_USER,
token: newToken,
question: normalizedQuestion,
deployment_id: deploymentId,
};
const retryResponse = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(retryRequestParams).toString(),
signal: abortController.signal,
});
const retryData = await retryResponse.json();
if (retryData.ok && retryData.response?.body) {
const responseText = retryData.response.body;
console.log('[VoiceContext] Retry succeeded:', responseText.slice(0, 100) + '...');
setLastResponse(responseText);
addTranscriptEntry('assistant', responseText);
await speak(responseText);
return responseText;
} else {
throw new Error(retryData.message || 'Could not get response after retry');
}
}
throw new Error(data.message || 'Could not get response');
}
@ -400,11 +433,15 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
},
onDone: () => {
console.log('[VoiceContext] TTS completed');
// Delay turning off green indicator to match STT restart delay (300ms)
// This keeps the visual indicator on during the transition period
// On iOS: Delay turning off green indicator to match STT restart delay (300ms)
// On Android: Turn off immediately (audio focus conflict with STT)
if (Platform.OS === 'ios') {
setTimeout(() => {
setIsSpeaking(false);
}, 300);
} else {
setIsSpeaking(false);
}
// Return to listening state after speaking (if session wasn't stopped)
if (!sessionStoppedRef.current) {
setStatus('listening');