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:
242
README.md
242
README.md
@@ -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 |
|
||||
|---|---|---|
|
||||
| 100–200 | 200+ | 0.1–0.3 (плохо) |
|
||||
| 300–500 | 500+ | 0.4–0.6 (минимум) |
|
||||
| 300–500 | 500+ | 0.4–0.6 (минимум для работы) |
|
||||
| 800–1500 | 1000+ | 0.7–0.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 позитивов с разнообразием.
|
||||
|
||||
Reference in New Issue
Block a user