From 76d93abf1eefb7cd00c64cec11ee2334a3ecf60b Mon Sep 17 00:00:00 2001 From: Sergei Date: Tue, 27 Jan 2026 16:55:29 -0800 Subject: [PATCH] docs: add Voice FAB PRD with local STT/TTS requirements --- PRD.md | 220 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 PRD.md diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..1f45821 --- /dev/null +++ b/PRD.md @@ -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; + stopListening: () => void; + hasPermission: boolean; + requestPermission: () => Promise; + } + ``` + - Язык: `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; + 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`: + - Обернуть `` в `` + `` + - 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/) — анимации