Files
home-voice-assistant/CLAUDE.md
Daniil Klimov a9001aef92 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
2026-04-16 17:10:59 +03:00

212 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Cosmo Voice Satellite
Голосовой ассистент дома — аналог Алисы, но поверх LLM (через OpenClaw Gateway). Два агента: **Cosmo** (владельца) и **Люся** (жены). Каждый активируется своим wake word и идёт на свой OpenClaw gateway.
## Архитектура
```
┌─────────────┐ wake word ┌──────────────┐ STT (Groq)
│ Microphone │ ────────────► │ Satellite │ ──────────────► OpenClaw Gateway
└─────────────┘ │ (Pi 5 / │ (N100, Proxmox)
│ Mac) │ ◄── LLM stream ──
└──────────────┘ │
│ │
▼ TTS текст │
ElevenLabs stream (mp3) │
│ │
▼ │
mpv (stdin) → speakers (BT/aux) ◄──┘
```
Сессия диалога теперь **на стороне OpenClaw** — satellite отправляет лишь `x-openclaw-session-key`, а история и память живут в gateway. Клиент stateless.
## Инфраструктура
- **Сервер**: N100 Mini-PC, `192.168.31.103`, Proxmox
- **Cosmo Gateway**: порт `18789`, агент `openclaw/main`, session_key `agent:voice:voice:home`
- **Люся Gateway**: порт `18790`, агент `openclaw/wife`, session_key `agent:wife:voice:home`
- **Модель**: `openai/gpt-5.4-mini` (через `x-ocplatform-model` header; переопределяется через `VOICE_MODEL`)
- **STT**: Groq API, `whisper-large-v3-turbo`, язык ru
- **TTS**: ElevenLabs (`eleven_flash_v2_5` / `eleven_turbo_v2_5` / `eleven_multilingual_v2` — выбирается через `ELEVENLABS_MODEL`)
- **Wake word**: openwakeword (`.onnx`, обучается на своих голосах через `training/step_*.py`). Раньше закладывали Porcupine — отказались.
## Структура проекта
```
home-voice-assistant/
├── .env # секреты (не в git)
├── .env.example # шаблон
├── requirements.txt
├── satellite.py # обёртка для запуска
├── satellite/
│ ├── __main__.py # entry: python -m satellite [--wake]
│ ├── config.py # env, AGENTS dict, keep-alive sessions
│ ├── text.py # clean_for_speech (+ pymorphy3/num2words для времени)
│ ├── stt.py # transcribe (Groq, BytesIO, без temp файла)
│ ├── audio.py # record (RMS VAD)
│ ├── tts.py # ElevenLabs streaming через mpv stdin
│ ├── llm.py # ask_agent_stream, strip_fillers, RESET_PATTERNS
│ └── modes.py # run_with_enter / run_with_porcupine + /new через slash
├── record_wav.py # запись обучающих wav-ов для wake word
├── remove_silent.py # чистка тихих записей
├── training/ # пайплайн обучения wake word (не в git)
└── deploy/
├── setup.sh # установка на Raspberry Pi
└── cosmo-satellite.service # systemd unit
```
## Ключевые инварианты
### Сессии диалога
- История и контекст **на стороне OpenClaw**. Клиент шлёт только текущий user-message + `x-openclaw-session-key`.
- Сброс: фраза «начни новую сессию» / «сбрось историю» / «очисти контекст» (паттерны в `llm.py::RESET_PATTERNS`) → `modes._handle_reset` делает прямой POST с `/new`.
- Автосброс по таймауту **пока не реализован** (кандидат Этапа 1).
### Оптимизации скорости
1. **Keep-alive HTTP сессии** (`requests.Session()`) — в `config.py::_make_session()`, переиспользует TCP/TLS.
2. **Streaming TTS** — ElevenLabs аудио пайпится в `mpv` через stdin, играет пока генерируется.
3. **STT без диска** — PCM → WAV в `BytesIO` → Groq.
4. **Низкокачественный mp3 для речи** (`mp3_22050_32`) — меньше латентность без заметной потери качества для голоса.
5. **`optimize_streaming_latency=3`** в ElevenLabs convert — выдаёт первый чанк быстрее.
### Роутинг по wake word
`modes.py::run_with_porcupine` сейчас грузит только модель Cosmo. Код Люси закомментирован до того, как модель обучена. Когда готова:
- index 0 → `AGENTS["cosmo"]` (:18789)
- index 1 → `AGENTS["lusya"]` (:18790)
### Нормализация речи перед TTS
Два слоя защиты:
1. `text.py::clean_for_speech` — regex-правила: эмодзи, `**жирный**`, числа с плюсом/минусом, проценты, слэши, градусы, аббревиатуры (`т.е.`, `т.к.`), UNIT_SLASH (км/ч → «километров в час»), время `HH:MM` → прописью в правильном падеже через pymorphy3 + num2words.
2. `llm.py::strip_fillers` — режет фразы-заглушки («сейчас посмотрю», «дай секунду») которые агент генерит перед вызовом tool.
### Ошибки не должны ронять сервис
Каждый слой (stt, tts, llm, audio, modes) ловит `Exception` и пишет в `errors.log` через `config.log`. Верхний уровень в modes.py ловит всё непредвиденное и продолжает цикл.
## Запуск
### macOS / Windows (разработка)
```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # заполнить ключи
python satellite.py # режим Enter (без wake word)
python satellite.py --wake # режим openwakeword (нужна обученная .onnx)
```
### 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`)
## Переменные окружения (ключевые)
| Переменная | Что |
|---|---|
| `GATEWAY_URL`, `LUSYA_GATEWAY_URL` | URL OpenClaw gateway |
| `GATEWAY_TOKEN`, `LUSYA_GATEWAY_TOKEN` | Bearer токены |
| `AGENT`, `LUSYA_AGENT` | Имя агента (`openclaw/main`, `openclaw/wife`) |
| `VOICE_MODEL`, `LUSYA_VOICE_MODEL` | LLM (передаётся в `x-ocplatform-model`) |
| `COSMO_SESSION_KEY`, `LUSYA_SESSION_KEY` | Идентификатор серверной сессии OpenClaw |
| `GROQ_API_KEY` | Groq для STT |
| `ELEVENLABS_API_KEY`, `ELEVENLABS_MODEL` | TTS |
| `COSMO_TTS_VOICE`, `LUSYA_TTS_VOICE` | Voice ID в ElevenLabs |
| `WAKE_WORD_COSMO`, `WAKE_WORD_LUSYA` | Пути к `.onnx` моделям wake word |
| `WAKE_THRESHOLD` | Порог активации (0..1, дефолт 0.5) |
| `AUDIO_SINK` | На Pi: `bluez_sink.XX_XX_XX.a2dp_sink`. На Mac/Win: пусто |
| `SILENCE_THRESHOLD`, `SILENCE_DURATION`, `MAX_DURATION`, `FOLLOWUP_TIMEOUT` | VAD |
| `TTS_MODE` | `full` (целостная интонация) или `stream` (быстрый старт, рваный) |
| `ECHO_WARMUP` | Сек пропуска в начале записи (гасит эхо от TTS) |
## Частые задачи
**Сменить голос у агента**: меняй `COSMO_TTS_VOICE` / `LUSYA_TTS_VOICE` в `.env`. Voice ID — на [elevenlabs.io/app/voice-library](https://elevenlabs.io/app/voice-library). Для русского лучше native-русские голоса, а не мультиязычные (не будет англоязычного акцента).
**Отладить VAD**: `SILENCE_THRESHOLD` (громкость) и `SILENCE_DURATION` (сек).
**Добавить третьего агента**: в `config.py::AGENTS` новый ключ + `WAKE_WORD_*` + раскомментировать блок в `modes.py::run_with_porcupine`.
**Сменить модель LLM**: `VOICE_MODEL` в `.env` — передаётся в header `x-ocplatform-model`. Поле `model` в JSON остаётся `openclaw/main` (это имя агента, а не LLM).
**Добавить фразу-заглушку**: в `llm.py::FILLER_PATTERNS` дополнить список. Эти фразы режутся из ответа перед TTS — агент генерит их до tool-call.
## Что НЕ делать
- Не комитить `.env` (есть в `.gitignore`)
- Не возвращать `say`/`espeak` — проект унифицирован на ElevenLabs + mpv
- Не хранить историю диалога на клиенте — это делает OpenClaw по `session_key`
- Не создавать temp файлы для WAV/mp3 — всё через `BytesIO` / stdin pipe
- Не включать `style>0` и `speed≠1.0` в VoiceSettings — усиливают «иностранный» акцент и ломают просодию
## Тренировка wake word
Пайплайн в `training/` (игнорируется в git):
- `record_wav.py <model> <positive|negative> [long <sec>]` — запись 16kHz mono PCM 16-bit
- `remove_silent.py` — чистка + перенумерация
- `step_1.py … step_5.py` — зависимости, датасеты, конфиг, обучение, экспорт
- `training_config.json` — параметры (`wake_word_list`, `use_own_samples`, пенальти, шаги)
- Под капотом: openwakeword (DNN .onnx)
Реалистично для своего голоса: 500+ positive и 1000+ negative, иначе recall < 0.4. Negative должны включать фонетически близкие слова («космос», «просто»).
## Состояние и планы
### ✅ Done
- [x] Модульная структура (`audio/stt/llm/tts/modes/config/text`)
- [x] ElevenLabs streaming + mpv pipe
- [x] Keep-alive HTTP сессии, STT через BytesIO
- [x] Серверные сессии OpenClaw через `x-openclaw-session-key` (не клиентская история)
- [x] Slash-команда `/new` на фразу «начни новую сессию»
- [x] Нормализация речи: числа, единицы, время через pymorphy3 + num2words
- [x] Пайплайн тренировки своего wake word + скрипты записи/чистки датасета
- [x] systemd unit и setup.sh для Pi 5
### 🚧 In progress / нужно сделать
- [ ] **Чистка**: удалить `start_barge_in_listener`/`was_barge_in` из `tts.py`, параметр `conv` из `llm.py`, импорты `sys`/`start_barge_in_listener`/`was_barge_in` из `modes.py`
- [ ] **`FOLLOWUP_TIMEOUT` реально применяется** — сейчас задекларирован, но после ответа ассистент ждёт полный `MAX_DURATION=15s` если пользователь молчит
- [ ] **Унифицировать дефолт session_key** в `config.py` и `.env.example` (сейчас `voice:home:cosmo` vs `agent:voice:voice:home`)
- [ ] **`max_tokens` → env** (`VOICE_MAX_TOKENS`), дефолт 300
- [ ] **Дообучить модель cosmo до recall ≥ 0.7** (нужно 500+ positive + разнообразие)
- [ ] **Подключить Люсю в `run_with_porcupine`** (код закомментирован, готов к включению)
- [ ] **Проверить systemd autostart на Pi в проде** — unit есть, в прод не поставлен
- [ ] **logrotate / size-cap на `errors.log`** — растёт неограниченно
### 📋 Roadmap Этап 2 — качество и надёжность
- [ ] **Автосброс OpenClaw сессии по таймауту** (>1 ч тишины → `/new`)
- [ ] **Retry с backoff** для gateway (3 попытки с экспонентой)
- [ ] **TTS-cache** для дежурных реплик («Начинаю новую сессию», «Не слышу», «Ошибка сервера»)
- [ ] **Persistent PyAudio input stream** (не пересоздавать на каждый `record()`)
- [ ] **Заменить RMS-VAD на `webrtcvad` или `silero-vad`** — RMS не работает с фоновой музыкой
- [ ] **Whisper `prompt` параметр** с «Космо, Люся, OpenClaw» — для имён собственных
- [ ] **SYSTEM_PROMPT опционально на клиенте** — подсказка про TTS-friendly формат чисел/дат, если OpenClaw-агент без неё
### 📋 Roadmap Этап 3 — новые фичи
- [ ] **Home Assistant tool** в OpenClaw: свет, климат, медиа голосом
- [ ] **Контекст окружения** в каждом запросе: время, комната, погода, кто говорит
- [ ] **Proactive notifications**: OpenClaw → WebSocket/SSE → satellite сам инициирует речь (таймеры, напоминания, входящее сообщение)
- [ ] **Realtime barge-in голосом** во время TTS (требует echo cancellation: speex AEC или SpeexDSP)
- [ ] **No-wake mode для доверенной комнаты** — VAD + whisper + intent filter без обязательного wake word
- [ ] **Streaming TTS пер-токен** — отправлять в TTS куски раньше чем целое предложение, с правильными интонационными точками
### 📋 Roadmap Этап 4 — амбициозное
- [ ] **Speaker identification** (`pyannote.audio` / `resemblyzer`) — разные персонализации по голосу
- [ ] **Multi-room координация** — MQTT/gRPC между сателлитами, отвечает тот, кто слышит громче
- [ ] **Локальный fallback LLM** на Pi (phi/llama) когда gateway недоступен — базовые команды без облака
- [ ] **Камера + vision** — агент видит кто в комнате, что происходит
- [ ] **Voice-memory hooks UX** — голосовые команды «запомни», «забудь» (OpenClaw уже умеет, нужен голосовой слой)
## Известные ограничения
- **Нет echo cancellation** — если колонки близко к микрофону (особенно BT колонки на Pi), TTS может триггерить wake-модель. Mitigation: разносить колонку и мик, поднимать `WAKE_THRESHOLD`, использовать наушники при отладке.
- **VAD не отличает голос от музыки/ТВ** — если что-то постоянно шумит выше `SILENCE_THRESHOLD`, ассистент «зависнет» слушать. Решение — `silero-vad`.
- **Одна сессия = бесконечный контекст OpenClaw** пока не вызван `/new`. Нужен автотаймаут.
- **Нет fallback для gateway-offline** — при потере связи ассистент молчит до явной ошибки.