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

16 KiB
Raw Permalink Blame History

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 (разработка)

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 (продакшн)

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. Для русского лучше 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

  • Модульная структура (audio/stt/llm/tts/modes/config/text)
  • ElevenLabs streaming + mpv pipe
  • Keep-alive HTTP сессии, STT через BytesIO
  • Серверные сессии OpenClaw через x-openclaw-session-key (не клиентская история)
  • Slash-команда /new на фразу «начни новую сессию»
  • Нормализация речи: числа, единицы, время через pymorphy3 + num2words
  • Пайплайн тренировки своего wake word + скрипты записи/чистки датасета
  • 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 — при потере связи ассистент молчит до явной ошибки.