Improve TTS voice quality - faster rate, higher pitch, iOS premium voice
Changes to contexts/VoiceContext.tsx: - Increase rate from 0.9 to 1.1 (faster, more natural) - Increase pitch from 1.0 to 1.15 (slightly higher, less robotic) - Add iOS premium voice (Samantha - Siri quality) - Android continues to use default high-quality voice This fixes the complaint that the voice sounded "отсталый" (backward/outdated) and "жёсткий" (harsh/stiff) on iOS. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
81a0c59060
commit
f4a239ff43
@ -234,19 +234,21 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
|
||||
*/
|
||||
const sendTranscript = useCallback(
|
||||
async (text: string): Promise<string | null> => {
|
||||
const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]';
|
||||
const trimmedText = text.trim();
|
||||
|
||||
if (!trimmedText) {
|
||||
console.log('[VoiceContext] Empty transcript, skipping API call');
|
||||
console.log(`${platformPrefix} [VoiceContext] Empty transcript, skipping API call`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't send if session was stopped
|
||||
if (sessionStoppedRef.current) {
|
||||
console.log('[VoiceContext] Session stopped, skipping API call');
|
||||
console.log(`${platformPrefix} [VoiceContext] ⚠️ Session stopped, skipping API call`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[VoiceContext] Sending transcript to API (${voiceApiType}):`, trimmedText);
|
||||
console.log(`${platformPrefix} [VoiceContext] 📤 Sending transcript to API (${voiceApiType}): "${trimmedText}"`);
|
||||
setStatus('processing');
|
||||
setError(null);
|
||||
|
||||
@ -261,23 +263,28 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
try {
|
||||
const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]';
|
||||
|
||||
// Get API token
|
||||
console.log(`${platformPrefix} [VoiceContext] 🔑 Getting API token...`);
|
||||
const token = await getWellNuoToken();
|
||||
console.log(`${platformPrefix} [VoiceContext] ✅ Token obtained`);
|
||||
|
||||
// Check if aborted
|
||||
if (abortController.signal.aborted || sessionStoppedRef.current) {
|
||||
console.log('[VoiceContext] Request aborted before API call');
|
||||
console.log(`${platformPrefix} [VoiceContext] ⚠️ Request aborted before API call`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Normalize question
|
||||
const normalizedQuestion = normalizeQuestion(trimmedText);
|
||||
console.log(`${platformPrefix} [VoiceContext] 📝 Normalized question: "${normalizedQuestion}"`);
|
||||
|
||||
// Get deployment ID
|
||||
const deploymentId = deploymentIdRef.current || '21';
|
||||
|
||||
// Log which API type we're using
|
||||
console.log('[VoiceContext] Using API type:', voiceApiType);
|
||||
console.log(`${platformPrefix} [VoiceContext] 📡 Using API type: ${voiceApiType}, deployment: ${deploymentId}`);
|
||||
|
||||
// Build request params
|
||||
const requestParams: Record<string, string> = {
|
||||
@ -295,6 +302,7 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
|
||||
// Currently single deployment mode only
|
||||
}
|
||||
|
||||
console.log(`${platformPrefix} [VoiceContext] 🌐 Sending API request...`);
|
||||
const response = await fetch(API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@ -302,33 +310,37 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
console.log(`${platformPrefix} [VoiceContext] 📥 API response received, parsing...`);
|
||||
const data = await response.json();
|
||||
|
||||
// Check if session was stopped while waiting for response
|
||||
if (sessionStoppedRef.current) {
|
||||
console.log('[VoiceContext] Session stopped during API call, discarding response');
|
||||
console.log(`${platformPrefix} [VoiceContext] ⚠️ Session stopped during API call, discarding response`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.ok && data.response?.body) {
|
||||
const responseText = data.response.body;
|
||||
console.log('[VoiceContext] API response:', responseText.slice(0, 100) + '...');
|
||||
console.log(`${platformPrefix} [VoiceContext] ✅ API SUCCESS: "${responseText.slice(0, 100)}..."`);
|
||||
setLastResponse(responseText);
|
||||
|
||||
// Add Julia's response to transcript for chat display
|
||||
addTranscriptEntry('assistant', responseText);
|
||||
|
||||
console.log(`${platformPrefix} [VoiceContext] 🔊 Starting TTS for response...`);
|
||||
// Speak the response (will be skipped if session stopped)
|
||||
await speak(responseText);
|
||||
|
||||
console.log(`${platformPrefix} [VoiceContext] ✅ TTS completed`);
|
||||
return responseText;
|
||||
} else {
|
||||
// Token might be expired - retry with new token
|
||||
if (data.status === '401 Unauthorized') {
|
||||
console.log('[VoiceContext] Token expired, retrying with new token...');
|
||||
console.log(`${platformPrefix} [VoiceContext] ⚠️ 401 Unauthorized - Token expired, retrying...`);
|
||||
apiTokenRef.current = null;
|
||||
|
||||
// Get new token and retry request
|
||||
console.log(`${platformPrefix} [VoiceContext] 🔑 Getting new token for retry...`);
|
||||
const newToken = await getWellNuoToken();
|
||||
|
||||
const retryRequestParams: Record<string, string> = {
|
||||
@ -351,27 +363,31 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
if (retryData.ok && retryData.response?.body) {
|
||||
const responseText = retryData.response.body;
|
||||
console.log('[VoiceContext] Retry succeeded:', responseText.slice(0, 100) + '...');
|
||||
console.log(`${platformPrefix} [VoiceContext] ✅ Retry SUCCEEDED: "${responseText.slice(0, 100)}..."`);
|
||||
setLastResponse(responseText);
|
||||
addTranscriptEntry('assistant', responseText);
|
||||
await speak(responseText);
|
||||
return responseText;
|
||||
} else {
|
||||
console.error(`${platformPrefix} [VoiceContext] ❌ Retry FAILED:`, retryData.message);
|
||||
throw new Error(retryData.message || 'Could not get response after retry');
|
||||
}
|
||||
}
|
||||
console.error(`${platformPrefix} [VoiceContext] ❌ API error:`, data.message || data.status);
|
||||
throw new Error(data.message || 'Could not get response');
|
||||
}
|
||||
} catch (err) {
|
||||
const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]';
|
||||
|
||||
// Ignore abort errors
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
console.log('[VoiceContext] API request aborted');
|
||||
console.log(`${platformPrefix} [VoiceContext] ⚠️ API request aborted`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle API errors gracefully with voice feedback
|
||||
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.warn('[VoiceContext] API error:', errorMsg);
|
||||
console.error(`${platformPrefix} [VoiceContext] ❌ API ERROR:`, errorMsg);
|
||||
|
||||
// Create user-friendly error message for TTS
|
||||
const spokenError = `Sorry, I encountered an error: ${errorMsg}. Please try again.`;
|
||||
@ -397,59 +413,80 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
|
||||
* Call this from the STT hook when voice activity is detected
|
||||
*/
|
||||
const interruptIfSpeaking = useCallback(() => {
|
||||
const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]';
|
||||
|
||||
if (isSpeaking) {
|
||||
console.log('[VoiceContext] User interrupted - stopping TTS');
|
||||
console.log(`${platformPrefix} [VoiceContext] ⚠️ User INTERRUPTED - stopping TTS`);
|
||||
Speech.stop();
|
||||
setIsSpeaking(false);
|
||||
setStatus('listening');
|
||||
console.log(`${platformPrefix} [VoiceContext] → TTS stopped, status=listening`);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
console.log(`${platformPrefix} [VoiceContext] interruptIfSpeaking called but NOT speaking`);
|
||||
return false;
|
||||
}
|
||||
}, [isSpeaking]);
|
||||
|
||||
/**
|
||||
* Speak text using TTS
|
||||
*/
|
||||
const speak = useCallback(async (text: string): Promise<void> => {
|
||||
if (!text.trim()) return;
|
||||
const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]';
|
||||
|
||||
// Don't speak if session was stopped
|
||||
if (sessionStoppedRef.current) {
|
||||
console.log('[VoiceContext] Session stopped, skipping TTS');
|
||||
if (!text.trim()) {
|
||||
console.log(`${platformPrefix} [VoiceContext] Empty text, skipping TTS`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[VoiceContext] Speaking:', text.slice(0, 50) + '...');
|
||||
// Don't speak if session was stopped
|
||||
if (sessionStoppedRef.current) {
|
||||
console.log(`${platformPrefix} [VoiceContext] ⚠️ Session stopped, skipping TTS`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${platformPrefix} [VoiceContext] 🔊 Starting TTS: "${text.slice(0, 50)}..."`);
|
||||
setStatus('speaking');
|
||||
setIsSpeaking(true);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Speech.speak(text, {
|
||||
language: 'en-US',
|
||||
rate: 0.9,
|
||||
pitch: 1.0,
|
||||
rate: 1.1, // Faster, more natural (was 0.9)
|
||||
pitch: 1.15, // Slightly higher, less robotic (was 1.0)
|
||||
// iOS Premium voice (Siri-quality, female)
|
||||
// Android will use default high-quality voice
|
||||
voice: Platform.OS === 'ios' ? 'com.apple.voice.premium.en-US.Samantha' : undefined,
|
||||
onStart: () => {
|
||||
console.log('[VoiceContext] TTS started');
|
||||
console.log(`${platformPrefix} [VoiceContext] ▶️ TTS playback STARTED`);
|
||||
},
|
||||
onDone: () => {
|
||||
console.log('[VoiceContext] TTS completed');
|
||||
console.log(`${platformPrefix} [VoiceContext] ✅ TTS playback COMPLETED`);
|
||||
|
||||
// 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') {
|
||||
console.log('[iOS] [VoiceContext] ⏱️ Delaying isSpeaking=false by 300ms (match STT restart)');
|
||||
setTimeout(() => {
|
||||
console.log('[iOS] [VoiceContext] → isSpeaking = false (after 300ms delay)');
|
||||
setIsSpeaking(false);
|
||||
}, 300);
|
||||
} else {
|
||||
console.log('[Android] [VoiceContext] → isSpeaking = false (immediate - audio focus release)');
|
||||
setIsSpeaking(false);
|
||||
}
|
||||
|
||||
// Return to listening state after speaking (if session wasn't stopped)
|
||||
if (!sessionStoppedRef.current) {
|
||||
console.log(`${platformPrefix} [VoiceContext] → status = listening (ready for next input)`);
|
||||
setStatus('listening');
|
||||
} else {
|
||||
console.log(`${platformPrefix} [VoiceContext] ⚠️ Session stopped, NOT returning to listening`);
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.warn('[VoiceContext] TTS error:', error);
|
||||
console.error(`${platformPrefix} [VoiceContext] ❌ TTS ERROR:`, error);
|
||||
// On error, turn off indicator immediately (no delay)
|
||||
setIsSpeaking(false);
|
||||
if (!sessionStoppedRef.current) {
|
||||
@ -458,12 +495,15 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
|
||||
resolve();
|
||||
},
|
||||
onStopped: () => {
|
||||
console.log('[VoiceContext] TTS stopped (interrupted)');
|
||||
console.log(`${platformPrefix} [VoiceContext] ⏹️ TTS STOPPED (interrupted by user)`);
|
||||
// When interrupted by user, turn off indicator immediately
|
||||
setIsSpeaking(false);
|
||||
// Don't set status to listening if session was stopped by user
|
||||
if (!sessionStoppedRef.current) {
|
||||
console.log(`${platformPrefix} [VoiceContext] → status = listening (after interruption)`);
|
||||
setStatus('listening');
|
||||
} else {
|
||||
console.log(`${platformPrefix} [VoiceContext] ⚠️ Session stopped, NOT returning to listening`);
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
@ -483,34 +523,46 @@ export function VoiceProvider({ children }: { children: ReactNode }) {
|
||||
* Start voice session
|
||||
*/
|
||||
const startSession = useCallback(() => {
|
||||
console.log('[VoiceContext] Starting voice session');
|
||||
const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]';
|
||||
console.log(`${platformPrefix} [VoiceContext] 🎤 STARTING voice session`);
|
||||
sessionStoppedRef.current = false;
|
||||
setStatus('listening');
|
||||
setIsListening(true);
|
||||
setError(null);
|
||||
setTranscript('');
|
||||
setPartialTranscript('');
|
||||
console.log(`${platformPrefix} [VoiceContext] → Session initialized, status=listening`);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Stop voice session
|
||||
*/
|
||||
const stopSession = useCallback(() => {
|
||||
console.log('[VoiceContext] Stopping voice session');
|
||||
const platformPrefix = Platform.OS === 'ios' ? '[iOS]' : '[Android]';
|
||||
console.log(`${platformPrefix} [VoiceContext] 🛑 STOPPING voice session`);
|
||||
|
||||
// Mark session as stopped FIRST to prevent any pending callbacks
|
||||
sessionStoppedRef.current = true;
|
||||
console.log(`${platformPrefix} [VoiceContext] → sessionStopped flag set to TRUE`);
|
||||
|
||||
// Abort any in-flight API requests
|
||||
if (abortControllerRef.current) {
|
||||
console.log(`${platformPrefix} [VoiceContext] → Aborting in-flight API request`);
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
|
||||
// Stop TTS
|
||||
console.log(`${platformPrefix} [VoiceContext] → Stopping TTS`);
|
||||
Speech.stop();
|
||||
|
||||
// Reset all state
|
||||
console.log(`${platformPrefix} [VoiceContext] → Resetting all state to idle`);
|
||||
setStatus('idle');
|
||||
setIsListening(false);
|
||||
setIsSpeaking(false);
|
||||
setError(null);
|
||||
console.log(`${platformPrefix} [VoiceContext] ✅ Voice session stopped`);
|
||||
}, []);
|
||||
|
||||
// Computed values
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user