OpenCapsula — открытый голосовой ассистент для умной колонки «Маруся» (VK Капсула) на базе Gemini Live API. Проект состоит из двух частей на Go:
| Бинарник | Где работает | Что делает |
|---|---|---|
marusya-agent |
на колонке (linux/arm64, статический, без cgo) | ждёт ключевое слово, пишет речь, стримит её на сервер, проигрывает ответ, выполняет команды (громкость, плеер, Bluetooth) |
marusya-server |
на любом сервере/ПК | принимает поток от колонки, держит сессию Gemini Live, пересылает аудио туда и обратно, превращает function calls модели в команды для колонки |
┌──────────── колонка ────────────┐ ┌──────── сервер ────────┐ ┌─────────────┐
│ mic ─► wakeword ─► VAD ─► agent │ ══ WS ═══► │ session ─► provider │ ═ WS ═► │ Gemini Live │
│ speaker ◄────────────── agent │ ◄═ PCM ═══ │ ◄─ audio/tool │ ◄═════ │ API │
│ amixer / bluetoothctl / mpv ◄── │ ◄─ command │ tools (function calls) │ └─────────────┘
└─────────────────────────────────┘ └────────────────────────┘
Нужен сервер с Docker (amd64 или arm64) в одной сети с колонкой и root по SSH на колонке.
Готовые образы ghcr.io/yawaflua/opencapsule-server и ghcr.io/yawaflua/opencapsule-wake публичные,
собирать ничего не надо. Старые имена marusya-server и marusya-wake устарели: под ними
публикуются те же образы для совместимости, но лучше перейти на новые.
1. Docker и папка для сервера
curl -fsSL https://get.docker.com | sh
mkdir -p ~/marusya/plugins && cd ~/marusya
sudo chown 65532 plugins # плагины хранят тут своё состояние
curl -fsSLO https://raw.githubusercontent.com/yawaflua/OpenCapsule/master/docker-compose.yaml2. Ключи в .env (рядом с docker-compose.yaml)
GEMINI_API_KEY=<ключ из https://aistudio.google.com/apikey>
MARUSYA_AUTH_TOKEN=<случайная строка: openssl rand -hex 24>
MARUSYA_PANEL_PASSWORD=<пароль от веб-панели>
MARUSYA_DEFAULT_LOCATION=Москва
MARUSYA_DEFAULT_TIMEZONE=Europe/MoscowОстальные настройки (прокси, Claude, Home Assistant, Spotify...) можно положить в
server.env рядом, их список в configs/server.env.example.
3. Запуск
docker compose up -d
docker compose ps # marusya-server должен стать healthy4. Колонка — из веб-панели
Откройте http://<ip-сервера>:8080/panel/ и войдите паролем MARUSYA_PANEL_PASSWORD. На вкладке
«Колонки» → «Добавить колонку» укажите IP колонки, пароль root и адрес сервера
ws://<ip-сервера>:8080/v1/stream, нажмите «Установить». Сервер сам поставит на колонку
сервис, и через несколько секунд она появится в списке. Всё, можно разговаривать.
Там же можно сразу поставить AirPlay 2 и Spotify (галочки в форме): для Spotify панель
покажет ссылку для входа в аккаунт. Чтобы «включи …» искало музыку в Spotify, вставьте ключи
приложения Spotify в «Настройки» → «Spotify».
Ключевое слово задаётся в панели («Настройки»). Обновление на новую версию:
docker compose pull && docker compose up -dКолонки обновляются сами: после перезапуска сервера они переподключаются, сверяют свой агент
с серверным (по SHA-256) и, если он другой, скачивают новый и перезапускаются, как только
освободятся. Настройки, профили колонок
и будильники лежат в томе marusya-data и при обновлении сохраняются.
Дальше — подробности: как всё устроено, сборка из исходников, все настройки.
- Ожидание. Агент постоянно пишет микрофон (
arecord, 16 kHz mono) и отдаёт кадры детектору ключевого слова. Активация: внешний keyword-spotter, аппаратная кнопка (evdev) илиSIGUSR1. - Слушание. После активации играет короткий сигнал, музыка ставится на паузу (ducking).
Энергетический VAD ловит начало речи; только тогда агент шлёт
turn_startи аудио (вместе с 300 мс pre-roll). Если речи нет, на сервер ничего не уходит. - Конец фразы. После ~0.9 с тишины уходит
turn_end. На стороне Gemini автоматический VAD выключен, границы реплики задаёт устройство (activityStart/activityEnd). - Ответ. Gemini отдаёт нативную речь (24 kHz PCM), сервер стримит её на колонку,
агент сразу проигрывает через
aplay. - Команды. Если модель вызывает функцию (
volume_set,media_control,bluetooth_pair, …), сервер шлёт колонкеcommand, агент выполняет её и возвращаетcommand_result, а сервер передаёт результат модели как function response. - Продолжение диалога. После ответа агент ещё 4 с слушает без ключевого слова
(
-follow_up). Сессия Gemini живёт-idle_timeout(по умолчанию 2 мин), поэтому контекст разговора сохраняется. Повторное ключевое слово во время ответа его прерывает (barge-in).
cmd/
marusya-agent/ точка входа агента (флаги, сборка зависимостей)
marusya-server/ точка входа сервера
internal/
models/ модели: протокол, аудиоформаты, команды и их результаты
transport/ WebSocket-транспорт (общий для обеих сторон)
provider/ интерфейс Provider/Session для realtime-модели
gemini/ реализация на Gemini Live API (google.golang.org/genai)
openairt/ OpenAI Realtime API (голос в голос)
pipeline/ речь → текстовая модель → речь, общий для текстовых моделей
claude/ Claude (Anthropic Messages API) поверх pipeline
openaichat/ OpenAI-совместимый Chat Completions поверх pipeline
registry/ провайдеры из веб-панели и переключение между ними
echo/ loopback-провайдер для отладки без квоты
speech/ распознавание и синтез речи для текстовых моделей
geminispeech/ через Gemini
openaispeech/ через OpenAI-совместимые /audio/transcriptions и /audio/speech
tools/ описание функций, доступных модели (не зависит от провайдера)
server/ HTTP/WS-сервер и мост устройство ⇄ провайдер
agent/ машина состояний устройства (idle → listening → thinking → speaking)
audio/ интерфейсы Capturer/Player/Stream, PCM-утилиты, earcons
alsa/ реализация через arecord/aplay
vad/ энергетический VAD и эндпоинтер фраз
wakeword/ интерфейс Detector и Multi
exec/ внешний keyword-spotter (stdin PCM → stdout строки)
button/ аппаратная кнопка через /dev/input/eventN
signal/ активация по SIGUSR1
indicator/ интерфейс индикатора состояния (+ Multi)
ringled/ LED-кольцо через spidev ядра
face/ мордашка на 7-сегментном экране (порт mordy)
display/seg7/ драйвер 7-сегментной панели (line-display sysfs)
sensor/light/ датчик освещённости (IIO)
touchkeys/ сенсорная панель: громкость, mute, next, центр = активация
surface/ кольцо, экран, датчики и кнопки вместе: ручное управление на время
dbusapi/ сервис ru.marusya.Agent на системной шине D-Bus
evdev/ чтение клавиш из /dev/input/eventN
controller/ интерфейсы Volume/Media/Bluetooth и Dispatcher команд
amixer/ громкость через ALSA amixer
bluez/ Bluetooth через bluetoothctl (или вендорский bt-pair)
mpv/ плеер через mpv JSON IPC
playerctl/ плеер через MPRIS/playerctl (spotifyd)
stream/ звук YouTube и радио, который декодирует сервер
media/ поиск и стриминг звука: интерфейс Source, HTTP-клиент колонки
ytdlp/ реализация через yt-dlp + ffmpeg (на сервере)
plugin/ запуск плагинов: манифесты, процессы, перезапуск, инструменты
syscmd/ запуск системных утилит (с подменой в тестах)
config/, logging/ общие хелперы для main
deploy/ Dockerfile, docker-compose, OpenRC-сервис для колонки
pkg/pluginsdk/ протокол плагинов и SDK для Go (jsonrpc/: транспорт)
sdk/ SDK плагинов для C++ (один заголовок) и Python
examples/plugins/ примеры плагинов: coin (Python), dice (Go), speaker-stats (C++ + Python)
configs/ примеры конфигов (server.env, /etc/conf.d/marusya-agent)
scripts/wakeword/ пример keyword-spotter на openWakeWord
Чтобы заменить бэкенд, достаточно реализовать нужный интерфейс:
provider.Provider (другая LLM), audio.Player/audio.Capturer (другой звук),
wakeword.Detector, controller.Volume|Media|Bluetooth. Новые функции для модели
удобнее добавлять плагинами, без пересборки (см. «Плагины»).
make allПолучатся bin/marusya-agent-linux-arm64 и bin/marusya-server. Проверить архитектуру
колонки можно командой uname -m: aarch64 значит arm64, armv7l значит 32-битный ARM.
Для второго случая:
make agent AGENT_ARCH=armТесты (включая сквозной: агент → сервер → echo-провайдер):
make testcp configs/server.env.example configs/server.env # вписать GEMINI_API_KEY и токен
set -a; . configs/server.env; set +a
./bin/marusya-server -listen :8080Образ содержит сервер и агент для колонки (/v1/agent/arm64). Колонка скачивает агент
с этого сервера при каждой загрузке, а при каждом переподключении проверяет, не изменился ли
он (сервер отдаёт SHA-256 в ETag, проверка без изменений — это ответ 304). Новый агент
колонка скачивает поверх старого и перезапускается, когда не занята разговором; выключить:
MARUSYA_AUTO_UPDATE=false в /etc/conf.d/marusya-agent. Собирается под архитектуру хоста
(amd64 или arm64).
cp configs/server.env.example configs/server.env # ключи, токен, место
docker compose -f deploy/server/docker-compose.yaml up -d --build
docker compose -f deploy/server/docker-compose.yaml logs -fГотовые образы для amd64 и arm64 CI собирает из master, stage и тегов v*
(.github/workflows/docker.yml): ghcr.io/yawaflua/opencapsule-server и
ghcr.io/yawaflua/opencapsule-wake. Те же образы пока выходят и под старыми именами
marusya-server и marusya-wake, но они помечены устаревшими (obsolete). Тег latest соответствует master, тег dev
соответствует stage. Чтобы не собирать локально:
docker compose -f deploy/server/docker-compose.yaml pull
docker compose -f deploy/server/docker-compose.yaml up -dЗатем на колонке поменяйте MARUSYA_SERVER в /etc/conf.d/marusya-agent на
ws://<сервер>:8080/v1/stream. Если сервер доступен из интернета, поставьте перед ним
reverse proxy с TLS (wss://) и задайте длинный MARUSYA_AUTH_TOKEN.
Задайте пароль в configs/server.env и перезапустите контейнер:
MARUSYA_PANEL_PASSWORD=придумайте-парольПанель откроется по адресу http://<сервер>:8080/panel/. Без пароля она выключена. В панели есть:
- Колонки: какие колонки подключены, с какого IP, слушают или думают ли они сейчас. Там же пульт: громкость, музыка (пауза, следующий трек, поток по ссылке), ночник и Bluetooth. Карточка «Кольцо и экран» зажигает кольцо любым цветом или анимацией, выводит на экран текст или часы и показывает датчики (подробнее в разделе «Кольцо и экран вручную»). Ещё можно задать вопрос текстом: Маруся ответит голосом из колонки. «Забыть разговор» сбрасывает контекст диалога.
- История: что услышала колонка, что ответила Маруся, какие команды и навыки вызывались, подключения и ошибки. Лента обновляется вживую и хранит последние 500 событий в памяти, поэтому после перезапуска сервера она очищается.
- Настройки: системный промпт, город и часовой пояс по умолчанию, язык, голос
и модель. Колонки, которые сейчас молчат, получают их сразу, а занятые разговором
получат со следующим вопросом. В Docker настройки сохраняются в томе
marusya-data(MARUSYA_SETTINGS_FILE=/data/settings.json) и важнее значений изserver.env. Кнопка «Вернуть как при запуске» удаляет сохранённые изменения. - Провайдеры: какие модели отвечают. Можно добавить Gemini Live, OpenAI Realtime,
OpenAI-совместимый чат (OpenAI, OpenRouter, DeepSeek, Groq, Ollama, LM Studio…) и
Claude, у каждого свой ключ, адрес API и модель, и переключаться между ними на лету.
Ключи хранятся на сервере (
MARUSYA_PROVIDERS_FILE=/data/providers.json, права 0600) и в панель не возвращаются. Подробнее в разделе «Другие модели».
Чтобы задавать вопросы текстом, нужен агент с поддержкой remote_turn. Колонка
скачивает агент с сервера при загрузке, так что после обновления сервера её достаточно
перезагрузить. Провайдер echo текстовые вопросы не принимает.
Пароль от панели передаётся открытым текстом, поэтому из интернета открывайте её только через reverse proxy с HTTPS.
Основные флаги: -gemini_model, -gemini_voice (Kore, Puck, Charon, Aoede, …),
-google_search, -system_prompt_file, -idle_timeout, -vertex -gcp_project …
(Vertex AI вместо Developer API), -provider echo (отладка без Gemini).
Навыки на стороне сервера (флаг -skills, включены по умолчанию) работают без ключей,
через Open-Meteo:
time_now: «сколько время» → «21:05 в Ашдоде». Можно спросить и про другой город.weather_forecast: текущая погода и прогноз до 7 дней.
Всё остальное (новости, курсы, факты) модель ищет через Google Search (-google_search).
Местоположение колонки задаётся на колонке (MARUSYA_LOCATION, MARUSYA_TIMEZONE в
/etc/conf.d/marusya-agent) или по умолчанию на сервере: -default_location Ашдод
(часовой пояс по умолчанию берётся с ПК).
Если колонки стоят у разных людей, каждой можно задать профиль: вкладка «Колонки» →
выбрать колонку → «Профиль колонки». В профиле есть название, город (для погоды и
времени), часовой пояс, язык речи и дополнительные языки (подсказки распознаванию,
например he-IL, en-US). Пустое поле берётся из общих настроек. Профиль важнее того,
что колонка сообщает о себе сама (MARUSYA_LOCATION, -language), и применяется сразу,
даже к колонке, которая сейчас на связи.
Профили хранятся по ID колонки (-device_id агента, по умолчанию hostname) в файле
MARUSYA_PROFILES_FILE, в Docker это /data/devices.json. Тот же API есть без панели:
GET/PUT/DELETE /panel/api/profiles/<id>.
Свой токен. «Новый токен» создаёт для колонки отдельный токен. Пропишите его на
колонке в MARUSYA_AUTH_TOKEN (/etc/conf.d/marusya-agent) вместо общего. С этим токеном
колонка входит только под своим ID: проданная колонка не сможет выдать себя за чужую, а
забрать у неё доступ можно, удалив токен или выпустив новый (она сразу отключится).
Общий токен сервера (-auth_token) продолжает работать для колонок без своего токена.
«Разбуди меня в 7:30», «будильник по будням на 7 на громкости 90», «разбуди под Кино»,
«поставь таймер на 10 минут», «какие у меня будильники», «удали будильник на 7:30».
Будильники хранит сервер (MARUSYA_ALARMS_FILE, в Docker /data/alarms.json), считает
время звонков по часовому поясу колонки (профиль или общие настройки) и заранее отправляет
колонке звонки на 8 дней вперёд. Колонка звонит сама, даже если сервер в этот момент
недоступен. «По будням» в Израиле — с воскресенья по четверг.
Звонок: нарастающий писк или музыка с YouTube, если её заказали, на заданной громкости (потом громкость возвращается). Выключить звонок: нажать на колонку или сказать «Маруся» — это только выключает будильник и не начинает разговор. Без реакции звонок сам замолкает через 10 минут. Будильники видно и можно добавить в панели, в карточке колонки.
«Маруся, включи Кино — Группа крови», «поставь лоу-фай», «включи видео про котов с ютуба»:
модель вызывает media_play_search, сервер находит видео на YouTube через
yt-dlp, ffmpeg декодирует звук, и колонка получает
готовый PCM (48 kHz mono) по HTTP (GET /v1/media?q=… или ?url=…, с токеном колонки).
Ссылки (YouTube, другие сайты, интернет-радио) включаются через media_play_url, их можно
вставить и в панели, в поле под кнопками музыки. Туда же можно написать название песни.
На колонке ничего ставить не нужно. Пауза, «продолжи» и «стоп» работают как обычно,
на время разговора с Марусей звук останавливается, а новый трек начинается после её ответа.
Ссылки spotify: и «следующий трек» по-прежнему идут в spotifyd.
В Docker-образе уже есть yt-dlp, ffmpeg и Node.js. Без Docker поставьте yt-dlp и
ffmpeg на сервер: если их нет, сервер пишет в лог media playback disabled.
Флаги: -media=false выключает функцию, -ytdlp_args (MARUSYA_YTDLP_ARGS) передаёт
yt-dlp дополнительные аргументы, например --js-runtimes node (в образе по умолчанию) или
--cookies /data/cookies.txt, если YouTube просит подтвердить, что вы не бот.
YouTube часто меняется: если поиск перестал работать, пересоберите образ, чтобы
скачался свежий yt-dlp. На колонке функцию выключает -server_streams=false.
Модель выбирается при запуске флагом -provider, а во вкладке «Провайдеры» веб-панели
можно добавить сколько угодно провайдеров и переключаться между ними без перезапуска.
| Тип | Как отвечает | Что нужно |
|---|---|---|
| Gemini Live | голос в голос | ключ Gemini; адрес API можно заменить на Gemini-совместимый шлюз |
| OpenAI Realtime | голос в голос (gpt-realtime), конец фразы определяет модель |
ключ OpenAI |
| OpenAI-совместимый чат | распознавание речи → текстовая модель → озвучка по предложениям | ключ и адрес API, например https://openrouter.ai/api/v1 или http://ollama:11434/v1 |
| Anthropic Claude | так же, через Claude | ключ Anthropic или профиль ant auth login |
Текстовым моделям нужны «слух» и «голос». В карточке провайдера их выбирают так:
Gemini сервера (GEMINI_API_KEY), «этот же API» (эндпоинты /audio/transcriptions и
/audio/speech, есть у OpenAI, LocalAI, Speaches) или ключ другого провайдера Gemini/OpenAI.
Если у нового провайдера другой формат звука или он сам определяет конец фразы, колонки
переподключатся примерно за секунду. Модель и голос из «Настроек» относятся к
выбранному провайдеру и сохраняются в нём.
Вход только по API-ключам: подписки ChatGPT и Claude.ai для сторонних приложений не предназначены.
OpenAI можно выбрать и при запуске:
OPENAI_API_KEY=... ./bin/marusya-server -provider openai-realtime -openai_voice marin
OPENAI_API_KEY=... ./bin/marusya-server -provider openai \
-openai_base_url https://openrouter.ai/api/v1 -openai_model google/gemini-2.5-flash-provider claude отвечает через Claude (по умолчанию claude-haiku-5-5, флаг
-claude_model). Claude работает только с текстом, поэтому ход устроен так:
распознавание речи через Gemini (-stt_model), ответ Claude с вызовом функций колонки,
навыками сервера и поиском в интернете (Claude web search, флаг -google_search),
озвучка через Gemini TTS (-tts_model, голос -gemini_voice) по предложениям, ещё во время
генерации. Нужны оба ключа: ANTHROPIC_API_KEY и GEMINI_API_KEY.
-claude_effort low (по умолчанию) даёт самые быстрые ответы. В этом режиме конец фразы
определяет колонка.
ANTHROPIC_API_KEY=... GEMINI_API_KEY=... ./bin/marusya-server -provider claude \
-auth_token secret -agent_dir bin -default_location АшдодКонец фразы определяет Gemini (-vad server, по умолчанию): он надёжнее работает в шумной
комнате. Энергетический детектор колонки остаётся запасным вариантом. -vad device
возвращает старое поведение.
Для работы через интернет используйте -tls_cert/-tls_key (wss://) или reverse proxy.
Если Gemini недоступен из вашей страны или сервер выходит в интернет только через прокси,
задайте -proxy (MARUSYA_PROXY). Поддерживаются http://, https:// (TLS до прокси),
socks5:// и socks5h:// (DNS на стороне прокси), логин и пароль пишутся в адресе:
MARUSYA_PROXY=socks5h://user:pass@proxy.example.com:1080 ./bin/marusya-server ...Через прокси идут все исходящие запросы сервера: Gemini (REST и Live API), Claude,
Spotify, Open-Meteo, Home Assistant и yt-dlp (ему передаётся --proxy, если его нет
в MARUSYA_YTDLP_ARGS; для https://-прокси yt-dlp нужен модуль requests).
-gemini_proxy(MARUSYA_GEMINI_PROXY) задаёт отдельный прокси только для Gemini. Значениеdirectподключает Gemini напрямую, даже если задан-proxy.-no_proxy(MARUSYA_NO_PROXY) перечисляет адреса, к которым сервер ходит напрямую: хосты, домены (.example.com) и подсети CIDR. По умолчанию это частные сети (192.168.0.0/16,10.0.0.0/8, …) и домены.local,.lan,.home.arpa, поэтому Home Assistant в домашней сети работает без прокси.localhostи имена без точки (сервисы Docker вродеmarusya-wake,homeassistant) всегда идут напрямую.
Без -proxy сервер, как и раньше, учитывает HTTPS_PROXY/HTTP_PROXY/NO_PROXY.
Нужен root по ssh. Агент — статический бинарник, поэтому musl ему не мешает. Зависимости ставятся из apk
(mpv лежит в репозитории community):
make device-deps DEVICE=root@<ip-колонки> # apk add alsa-utils bluez mpv
make deploy-agent DEVICE=root@<ip-колонки> # бинарник, OpenRC-сервис, rc-update addБез репозитория колонку можно добавить из веб-панели: вкладка «Колонки» → «Добавить
колонку». Укажите IP колонки, пароль root и адрес сервера, каким его видит колонка
(ws://<ip-сервера>:8080/v1/stream). Сервер сам зайдёт на колонку по SSH, поставит
OpenRC-сервис с адресом и токеном этого сервера и запустит его. Пароль не сохраняется. Если
конфиг на колонке уже есть, в нём меняются только MARUSYA_SERVER и MARUSYA_AUTH_TOKEN.
Работает, когда сервер раздаёт агента (MARUSYA_AGENT_DIR, в Docker-образе уже задан).
Настройки лежат в /etc/conf.d/marusya-agent на колонке; шаблон — в
configs/agent.conf.d.example, при первом деплое он копируется сам.
Управление сервисом:
rc-service marusya-agent restart
rc-service marusya-agent wake # тестовая активация без ключевого слова (SIGUSR1)
tail -f /tmp/marusya-agent.logАгент работает с железом напрямую через интерфейсы ядра, утилиты прошивки (ringled,
mordy, clockdisp, lightmon) ему не нужны:
| Устройство | Интерфейс ядра |
|---|---|
| экран | line-display (/sys/devices/platform/display/linedisp.0) и сегменты /sys/class/leds/clock:d1:a … clock:d4:g, точки clock:colon:* |
| датчик освещённости BH1730 | IIO, /sys/bus/iio/devices/iio:device0/in_illuminance_input |
| LED-кольцо | spidev (/dev/spidev0.0) на контроллере meson-spicc |
| сенсорные кнопки | evdev, /dev/input/eventN |
Сборки на стоковом ядре 4.9 устроены иначе, и агент это определяет сам. На них нет line-display,
а сегменты экрана называются clock-led1 … clock-led28: по 7 на цифру, сегменты a…g по порядку,
clock-led30 — точка двоеточия. Кольцо там висит на втором контроллере is31fl32xx и видно как
top-led1 … top-led36, по три светодиода (R, G, B) на пиксель. Если мордашка на таком экране
вверх ногами, задайте MARUSYA_DISPLAY_ROTATE=true.
Сервис mordy нужно выключить (rc-service mordy stop; rc-update del mordy), иначе он тоже
будет рисовать на экране.
| Кнопка | Действие |
|---|---|
| центр, коротко | активировать ассистента; повторное нажатие, пока колонка слушает, отменяет вопрос |
| центр, удержание 1.5 с | bt-pair (режим Bluetooth-колонки) |
| VOL+ / VOL− | громкость ±5%, удержание — автоповтор, значение на экране |
| PROG1 | следующий трек (playerctl, spotifyd) |
| MICMUTE | выключить микрофон: агент получает тишину, мордашка «закрывает глаза» |
Закончить разговор можно и голосом: на «пока», «хватит», «отстань» и грубости модель
вызывает conversation_end. Колонка коротко прощается, не ждёт продолжения, а сервер
забывает контекст, так что следующий вопрос начинается с чистого листа.
Кольцо (по приоритету): слушает — синее, думает — фиолетовое, отвечает — зелёное, ошибка/нет связи — красное; микрофон выключен — красное; «включи ночник» — жёлтое (можно попросить другой цвет/яркость); иначе выключено.
Датчик света (bh1730, IIO) приглушает экран и в темноте укладывает мордашку спать.
Мордашка: в ожидании моргает и смотрит по сторонам (глаза 0 . 0, при взгляде вбок
сдвигаются 00. / .00), слушает — большие глаза, думает — глаза бегают,
отвечает — довольные ∩ . ∩, ошибка — грустная, нет связи — грустная и засыпает.
Кольцом и экраном можно управлять из веб-панели (карточка «Кольцо и экран»), по D-Bus прямо на колонке, а кольцом ещё и голосом: «сделай кольцо синим», «включи радугу».
- Кольцо: цвет, яркость и анимация: ровный цвет, дыхание, бегущий огонь, пульс, радуга или любой из обычных видов Маруси («слушаю», «думаю», …).
- Экран: текст до 4 знаков (латиница, цифры, знаки, как умеет 7-сегментный экран), часы, «спать», «не спать» (не засыпать в темноте), погасить и яркость. Яркость 0 значит «по датчику света».
- Датчики: освещённость, спит ли мордашка, выключен ли микрофон кнопкой, последняя нажатая кнопка и что сейчас делает Маруся. Панель обновляет их раз в 5 секунд.
Ручной режим всегда временный: по умолчанию 10 минут, максимум 12 часов, потом колонка сама возвращается к обычному виду. «Как обычно» возвращает сразу. Пока Маруся слушает или отвечает, кольцо и экран показывают это поверх, а затем снова ручной режим. Для постоянного света есть ночник.
Карточка появляется, только если агент на колонке это умеет. После обновления сервера перезагрузите колонку, и она скачает новый агент.
Агент публикует на системной шине сервис ru.marusya.Agent, объект /ru/marusya/Agent.
Им могут пользоваться свои скрипты и плагины на колонке, сервер для этого не нужен.
| Интерфейс | Методы | Свойства |
|---|---|---|
ru.marusya.Agent |
State (idle, listening, thinking, speaking, …), MicMuted, Version |
|
ru.marusya.Agent.Ring |
Play(s scene, s color, i brightness, u seconds), SetColor(s color, i brightness, u seconds), Reset() |
Scenes, Override |
ru.marusya.Agent.Display |
ShowText(s text, u seconds), ShowClock(u seconds), SetMode(s mode, s text, i brightness, u seconds), Reset() |
Modes, Override, Asleep |
ru.marusya.Agent.Sensors |
сигнал Key(s name, q code, b pressed) |
AmbientLight (лк), HasAmbientLight, LastKey |
Все свойства присылают PropertiesChanged. seconds = 0 значит 10 минут, пустой цвет и
нулевая яркость дают значения по умолчанию. Override — словарь active, scene/mode,
text, color, brightness, until (Unix-время); active = false, когда всё как обычно.
Кнопки называются voice, next, mic, volume_up, volume_down.
Сервису нужно разрешение на шине. Проще всего оставить включённым «D-Bus» в форме
«Добавить колонку»: сервер поставит dbus (если его нет), положит
deploy/agent/ru.marusya.Agent.conf в
/etc/dbus-1/system.d/, создаст группу marusya, добавит dbus в автозагрузку, перечитает
правила шины и включит MARUSYA_DBUS=true. Вручную: скопируйте этот файл в
/etc/dbus-1/system.d/, выполните addgroup -S marusya и rc-service dbus reload. Управлять кольцом и экраном
могут root и группа marusya, читать свойства и слушать сигналы может кто угодно.
Без шины или без разрешения агент просто работает дальше без сервиса, выключить его
можно MARUSYA_DBUS=false.
# радуга на минуту
dbus-send --system --print-reply --dest=ru.marusya.Agent /ru/marusya/Agent \
ru.marusya.Agent.Ring.Play string:rainbow string: int32:0 uint32:60
# текст на экране на 10 минут
dbus-send --system --print-reply --dest=ru.marusya.Agent /ru/marusya/Agent \
ru.marusya.Agent.Display.ShowText string:HI uint32:0
# освещённость
dbus-send --system --print-reply --dest=ru.marusya.Agent /ru/marusya/Agent \
org.freedesktop.DBus.Properties.Get string:ru.marusya.Agent.Sensors string:AmbientLight
# следить за кнопками и датчиками
dbus-monitor --system "sender='ru.marusya.Agent'"Если на колонке есть bt-pair (режим сопряжения, после которого Капсулу можно использовать
как Bluetooth-колонку), агент находит его в PATH сам (-bt_pair_command auto, значение по умолчанию).
На фразу «включи блютуз» или «хочу подключить телефон» Gemini вызывает bluetooth_pair, и агент запускает bt-pair.
Агент не убивает bt-pair по таймауту: утилита сама решает, сколько держать режим колонки.
Повторный вызов, пока она работает, ничего не делает. Если bt-pair нет, используется встроенный
сценарий через bluetoothctl (агент NoInputNoOutput, discoverable на timeout_seconds).
Полезные флаги агента:
| Флаг | Назначение |
|---|---|
-capture_device, -playback_device |
ALSA-устройства. Массив микрофонов часто многоканальный: заведите plug/dsnoop в asound.conf с понижением до mono 16 kHz |
-wake_command |
keyword-spotter: читает PCM из stdin, на каждое срабатывание печатает строку |
-button_device, -button_code |
кнопка активации (evdev) |
-mixer_control |
ALSA-контрол громкости (Master, PCM, Speaker…, см. amixer scontrols); пустое значение отключает управление громкостью |
-bt_pair_command |
утилита сопряжения: auto (по умолчанию) — bt-pair, если он есть в PATH; можно указать bt-pair {timeout}; пустая строка — встроенный сценарий через bluetoothctl |
-mpv_socket |
сокет mpv; пустое значение отключает управление музыкой |
-vad_threshold_db, -vad_silence |
чувствительность VAD и длина паузы, завершающей фразу |
-ring, -ringled_brightness |
LED-кольцо: auto (по умолчанию) включает его на VK Капсуле Мини, которую агент узнаёт по device tree (vk,capsula-mini), или на стоковом ядре, где кольцо — это top-led1…36; on включает всегда, пустое значение отключает. Слушает — синяя вспышка волной от кнопки мута, думает — фиолетовая «комета» крутится по кольцу, отвечает — зелёное дыхание, ошибка — два красных мигания, нет связи — тусклый красный. Мут — красный растекается от кнопки MICMUTE на всё кольцо и уходит обратно в неё; громкость — белая дуга на полторы секунды |
-display_rotate |
перевернуть мордашку на экране из clock-led1…30 (сборки на стоковом ядре) |
-ring_dev |
spidev-узел кольца, по умолчанию /dev/spidev0.0; кадры пишутся прямо в него, 30 к/с |
-ring_demo |
показать все анимации кольца по очереди и выйти (остановите сервис перед запуском) |
-state_command |
команда, которой передаётся состояние ассистента последним аргументом: idle, listening, thinking, speaking, error, disconnected, announcing. Для колонок, где кольцом или экраном владеет другой процесс: агент сообщает состояние, процесс рисует. Повторы пропускаются, команда не блокирует |
-follow_up, -duck_media, -earcons |
поведение диалога |
Есть два способа: распознавание на сервере (слово меняется в веб-панели) и модель на самой колонке.
На сервере. Пока колонка ждёт, она шлёт звук с микрофона на сервер, а сервер передаёт его
в сервис marusya-wake (deploy/wake). Услышав слово, сервис будит колонку.
# configs/server.env
MARUSYA_WAKE_URL=ws://marusya-wake:2700
MARUSYA_WAKE_WORDS=антон, אנטון # слова по умолчанию; меняются в панели → Настройки
cd deploy/server && docker compose --profile wake up -d --build- Русские слова распознаёт Vosk (small-ru, модель уже в образе). Он работает с грамматикой из ключевых слов и срабатывает примерно через полсекунды после слова. Если слова нет в словаре Vosk, его распознаёт Whisper.
- Иврит и другая не кириллица идут через faster-whisper (
WHISPER_MODEL, по умолчаниюsmall, ~500 МБ скачиваются в volume при первом запуске). Whisper расшифровывает короткие фразы и сравнивает их со словом нечётко: огласовки и конечные буквы не важны, похожие на слух буквы (א/ע, ט/ת, כ/ק) считаются одинаковыми. На CPU это 1–3 секунды задержки, поэтому лучше сделать паузу после слова: «אנטון… מה מזג האוויר?». Модельbaseбыстрее, но имена путает. - Можно задать до 10 слов через запятую. Панель предупредит, если слово не поддерживается.
- Кнопка и
-wake_commandна колонке продолжают работать. БезMARUSYA_WAKE_URL, а также со старым агентом звук в режиме ожидания никуда не отправляется. - Когда к серверу не подключена ни одна колонка, сервис засыпает: через
WAKE_SLEEP_AFTERсекунд (по умолчанию 60) он выгружает модели Vosk и Whisper из памяти и загружает их снова, когда колонка вернётся. Первое срабатывание после сна приходит на пару секунд позже.WAKE_SLEEP_AFTER=0держит модели загруженными всегда.
Важно: при серверном слове микрофон слушает постоянно, и звук уходит на ваш сервер в локальной сети. Дальше сервера он не идёт: в Gemini попадает только запрос после срабатывания. Кнопка MICMUTE останавливает отправку.
На колонке (оставлено на будущее). В чистом Go нет готового движка распознавания ключевых
слов, поэтому агент работает с ним через простой контракт -wake_command: внешний процесс
получает сырой PCM в stdin и печатает строку при срабатывании. Подойдёт openWakeWord
с обученной моделью «Антон» (пример в scripts/wakeword/), Porcupine, microWakeWord,
Vosk с грамматикой из одного слова или штатный детектор прошивки, если его удастся так обернуть.
Любая программа может заговорить через колонку и, если нужно, дождаться ответа голосом:
curl -s http://192.168.31.80:8080/v1/ask -H "Authorization: Bearer $MARUSYA_AUTH_TOKEN" \
-d '{"text": "Закрыть шторы?", "listen": true, "timeout": 120}'
# {"device":"vk-mini-a113x","answered":true,"answer":"да, закрой"}Как это происходит:
- Сервер синтезирует текст голосом Gemini (нужен
GEMINI_API_KEY, Vertex AI не подойдёт). - Если колонка занята разговором, сервер ждёт, пока она освободится.
- Колонка играет звонок, кольцо пульсирует оранжевым, текст зачитывается.
- С
listenколонка 10 секунд ждёт ответа. Ответ распознаётся и возвращается в JSON. Модели он не передаётся, Маруся сама ничего не отвечает. - Если нажать кнопку во время чтения, колонка перестанет читать и сразу начнёт слушать.
Если никто не ответил, придёт "answered": false. Если к серверу не подключена ни одна
колонка, сервер спит: запрос сразу пропускается с "skipped": true (код 200), ничего не
озвучивается и не ждёт. Хук marusya-ask в этом случае молча завершается, и Claude Code
работает как без него. Поля запроса:
text— что сказать, до 1000 символов;listen— ждать ли ответа;device— ID колонки (по умолчанию первая);timeout— сколько ждать, в секундах (по умолчанию 120, максимум 600);source— кто спрашивает, для истории в панели.
Нужен агент с поддержкой объявлений: перезагрузите колонку после обновления сервера.
Текст озвучивает Live-модель (-gemini_model): её просят прочитать его дословно, и тратится
квота Live API, а не TTS (на бесплатном тарифе у TTS всего 10 запросов в день).
-ask_tts tts (MARUSYA_ASK_TTS) возвращает озвучку через -tts_model.
marusya-ask — хук для Claude Code (и вкладки Code в Claude Desktop):
- Claude закончил работу и задал вопрос — колонка зачитывает его: «Клод в проекте X спрашивает: …».
- Вы отвечаете голосом — Claude продолжает с вашим ответом. Технически хук
Stopвозвращаетdecision: block. - Если промолчать, Claude останавливается как обычно, и ответить можно с клавиатуры.
- Нужно разрешение на команду — колонка об этом скажет. Подтверждать по-прежнему нужно на компьютере.
- Claude задаёт вопрос диалогом с вариантами — колонка читает вопрос и варианты. Ответ голосом закрывает диалог, и Claude продолжает с ним. Если промолчать, диалог остаётся на экране.
go install ./cmd/marusya-ask # → ~/go/bin/marusya-ask
marusya-ask -server http://192.168.31.80:8080 -token "$MARUSYA_AUTH_TOKEN" -listen "Проверка связи?"~/.claude/settings.json:
{
"env": {
"MARUSYA_URL": "http://192.168.31.80:8080",
"MARUSYA_AUTH_TOKEN": "токен из configs/server.env"
},
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "$HOME/go/bin/marusya-ask hook", "timeout": 600 }] }
],
"PreToolUse": [
{ "matcher": "AskUserQuestion", "hooks": [{ "type": "command", "command": "$HOME/go/bin/marusya-ask hook", "timeout": 600 }] }
],
"Notification": [
{ "matcher": "permission_prompt", "hooks": [{ "type": "command", "command": "$HOME/go/bin/marusya-ask hook" }] }
]
}
}Что колонка зачитывает:
- если в конце ответа Claude есть вопросы — последние из них, до 300 символов;
- если вопросов нет — «закончил:» и первую фразу. Чтобы в этом случае колонка молчала, добавьте
-on_done=false.
Код, ссылки и markdown выбрасываются. Ответ передаётся Claude как распознанная речь, с пометкой, что возможны ошибки распознавания.
Колонка принимает AirPlay 2 с iPhone, iPad и Mac через shairport-sync. Готовая сборка
для Капсулы Мини (shairport-sync, nqptp, avahi, сервисы OpenRC):
ryysel/capsula-mini-airplay2.
Проще всего поставить её из веб-панели: в форме «Добавить колонку» включите «AirPlay 2».
Сервер скачает сборку (закреплённую версию), передаст её на колонку и запустит
install.sh --upgrade --name <имя колонки>. Вручную — по инструкции из того репозитория.
Агент сам находит shairport-sync на системной D-Bus (org.gnome.ShairportSync), отдельно
ничего настраивать не нужно. Пока телефон играет на колонку:
- «пауза», «дальше», «что играет» голосом и в панели уходят на телефон, а не в mpv или spotifyd;
- на время вопроса музыка ставится на паузу и потом продолжается, как и для остальных плееров;
- если попросить включить что-то другое (YouTube, Spotify), AirPlay ставится на паузу.
Если shairport-sync не запущен или телефон отключился, команды идут обычному плееру.
Выключить: MARUSYA_AIRPLAY=false. Команды доходят до телефона с задержкой около секунды
(протокол DACP).
Если Spotify настроен, музыка по умолчанию идёт из него: «Маруся, включи Кино — Группа
крови», «поставь альбом Nevermind». Модель вызывает серверный навык spotify_play. Сервер
ищет музыку в каталоге Spotify и отправляет колонке ссылку spotify:…, а агент открывает
её в spotifyd через MPRIS. Если в Spotify такого нет или spotifyd не отвечает, то же самое
само включается с YouTube. Видео, подкасты и «включи с ютуба» по-прежнему идут в YouTube.
Пауза, «дальше» и пауза на время разговора работают как раньше.
Сервер ищет от имени приложения (client credentials), без аккаунтов пользователей, поэтому одного приложения хватает на любое число колонок. Играет колонка с того аккаунта, в который залогинен её spotifyd. Нужен Premium.
На сервере создайте приложение на
developer.spotify.com (Web API) и вставьте его
Client ID и Client Secret в веб-панели: «Настройки» → «Spotify». Сервер сначала проверит
ключи у Spotify и только потом сохранит их в spotify.json рядом с MARUSYA_SETTINGS_FILE
(в Docker — том /data). Там же можно ограничить поиск страной (RU, IL). Новые ключи
действуют с нового разговора. Можно и через переменные окружения, ключи из панели важнее:
MARUSYA_SPOTIFY_CLIENT_ID=…
MARUSYA_SPOTIFY_CLIENT_SECRET=…
На колонке нужен spotifyd 0.4+ с MPRIS. Из веб-панели: в форме «Добавить колонку»
включите «Spotify». Сервер поставит spotifyd playerctl dbus из apk, конфиг
deploy/agent/spotifyd.conf (если своего ещё нет), правила
D-Bus deploy/agent/spotifyd-dbus.conf и сервис
deploy/agent/spotifyd.openrc. Затем панель покажет ссылку
для входа в Spotify: войдите, браузер перейдёт на http://127.0.0.1:8000/login?code=… и
покажет ошибку — скопируйте этот адрес в панель, и сервер передаст его spotifyd на колонке
(spotifyd authenticate). Вход сохраняется в /var/cache/spotifyd, после перезагрузки
spotifyd логинится сам. Без ссылки тоже можно: выберите колонку в приложении Spotify в той
же сети Wi-Fi.
Вручную: apk add spotifyd playerctl dbus, скопируйте эти три файла в /etc/spotifyd.conf,
/etc/dbus-1/system.d/spotifyd.conf и /etc/init.d/spotifyd, выполните
rc-update add dbus default && rc-update add spotifyd default и войдите через
spotifyd authenticate. Графической сессии на колонке нет, поэтому spotifyd работает на
системной шине D-Bus.
spotifyd 0.4 показывает плеер MPRIS, только пока он активное устройство Spotify Connect.
Поэтому агент перед открытием ссылки spotify: сам делает его активным
(rs.spotifyd.Controls.TransferPlayback), и выбирать колонку в приложении не нужно.
Ищутся только публичные плейлисты: личные плейлисты пользователя серверу не видны.
Плагин добавляет модели новые функции и реагирует на события, не требуя пересборки сервера
или агента. Это отдельная программа в своей папке рядом с файлом plugin.json. Сервер или
агент запускает её и общается с ней по JSON-RPC 2.0 через stdin/stdout, по одному сообщению
на строку. Писать плагины можно на любом языке; готовые SDK есть для Go
(pkg/pluginsdk), C++ (sdk/cpp, один заголовок
без зависимостей) и Python (sdk/python, только стандартная
библиотека). Протокол описан в sdk/README.md.
Плагин может работать в одном из трёх вариантов:
- на сервере: интернет-сервисы, ключи API, общие данные всех колонок;
- на колонке: железо, файлы и процессы колонки (агент собран статически, поэтому
плагин для колонки удобнее всего писать на Go или C++ и собирать под
linux/arm64статически); - в обеих половинах сразу: половины обмениваются сообщениями через соединение колонки с сервером.
{
"id": "dice",
"name": "Кубики",
"version": "1.0.0",
"description": "Бросает игральные кубики",
"server": {"command": ["./dice"]},
"agent": {"command": ["./dice-agent"], "env": {"X": "1"}},
"config": {"max_dice": 10}
}Включаются плагины каталогом: MARUSYA_PLUGINS_DIR (флаг -plugins_dir) на сервере и в
/etc/conf.d/marusya-agent на колонке. В каталоге лежит по папке на плагин.
Что умеет плагин:
- объявлять функции для модели. Модель вызывает их как встроенные. Имена, занятые встроенными функциями, отклоняются;
- выполнять действия на колонке (
host.command): громкость, музыка, кольцо — всё, что есть вhello.capabilities; - говорить через колонку (
host.say, только на сервере), в том числе с вопросом и ответом, как/v1/ask; - получать события: на сервере
connect,disconnect,turn,command,skill,wake; на колонкеstate(idle,listening,thinking,speaking...); - писать в лог сервера или агента (а также через stderr);
- на сервере ещё быть моделью (провайдер типа «Плагин»), синтезировать и распознавать
речь, отвечать на HTTP по адресу
/plugins/<id>/, дополнять системную инструкцию (характер, правила, знания) и задавать Марусе вопросы от имени пользователя (host.request). Подробнее ниже.
Упавший плагин перезапускается с паузой от 1 секунды до минуты. Во вкладке «Плагины»
веб-панели видно состояние каждого плагина и его функции. Там же плагин можно выключить,
перезапустить и поменять его настройки (поле config, хранится в
<каталог>/.state.json). Новые папки подхватываются кнопкой «Найти новые». Функции плагинов
колонок тоже показаны в панели. Когда у плагина колонки меняется набор функций, агент
переподключается к серверу и сообщает новый список.
Примеры в examples/plugins:
# самый простой: «подбрось монетку», Python, собирать не нужно
cp -r examples/plugins/coin plugins/
cp sdk/python/marusya_plugin.py plugins/coin/
# серверный плагин на Go
go build -o examples/plugins/dice/dice ./examples/plugins/dice
# двусторонний: колонка на C++, сервер на Python
make -C examples/plugins/speaker-stats CXX=aarch64-linux-gnu-g++ LDFLAGS=-static| Пример | Где | Что показывает |
|---|---|---|
| coin | сервер, Python | одна функция, минимум кода |
| dice | сервер, Go | функция с параметрами, config |
| speaker-stats | колонка C++ + сервер Python | события и обмен между половинами |
| airplay | колонка + сервер, Go | на колонке следит за shairport-sync, светит кольцом и экраном через host.command, сервер даёт модели airplay_status |
| mood | сервер, Python | меняет характер Маруси: системная инструкция, ring.set, своя страница в панели |
| ytmusic | сервер, Python | музыка с YouTube Music: yt-dlp на сервере, media.play_url на колонке, страница с поиском |
| ollama | сервер, Python | своя модель (провайдер): история, потоковый ответ, вызовы функций |
| piper | сервер, Python | офлайн-синтез речи |
Плагин на Go — обычная программа:
p := pluginsdk.New()
p.Tool(pluginsdk.ToolSpec{
Name: "roll_dice",
Description: "Rolls dice and returns the values.",
Params: []pluginsdk.Param{{Name: "count", Type: pluginsdk.TypeInteger}},
}, func(ctx context.Context, c pluginsdk.Call) (any, error) {
p.Command(ctx, c.Device.ID, "ring.set", map[string]any{"scene": "rainbow"})
return map[string]any{"values": []int{4}}, nil
})
log.Fatal(p.Run())Плагины выполняются с правами сервера или агента (на колонке это root). Ставьте только те плагины, которым доверяете.
Плагин колонки разговаривает с агентом двумя путями.
JSON-RPC (stdin/stdout) — основной путь, он одинаков на колонке и на сервере:
initializeприноситdevice(id, язык, город, часовой пояс колонки) иconfig; в окружении естьMARUSYA_PLUGIN_ID,MARUSYA_PLUGIN_DIRиMARUSYA_PLUGIN_HOST=agent;host.commandвыполняет любое действие колонки так, будто его прислал сервер: громкость (volume.set,volume.adjust), музыку (media.play_url,media.play_search,media.control,media.status), кольцо и экран (ring.set,ring.reset,display.show,display.reset), ночник (light.night), Bluetooth, датчики (sensors.get). Полеdeviceна колонке можно оставить пустым;- событие
stateсообщает, что делает Маруся:idle,listening,thinking,speaking,error,announcing,disconnected. Например, поlisteningможно приглушить свою музыку; - событие
volumeприходит при любой смене громкости колонки (голосом, кнопками, из панели или другим плагином):data.levelот 0 до 100 иdata.muted. По немуairplayдвигает ползунок громкости на телефоне; - функции плагина колонки попадают в список функций модели, когда колонка подключается
(
hello.plugin_tools). Модель вызывает их, сервер отправляет вызов на колонку, агент передаёт его плагину; plugin.sendи уведомлениеmessageсвязывают половины одного плагина через соединение колонки с сервером. Так сделанairplay: колонка сообщает, что играет, а серверная половина отвечает модели;logи stderr попадают в лог агента (/tmp/marusya-agent.log).
D-Bus ru.marusya.Agent (см. раздел про D-Bus) — для того, чего нет в
протоколе плагинов, и для программ, которые не являются плагинами (скрипты, cron, свои
демоны). Через D-Bus можно:
- слушать нажатия кнопок (сигнал
Sensors.Key) — в протоколе плагинов такого события нет; - следить за освещённостью, состоянием, мордашкой и выключенным микрофоном через
PropertiesChanged, без опроса; - управлять кольцом и экраном из shell (
dbus-send), Go (godbus/dbus), C/C++ (libdbus, sd-bus) или Python (dbus-next,jeepney).
Плагины колонки работают от root, поэтому правила шины им всё разрешают; своим демонам
под другим пользователем добавьте группу marusya. Чтобы плагин работал и на колонках без
D-Bus, используйте host.command, а D-Bus — только как дополнение.
# из плагина или скрипта на колонке: что делает Маруся и какой свет в комнате
dbus-send --system --print-reply --dest=ru.marusya.Agent /ru/marusya/Agent \
org.freedesktop.DBus.Properties.GetAll string:ru.marusya.Agent.Sensors
# ждать нажатия кнопки «следующий»
dbus-monitor --system "type='signal',sender='ru.marusya.Agent',interface='ru.marusya.Agent.Sensors',member='Key'" |
grep -m1 -A1 'string "next"'Кроме функций и событий серверный плагин может объявить в ответе на initialize (в SDK для
этого есть свои вызовы):
- Свою модель —
Provider/@plugin.provider. Во вкладке «Провайдеры» появляется тип «Плагин сервера (своя модель)», модель записывается как<плагин>/<провайдер>, напримерollama/chat. Сервер сам распознаёт речь, передаёт плагину текст, системную инструкцию и список функций (provider.turn) и озвучивает ответ. Плагин хранит историю беседы поsession, может стримить ответ кусками (turn.write, колонка начинает говорить раньше) и вернуть вызовы функций. Их выполняет сервер: громкость, музыка, будильники, функции других плагинов. Потом он снова вызываетprovider.turnс результатами, до 8 раз за реплику. Когда беседа заканчивается, приходитprovider.close. Пример — ollama. - Синтез и распознавание речи —
Speech/@plugin.synthesizer,@plugin.transcriber. В поле «Речь» у любого провайдера с распознаванием и синтезом на сервере (OpenAI, Claude, плагин) появляетсяplugin:<плагин>/<движок>. Синтез получает текст по предложению и возвращает 16-битный моно PCM с объявленной частотой, распознавание получает такой же PCM. Если движок что-то не умеет или плагин не запущен, работает Gemini сервера. Пример — piper. - HTTP —
HTTP/@plugin.http. Запросы кhttps://<сервер>/plugins/<id>/<путь>идут в плагин (http.request): метод, путь, query, заголовки, тело до 4 МБ, ответ за минуту. По умолчанию нужен вход в панель (для запросов кроме GET — ещё заголовокX-Marusya-Panel: 1) илиAuthorization: Bearer <MARUSYA_AUTH_TOKEN>. Сpublicадрес открыт всем, это подходит для вебхуков, а проверять подпись или свой токен плагин должен сам. Cookie панели и токен сервера плагин не видит. Ссылка на страницу плагина есть во вкладке «Плагины». Примеры — ytmusic (поиск и запуск музыки), mood. - Дополнение к системной инструкции — поле
instructionsпри старте иhost.instructionsна ходу: характер, манера речи, знания о доме. Сервер добавляет тексты всех плагинов в инструкцию модели и обновляет беседы к следующей реплике. Пример — mood: «Маруся, будь пираткой». - Вопрос от имени пользователя —
host.request: Маруся на колонке отвечает на текст так, будто его сказали вслух, может вызывать функции и говорит ответ.host.sayдля сравнения просто произносит готовый текст. Подходит для вебхуков («в дверь позвонили — скажи, кто пришёл, по данным камеры»).
plugin = Plugin()
plugin.instructions = "Ты живёшь на кухне и знаешь рецепты бабушки."
@plugin.provider("chat", "Моя модель")
def chat(turn): # turn.text, turn.tool_results, turn.system, turn.tools
turn.write("Сейчас посмотрю. ") # говорится сразу
return "Готово." # или {"text": ..., "tool_calls": [{"id","name","args"}]}
@plugin.synthesizer("voice", "Мой голос", sample_rate=22050)
def tts(text, language, voice):
return pcm_bytes
@plugin.http(public=True) # /plugins/<id>/...
def hook(req):
plugin.request("В дверь позвонили, скажи об этом", device="hall")
return {"ok": True}C++ SDK пока умеет только общее для обеих сторон (функции, события, host.command,
plugin.send), поэтому подходит для плагинов колонки. Серверные возможности есть в Go и Python
SDK, а для другого языка их легко реализовать по sdk/README.md.
Во вкладке «Плагины» есть поиск по каталогу плагинов, по умолчанию
marusya.ywfl.dev. Другой адрес задаётся в MARUSYA_PLUGIN_REGISTRY
(флаг -plugin_registry), пустое значение прячет каталог. Скачивать плагины можно без аккаунта.
Кнопка «Установить» скачивает последнюю версию и сверяет её SHA-512 с тем, что каталог прислал в
X-Checksum-Sha512 (и в списке версий). Если сумма совпала, архив распаковывается в
MARUSYA_PLUGINS_DIR/<id>, старая версия заменяется, плагин запускается. Плагины, поставленные из
каталога, помечены файлом .catalog.json, и панель может их удалить. Папки, которые вы положили
сами, панель не удаляет.
Половину agent (плагин «только для колонки» или колоночную часть двустороннего) панель ставит на
колонки: у плагина в результатах поиска есть кнопка для каждой подключённой колонки, а в списке
колонок ниже — «Удалить» у каждого плагина из каталога. Сервер скачивает архив из каталога, сверяет
SHA-512 и хранит его в MARUSYA_PLUGINS_DIR/.speakers/<id>/ вместе со списком колонок, которым
этот плагин нужен (так что серверу тоже нужен MARUSYA_PLUGINS_DIR). При каждом подключении колонки и
при каждом изменении сервер присылает агенту полный список его плагинов из каталога. Агент скачивает
новые и изменившиеся архивы с сервера (GET /v1/plugins/<id> с токеном колонки; колонка со своим
токеном получает только свои плагины), сверяет SHA-512 со списком, распаковывает их в свой
MARUSYA_PLUGINS_DIR/<id> с теми же проверками, что и сервер (пути вне папки, ссылки, больше 64 МБ в
архиве или 512 МБ в распакованном виде — отказ), помечает файлом .catalog.json и перечитывает
плагины. Плагины из каталога, которых нет в списке, агент удаляет. Папки, которые вы положили на
колонку сами, он не трогает, даже если id совпадает. Если функции плагинов изменились, агент
переподключается, чтобы объявить их серверу. Так умеют агенты с MARUSYA_PLUGINS_DIR, которые
объявляют возможность catalog_plugins. Старым агентам сервер список не шлёт, и панель предлагает их
обновить.
Каталог — отдельный сервис в registry/: бэкенд на Go (gRPC-gateway + PostgreSQL) и сайт
на Svelte. Авторы регистрируются, создают проект и загружают версии, модераторы проверяют проект
перед публикацией. Версия — это ZIP, в корне которого (или в единственной папке) лежит
plugin.json. id в нём должен совпадать с адресом проекта, а version, если указан, с номером
версии. Где работает плагин (сервер, колонка или оба), каталог берёт из plugin.json. Поднять
свой каталог:
cd registry
cp backend/env.example .env # задайте JWT_SECRET, пароли и REGISTRY_DOMAIN
docker compose up -d --build # PostgreSQL и один контейнер с сайтом и API на :8080; перед ним нужен TLS-проксиМаруся управляет домом. Задайте в configs/server.env:
MARUSYA_HA_URL=http://homeassistant.local:8123
MARUSYA_HA_TOKEN=<долгосрочный токен: профиль → Безопасность>
Модель получает навыки home_find (найти устройства и узнать их состояние) и home_control (управлять ими). Примеры фраз:
- «включи свет на кухне»;
- «яркость в спальне на 30»;
- «какая температура в детской»;
- «закрой шторы», «запусти сцену кино».
По умолчанию голосом можно управлять светом, розетками, вентиляторами, шторами, климатом, плеерами, сценами, скриптами, input_boolean, пылесосами, увлажнителями и кнопками. Датчики только читаются. Замки и сигнализация недоступны, иначе дверь сможет открыть любой, кто стоит рядом с колонкой. Список меняется через MARUSYA_HA_DOMAINS=light,switch,lock.
Home Assistant говорит через Марусю — через тот же /v1/ask. Добавьте в configuration.yaml:
rest_command:
marusya_say:
url: http://192.168.31.80:8080/v1/ask
method: POST
headers:
Authorization: !secret marusya_bearer # в secrets.yaml: marusya_bearer: "Bearer <токен>"
content_type: application/json
payload: '{"text": {{ text | to_json }}, "source": "homeassistant"}'
timeout: 60
marusya_ask:
url: http://192.168.31.80:8080/v1/ask
method: POST
headers:
Authorization: !secret marusya_bearer
content_type: application/json
payload: '{"text": {{ text | to_json }}, "listen": true, "timeout": 120, "source": "homeassistant"}'
timeout: 150Пример автоматизации с вопросом:
actions:
- action: rest_command.marusya_ask
data:
text: "Стирка закончилась. Включить сушилку?"
response_variable: r
- if: "{{ r.status == 200 and r.content.answered and 'да' in r.content.answer | lower }}"
then:
- action: switch.turn_on
target: { entity_id: switch.dryer }WebSocket GET /v1/stream, авторизация Authorization: Bearer <token>.
Текстовые кадры содержат JSON models.Message, бинарные кадры содержат PCM. Полное описание
находится в internal/models/doc.go.
| Тип | Направление | Смысл |
|---|---|---|
hello / ready |
→ / ← | рукопожатие, формат аудио, список возможностей устройства |
turn_start / turn_end / turn_cancel |
→ | границы реплики пользователя |
transcript |
← | распознанный текст пользователя и текст ответа |
command / command_result |
← / → | вызов функции модели и результат |
interrupted / turn_complete |
← | ответ прерван / завершён |
error |
← | ошибка |
plugin |
↔ | данные между половинами плагина (только при features: ["plugins"]) |
Модели передаются только инструменты, которые устройство объявило в hello.capabilities.
Из действий кольца, экрана и датчиков модель получает только ring_set; ring.reset,
display.show, display.reset и sensors.get доступны панели, но не модели.
Проект распространяется под лицензией Apache License 2.0.
Copyright © 2026 The OpenCapsula Authors — Дмитрий «yawaflua» Шиманский и контрибьюторы (см. AUTHORS и NOTICE).
Как предложить изменения, описано в CONTRIBUTING.md.
Проект развивается независимыми энтузиастами. Он не аффилирован с VK (МКПАО «ВК»), не одобрен и не поддерживается компанией или её сотрудниками. «Маруся», «VK Капсула» и связанные названия являются товарными знаками их правообладателей и используются здесь только для описания совместимости.