docs: add Voice FAB PRD with local STT/TTS requirements
This commit is contained in:
parent
6d339acc64
commit
76d93abf1e
220
PRD.md
Normal file
220
PRD.md
Normal 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/) — анимации
|
||||
Loading…
x
Reference in New Issue
Block a user