refactor: VAD upgrade, retry, dead code cleanup, AGENT removal

- 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
This commit is contained in:
2026-04-16 17:10:59 +03:00
parent a885cbe74b
commit a9001aef92
9 changed files with 541 additions and 358 deletions

242
README.md
View File

@@ -1,74 +1,87 @@
# Cosmo Voice Satellite
Домашний голосовой ассистент. Слушает wake word, распознаёт речь, ходит в OpenClaw gateway, проигрывает ответ через ElevenLabs.
Домашний голосовой ассистент поверх LLM — аналог Алисы/Siri, но умнее, потому что за ним стоит полноценный агент OpenClaw с памятью, tools и инструментами.
Два агента: **Cosmo** (владельца) и **Люся** (жены) — каждый со своим wake word и своим gateway.
Два агента **Cosmo** (владельца) и **Люся** (жены). Каждый со своим wake word, своим голосом ElevenLabs и своим OpenClaw gateway.
## Архитектура
```
mic ─► wake word (openwakeword) ─► STT (Groq) ─► OpenClaw gateway ─► TTS (ElevenLabs) ─► mpv ─► speakers
mic ─► wake word (openwakeword)
└► STT (Groq whisper) ─► OpenClaw Gateway (session_key, N100) ─► LLM
▼ streamed text
ElevenLabs TTS ─► mpv stdin ─► speakers
```
- **Wake word:** openwakeword (обучается на своих записях, см. ниже). Раньше планировался Porcupine — отказались.
- **STT:** Groq API, `whisper-large-v3-turbo`, ru.
- **LLM:** OpenClaw gateway на N100 (`192.168.31.103:18789` для cosmo, `:18790` для lusya), `openai/gpt-5.4-mini`.
- **TTS:** ElevenLabs `eleven_flash_v2_5` стримом через mpv stdin.
- **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, text.py
│ ├── stt.py, audio.py, tts.py, llm.py
── modes.py # run_with_enter / run_with_porcupine (wake word)
├── record_wav.py # запись датасета для wake word
├── remove_silent.py # чистка тихих + перенумерация
├── training/ # пайплайн обучения wake word
── step_1.py … step_5.py
│ ├── training_config.json
│ ├── own_samples/<word>/{positive,negative}/*.wav
│ ├── openwakeword/ # форк
│ └── my_custom_model/<word>/ # фичи + .onnx
── data/models/ # готовые .onnx wake word моделей
└── deploy/ # setup.sh + systemd unit для Pi
├── 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 # заполнить ключи
cp .env.example .env # заполнить ключи
python satellite.py # режим Enter (без wake word, для отладки)
python satellite.py --wake # режим wake word (нужна обученная модель в data/models/)
python satellite.py # режим Enter (отладка)
python satellite.py --wake # режим wake word (нужна модель в data/models/)
```
Системные зависимости:
- Python 3.12+
- `portaudio` `brew install portaudio`
- `mpv``brew install mpv`
### Системные зависимости
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/`:
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_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 секунды → сохраняем)
# по одной записи (Enter → 2 с → сохраняем)
python record_wav.py cosmo positive
python record_wav.py cosmo negative
@@ -77,7 +90,7 @@ 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` (изменить можно константой в начале файла).
`record_wav.py` отбраковывает тихие записи по `MIN_RMS=300`.
### Чистка
@@ -89,85 +102,126 @@ python remove_silent.py
### Тренировка
1. В `training/training_config.json` укажи `wake_word_list`, `use_own_samples: true`, параметры:
```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
}
```
2. В `training/openwakeword/examples/custom_model.yml` подними `augmentation_rounds: 10` (или больше).
3. Снеси кэш если был старый запуск:
```bash
rm -rf training/my_custom_model/<word> data/models/<word>.onnx
```
4. Запусти:
```bash
python training/step_4.py
```
5. Пропиши в `.env`: `WAKE_WORD_COSMO=data/models/cosmo.onnx`.
```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 |
| Positive | Negative | Ожидаемый recall |
|---|---|---|
| 100200 | 200+ | 0.10.3 (плохо) |
| 300500 | 500+ | 0.40.6 (минимум) |
| 300500 | 500+ | 0.40.6 (минимум для работы) |
| 8001500 | 1000+ | 0.70.85 |
| 2000+ | 2000+ | 0.9+ |
Главное — **разнообразие**: разные дистанции до микрофона, интонации, время дня, фоны. Аугментация (`augmentation_rounds`) умножит твой датасет в N раз во время обучения.
Негативы должны включать **фонетически близкие** слова ("космос", "косо", "просто"), обычную речь, имена других ассистентов ("алиса", "сири"), бытовые звуки.
## Архитектурные решения
- **Одна сессия диалога на день** на агента (`Conversation` в `llm.py`). История хранится клиентом, отправляется целиком. Сброс — фразой "сбрось историю" или сменой даты.
- **Keep-alive HTTP** (`requests.Session`) — переиспользует TCP/TLS.
- **Streaming TTS** — ElevenLabs пайпится в `mpv` через stdin, играет пока генерируется.
- **STT без диска** — PCM → WAV в `BytesIO` → Groq.
- **Barge-in** — `stop_speaking()` убивает mpv при новой активации.
- **Ошибки не роняют сервис** — каждый слой ловит `Exception`, пишет в `errors.log`.
Главное — **разнообразие**: разные дистанции, интонации, время дня, фоны. Негативы должны включать фонетически близкие слова («космос», «косо», «просто»), обычную речь, имена других ассистентов («Алиса», «Сири»).
## .env (ключевые переменные)
| Переменная | Что |
|---|---|
| `GATEWAY_URL`, `LUSYA_GATEWAY_URL` | OpenClaw gateways |
| `GATEWAY_TOKEN`, `LUSYA_GATEWAY_TOKEN` | Авторизация |
| `AGENT`, `LUSYA_AGENT` | `openclaw/main`, `openclaw/wife` |
| `VOICE_MODEL` | LLM для голоса (передаётся в `x-openclaw-model`) |
| `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`, `COSMO_TTS_VOICE`, `LUSYA_TTS_VOICE` | TTS |
| `WAKE_WORD_COSMO`, `WAKE_WORD_LUSYA` | Пути к `.onnx` моделям |
| `SILENCE_THRESHOLD`, `SILENCE_DURATION` | VAD |
| `MAX_HISTORY` | Лимит сообщений в сессии |
| `AUDIO_SINK` | На Pi: `bluez_sink.XX_XX_XX.a2dp_sink` |
| `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
## Деплой на 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
- [x] Модулизация satellite
- [x] ElevenLabs streaming + barge-in
- [x] Сессии диалога с автосбросом
- [x] Пайплайн тренировки wake word на своих записях
- [ ] Обучить рабочую модель cosmo (нужно ~500+ позитивов)
- [ ] Подключить Люсю в `run_with_porcupine` (сейчас грузится только cosmo)
- [ ] Проверить systemd autostart на Pi в проде
- [ ] Home Assistant tool в OpenClaw
- [ ] Real-time barge-in (прерывание голосом во время TTS)
- [ ] Контекст окружения в system prompt
- [ ] Speaker identification
- [ ] Проактивные уведомления через WebSocket
### ✅ Сделано
- Модульная структура 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 позитивов с разнообразием.