wellnua-lite/PRD.md

12 KiB
Raw Blame History

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)

  • @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.tsxcontexts/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.json plugins секцию:

    ["@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

@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), ActivityIndicator
      • speaking: зелёный фон (#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 компилируется без ошибок

Технические ресурсы