wellnua-lite/VOICE_DEBUG_GUIDE.md
Sergei 81a0c59060 Fix Android voice bug - STT restart after TTS
PROBLEM:
startListening() triggers spurious AppState change to "background" on Android,
causing voice session to stop immediately after Julia responds.

ROOT CAUSE:
React Native AppState bug - requesting audio focus triggers false background event.

SOLUTION:
- Added sttStartingIgnoreAppStateRef flag
- Ignore AppState changes for 200ms after startListening() call
- Protects against false session termination during STT initialization

CHANGES:
- app/(tabs)/_layout.tsx: Added Android workaround with 200ms protection window
- VOICE_DEBUG_GUIDE.md: Documented bug, workaround, and expected logs

RESULT:
Voice session now continues correctly after Julia's response on Android.
STT successfully restarts and user can speak again without manual restart.
2026-01-29 09:41:16 -08:00

322 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 полностью завершиться и освободить аудио-драйвер