# Cosmo Voice Satellite Голосовой ассистент дома — аналог Алисы через OpenClaw. Два агента: **Cosmo** (владельца) и **Люся** (жены). Каждый активируется своим wake word и идёт на свой OpenClaw gateway. ## Архитектура ``` ┌─────────────┐ wake word ┌──────────────┐ STT (Groq) │ Microphone │ ───────────► │ Satellite │ ──────────────► └─────────────┘ └──────────────┘ │ │ ▼ │ ┌──────────────┐ │ │ OpenClaw │ │ │ Gateway │ │ │ (N100 PC) │ │ stream response └──────────────┘ ▼ │ ┌──────────────┐ │ │ ElevenLabs │ ◄─────────────────┘ │ TTS │ └──────────────┘ │ ▼ mp3 stream ┌──────────────┐ │ mpv │ → speakers (BT) └──────────────┘ ``` ## Инфраструктура - **Сервер**: N100 Mini-PC, `192.168.31.103`, Proxmox - **Cosmo Gateway**: порт `18789`, агент `openclaw/main` - **Люся Gateway**: порт `18790`, агент `openclaw/wife` - **Модель**: `openai/gpt-5.4-mini` (через `x-openclaw-model` header) - **STT**: Groq API, `whisper-large-v3-turbo`, язык ru - **TTS**: ElevenLabs, `eleven_flash_v2_5` (~75ms латентность) - **Wake word**: Porcupine (на Pi), Enter (при разработке) ## Структура проекта ``` home-voice-assistant/ ├── .env # секреты (не в git) ├── .env.example # шаблон ├── requirements.txt ├── satellite.py # обёртка для запуска ├── satellite/ │ ├── __init__.py │ ├── __main__.py # entry: python -m satellite [--wake] │ ├── config.py # env, AGENTS dict, keep-alive sessions │ ├── text.py # clean_for_speech, find_sentence_end │ ├── stt.py # transcribe (Groq, BytesIO, без temp файла) │ ├── audio.py # record, record_with_timeout (VAD) │ ├── tts.py # ElevenLabs streaming через mpv, barge-in │ ├── llm.py # ask_agent_stream, Conversation (history) │ └── modes.py # run_with_enter, run_with_porcupine └── deploy/ ├── setup.sh # установка на Raspberry Pi └── cosmo-satellite.service # systemd unit ``` ## Что важно знать ### Сессии диалога - **Одна сессия на день** для каждого агента. Это осознанное решение: каждая новая сессия в OpenClaw тяжёлая (чтение памяти, большой контекст). - История хранится в `Conversation.messages[]` на клиенте и отправляется целиком с каждым запросом (stateless к серверу). - Сброс сессии: фраза "начни новую сессию" / "сбрось историю" / "очисти контекст" — паттерны в `RESET_PATTERNS` в `llm.py`. - Автосброс при смене даты (`Conversation.is_expired()`). - `MAX_HISTORY=20` — лимит сообщений, чтобы не раздувать контекст. ### Оптимизации скорости (все уже внедрены) 1. **Keep-alive HTTP сессии** (`requests.Session()`) — в `config.py._make_session()`, переиспользуется TCP/TLS. 2. **Streaming TTS** — ElevenLabs аудио пайпится в `mpv` через stdin, играет пока генерируется. 3. **STT без диска** — PCM → WAV в `BytesIO` → Groq, без temp файлов. 4. **Barge-in** — `stop_speaking()` вызывается при каждой активации, убивает текущий mpv процесс. ### Роутинг по wake word В `modes.py::run_with_porcupine` Porcupine грузит оба wake word: - index 0 = Cosmo → `AGENTS["cosmo"]` (:18789) - index 1 = Люся → `AGENTS["lusya"]` (:18790) Каждый агент имеет свой `tts_voice` в ElevenLabs. ### Ошибки не должны ронять сервис Каждый слой (stt, tts, llm, audio, modes) ловит `Exception` и пишет в `errors.log` через `config.log`. Верхний уровень в modes.py ловит всё непредвиденное и продолжает цикл. ## Запуск ### macOS / Windows (разработка) ```bash python -m venv .venv # macOS/Linux: source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt cp .env.example .env # заполнить ключи python satellite.py # режим Enter (без wake word) python satellite.py --wake # режим Porcupine (нужны .ppn + PORCUPINE_KEY) ``` ### 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`) - **ffmpeg** — опционально, для совместимости форматов ### Windows - Python 3.12+ с pip - `pip install pyaudio` — обычно работает через колеса pipwin или pre-built wheels. Если нет — `pip install pipwin && pipwin install pyaudio` - mpv: скачать с [mpv.io](https://mpv.io/installation/), положить `mpv.exe` в PATH - Porcupine работает и на Windows — wake word модель нужна под платформу windows (качать отдельную `.ppn`) ## Переменные окружения Все в `.env`. Ключевые: | Переменная | Что | |-----------|-----| | `GATEWAY_URL`, `LUSYA_GATEWAY_URL` | URL OpenClaw gateway на N100 | | `GATEWAY_TOKEN`, `LUSYA_GATEWAY_TOKEN` | Токены авторизации | | `AGENT`, `LUSYA_AGENT` | Имя агента в OpenClaw (`openclaw/main`, `openclaw/wife`) | | `VOICE_MODEL`, `LUSYA_VOICE_MODEL` | Модель LLM для голоса | | `GROQ_API_KEY` | Groq для STT | | `ELEVENLABS_API_KEY` | ElevenLabs TTS | | `COSMO_TTS_VOICE`, `LUSYA_TTS_VOICE` | Voice ID в ElevenLabs | | `ELEVENLABS_MODEL` | `eleven_flash_v2_5` (быстрый) | | `AUDIO_SINK` | На Pi: `bluez_sink.XX_XX_XX.a2dp_sink`. На Mac/Win: пусто. | | `PORCUPINE_KEY`, `WAKE_WORD_COSMO`, `WAKE_WORD_LUSYA` | Только для `--wake` режима | | `SILENCE_THRESHOLD=500` | VAD: чувствительность (ниже = ловит тихую речь) | | `SILENCE_DURATION=1.5` | Сек тишины = конец фразы | | `FOLLOWUP_TIMEOUT=8` | Сек ожидания продолжения диалога | | `MAX_HISTORY=20` | Макс. сообщений в сессии | ## Частые задачи **Сменить голос у агента**: меняй `COSMO_TTS_VOICE` / `LUSYA_TTS_VOICE` в `.env`. Voice ID берётся на [elevenlabs.io/app/voice-library](https://elevenlabs.io/app/voice-library). **Отладить VAD (ассистент не слышит / слушает слишком долго)**: `SILENCE_THRESHOLD` (громкость) и `SILENCE_DURATION` (сек). **Добавить третьего агента**: в `config.py::AGENTS` новый ключ, в `modes.py::run_with_porcupine` добавить `WAKE_WORD_*` и `wake_word_map.append(...)`. **Сменить модель LLM**: `VOICE_MODEL` в `.env` — передаётся в header `x-openclaw-model`. Модель `openclaw/main` остаётся как agent (это маршрут в OpenClaw). ## Что НЕ делать - Не комитить `.env` (есть в `.gitignore`) - Не возвращать fallback на macOS `say` — проект специально унифицирован на ElevenLabs + mpv - Не создавать новую сессию Conversation на каждую активацию — это было в старой версии, сейчас одна сессия на день - Не добавлять temp файлы для WAV/mp3 — всё идёт через `BytesIO` / stdin pipe ## Тренировка своего wake word Пайплайн в `training/`: - `record_wav.py ` — запись 16kHz mono PCM 16-bit в `training/own_samples//` - `training/step_1.py` … `step_5.py` — установка зависимостей, конвертация датасетов, генерация конфига, обучение, экспорт в `data/models/.onnx` - `training/training_config.json` — параметры (`wake_word_list`, `use_own_samples`, штрафы, шаги) - `training/openwakeword/` — форк openwakeword, `examples/custom_model.yml` — базовый шаблон конфига - Под капотом: openwakeword (НЕ Porcupine, несмотря на легаси-имена в коде). Wake word работает через DNN-модель .onnx. Реалистичные цифры для своего голоса: 500+ positive и 1000+ negative wav-файлов, иначе recall/FP/hour не сходятся. Negative должны включать фонетически близкие слова. ## Roadmap ### Done - [x] Модулизация satellite.py (audio/stt/llm/tts/modes/config) - [x] ElevenLabs streaming TTS + mpv pipe - [x] Keep-alive HTTP сессии, STT через BytesIO, barge-in - [x] Сессии диалога (одна на день, MAX_HISTORY, паттерны сброса) - [x] Пайплайн тренировки своего wake word на собственных записях ### In progress - [ ] Дообучение модели cosmo (на текущем датасете 300 pos / 117 neg метрики плохие — recall 25%, FP/hr 32). Нужно дозаписать данные. - [ ] Подключить Люсю в `run_with_wakeword` (сейчас грузится только модель cosmo, lusya wake word не работает) ### Planned - [ ] systemd autostart на Raspberry Pi (`deploy/cosmo-satellite.service` есть, но не проверен в проде) - [ ] Home Assistant tool в OpenClaw воркспейсе (управление светом/температурой через голос) - [ ] Real-time barge-in (прерывание по голосу во время озвучки, не только по новой активации) - [ ] Контекст окружения в system prompt (время, погода, состояние устройств) - [ ] Speaker identification (определять кто говорит без разных wake words) - [ ] Проактивные уведомления (WebSocket от сервера → satellite сам начинает говорить)