- 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
16 KiB
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_keyagent:voice:voice:home - Люся Gateway: порт
18790, агентopenclaw/wife, session_keyagent:wife:voice:home - Модель:
openai/gpt-5.4-mini(черезx-ocplatform-modelheader; переопределяется через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).
Оптимизации скорости
- Keep-alive HTTP сессии (
requests.Session()) — вconfig.py::_make_session(), переиспользует TCP/TLS. - Streaming TTS — ElevenLabs аудио пайпится в
mpvчерез stdin, играет пока генерируется. - STT без диска — PCM → WAV в
BytesIO→ Groq. - Низкокачественный mp3 для речи (
mp3_22050_32) — меньше латентность без заметной потери качества для голоса. 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
Два слоя защиты:
text.py::clean_for_speech— regex-правила: эмодзи,**жирный**, числа с плюсом/минусом, проценты, слэши, градусы, аббревиатуры (т.е.,т.к.), UNIT_SLASH (км/ч → «километров в час»), времяHH:MM→ прописью в правильном падеже через pymorphy3 + num2words.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-bitremove_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:cosmovsagent: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 — при потере связи ассистент молчит до явной ошибки.