12 KiB
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_aiAPI - Функция
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)
- @worker1 Удалить из
package.json:@livekit/react-native,@livekit/react-native-expo-plugin,livekit-client,react-native-webrtc,@config-plugins/react-native-webrtc - @worker1 Добавить в
package.json:@jamsch/expo-speech-recognition,expo-speech - @worker1 Удалить файл
services/livekitService.ts - @worker1 Удалить файл
hooks/useLiveKitRoom.ts(если существует) - @worker1 Удалить из
chat.tsx: все LiveKit импорты,registerGlobals(),LiveKitRoomкомпонент,VoiceCallTranscriptHandler,getToken(), состоянияcallState/isCallActive - @worker1 Переделать
contexts/VoiceCallContext.tsx→contexts/VoiceContext.tsxс интерфейсом:interface VoiceContextValue { isActive: boolean; // сессия активна isListening: boolean; // STT слушает isSpeaking: boolean; // TTS говорит transcript: string; // текущий текст STT startSession: () => void; // начать сессию stopSession: () => void; // остановить сессию } - @worker1 Интегрировать отправку в API: transcript →
sendTextMessage()→ response → TTS - @worker1 Добавить функцию
interruptIfSpeaking()в VoiceContext — останавливает TTS если говорит, возвращает true/false - @worker1 Экспортировать
interruptIfSpeakingв VoiceContextValue интерфейс
@worker2 — STT/TTS хуки (файлы: hooks/, app.json)
-
@worker2 Добавить в
app.jsonplugins секцию:["@jamsch/expo-speech-recognition"] -
@worker2 Создать
hooks/useSpeechRecognition.ts: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 сек
- Язык:
-
@worker2 Создать
hooks/useTextToSpeech.ts:interface UseTextToSpeechReturn { isSpeaking: boolean; speak: (text: string) => Promise<void>; stop: () => void; }- Язык:
en-US - Автостоп при начале новой записи
- Язык:
-
@worker2 Добавить permissions в
app.json:- iOS:
NSMicrophoneUsageDescription,NSSpeechRecognitionUsageDescription - Android:
RECORD_AUDIO
- iOS:
@worker3 — UI/FAB (файлы: components/, _layout.tsx)
-
@worker3 Создать
components/VoiceFAB.tsx:- Позиция: по центру таб-бара, выступает на 20px вверх
- Размер: 64x64px
- Состояния:
idle: белый фон, иконка микрофона (#007AFF)listening: красный фон (#FF3B30), белая иконка, пульсация (scale 1.0↔1.15, 600ms loop)processing: синий фон (#007AFF), ActivityIndicatorspeaking: зелёный фон (#34C759), иконка звука
- Анимации: Reanimated для пульсации, withTiming для переходов цвета
- Shadow: iOS shadow + Android elevation
- Z-index: 1000 (над всем)
-
@worker3 Интегрировать FAB в
app/(tabs)/_layout.tsx:- Обернуть
<Tabs>в<View style={{flex:1}}>+<VoiceFAB /> - FAB должен быть абсолютно позиционирован
- Подключить к VoiceContext
- Обернуть
-
@worker3 Добавить haptic feedback при нажатии (expo-haptics)
-
@worker3 Интегрировать прерывание в VoiceFAB: вызывать
interruptIfSpeaking()при обнаружении голоса во времяspeakingсостояния -
@worker3 STT должен продолжать слушать даже во время TTS playback (для детекции прерывания)
Error Handling
| Ситуация | Действие |
|---|---|
| Permissions denied | Toast + ссылка на Settings |
| STT не распознал речь | Игнорировать пустой результат |
| API ошибка | Toast "Ошибка соединения", retry через 3 сек |
| TTS ошибка | Показать только текст в чате |
| Нет интернета | Toast, fallback на текстовый ввод |
Dev Build Requirements
ВАЖНО: STT требует development build!
# Создать dev build
npx expo prebuild
npx expo run:ios # или run:android
# В Expo Go показать:
Alert.alert("Требуется Dev Build", "Голосовые функции недоступны в Expo Go")
Критерии готовности
- LiveKit полностью удалён (нет в package.json, нет импортов)
- FAB отображается на всех табах по центру
- Tap на FAB = toggle listening mode
- FAB пульсирует красным во время listening
- STT распознаёт английскую речь
- Распознанный текст отправляется в ask_wellnuo_ai
- Ответ Julia отображается в чате
- TTS озвучивает ответ
- После TTS автоматически продолжает слушать
- Переход между табами НЕ прерывает сессию
- Пользователь может перебить Julia голосом — TTS останавливается, STT слушает
- TypeScript компилируется без ошибок
Технические ресурсы
- expo-speech-recognition — STT
- expo-speech — TTS
- Reanimated — анимации