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