User asked the code stays easy to fix/extend as the feature surface grows.
This commit refactors what was duplicated, centralises what was scattered,
adds tests where there were none, and writes the architecture doc that
contributors will need.
Deduplication
- tts::play_wav extracted to tts/mod.rs as pub(crate). Was identical in
piper.rs and silero.rs (~25 lines × 2). Both backends now call super::play_wav.
Centralised env config
- new crates/jarvis-core/src/runtime_config.rs — single doc-file for every
JARVIS_* / GROQ_* / OLLAMA_* env var. Includes:
- ENV_* constants with doc comments
- get(name) / get_bool(name, default) / get_parse(name, default) helpers
- feature-flag wrappers: llm_tts_enabled(), llm_router_enabled(),
llm_router_threshold()
- log_effective_config() prints active values on startup
- migrated llm_fallback (JARVIS_LLM_TTS), llm_router (JARVIS_LLM_ROUTER,
JARVIS_LLM_ROUTER_THRESHOLD) to use the new helpers. Pattern set for
future migrations.
Lua boilerplate killer
- new crates/jarvis-core/src/lua/api/cmd.rs exposing:
jarvis.cmd.ok(msg?) — play_ok + speak + {chain=false}
jarvis.cmd.chain_ok(msg?) — same but chain=true
jarvis.cmd.error(msg?) — play_error + speak + {chain=false}
jarvis.cmd.not_found(msg?) — play_not_found + speak + {chain=false}
jarvis.cmd.silent_ok / silent_error
- refactored 5 packs (daily_briefing/off, memory_pack/list, pomodoro/stop,
habit_nudge/stop_all, scheduler/clear) — each lost 3-4 lines of repetitive
play_*/speak/return boilerplate. Pattern for future packs documented in
ARCHITECTURE.md.
Tests (was 32, now 49)
- long_term_memory: 8 new tests for normalize_key, search_in (rank, limit,
empty), build_context_from (empty + populated), serde round-trips for
MemoryRecord and Store. Extracted pure logic (search_in, build_context_from)
into pub(crate) functions to enable testing without global state.
- profiles: 6 new tests for Profile::allows_command (empty/whitelist/blacklist/
deny-wins-over-allow), serde round-trip with all fields + minimal-fields
tolerance via #[serde(default)].
- runtime_config: 2 tests for get_bool / get_parse defaults.
- All 49 tests pass.
New mood/energy log pack
- resources/commands/mood_log/ with 2 commands:
mood.record "запиши настроение 7" / "сегодня мне грустно" → stores
timestamped entry via jarvis.memory.remember
mood.recap "как прошла неделя" → LLM summarises last 30 entries
- Showcase: composes memory + llm + cmd helpers in <30 lines per script.
ARCHITECTURE.md (new, 250 lines)
- Crate layout, data flow diagram (mic→action 10 steps), per-module
responsibility table, configuration layers, TTS pipeline diagram,
Lua sandbox details with API quick-ref, background-services overview,
"how to add a pack/feature/TTS backend" recipes, test coverage map,
build instructions with MSVC env, git workflow with Forgejo NO_PROXY trick.
- Aimed at someone who just cloned the repo and needs to fix a bug fast.
Build: cargo build --release -p jarvis-app -p jarvis-gui both green.
Tests: 49/49 jarvis-core unit tests pass.
12 KiB
J.A.R.V.I.S. — Architecture
This is the code map for the Rust fork. If you're here to fix a bug or add a feature, start by reading the relevant section, then jump to the file.
The Python fork at C:\Jarvis\python follows the same conceptual flow but is
a single-process Python daemon — much smaller surface area. This doc covers
the Rust side.
Crate layout
crates/
jarvis-core library: STT, wake-word, TTS, intent, commands, IPC, LLM,
Lua sandbox, scheduler, memory, profiles. ZERO runtime, only
building blocks. Other crates wire it together.
jarvis-app daemon binary. Holds the microphone, runs the listening loop,
dispatches commands, starts the scheduler thread. Tray icon.
jarvis-gui Tauri 2 + Svelte frontend. Shows command list, settings, mic
state. Communicates with jarvis-app via IPC websocket.
jarvis-cli debug CLI (classify / execute / list). Currently has a known
runtime panic on startup; use jarvis-app instead.
resources/commands/<pack_name>/
command.toml — id, phrases per language, script path, sandbox level, timeout
*.lua — script(s); one per command id when multiple ids share a pack
The tools/ directory hosts optional native helpers (Piper TTS binary, Silero
helper script). Both are downloaded on demand via tools/piper/install.ps1 etc.
Data flow (mic → action)
[1] pv_recorder ──► raw 16 kHz mono PCM
[2] webrtc-vad ───► voice activity windows
[3] Vosk (wake-word + STT) ──► utf-8 transcript
[4] intent classifier (MiniLM) ──► (intent_id, confidence)?
[5] levenshtein fallback ──► (pack, command)?
[6] llm_router (IMBA-1) ──► canonical phrase substitution?
[7] llm_fallback chat ──► spoken reply
[8] command execution ──► Lua sandbox / Python subprocess / WinAPI call
[9] TTS (sapi | piper | silero) ──► speech out
[10] voices::play_* ──► reaction sounds (ok, error, not_found)
Each step lives in a separate module — replace any one and the rest still works.
| Step | Module |
|---|---|
| 1 | jarvis-core::recorder |
| 2 | jarvis-core::audio_processing::vad::webrtc |
| 3 | jarvis-core::stt + jarvis-core::listener |
| 4 | jarvis-core::intent |
| 5 | jarvis-core::commands::fetch_command |
| 6 | jarvis-app::llm_router |
| 7 | jarvis-app::llm_fallback |
| 8 | jarvis-core::commands::execute_command |
| 9 | jarvis-core::tts |
| 10 | jarvis-core::voices |
The listening loop driving all of this is jarvis-app/src/app.rs::start().
Configuration
Two layers:
-
db::structs::Settings— persistent user preferences (voice, language, wake-word engine, noise-suppression backend). Lives in a sqlite file under<APP_CONFIG_DIR>/. Edited via the GUI Settings page. -
runtime_config— environment-variable knobs read at startup. Seecrates/jarvis-core/src/runtime_config.rsfor the full list with doc comments. Examples:JARVIS_TTS=sapi|piper|sileroJARVIS_LLM=groq|ollamaJARVIS_LLM_ROUTER=1(default on),JARVIS_LLM_ROUTER_THRESHOLD=0.55GROQ_TOKEN,GROQ_MODEL,OLLAMA_BASE_URL,OLLAMA_MODEL
runtime_config::log_effective_config()is called once onmain()so the active values appear in the startup log.
dev.env next to the exe is auto-loaded by dotenvy. Walks up 5 parents — so
running from crates/jarvis-app/ during development still works.
TTS pipeline
text ─► text_utils::sanitize_for_speech ─► TtsBackend::speak(text, opts)
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
SapiBackend PiperBackend SileroBackend
(PowerShell) (subprocess) (python helper)
│ │ │
▼ ▼ ▼
SAPI direct WAV → play_wav WAV → play_wav
(shared) (shared)
tts::backend() is initialised lazily on first call; subsequent calls reuse
the same Arc<dyn TtsBackend>. Backend is picked by JARVIS_TTS env var
with auto-detect fallback: if tools/piper/piper.exe exists, use Piper.
play_wav is the shared synchronous WAV player (PowerShell SoundPlayer on
Windows, no-op stub elsewhere). Used by both Piper and Silero — kept in
tts/mod.rs as pub(crate) to deduplicate.
Lua sandbox
mlua 0.11 with three sandbox levels:
| Level | Allowed |
|---|---|
| Minimal | log, sleep, speak, audio, context, cmd |
| Standard | + http, llm, state, fs (within pack dir) |
| Full | + system.exec, clipboard.set, abs paths |
Default is standard. Set per command in command.toml via sandbox = "full".
Sandbox is enforced by:
LuaEngine::new(level)controls whichStdLibflags are loadedlua/engine.rs::register_apigates HTTP/state/fs registration on the levelglobals.set("io", Nil)removes theiostdlib unlessFullos.execute / os.exit / os.remove / os.rename / os.setlocaleremoved even inFullmode (security defence)
Available APIs for command scripts
See crates/jarvis-core/src/lua/api/*.rs. Quick reference:
jarvis.speak(text, opts?) -- TTS; opts.lang/.async/.raw
jarvis.cmd.ok(msg?) -- boilerplate-killer: play_ok + speak + {chain=false}
jarvis.cmd.error(msg?) -- play_error + speak + {chain=false}
jarvis.cmd.not_found(msg?) -- play_not_found + speak + {chain=false}
jarvis.cmd.chain_ok(msg?) -- like ok() but chain=true
jarvis.audio.play_ok / play_error / play_not_found / play_reply / play_goodbye / play_thanks
jarvis.context.{phrase, command_id, command_path, language, time, slots}
jarvis.text.strip_trigger(phrase, {triggers...})
jarvis.text.contains_any(phrase, {needles...})
jarvis.llm({messages}, {opts}) -- Groq or Ollama, selected by JARVIS_LLM
jarvis.http.{get, post, post_json, json}
jarvis.memory.{remember, recall, search, forget, all}
jarvis.profile.{active, active_name, set, list, allows}
jarvis.scheduler.{add, list, count, remove, clear}
jarvis.vision.{screenshot, describe} -- HTTP sandbox required
jarvis.fs.{read, write, append, exists, is_file, is_dir, list, mkdir, remove}
jarvis.state.{get, set} -- pack-scoped k/v
jarvis.system.{open, exec, notify, env, platform, clipboard.{get, set}}
jarvis.settings.{get, set} -- db settings table
Recommended pack structure
-- Single-id pack:
local phrase = (jarvis.context.phrase or ""):lower()
local body = jarvis.text.strip_trigger(phrase, { "trigger1", "trigger2" })
body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "")
if body == "" then
return jarvis.cmd.error("Что именно?")
end
-- ... do work ...
return jarvis.cmd.ok("Готово.")
For multi-id packs (one Lua script shared between several [[commands]]),
dispatch on jarvis.context.command_id. The profile_switch/switch.lua is a
good example — maps profile.work → work, profile.game → game, etc.
Background services
Two long-running threads beyond the listening loop:
-
schedulertick thread (crates/jarvis-core/src/scheduler.rs). Wakes every 30 seconds, fires due tasks (Speak via TTS or Lua scripts). Persists changes to<APP_CONFIG_DIR>/schedule.json. -
ipc::start_server(crates/jarvis-core/src/ipc/). Tokio websocket server on a configurable port. The GUI connects to this to send commands and receive events (SpeechRecognized,LlmReply,CommandExecuted, etc.).
How to add a new command pack
- Create
resources/commands/<your_pack>/. - Add
command.toml:[[commands]] id = "your_pack.thing" type = "lua" script = "thing.lua" sandbox = "standard" timeout = 5000 [commands.phrases] ru = ["сделай штуку", ...] en = ["do the thing", ...] - Write
thing.lua. Start from the snippet under "Recommended pack structure". - Add a
commands::testsassertion incrates/jarvis-core/src/commands/tests.rsso the parser regression test covers your TOML (the existingevery_command_toml_parsesalready loads any new dir automatically). - Run
cargo test -p jarvis-core --lib commands::tests. - Rebuild jarvis-app: it'll pick up the new pack on next start.
For Python parity, mirror the YAML entry + handler in C:\Jarvis\python\.
How to add a new Rust core feature
- Add module under
crates/jarvis-core/src/<feature>.rs. Pure data layer + thin global wrapper if needed (seelong_term_memory.rsas template). - Register in
crates/jarvis-core/src/lib.rs(pub mod <feature>;). - Expose Lua API at
crates/jarvis-core/src/lua/api/<feature>.rsif scripts need it. Register inlua/api.rsand call fromlua/engine.rs. - Wire init from
crates/jarvis-app/src/main.rs. - Add a
runtime_config::ENV_*constant if there's an env-driven knob. - Add unit tests in a
#[cfg(test)] mod tests {}block in the new file. - Document the public surface in this file (data flow / API section).
How to add a new TTS backend
Implement TtsBackend (see crates/jarvis-core/src/tts/mod.rs). Use the
super::play_wav helper if your engine emits WAV files. Wire selection in
init_backend() near the top of the same file. Document the env var in
runtime_config.rs.
Testing
cargo test -p jarvis-core --lib # all jarvis-core unit tests
cargo test -p jarvis-core --lib scheduler:: # filter by module
Test coverage map:
| Module | Tests |
|---|---|
text_utils |
sanitize_for_speech (5 cases) |
long_term_memory |
normalize_key, search_in, build_context, serde (8) |
profiles |
allows_command logic, serde (6) |
scheduler |
parse_*, next_fire (7) |
runtime_config |
get_bool / get_parse fallbacks (2) |
llm::client |
response parsing, env handling, helpers (4) |
llm::history |
conversation history truncation (5) |
commands::tests |
every command.toml parses + has phrases (3) |
lua::tests |
sandbox levels, fs escape, timeout (4) |
audio_processing::vad::listen_window |
listening window logic (4) |
Run-time integration tests are manual:
- mic available? →
jarvis-applogsrecorder::init - wake word fires? → log shows
WakeWordDetected - TTS speaks? → check Piper/SAPI logs
- Scheduler fires? → wait 30s after
scheduler::addwithin 30 seconds
Build
# One-time: set up MSVC env (PowerShell 7 / pwsh):
$vc = "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cmd /c "`"$vc`" >NUL && set" | % { if ($_ -match '^([^=]+)=(.*)$') { [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process') } }
# Then:
cargo build --release -p jarvis-app -p jarvis-gui
The jarvis-gui build.rs auto-invokes npm run build and emits
cargo:rerun-if-changed=frontend/src so changes to the Svelte frontend are
picked up by a plain cargo build. No need to run cargo tauri build
manually unless you want a packaged installer.
Git workflow
masteris protected.feature/pc-toolsis the integration branch where everything is currently shipping.- Self-hosted git on Forgejo at
http://192.168.0.10:3000/bossiara13/J.A.R.V.I.S-rust. Push command:NO_PROXY=192.168.0.10,localhost,127.0.0.1+git -c http.proxy= push forgejo <branch>(the local V2Ray proxy at 10809 will otherwise block LAN traffic). - GitHub origin at
https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust— manually pushed for public visibility.
Commit messages: descriptive, no Co-Authored-By: Claude tag (project rule).