wellnua-lite/PRD.md

221 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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.

# 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/) — анимации