Single biggest user-facing change: you can now switch local↔cloud LLM by voice
without restarting. Plus four new packs and a perf nudge.
Shared LLM global (crates/jarvis-core/src/llm/mod.rs)
- GLOBAL: Lazy<RwLock<Option<Arc<LlmClient>>>>. Single source of truth.
- init_global() / current() / swap_to(LlmBackend) / current_backend_name() / parse_backend(str).
- llm_fallback + llm_router migrated: they no longer own a separate LlmClient,
they read from the global on each request. Hot-swap is now LIVE — change
backend, next chat call uses the new one.
- parse_backend accepts both english and russian aliases:
"groq"/"cloud"/"облако"/"клауд" → Groq
"ollama"/"local"/"локал"/"локальный" → Ollama
Voice + Lua LLM switcher (resources/commands/llm_switch/, 3 commands)
- "переключись на локальный" / "перейди на оллама" → swap to Ollama
- "переключись на облако" / "используй грок" → swap to Groq
- "какой у тебя мозг" / "облако или локально" → status query
Underlying Lua API (registered globally):
jarvis.llm({messages}, opts) -- as before, now uses global client
jarvis.llm_status() -> "groq" | "ollama" | "none"
jarvis.llm_switch(name) -> name on success, nil on failure
Media keys pack (resources/commands/media_keys/, 4 commands)
- "пауза" / "плей" → VK_MEDIA_PLAY_PAUSE
- "следующий трек" → VK_MEDIA_NEXT_TRACK
- "предыдущий трек" → VK_MEDIA_PREV_TRACK
- "стоп музыка" → VK_MEDIA_STOP
- Helper _media.ps1 uses user32.keybd_event (P/Invoke through Add-Type).
- Works with anything that listens to global media keys: Spotify, Yandex Music,
YouTube (focused tab), Foobar2000, Winamp, etc. No OAuth, no API keys.
Codebase Q&A pack (resources/commands/codebase_qa/, 3 commands)
- "укажи проект <path>" → stores path in jarvis.memory under codebase.root
- "какой проект сейчас" → speaks current path
- "что делает функция X" / "найди в коде Y" / "объясни код"
→ walks the folder (depth 3, 30 files cap, 4KB per file, 50KB total),
filters by source extensions (rs/py/ts/lua/go/...), feeds digest to LLM
with a "senior reviewer" system prompt, speaks 3-5 sentence answer.
Scheduler cancel-by-text (crates/jarvis-core/src/scheduler.rs)
- new pub fn find_by_text(query, limit) -> Vec<ScheduledTask>
- new pub fn remove_by_text(query) -> usize (count removed)
- new pure helper find_by_text_in(tasks, query, limit) for tests
- Lua: jarvis.scheduler.{find_by_text, remove_by_text}
- Voice: "отмени напоминание про воду" / "удали задачу про разминку"
- 3 new unit tests (52 total in jarvis-core).
TTS pre-warm (crates/jarvis-app/src/main.rs)
- Call jarvis_core::tts::backend() once on startup so the first speak()
doesn't pay Piper binary discovery + voice loading cost. Cuts first-speak
latency by ~150-300ms depending on backend.
Build: cargo build --release -p jarvis-app -p jarvis-gui both green.
Tests: 52/52 jarvis-core unit tests pass.
|
||
|---|---|---|
| crates | ||
| frontend | ||
| lib | ||
| resources | ||
| tools | ||
| .gitignore | ||
| ARCHITECTURE.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE.txt | ||
| make_ico.py | ||
| post_build.py | ||
| poster.jpg | ||
| README.md | ||
| run.bat | ||
J.A.R.V.I.S (Rust) — форк Bossiara13
Форк Rust-переписки голосового ассистента Priler/jarvis. Текущий репозиторий: https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust.
Что это
Голосовой ассистент, написанный на Rust, работающий локально (без облака). Текущий стек:
- Vosk — Speech-to-Text (через
vosk-rs). - fastembed + ort — локальные эмбеддинги для intent-классификации (MiniLM L6/L12 ONNX).
- Picovoice Porcupine / Rustpotter / Vosk — три опциональных движка wake-word.
- mlua (Lua 5.5, vendored) — скрипты пользовательских команд.
- Tauri + Vite/Svelte — GUI-оболочка (фронтенд в отдельной папке
frontend/). - nnnoiseless — подавление шума.
- fluent / unic-langid — i18n (
ru,ua,en).
LLM-клиент (Groq / OpenAI-совместимый) добавлен в jarvis-core::llm и подключён к голосовому циклу. Если фраза начинается с триггера («скажи», «ответь», «произнеси»), она уходит в Groq и ответ возвращается через IPC-событие LlmReply. Без триггера всё работает как раньше — wake-word + intent + Lua. Это следующий шаг после CLI-only LLM из v0.2.0.
Это форк
Оригинальный автор — Abraham Tugalov (Priler).
Апстрим: https://github.com/Priler/jarvis.
Лицензия сохранена: CC BY-NC-SA 4.0 (см. LICENSE.txt).
Атрибуция в Cargo.toml и voice.toml пакетов озвучки не изменена.
Что отличается от апстрима
- Обновлён список авторов в
Cargo.toml(добавленBossiara13 (fork), оригинал сохранён). - README переписан и отражает фактическую архитектуру (апстримный README называет проект "Tauri+Svelte", что давно не соответствует действительности — это workspace из 4-х крейтов).
- Отсутствующие в апстриме ONNX-модели (
all-MiniLM-L6-v2,paraphrase-multilingual-MiniLM-L12-v2-onnx-Q) подтянуты через Git LFS из HuggingFace (Qdrant) и запушены в форк.
Структура репозитория
Cargo workspace из четырёх крейтов:
| Крейт | Назначение |
|---|---|
jarvis-core |
Библиотека: конфиг, intent, STT, wake-word, аудио, Lua-бэкенд, i18n. |
jarvis-app |
Бинарь-«демон»: собирает всё вместе, tray, IPC. |
jarvis-gui |
Tauri-приложение (использует frontend/dist/client). |
jarvis-cli |
CLI для отладки: классификация intent, список команд, dump конфига. |
Прочее:
frontend/— Vite + Svelte UI дляjarvis-gui. Собирается отдельно.lib/windows/amd64/— нативные DLL/LIB для Vosk, Porcupine, PvRecorder.resources/— голоса, модели, конфиги по умолчанию. ONNX-модели хранятся в Git LFS.post_build.py— постпроцессинг артефактов сборки (Python 3).
Сборка
Требования:
- Rust 1.93+ (собирается на stable MSVC).
- Node 24+ и npm — для фронтенда.
- Python 3 — для
post_build.py. - MSVC build tools (Windows, x64).
- Установленные
libvosk.lib,libpv_porcupine.dll,libpv_recorder.dllвlib/windows/amd64/(уже в репозитории).
Перед сборкой jarvis-gui нужно собрать фронтенд:
cd frontend
npm install
npm run build
cd ..
Затем workspace:
cargo build --workspace
Холодная сборка занимает около 10 минут (ONNX runtime, aws-lc-rs, tauri).
Статус сборки в этом форке
На моей машине (cargo build --workspace, stable MSVC) итог:
jarvis-core— собрался (1 warning, unused import).jarvis-app— собрался, бинарникtarget/debug/jarvis-app.exeсоздан.jarvis-cli— падает на линковке:LNK1181: cannot open input file "libvosk.lib". Причина: уjarvis-cliнет своегоbuild.rs, а.cargo/config.tomlсrustc-link-searchлежит только внутриcrates/jarvis-app/и не подтягивается дляjarvis-cli. Лечится либо добавлением такого жеbuild.rsвcrates/jarvis-cli/, либо вынесениемconfig.tomlв корень. Сознательно не трогал — фикс выходит за рамки рефакторинга (v0.0.1-import фиксирует поведение апстрима как есть).jarvis-gui— падает вtauri::generate_context!():frontendDist = "../../frontend/dist/client"не существует. Это ожидаемо, если не запуститьnpm run buildвfrontend/заранее (см. секцию «Сборка»).
Запуск уже собранного:
./target/debug/jarvis-app.exe
Для CLI (jarvis-cli --help, команды classify, execute, list, phrases) нужно сначала починить линковку Vosk (см. выше).
LLM (Groq)
В jarvis-core есть модуль llm — блокирующий клиент для OpenAI-совместимого эндпоинта chat completions. По умолчанию настроен на Groq. Используется через фиче-флаг llm (включён в дефолтный набор jarvis_app, также подтянут в jarvis-cli).
Переменные окружения:
| Переменная | Обязательна | Значение по умолчанию |
|---|---|---|
GROQ_TOKEN |
да | — |
GROQ_BASE_URL |
нет | https://api.groq.com/openai/v1 |
GROQ_MODEL |
нет | llama-3.3-70b-versatile |
Быстрая проверка через CLI:
set GROQ_TOKEN=gsk_...
jarvis-cli ask "скажи привет одной фразой"
Ответ печатается в stdout. Без GROQ_TOKEN команда завершится с кодом 2 и сообщением об ошибке. При ошибке API — код 1 и тело ответа.
Программное использование из Rust:
use jarvis_core::llm::{LlmClient, ChatMessage};
let client = LlmClient::from_env()?;
let reply = client.complete(&[ChatMessage::user("привет")], 256)?;
println!("{}", reply);
Подключение к голосовому циклу
Помимо CLI, LLM подключён напрямую в jarvis-app. Логика в crates/jarvis-app/src/llm_fallback.rs:
- При старте
jarvis-appпытается прочитатьGROQ_TOKEN. Если переменной нет — фоллбэк отключается, в лог пишется warning, голосовые команды продолжают работать как раньше. - Распознанная фраза (как из микрофона, так и из текстовой панели GUI) проверяется на префиксы-триггеры из
config::get_llm_trigger_phrases(дляru/ua:скажи,ответь,произнеси; дляen:say,tell,answer). - Если триггер найден — остаток фразы уходит в
LlmClient::complete(), ответ публикуется в IPC какIpcEvent::LlmReply { text }(UI/GUI слушает этот ивент и проговаривает текст уже на своей стороне), а звук-«ок» проигрывается из текущего голосового пресета. - История разговора хранится в
ConversationHistoryс потолкомLLM_DEFAULT_MAX_HISTORY = 8ходов; system-prompt всегда сохраняется при вытеснении старых ходов. - При сетевой/API-ошибке последний user-turn убирается из истории, в IPC уходит
LlmReplyс короткой русской фразой («Не могу связаться с сервером, сэр.»), играется звук-«error». Голосовой цикл не падает.
Системный промпт (русский) описывает J.A.R.V.I.S. как британского дворецкого Тони Старка — короткие реплики (1–3 предложения), обращение «сэр», без излишней цензуры. Меняется в config::LLM_SYSTEM_PROMPT_RU.
VAD: умное завершение команды по тишине
После срабатывания wake-word jarvis-app слушает команду. Раньше окно закрывалось либо по таймауту Vosk, либо по жёсткому пределу из config::CMS_WAIT_DELAY — короткие команды («стоп», «громче») всё равно ждали несколько секунд тишины.
Теперь окно закрывается, как только пользователь замолчал. Реализовано через webrtc-vad (чистый Rust, тот же алгоритм, что в Python-версии). Это паритет с Python v0.2.0, где использовался webrtcvad с теми же параметрами.
Логика (state-машина в crates/jarvis-core/src/audio_processing/vad/listen_window.rs):
- Кадр VAD: 30 мс при 16 кГц = 480 сэмплов = 960 байт (mono i16). Кадры с микрофона (по 512 сэмплов от
pv_recorder) аккумулируются и режутся на VAD-кадры адаптеромWebRtcVad::push_samples. - Каждый VAD-кадр учитывается в счётчиках
speech_ms/silence_ms. - Окно закрывается, когда
silence_ms >= VAD_COMMAND_END_SILENCE_MSИspeech_ms >= VAD_COMMAND_MIN_SPEECH_MS. - До истечения
VAD_COMMAND_MIN_LISTEN_MSокно не закрывается — пользователю даётся время начать говорить. - При достижении
VAD_COMMAND_MAX_LISTEN_MSсрабатывает hard-cap и управление возвращается в режим ожидания wake-word. - На событии «закрыть окно» вызывается
stt::finalize_speech()— Vosk форсированно отдаёт финальный результат, не дожидаясь собственного таймаута.
Параметры (crates/jarvis-core/src/config.rs):
| Константа | Значение | Назначение |
|---|---|---|
VAD_AGGRESSIVENESS |
2 |
Уровень WebRTC VAD: 0 — Quality, 3 — VeryAggressive. |
VAD_COMMAND_END_SILENCE_MS |
1200 |
Сколько тишины подряд считать «команда закончилась». |
VAD_COMMAND_MIN_SPEECH_MS |
500 |
Минимум речи в окне — иначе закрытие игнорируется. |
VAD_COMMAND_MIN_LISTEN_MS |
1000 |
Минимальная длительность окна (страховка от ранних закрытий) |
VAD_COMMAND_MAX_LISTEN_MS |
15000 |
Жёсткий потолок окна. |
На короткие команды отзыв стал заметно быстрее (мы выходим на распознавание сразу после паузы пользователя, а не по 5-секундному порогу Vosk).
Лицензия
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Полный текст — в LICENSE.txt. Атрибуция оригинального автора (Abraham Tugalov) сохранена.
В Cargo.toml декларирован license = "GPL-3.0-only" — это несоответствие унаследовано от апстрима и не правилось, чтобы не расходиться с upstream-конфигом. Приоритет имеет LICENSE.txt.
Python-версия
Старая версия ассистента была на Python. Последний коммит с Python-кодом в апстриме — 943efbf.