refactor: maintainability pass — dedup, central env config, cmd helpers, tests, ARCHITECTURE.md
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.
This commit is contained in:
parent
45243c3e3c
commit
6225198821
22 changed files with 891 additions and 93 deletions
295
ARCHITECTURE.md
Normal file
295
ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
# 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:
|
||||
|
||||
1. **`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.
|
||||
|
||||
2. **`runtime_config`** — environment-variable knobs read at startup. See
|
||||
`crates/jarvis-core/src/runtime_config.rs` for the full list with doc
|
||||
comments. Examples:
|
||||
- `JARVIS_TTS=sapi|piper|silero`
|
||||
- `JARVIS_LLM=groq|ollama`
|
||||
- `JARVIS_LLM_ROUTER=1` (default on), `JARVIS_LLM_ROUTER_THRESHOLD=0.55`
|
||||
- `GROQ_TOKEN`, `GROQ_MODEL`, `OLLAMA_BASE_URL`, `OLLAMA_MODEL`
|
||||
|
||||
`runtime_config::log_effective_config()` is called once on `main()` 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 which `StdLib` flags are loaded
|
||||
- `lua/engine.rs::register_api` gates HTTP/state/fs registration on the level
|
||||
- `globals.set("io", Nil)` removes the `io` stdlib unless `Full`
|
||||
- `os.execute / os.exit / os.remove / os.rename / os.setlocale` removed
|
||||
even in `Full` mode (security defence)
|
||||
|
||||
### Available APIs for command scripts
|
||||
|
||||
See `crates/jarvis-core/src/lua/api/*.rs`. Quick reference:
|
||||
|
||||
```lua
|
||||
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
|
||||
|
||||
```lua
|
||||
-- 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:
|
||||
|
||||
1. **`scheduler` tick 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`.
|
||||
|
||||
2. **`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
|
||||
|
||||
1. Create `resources/commands/<your_pack>/`.
|
||||
2. Add `command.toml`:
|
||||
```toml
|
||||
[[commands]]
|
||||
id = "your_pack.thing"
|
||||
type = "lua"
|
||||
script = "thing.lua"
|
||||
sandbox = "standard"
|
||||
timeout = 5000
|
||||
|
||||
[commands.phrases]
|
||||
ru = ["сделай штуку", ...]
|
||||
en = ["do the thing", ...]
|
||||
```
|
||||
3. Write `thing.lua`. Start from the snippet under "Recommended pack structure".
|
||||
4. Add a `commands::tests` assertion in `crates/jarvis-core/src/commands/tests.rs`
|
||||
so the parser regression test covers your TOML (the existing
|
||||
`every_command_toml_parses` already loads any new dir automatically).
|
||||
5. Run `cargo test -p jarvis-core --lib commands::tests`.
|
||||
6. 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
|
||||
|
||||
1. Add module under `crates/jarvis-core/src/<feature>.rs`. Pure data layer +
|
||||
thin global wrapper if needed (see `long_term_memory.rs` as template).
|
||||
2. Register in `crates/jarvis-core/src/lib.rs` (`pub mod <feature>;`).
|
||||
3. Expose Lua API at `crates/jarvis-core/src/lua/api/<feature>.rs` if scripts
|
||||
need it. Register in `lua/api.rs` and call from `lua/engine.rs`.
|
||||
4. Wire init from `crates/jarvis-app/src/main.rs`.
|
||||
5. Add a `runtime_config::ENV_*` constant if there's an env-driven knob.
|
||||
6. Add unit tests in a `#[cfg(test)] mod tests {}` block in the new file.
|
||||
7. 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-app` logs `recorder::init`
|
||||
- wake word fires? → log shows `WakeWordDetected`
|
||||
- TTS speaks? → check Piper/SAPI logs
|
||||
- Scheduler fires? → wait 30s after `scheduler::add` with `in 30 seconds`
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
# 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
|
||||
|
||||
- `master` is protected.
|
||||
- `feature/pc-tools` is 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue