# Cosmo Voice Satellite Голосовой ассистент дома — аналог Алисы, но поверх LLM (через OpenClaw Gateway). Два агента: **Cosmo** (владельца) и **Люся** (жены). Каждый активируется своим wake word и идёт на свой OpenClaw gateway. ## Архитектура ``` ┌─────────────┐ wake word ┌──────────────┐ STT (Groq) │ Microphone │ ────────────► │ Satellite │ ──────────────► OpenClaw Gateway └─────────────┘ │ (Pi 5 / │ (N100, Proxmox) │ Mac) │ ◄── LLM stream ── └──────────────┘ │ │ │ ▼ TTS текст │ ElevenLabs stream (mp3) │ │ │ ▼ │ mpv (stdin) → speakers (BT/aux) ◄──┘ ``` Сессия диалога теперь **на стороне OpenClaw** — satellite отправляет лишь `x-openclaw-session-key`, а история и память живут в gateway. Клиент stateless. ## Инфраструктура - **Сервер**: N100 Mini-PC, `192.168.31.103`, Proxmox - **Cosmo Gateway**: порт `18789`, агент `openclaw/main`, session_key `agent:voice:voice:home` - **Люся Gateway**: порт `18790`, агент `openclaw/wife`, session_key `agent:wife:voice:home` - **Модель**: `openai/gpt-5.4-mini` (через `x-ocplatform-model` header; переопределяется через `VOICE_MODEL`) - **STT**: Groq API, `whisper-large-v3-turbo`, язык ru - **TTS**: ElevenLabs (`eleven_flash_v2_5` / `eleven_turbo_v2_5` / `eleven_multilingual_v2` — выбирается через `ELEVENLABS_MODEL`) - **Wake word**: openwakeword (`.onnx`, обучается на своих голосах через `training/step_*.py`). Раньше закладывали Porcupine — отказались. ## Структура проекта ``` home-voice-assistant/ ├── .env # секреты (не в git) ├── .env.example # шаблон ├── requirements.txt ├── satellite.py # обёртка для запуска ├── satellite/ │ ├── __main__.py # entry: python -m satellite [--wake] │ ├── config.py # env, AGENTS dict, keep-alive sessions │ ├── text.py # clean_for_speech (+ pymorphy3/num2words для времени) │ ├── stt.py # transcribe (Groq, BytesIO, без temp файла) │ ├── audio.py # record (RMS VAD) │ ├── tts.py # ElevenLabs streaming через mpv stdin │ ├── llm.py # ask_agent_stream, strip_fillers, RESET_PATTERNS │ └── modes.py # run_with_enter / run_with_porcupine + /new через slash ├── record_wav.py # запись обучающих wav-ов для wake word ├── remove_silent.py # чистка тихих записей ├── training/ # пайплайн обучения wake word (не в git) └── deploy/ ├── setup.sh # установка на Raspberry Pi └── cosmo-satellite.service # systemd unit ``` ## Ключевые инварианты ### Сессии диалога - История и контекст **на стороне OpenClaw**. Клиент шлёт только текущий user-message + `x-openclaw-session-key`. - Сброс: фраза «начни новую сессию» / «сбрось историю» / «очисти контекст» (паттерны в `llm.py::RESET_PATTERNS`) → `modes._handle_reset` делает прямой POST с `/new`. - Автосброс по таймауту **пока не реализован** (кандидат Этапа 1). ### Оптимизации скорости 1. **Keep-alive HTTP сессии** (`requests.Session()`) — в `config.py::_make_session()`, переиспользует TCP/TLS. 2. **Streaming TTS** — ElevenLabs аудио пайпится в `mpv` через stdin, играет пока генерируется. 3. **STT без диска** — PCM → WAV в `BytesIO` → Groq. 4. **Низкокачественный mp3 для речи** (`mp3_22050_32`) — меньше латентность без заметной потери качества для голоса. 5. **`optimize_streaming_latency=3`** в ElevenLabs convert — выдаёт первый чанк быстрее. ### Роутинг по wake word `modes.py::run_with_porcupine` сейчас грузит только модель Cosmo. Код Люси закомментирован до того, как модель обучена. Когда готова: - index 0 → `AGENTS["cosmo"]` (:18789) - index 1 → `AGENTS["lusya"]` (:18790) ### Нормализация речи перед TTS Два слоя защиты: 1. `text.py::clean_for_speech` — regex-правила: эмодзи, `**жирный**`, числа с плюсом/минусом, проценты, слэши, градусы, аббревиатуры (`т.е.`, `т.к.`), UNIT_SLASH (км/ч → «километров в час»), время `HH:MM` → прописью в правильном падеже через pymorphy3 + num2words. 2. `llm.py::strip_fillers` — режет фразы-заглушки («сейчас посмотрю», «дай секунду») которые агент генерит перед вызовом tool. ### Ошибки не должны ронять сервис Каждый слой (stt, tts, llm, audio, modes) ловит `Exception` и пишет в `errors.log` через `config.log`. Верхний уровень в modes.py ловит всё непредвиденное и продолжает цикл. ## Запуск ### macOS / Windows (разработка) ```bash python -m venv .venv && source .venv/bin/activate pip install -r requirements.txt cp .env.example .env # заполнить ключи python satellite.py # режим Enter (без wake word) python satellite.py --wake # режим openwakeword (нужна обученная .onnx) ``` ### Raspberry Pi (продакшн) ```bash sudo bash deploy/setup.sh sudo systemctl start cosmo-satellite sudo journalctl -u cosmo-satellite -f ``` ## Зависимости системы - Python 3.12+ - `portaudio` — для `pyaudio` (`brew install portaudio` / `apt install portaudio19-dev`) - `mpv` — для воспроизведения TTS (`brew install mpv` / `apt install mpv`) ## Переменные окружения (ключевые) | Переменная | Что | |---|---| | `GATEWAY_URL`, `LUSYA_GATEWAY_URL` | URL OpenClaw gateway | | `GATEWAY_TOKEN`, `LUSYA_GATEWAY_TOKEN` | Bearer токены | | `AGENT`, `LUSYA_AGENT` | Имя агента (`openclaw/main`, `openclaw/wife`) | | `VOICE_MODEL`, `LUSYA_VOICE_MODEL` | LLM (передаётся в `x-ocplatform-model`) | | `COSMO_SESSION_KEY`, `LUSYA_SESSION_KEY` | Идентификатор серверной сессии OpenClaw | | `GROQ_API_KEY` | Groq для STT | | `ELEVENLABS_API_KEY`, `ELEVENLABS_MODEL` | TTS | | `COSMO_TTS_VOICE`, `LUSYA_TTS_VOICE` | Voice ID в ElevenLabs | | `WAKE_WORD_COSMO`, `WAKE_WORD_LUSYA` | Пути к `.onnx` моделям wake word | | `WAKE_THRESHOLD` | Порог активации (0..1, дефолт 0.5) | | `AUDIO_SINK` | На Pi: `bluez_sink.XX_XX_XX.a2dp_sink`. На Mac/Win: пусто | | `SILENCE_THRESHOLD`, `SILENCE_DURATION`, `MAX_DURATION`, `FOLLOWUP_TIMEOUT` | VAD | | `TTS_MODE` | `full` (целостная интонация) или `stream` (быстрый старт, рваный) | | `ECHO_WARMUP` | Сек пропуска в начале записи (гасит эхо от TTS) | ## Частые задачи **Сменить голос у агента**: меняй `COSMO_TTS_VOICE` / `LUSYA_TTS_VOICE` в `.env`. Voice ID — на [elevenlabs.io/app/voice-library](https://elevenlabs.io/app/voice-library). Для русского лучше native-русские голоса, а не мультиязычные (не будет англоязычного акцента). **Отладить VAD**: `SILENCE_THRESHOLD` (громкость) и `SILENCE_DURATION` (сек). **Добавить третьего агента**: в `config.py::AGENTS` новый ключ + `WAKE_WORD_*` + раскомментировать блок в `modes.py::run_with_porcupine`. **Сменить модель LLM**: `VOICE_MODEL` в `.env` — передаётся в header `x-ocplatform-model`. Поле `model` в JSON остаётся `openclaw/main` (это имя агента, а не LLM). **Добавить фразу-заглушку**: в `llm.py::FILLER_PATTERNS` дополнить список. Эти фразы режутся из ответа перед TTS — агент генерит их до tool-call. ## Что НЕ делать - Не комитить `.env` (есть в `.gitignore`) - Не возвращать `say`/`espeak` — проект унифицирован на ElevenLabs + mpv - Не хранить историю диалога на клиенте — это делает OpenClaw по `session_key` - Не создавать temp файлы для WAV/mp3 — всё через `BytesIO` / stdin pipe - Не включать `style>0` и `speed≠1.0` в VoiceSettings — усиливают «иностранный» акцент и ломают просодию ## Тренировка wake word Пайплайн в `training/` (игнорируется в git): - `record_wav.py [long ]` — запись 16kHz mono PCM 16-bit - `remove_silent.py` — чистка + перенумерация - `step_1.py … step_5.py` — зависимости, датасеты, конфиг, обучение, экспорт - `training_config.json` — параметры (`wake_word_list`, `use_own_samples`, пенальти, шаги) - Под капотом: openwakeword (DNN .onnx) Реалистично для своего голоса: 500+ positive и 1000+ negative, иначе recall < 0.4. Negative должны включать фонетически близкие слова («космос», «просто»). ## Состояние и планы ### ✅ Done - [x] Модульная структура (`audio/stt/llm/tts/modes/config/text`) - [x] ElevenLabs streaming + mpv pipe - [x] Keep-alive HTTP сессии, STT через BytesIO - [x] Серверные сессии OpenClaw через `x-openclaw-session-key` (не клиентская история) - [x] Slash-команда `/new` на фразу «начни новую сессию» - [x] Нормализация речи: числа, единицы, время через pymorphy3 + num2words - [x] Пайплайн тренировки своего wake word + скрипты записи/чистки датасета - [x] systemd unit и setup.sh для Pi 5 ### 🚧 In progress / нужно сделать - [ ] **Чистка**: удалить `start_barge_in_listener`/`was_barge_in` из `tts.py`, параметр `conv` из `llm.py`, импорты `sys`/`start_barge_in_listener`/`was_barge_in` из `modes.py` - [ ] **`FOLLOWUP_TIMEOUT` реально применяется** — сейчас задекларирован, но после ответа ассистент ждёт полный `MAX_DURATION=15s` если пользователь молчит - [ ] **Унифицировать дефолт session_key** в `config.py` и `.env.example` (сейчас `voice:home:cosmo` vs `agent:voice:voice:home`) - [ ] **`max_tokens` → env** (`VOICE_MAX_TOKENS`), дефолт 300 - [ ] **Дообучить модель cosmo до recall ≥ 0.7** (нужно 500+ positive + разнообразие) - [ ] **Подключить Люсю в `run_with_porcupine`** (код закомментирован, готов к включению) - [ ] **Проверить systemd autostart на Pi в проде** — unit есть, в прод не поставлен - [ ] **logrotate / size-cap на `errors.log`** — растёт неограниченно ### 📋 Roadmap Этап 2 — качество и надёжность - [ ] **Автосброс OpenClaw сессии по таймауту** (>1 ч тишины → `/new`) - [ ] **Retry с backoff** для gateway (3 попытки с экспонентой) - [ ] **TTS-cache** для дежурных реплик («Начинаю новую сессию», «Не слышу», «Ошибка сервера») - [ ] **Persistent PyAudio input stream** (не пересоздавать на каждый `record()`) - [ ] **Заменить RMS-VAD на `webrtcvad` или `silero-vad`** — RMS не работает с фоновой музыкой - [ ] **Whisper `prompt` параметр** с «Космо, Люся, OpenClaw» — для имён собственных - [ ] **SYSTEM_PROMPT опционально на клиенте** — подсказка про TTS-friendly формат чисел/дат, если OpenClaw-агент без неё ### 📋 Roadmap Этап 3 — новые фичи - [ ] **Home Assistant tool** в OpenClaw: свет, климат, медиа голосом - [ ] **Контекст окружения** в каждом запросе: время, комната, погода, кто говорит - [ ] **Proactive notifications**: OpenClaw → WebSocket/SSE → satellite сам инициирует речь (таймеры, напоминания, входящее сообщение) - [ ] **Realtime barge-in голосом** во время TTS (требует echo cancellation: speex AEC или SpeexDSP) - [ ] **No-wake mode для доверенной комнаты** — VAD + whisper + intent filter без обязательного wake word - [ ] **Streaming TTS пер-токен** — отправлять в TTS куски раньше чем целое предложение, с правильными интонационными точками ### 📋 Roadmap Этап 4 — амбициозное - [ ] **Speaker identification** (`pyannote.audio` / `resemblyzer`) — разные персонализации по голосу - [ ] **Multi-room координация** — MQTT/gRPC между сателлитами, отвечает тот, кто слышит громче - [ ] **Локальный fallback LLM** на Pi (phi/llama) когда gateway недоступен — базовые команды без облака - [ ] **Камера + vision** — агент видит кто в комнате, что происходит - [ ] **Voice-memory hooks UX** — голосовые команды «запомни», «забудь» (OpenClaw уже умеет, нужен голосовой слой) ## Известные ограничения - **Нет echo cancellation** — если колонки близко к микрофону (особенно BT колонки на Pi), TTS может триггерить wake-модель. Mitigation: разносить колонку и мик, поднимать `WAKE_THRESHOLD`, использовать наушники при отладке. - **VAD не отличает голос от музыки/ТВ** — если что-то постоянно шумит выше `SILENCE_THRESHOLD`, ассистент «зависнет» слушать. Решение — `silero-vad`. - **Одна сессия = бесконечный контекст OpenClaw** пока не вызван `/new`. Нужен автотаймаут. - **Нет fallback для gateway-offline** — при потере связи ассистент молчит до явной ошибки.