docs: add Voice FAB PRD with local STT/TTS requirements

This commit is contained in:
Sergei 2026-01-27 16:55:29 -08:00
parent 6d339acc64
commit 76d93abf1e

220
PRD.md Normal file
View File

@ -0,0 +1,220 @@
# PRD — Voice FAB с локальным STT/TTS (замена LiveKit)
## 📝 Изначальное требование
> Нам нужно вырезать полностью LiveKit передачу сигнала. Нужно вывести по центру такую иконку — когда мы нажимаем, она светится/пульсирует и воспринимает звук. Я могу с LLM-кой поговорить, которая будет использовать Speech-to-Text и Text-to-Speech. Помимо этого я вижу историю переписки в чате.
>
> FAB — плавающая кнопка поверх таб-бара (как в TikTok). Она должна пульсировать когда мы с ней разговариваем.
>
> Локально использовать Speech-to-Text, Text-to-Speech. Ответы отправляем в ту же LLM (WellNuo API ask_wellnuo_ai), к которой уже обращаемся в текстовом чате.
>
> Чат остаётся чатом — голос просто говорит через динамики. Отдельного интерфейса голосового нет.
### Уточнения от пользователя:
- **Continuous listening mode**: нажал один раз = начал слушать, нажал ещё раз = остановил. Слушает постоянно пока активен.
- **FAB виден на всех табах**: можно переходить между вкладками, FAB продолжает пульсировать и слушать
- **Язык STT**: только английский (en-US)
- **Прерывание TTS**: если пользователь начинает говорить пока Julia озвучивает ответ — TTS немедленно останавливается и STT начинает слушать пользователя
---
## Цель
Заменить LiveKit на локальное распознавание речи (STT) и синтез речи (TTS). Добавить пульсирующую FAB-кнопку по центру таб-бара для голосового взаимодействия с Julia AI. FAB работает в режиме continuous listening — виден и активен на всех табах.
---
## Контекст проекта
- **Тип:** Expo / React Native (SDK 54)
- **Стек:** TypeScript, Expo Router, React Native Reanimated
- **Текущий LLM:** WellNuo API `ask_wellnuo_ai` (уже работает в текстовом чате)
- **Удаляем:** LiveKit (@livekit/react-native, livekit-client)
### Ключевые файлы:
| Файл | Действие |
|------|----------|
| `app/(tabs)/_layout.tsx` | Добавить FAB компонент |
| `app/(tabs)/chat.tsx` | Убрать LiveKit код, интегрировать STT/TTS |
| `services/livekitService.ts` | УДАЛИТЬ |
| `hooks/useLiveKitRoom.ts` | УДАЛИТЬ |
| `contexts/VoiceCallContext.tsx` | Переделать в VoiceContext для STT/TTS |
| `package.json` | Удалить livekit deps, добавить STT |
---
## Архитектура
### STT (Speech-to-Text) — локально
**Библиотека:** `@jamsch/expo-speech-recognition`
- Нативный iOS SFSpeechRecognizer / Android SpeechRecognizer
- Язык: `en-US` (фиксированный)
- Требует dev build (не работает в Expo Go)
### TTS (Text-to-Speech) — локально
**Библиотека:** `expo-speech`
- Встроено в Expo SDK
- Системный голос устройства
### LLM — без изменений
- Существующий `ask_wellnuo_ai` API
- Функция `sendTextMessage()` в chat.tsx
---
## User Flow
| # | Действие | UI состояние FAB | Что происходит |
|---|----------|------------------|----------------|
| 1 | Видит FAB на любом табе | `idle` (микрофон, белый) | Готов к активации |
| 2 | Нажимает FAB | `listening` (красный, пульсирует) | STT начинает слушать |
| 3 | Говорит | `listening` | STT распознаёт в реальном времени |
| 4 | Пауза в речи | `processing` (синий) | Отправка в API |
| 5 | Ответ получен | `speaking` (зелёный) | TTS озвучивает |
| 6 | TTS закончил | `listening` (красный) | Снова слушает |
| 7 | Нажимает FAB повторно | `idle` (белый) | Остановка сессии |
| 8 | **Прерывание**: говорит во время `speaking` | `listening` (красный) | TTS останавливается, STT слушает |
**Важно:**
- FAB виден и активен на ВСЕХ табах. Переход между табами НЕ прерывает сессию.
- Пользователь может перебить Julia в любой момент — TTS немедленно замолкает.
---
## Задачи
### @worker1 — Backend/Cleanup (файлы: package.json, services/, contexts/, chat.tsx)
- [x] @worker1 Удалить из `package.json`: `@livekit/react-native`, `@livekit/react-native-expo-plugin`, `livekit-client`, `react-native-webrtc`, `@config-plugins/react-native-webrtc`
- [x] @worker1 Добавить в `package.json`: `@jamsch/expo-speech-recognition`, `expo-speech`
- [x] @worker1 Удалить файл `services/livekitService.ts`
- [x] @worker1 Удалить файл `hooks/useLiveKitRoom.ts` (если существует)
- [x] @worker1 Удалить из `chat.tsx`: все LiveKit импорты, `registerGlobals()`, `LiveKitRoom` компонент, `VoiceCallTranscriptHandler`, `getToken()`, состояния `callState/isCallActive`
- [x] @worker1 Переделать `contexts/VoiceCallContext.tsx``contexts/VoiceContext.tsx` с интерфейсом:
```typescript
interface VoiceContextValue {
isActive: boolean; // сессия активна
isListening: boolean; // STT слушает
isSpeaking: boolean; // TTS говорит
transcript: string; // текущий текст STT
startSession: () => void; // начать сессию
stopSession: () => void; // остановить сессию
}
```
- [x] @worker1 Интегрировать отправку в API: transcript → `sendTextMessage()` → response → TTS
- [x] @worker1 Добавить функцию `interruptIfSpeaking()` в VoiceContext — останавливает TTS если говорит, возвращает true/false
- [x] @worker1 Экспортировать `interruptIfSpeaking` в VoiceContextValue интерфейс
### @worker2 — STT/TTS хуки (файлы: hooks/, app.json)
- [x] @worker2 Добавить в `app.json` plugins секцию:
```json
["@jamsch/expo-speech-recognition"]
```
- [x] @worker2 Создать `hooks/useSpeechRecognition.ts`:
```typescript
interface UseSpeechRecognitionReturn {
isListening: boolean;
transcript: string;
partialTranscript: string;
error: string | null;
startListening: () => Promise<void>;
stopListening: () => void;
hasPermission: boolean;
requestPermission: () => Promise<boolean>;
}
```
- Язык: `en-US` (фиксированный)
- Continuous mode: true
- Partial results: true (для live preview)
- Автоотправка при паузе >2 сек
- [x] @worker2 Создать `hooks/useTextToSpeech.ts`:
```typescript
interface UseTextToSpeechReturn {
isSpeaking: boolean;
speak: (text: string) => Promise<void>;
stop: () => void;
}
```
- Язык: `en-US`
- Автостоп при начале новой записи
- [x] @worker2 Добавить permissions в `app.json`:
- iOS: `NSMicrophoneUsageDescription`, `NSSpeechRecognitionUsageDescription`
- Android: `RECORD_AUDIO`
### @worker3 — UI/FAB (файлы: components/, _layout.tsx)
- [x] @worker3 Создать `components/VoiceFAB.tsx`:
- Позиция: по центру таб-бара, выступает на 20px вверх
- Размер: 64x64px
- Состояния:
- `idle`: белый фон, иконка микрофона (#007AFF)
- `listening`: красный фон (#FF3B30), белая иконка, пульсация (scale 1.0↔1.15, 600ms loop)
- `processing`: синий фон (#007AFF), ActivityIndicator
- `speaking`: зелёный фон (#34C759), иконка звука
- Анимации: Reanimated для пульсации, withTiming для переходов цвета
- Shadow: iOS shadow + Android elevation
- Z-index: 1000 (над всем)
- [x] @worker3 Интегрировать FAB в `app/(tabs)/_layout.tsx`:
- Обернуть `<Tabs>` в `<View style={{flex:1}}>` + `<VoiceFAB />`
- FAB должен быть абсолютно позиционирован
- Подключить к VoiceContext
- [x] @worker3 Добавить haptic feedback при нажатии (expo-haptics)
- [x] @worker3 Интегрировать прерывание в VoiceFAB: вызывать `interruptIfSpeaking()` при обнаружении голоса во время `speaking` состояния
- [x] @worker3 STT должен продолжать слушать даже во время TTS playback (для детекции прерывания)
---
## Error Handling
| Ситуация | Действие |
|----------|----------|
| Permissions denied | Toast + ссылка на Settings |
| STT не распознал речь | Игнорировать пустой результат |
| API ошибка | Toast "Ошибка соединения", retry через 3 сек |
| TTS ошибка | Показать только текст в чате |
| Нет интернета | Toast, fallback на текстовый ввод |
---
## Dev Build Requirements
**ВАЖНО:** STT требует development build!
```bash
# Создать dev build
npx expo prebuild
npx expo run:ios # или run:android
# В Expo Go показать:
Alert.alert("Требуется Dev Build", "Голосовые функции недоступны в Expo Go")
```
---
## Критерии готовности
- [x] LiveKit полностью удалён (нет в package.json, нет импортов)
- [x] FAB отображается на всех табах по центру
- [x] Tap на FAB = toggle listening mode
- [x] FAB пульсирует красным во время listening
- [x] STT распознаёт английскую речь
- [x] Распознанный текст отправляется в ask_wellnuo_ai
- [x] Ответ Julia отображается в чате
- [x] TTS озвучивает ответ
- [x] После TTS автоматически продолжает слушать
- [x] Переход между табами НЕ прерывает сессию
- [x] Пользователь может перебить Julia голосом — TTS останавливается, STT слушает
- [x] TypeScript компилируется без ошибок
---
## Технические ресурсы
- [expo-speech-recognition](https://github.com/jamsch/expo-speech-recognition) — STT
- [expo-speech](https://docs.expo.dev/versions/latest/sdk/speech/) — TTS
- [Reanimated](https://docs.swmansion.com/react-native-reanimated/) — анимации