- audio: switch VAD to webrtcvad with RMS gate + fallback to RMS - audio: honor FOLLOWUP_TIMEOUT — short silence wait after bot response - llm: retry with exponential backoff on network errors and 5xx - llm: VOICE_MAX_TOKENS env (default 300) instead of hardcoded 150 - tts: optional VAD-based barge-in (BARGE_IN_ENABLED, off by default) - tts: remove dead start_barge_in_listener / was_barge_in helpers - config: drop AGENT/LUSYA_AGENT — routing happens via session_key - modes: remove unused imports, pass FOLLOWUP_TIMEOUT to follow-up record() - docs: full rewrite of README and CLAUDE.md to match current architecture
228 lines
12 KiB
Markdown
228 lines
12 KiB
Markdown
# Cosmo Voice Satellite
|
||
|
||
Домашний голосовой ассистент поверх LLM — аналог Алисы/Siri, но умнее, потому что за ним стоит полноценный агент OpenClaw с памятью, tools и инструментами.
|
||
|
||
Два агента — **Cosmo** (владельца) и **Люся** (жены). Каждый со своим wake word, своим голосом ElevenLabs и своим OpenClaw gateway.
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
mic ─► wake word (openwakeword)
|
||
└► STT (Groq whisper) ─► OpenClaw Gateway (session_key, N100) ─► LLM
|
||
│
|
||
▼ streamed text
|
||
ElevenLabs TTS ─► mpv stdin ─► speakers
|
||
```
|
||
|
||
- **Wake word**: openwakeword (`.onnx`, обученная на своих записях)
|
||
- **STT**: Groq API, `whisper-large-v3-turbo`, ru
|
||
- **Агент**: OpenClaw на N100, модели через `x-ocplatform-model` header (`openai/gpt-5.4-mini` и т.п.)
|
||
- **История диалога**: на сервере OpenClaw (per `session_key`), клиент stateless
|
||
- **TTS**: ElevenLabs streaming через mpv
|
||
|
||
## Структура
|
||
|
||
```
|
||
home-voice-assistant/
|
||
├── satellite.py # entry-обёртка
|
||
├── satellite/ # рантайм
|
||
│ ├── __main__.py # python -m satellite [--wake]
|
||
│ ├── config.py # AGENTS, keep-alive sessions
|
||
│ ├── audio.py # запись + RMS VAD
|
||
│ ├── stt.py # Groq whisper
|
||
│ ├── llm.py # ask_agent_stream, strip_fillers, RESET_PATTERNS
|
||
│ ├── tts.py # ElevenLabs → mpv stdin
|
||
│ ├── text.py # clean_for_speech (+ pymorphy3 для времени)
|
||
│ └── modes.py # run_with_enter / run_with_porcupine
|
||
├── record_wav.py # запись датасета wake word
|
||
├── remove_silent.py # чистка тихих + перенумерация
|
||
├── training/ # openwakeword пайплайн (в .gitignore)
|
||
├── data/models/ # готовые .onnx wake word моделей
|
||
└── deploy/ # setup.sh + systemd unit для Pi 5
|
||
```
|
||
|
||
## Быстрый старт
|
||
|
||
```bash
|
||
python -m venv .venv && source .venv/bin/activate
|
||
pip install -r requirements.txt
|
||
cp .env.example .env # заполнить ключи
|
||
|
||
python satellite.py # режим Enter (отладка)
|
||
python satellite.py --wake # режим wake word (нужна модель в data/models/)
|
||
```
|
||
|
||
### Системные зависимости
|
||
|
||
macOS: `brew install portaudio mpv`
|
||
Linux: `apt install portaudio19-dev mpv`
|
||
Windows: мpv с [mpv.io](https://mpv.io), `pip install pipwin && pipwin install pyaudio`
|
||
|
||
## Сессия диалога
|
||
|
||
История и память живут **на стороне OpenClaw**. Satellite отправляет только текущее сообщение + `x-openclaw-session-key`. Сервер сам подклеивает контекст.
|
||
|
||
Сброс сессии:
|
||
- Голосом: «начни новую сессию» / «сбрось историю» / «очисти контекст» → satellite шлёт slash-команду `/new` в OpenClaw
|
||
- Программно: меняй `COSMO_SESSION_KEY` в `.env`
|
||
|
||
## Обучение своего wake word
|
||
|
||
OpenWakeWord тренирует DNN-модель на твоих записях слова. Пайплайн в `training/`:
|
||
|
||
| Шаг | Что делает |
|
||
|-----|-----------|
|
||
| `step_1.py` | Установка зависимостей (piper, openwakeword) |
|
||
| `step_2.py` | Создаёт `training_config.json` |
|
||
| `step_3.py` | Скачивает датасеты (audioset, fma, RIRs, ACAV features, ~17 GB) |
|
||
| `step_4.py` | Аугментация → тренировка → экспорт `.onnx` в `data/models/` |
|
||
| `step_5.py` | Проверка моделей и подсказки для `.env` |
|
||
|
||
### Запись датасета
|
||
|
||
```bash
|
||
# по одной записи (Enter → 2 с → сохраняем)
|
||
python record_wav.py cosmo positive
|
||
python record_wav.py cosmo negative
|
||
|
||
# непрерывно N секунд → нарезаем по 2с, тишину выкидываем
|
||
python record_wav.py cosmo negative long 300
|
||
INPUT_DEVICE=1 python record_wav.py cosmo negative long 600
|
||
```
|
||
|
||
`record_wav.py` отбраковывает тихие записи по `MIN_RMS=300`.
|
||
|
||
### Чистка
|
||
|
||
```bash
|
||
python remove_silent.py
|
||
```
|
||
|
||
Удаляет файлы с RMS ниже порога и переименовывает оставшиеся в `001.wav … NNN.wav`.
|
||
|
||
### Тренировка
|
||
|
||
```bash
|
||
# в training/training_config.json:
|
||
# {
|
||
# "wake_word_list": ["cosmo"],
|
||
# "use_own_samples": true,
|
||
# "false_activation_penalty": 100,
|
||
# "target_false_positives_per_hour": 3.0,
|
||
# "target_recall": 0.5,
|
||
# "number_of_training_steps": 3000,
|
||
# "layer_size": 64
|
||
# }
|
||
|
||
rm -rf training/my_custom_model/cosmo data/models/cosmo.onnx
|
||
python training/step_4.py
|
||
# → .env: WAKE_WORD_COSMO=data/models/cosmo.onnx
|
||
```
|
||
|
||
### Сколько данных нужно
|
||
|
||
| Positive | Negative | Ожидаемый recall |
|
||
|---|---|---|
|
||
| 100–200 | 200+ | 0.1–0.3 (плохо) |
|
||
| 300–500 | 500+ | 0.4–0.6 (минимум для работы) |
|
||
| 800–1500 | 1000+ | 0.7–0.85 |
|
||
| 2000+ | 2000+ | 0.9+ |
|
||
|
||
Главное — **разнообразие**: разные дистанции, интонации, время дня, фоны. Негативы должны включать фонетически близкие слова («космос», «косо», «просто»), обычную речь, имена других ассистентов («Алиса», «Сири»).
|
||
|
||
## .env (ключевые переменные)
|
||
|
||
| Переменная | Что |
|
||
|---|---|
|
||
| `GATEWAY_URL`, `LUSYA_GATEWAY_URL` | URL OpenClaw gateway |
|
||
| `GATEWAY_TOKEN`, `LUSYA_GATEWAY_TOKEN` | Bearer токены |
|
||
| `AGENT`, `LUSYA_AGENT` | Имя агента в OpenClaw |
|
||
| `VOICE_MODEL` | LLM (передаётся в `x-ocplatform-model`) |
|
||
| `COSMO_SESSION_KEY`, `LUSYA_SESSION_KEY` | Идентификатор серверной сессии |
|
||
| `GROQ_API_KEY` | STT |
|
||
| `ELEVENLABS_API_KEY`, `ELEVENLABS_MODEL` | TTS |
|
||
| `COSMO_TTS_VOICE`, `LUSYA_TTS_VOICE` | Voice ID |
|
||
| `WAKE_WORD_COSMO`, `WAKE_WORD_LUSYA` | Пути к `.onnx` |
|
||
| `WAKE_THRESHOLD` | Порог активации (дефолт 0.5) |
|
||
| `TTS_MODE` | `full` (цельная интонация) / `stream` (быстрый старт) |
|
||
| `AUDIO_SINK` | На Pi: BT sink. На Mac/Win: пусто |
|
||
| `SILENCE_THRESHOLD`, `SILENCE_DURATION`, `MAX_DURATION`, `FOLLOWUP_TIMEOUT` | VAD |
|
||
|
||
## Деплой на Raspberry Pi 5
|
||
|
||
```bash
|
||
sudo bash deploy/setup.sh
|
||
# подключи BT колонку, пропиши AUDIO_SINK в .env
|
||
# положи обученную .onnx в data/models/cosmo.onnx
|
||
sudo systemctl start cosmo-satellite
|
||
sudo journalctl -u cosmo-satellite -f
|
||
```
|
||
|
||
## Roadmap
|
||
|
||
### ✅ Сделано
|
||
|
||
- Модульная структура satellite
|
||
- ElevenLabs streaming TTS через mpv pipe
|
||
- Keep-alive HTTP + STT без диска
|
||
- Серверные сессии OpenClaw (`x-openclaw-session-key`)
|
||
- Slash-команда `/new` для сброса голосом
|
||
- Нормализация речи (числа, время, единицы) через pymorphy3 + num2words
|
||
- Пайплайн тренировки wake word на своих записях
|
||
- systemd unit для Pi
|
||
|
||
### 🚧 В работе
|
||
|
||
- Дообучить wake-модель до recall ≥ 0.7 (нужно 500+ позитивов + разнообразие)
|
||
- Подключить Люсю в `run_with_porcupine` (код готов, закомментирован)
|
||
- Чистка мёртвого кода (`start_barge_in_listener`, `conv` и т.п.)
|
||
- Починить `FOLLOWUP_TIMEOUT` (сейчас после ответа ассистент ждёт полный 15 с)
|
||
|
||
### 📋 Этап 2 — качество и надёжность
|
||
|
||
- Автосброс OpenClaw сессии по таймауту (>1 ч → `/new`)
|
||
- Retry с backoff для gateway
|
||
- TTS-cache дежурных реплик
|
||
- Persistent PyAudio stream (быстрее запись на Pi)
|
||
- Заменить RMS-VAD на `webrtcvad` / `silero-vad` — RMS ломается с фоновой музыкой
|
||
- Whisper `prompt` с именами собственными
|
||
- Size-cap / logrotate для `errors.log`
|
||
|
||
### 📋 Этап 3 — «умнее Алисы»
|
||
|
||
- **Home Assistant tool** в OpenClaw: свет/климат/медиа голосом
|
||
- **Контекст окружения** в каждом запросе: время, комната, погода, кто говорит
|
||
- **Proactive notifications**: OpenClaw → WebSocket/SSE → satellite сам начинает говорить (таймеры, напоминания, входящие)
|
||
- **Realtime barge-in голосом** — прерывать TTS, когда пользователь начал говорить (требует echo cancellation)
|
||
- **No-wake mode** в доверенной комнате — VAD + STT + intent filter без обязательного wake word
|
||
- **Streaming TTS пер-токен** — выдавать в речь куски раньше полного предложения
|
||
|
||
### 📋 Этап 4 — амбициозное
|
||
|
||
- **Speaker identification** (`pyannote.audio` / `resemblyzer`) — разные персонализации по голосу
|
||
- **Multi-room координация** — MQTT между сателлитами, отвечает тот, кто слышит громче
|
||
- **Локальный fallback LLM** на Pi когда gateway оффлайн (phi/llama для простых команд)
|
||
- **Камера + vision** — агент видит кто в комнате, что происходит
|
||
- **Voice-memory hooks UX** — голосовое «запомни/забудь»
|
||
|
||
## Известные ограничения
|
||
|
||
- Нет echo cancellation — если колонки близко к мику, TTS может триггерить wake-модель (поднимай `WAKE_THRESHOLD`).
|
||
- RMS-VAD не отличает голос от музыки/ТВ — ассистент может «залипнуть» в шумной среде.
|
||
- OpenClaw-сессия живёт вечно, пока не сказать «сбрось» — контекст раздувается.
|
||
- При потере связи с gateway — ассистент молчит до явной ошибки.
|
||
|
||
## Troubleshooting
|
||
|
||
**Ассистент «иностранец», монотонно читает** → смени `ELEVENLABS_MODEL` на `eleven_multilingual_v2`, возьми native-русский голос из Voice Library (не default English-speakers), в `tts.py` поставь `style=0.0, speed=1.0` (style/speed ломают просодию).
|
||
|
||
**Числа читаются цифрами вместо слов** → проверь `text.py::clean_for_speech` применяется. Для времени нужны `num2words` и `pymorphy3` в requirements.
|
||
|
||
**Wake срабатывает сам по себе, когда TTS говорит** → `WAKE_THRESHOLD=0.7`, разнеси колонку и мик. В будущем — AEC.
|
||
|
||
**Ассистент не реагирует на тихий голос** → понижай `SILENCE_THRESHOLD` (дефолт 500).
|
||
|
||
**`pyaudio` не ставится на Windows** → `pip install pipwin && pipwin install pyaudio`, или бери pre-built wheel.
|
||
|
||
**Wake модель плохо работает** → дело в данных. Смотри таблицу выше — нужно минимум 500 позитивов с разнообразием.
|