Two-agent voice assistant (Cosmo + Люся) via OpenClaw Gateway. Streaming STT (Groq) + LLM + TTS (ElevenLabs) pipeline with keep-alive sessions, barge-in, and daily conversation sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
166 lines
9.9 KiB
Markdown
166 lines
9.9 KiB
Markdown
# 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
|
||
|
||
## Roadmap
|
||
|
||
- [ ] Speaker identification (определять кто говорит без разных wake words)
|
||
- [ ] Проактивные уведомления (WebSocket от сервера → satellite сам начинает говорить)
|
||
- [ ] Контекст окружения в system prompt (время, погода, состояние устройств)
|
||
- [ ] Real-time barge-in (прерывание по голосу во время озвучки, не только по новой активации)
|