# Voice Recognition Debug Guide ## Как работает голосовая система на Android vs iOS ### Основная проблема: Audio Focus на Android **Android:** STT (Speech-to-Text) и TTS (Text-to-Speech) **НЕ МОГУТ** работать одновременно из-за конфликта Audio Focus. Когда TTS начинает говорить, она захватывает аудио-фокус, и STT автоматически останавливается системой. **iOS:** STT и TTS **МОГУТ** работать одновременно. STT может слушать даже во время воспроизведения TTS (что позволяет прерывать Julia голосом). ### КРИТИЧЕСКАЯ проблема Android: startListening() → AppState background **На Android обнаружен баг React Native:** - При вызове `startListening()` AppState кратковременно переходит в `"background"` или `"inactive"` - Это происходит из-за запроса аудио-фокуса и системных разрешений - **НЕ связано** с действиями пользователя (блокировка экрана, переключение приложений) **Workaround (реализован в коде):** - Добавлен флаг `sttStartingIgnoreAppStateRef` - Игнорируем AppState changes на 200ms после вызова `startListening()` - Защищает от ложной остановки сессии во время запуска STT **Логи при срабатывании workaround:** ``` LOG [Android] [TabLayout] 🛡️ Ignoring AppState changes for 200ms (STT start workaround) LOG [Android] [TabLayout] ▶️ STARTING STT... (status: listening) LOG [Android] [TabLayout] 📱 AppState changed to: "background" LOG [Android] [TabLayout] 🛡️ IGNORING AppState change (STT start protection active) LOG [Android] [TabLayout] ✅ STT started successfully LOG [Android] [TabLayout] ✅ AppState monitoring resumed ``` --- ## Нормальный Flow (Должен работать так) ### 1️⃣ **Юзер нажимает кнопку "Start"** **Android:** ``` [Android] [TabLayout] 🎯 FAB PRESSED - isSessionActive: false [Android] [TabLayout] ▶️ STARTING session (FAB start) [Android] [VoiceContext] 🎤 STARTING voice session [Android] [VoiceContext] → Session initialized, status=listening [Android] [TabLayout] 🎤 Voice session STARTED - starting STT [Android] [TabLayout] ▶️ STARTING STT... (status: listening) [Android] [TabLayout] ✅ STT started successfully ``` **iOS:** ``` [iOS] [TabLayout] 🎯 FAB PRESSED - isSessionActive: false [iOS] [TabLayout] ▶️ STARTING session (FAB start) [iOS] [VoiceContext] 🎤 STARTING voice session [iOS] [VoiceContext] → Session initialized, status=listening [iOS] [TabLayout] 🎤 Voice session STARTED - starting STT [iOS] [TabLayout] ▶️ STARTING STT... (status: listening) [iOS] [TabLayout] ✅ STT started successfully ``` ### 2️⃣ **Юзер говорит что-то** **Android:** ``` [Android] [TabLayout] handleSpeechResult - isFinal: false, status: listening, transcript: "hello..." [Android] [TabLayout] → Updating PARTIAL transcript [Android] [TabLayout] Partial changed: "" → "hello" [Android] [TabLayout] → Started 2s silence timer ``` **iOS:** (то же самое) ### 3️⃣ **2 секунды тишины → auto-stop** **Android:** ``` [Android] [TabLayout] 🤖 AUTO-STOP: 2s silence - stopping STT [Android] [TabLayout] handleSTTEnd - sessionActive: true, status: listening [Android] [TabLayout] → shouldRestartSTT set to TRUE [Android] [TabLayout] handleSpeechResult - isFinal: true, status: listening, transcript: "hello" [Android] [TabLayout] → Processing FINAL transcript, sending to API ``` **iOS:** (то же самое, но с префиксом `[iOS]`) ### 4️⃣ **API запрос** **Android:** ``` [Android] [VoiceContext] 📤 Sending transcript to API (ask_wellnuo_ai): "hello" [Android] [VoiceContext] 🔑 Getting API token... [Android] [VoiceContext] ✅ Token obtained [Android] [VoiceContext] 📝 Normalized question: "how is dad doing" [Android] [VoiceContext] 📡 Using API type: ask_wellnuo_ai, deployment: 21 [Android] [VoiceContext] 🌐 Sending API request... [Android] [TabLayout] Status transition: listening → processing, sttIsListening: true [Android] [TabLayout] ⏸️ Stopping STT during processing (echo prevention) ``` **Важно на Android:** STT останавливается во время `processing`, чтобы не слышать эхо от TTS. ### 5️⃣ **API ответ получен** **Android:** ``` [Android] [VoiceContext] 📥 API response received, parsing... [Android] [VoiceContext] ✅ API SUCCESS: "Your dad Ferdinand is doing well. He spent most of his time..." [Android] [VoiceContext] 🔊 Starting TTS for response... [Android] [VoiceContext] 🔊 Starting TTS: "Your dad Ferdinand is doing well..." [Android] [VoiceContext] ▶️ TTS playback STARTED [Android] [TabLayout] Status transition: processing → speaking, sttIsListening: false ``` **iOS:** (то же самое) ### 6️⃣ **TTS заканчивает говорить** **Android:** ``` [Android] [VoiceContext] ✅ TTS playback COMPLETED [Android] [VoiceContext] → isSpeaking = false (immediate - audio focus release) [Android] [VoiceContext] → status = listening (ready for next input) [Android] [VoiceContext] ✅ TTS completed [Android] [TabLayout] Status transition: speaking → listening, sttIsListening: false [Android] [TabLayout] 🔄 TTS FINISHED - preparing to restart STT [Android] [TabLayout] ⏱️ Waiting 50ms before restarting STT (audio focus release) ``` **iOS:** ``` [iOS] [VoiceContext] ✅ TTS playback COMPLETED [iOS] [VoiceContext] ⏱️ Delaying isSpeaking=false by 300ms (match STT restart) [iOS] [VoiceContext] → status = listening (ready for next input) [iOS] [VoiceContext] ✅ TTS completed [iOS] [TabLayout] Status transition: speaking → listening, sttIsListening: false [iOS] [TabLayout] 🔄 TTS FINISHED - preparing to restart STT [iOS] [TabLayout] ⏱️ Waiting 300ms before restarting STT (audio focus release) [iOS] [VoiceContext] → isSpeaking = false (after 300ms delay) ``` **Ключевая разница:** - **Android:** `isSpeaking = false` **сразу** (50ms delay для STT restart) - **iOS:** `isSpeaking = false` через **300ms** (плавный переход, matches STT restart) ### 7️⃣ **STT рестартует после TTS** **Android:** ``` [Android] [TabLayout] ⏰ Delay complete - restarting STT now [Android] [TabLayout] ▶️ STARTING STT... (status: listening) [Android] [TabLayout] ✅ STT started successfully ``` **iOS:** (то же самое) ### 8️⃣ **Юзер может говорить снова → повтор с шага 2** --- ## Что НЕ ДОЛЖНО происходить (баги) ### ❌ БАГ 1: STT не рестартует после TTS (Android) **Симптомы:** ``` [Android] [VoiceContext] ✅ TTS playback COMPLETED [Android] [TabLayout] Status transition: speaking → listening, sttIsListening: false [Android] [TabLayout] 🔄 TTS FINISHED - preparing to restart STT [Android] [TabLayout] ⏱️ Waiting 50ms before restarting STT (audio focus release) [Android] [TabLayout] ⏰ Delay complete - restarting STT now [Android] [TabLayout] safeStartSTT - already listening or starting, skipping ❌ ПРОБЛЕМА! ``` **Причина:** `sttIsListening` или `sttStartingRef.current` остались `true`, хотя STT на самом деле не работает. **Решение:** Убедиться что `stopListening()` вызывается корректно и флаги сбрасываются. ### ❌ БАГ 2: STT не останавливается во время TTS (эхо) **Симптомы:** ``` [Android] [TabLayout] Status transition: processing → speaking, sttIsListening: true ❌ ПРОБЛЕМА! ``` **Причина:** STT не остановилось при переходе в `speaking`, и будет слышать Julia как эхо. **Решение:** Проверить что `stopListening()` вызывается при `status === 'speaking'`. ### ❌ БАГ 3: isSpeaking не сбрасывается **Симптомы (Android):** ``` [Android] [VoiceContext] ✅ TTS playback COMPLETED [Android] [VoiceContext] → isSpeaking = false (immediate - audio focus release) ... но FAB остаётся зелёным (isSpeaking === true в UI) ``` **Причина:** `setIsSpeaking(false)` не отрабатывает или есть race condition. **Решение:** Проверить что `setIsSpeaking(false)` вызывается в `onDone` TTS callback. --- ## Как читать логи ### Префиксы платформ: - `[iOS]` — событие происходит на iOS - `[Android]` — событие происходит на Android ### Префиксы модулей: - `[TabLayout]` — события из `app/(tabs)/_layout.tsx` (STT orchestration) - `[VoiceContext]` — события из `contexts/VoiceContext.tsx` (API + TTS) ### Эмодзи: - 🎯 — FAB нажат - 🎤 — Voice session start/stop - ▶️ — STT start - ⏸️ — STT stop - 📤 — Sending to API - 📥 — API response - 🔊 — TTS start/playback - ✅ — Success - ❌ — Error - ⚠️ — Warning - 🔄 — Restart/retry - ⏱️ — Timer/delay - ⏰ — Timer triggered - 🛑 — Force stop --- ## Как тестировать ### Тест 1: Базовый цикл (1 вопрос → 1 ответ → 1 вопрос) 1. Нажать FAB (зелёная кнопка) 2. Сказать: "Hello" 3. Подождать 2 секунды (auto-stop) 4. **Проверить:** Julia отвечает 5. **Проверить:** После ответа Julia, STT рестартует автоматически 6. Сказать: "How is dad?" 7. **Проверить:** Julia отвечает снова **Ожидаемые логи (Android):** ``` [Android] [TabLayout] 🎯 FAB PRESSED - isSessionActive: false [Android] [TabLayout] ▶️ STARTING session (FAB start) [Android] [TabLayout] ▶️ STARTING STT... (status: listening) [Android] [TabLayout] ✅ STT started successfully ... пользователь говорит ... [Android] [TabLayout] 🤖 AUTO-STOP: 2s silence - stopping STT [Android] [TabLayout] → Processing FINAL transcript, sending to API [Android] [VoiceContext] 📤 Sending transcript to API [Android] [TabLayout] ⏸️ Stopping STT during processing (echo prevention) [Android] [VoiceContext] ✅ API SUCCESS [Android] [VoiceContext] 🔊 Starting TTS for response... [Android] [VoiceContext] ▶️ TTS playback STARTED ... Julia говорит ... [Android] [VoiceContext] ✅ TTS playback COMPLETED [Android] [TabLayout] 🔄 TTS FINISHED - preparing to restart STT [Android] [TabLayout] ⏱️ Waiting 50ms before restarting STT [Android] [TabLayout] ⏰ Delay complete - restarting STT now [Android] [TabLayout] ▶️ STARTING STT... (status: listening) [Android] [TabLayout] ✅ STT started successfully ← КРИТИЧНО! Если нет этой строки — БАГ! ``` ### Тест 2: Прерывание Julia голосом (только iOS) **На iOS:** 1. Нажать FAB 2. Сказать: "Tell me a long story" 3. Пока Julia говорит, сказать: "Stop" 4. **Проверить:** Julia прервалась 5. **Проверить:** "Stop" отправилось как новый запрос **На Android:** Прерывание голосом **НЕ РАБОТАЕТ** (STT выключен во время TTS). Прервать можно только кнопкой FAB. --- ## Checklist для отладки Если STT не рестартует после TTS на Android: - [ ] Проверь что `stopListening()` вызывается при `status === 'speaking'` - [ ] Проверь что `sttIsListening === false` после `stopListening()` - [ ] Проверь что `safeStartSTT()` вызывается после задержки 50ms - [ ] Проверь что `sttStartingRef.current === false` перед вызовом `safeStartSTT()` - [ ] Проверь что `sessionActiveRef.current === true` (сессия активна) - [ ] Проверь логи на наличие `[Android] [TabLayout] ✅ STT started successfully` - [ ] Проверь что нет логов `[Android] [TabLayout] ⚠️ SKIPPING STT start - TTS is playing` --- ## Audio Focus на Android (детально) ### Как работает Audio Focus: 1. **TTS запрашивает Audio Focus:** - Когда `Speech.speak()` вызывается, Android TTS запрашивает `AUDIOFOCUS_GAIN_TRANSIENT` - Это **автоматически** останавливает все другие аудио-источники, включая STT 2. **STT теряет Audio Focus:** - Expo Speech Recognition получает событие `onAudioFocusLoss` - STT автоматически останавливается (не нужно вручную вызывать `stopListening()`) 3. **TTS освобождает Audio Focus:** - Когда TTS заканчивает (`onDone` callback), Audio Focus освобождается - Но STT **НЕ** рестартует автоматически — нужно вручную вызвать `startListening()` ### Почему 50ms delay на Android? - TTS может освободить Audio Focus с небольшой задержкой - Если попытаться запустить STT слишком рано, STT может не получить Audio Focus - 50ms — достаточно для освобождения, но незаметно для юзера ### Почему 300ms delay на iOS? - iOS не имеет проблем с Audio Focus - 300ms delay нужен для плавного перехода анимаций (зелёный индикатор) - Также даёт время для TTS полностью завершиться и освободить аудио-драйвер