feat: Wave 3 — plugin system + custom wake-word trainer

#29 Plugin system:
- New jarvis-core::plugins module: discovers user packs in
  %APPDATA%\com.priler.jarvis\plugins\<name>\command.toml so authors can
  ship voice commands without rebuilding. Sandbox capped at "standard" —
  plugins cannot escalate to "full" (which would expose `os`). Disabled
  via a `disabled` flag file. Malformed packs warn and skip; never poison
  the rest of the list. 8 unit tests.
- commands::parse_commands() merges plugins into the loaded list.
- New /plugins GUI page with enable/disable switches, error reporting,
  "Open folder" button. Tauri commands plugins_list / plugins_set_enabled
  / plugins_open_folder. Header gets a "Плагины" button. i18n keys added
  for ru/en/ua.

#8 Custom wake-word trainer wizard:
- New jarvis-core::wake_trainer module: opens its own pv_recorder
  instance, records N short PCM clips, WAV-encodes them in memory, then
  calls rustpotter's WakewordRef::new_from_sample_buffers to train and
  persist a .rpw model under %APPDATA%/com.priler.jarvis/wake_words/.
  Keepsake WAVs are also dumped for retraining later. Sanitises names to
  block path traversal. 5 unit tests.
- Settings gain `custom_wake_word: String`; listener/rustpotter.rs now
  loads the user's selected model first and always falls back to the
  bundled default so the assistant keeps working even if the custom
  file is missing.
- New /wake-trainer GUI page: stepper UI for record-sample → train,
  shows recorded count, threshold slider, refuses to start while
  jarvis-app is running (mic exclusivity). Lists existing trained
  models with size + delete button.
- 8 Tauri commands wired through (status/defaults/start/record_sample/
  finish/cancel/list_models/delete_model).

Tests: 115 → 128 (+8 plugins +5 wake_trainer). Release builds green for
all three binaries (jarvis-app / jarvis-cli / jarvis-gui).
This commit is contained in:
Bossiara13 2026-05-16 13:26:42 +03:00
parent c4b22618f8
commit 4e21024509
17 changed files with 1625 additions and 24 deletions

View file

@ -133,6 +133,21 @@ fn main() {
tauri_commands::profile_list,
tauri_commands::profile_active,
tauri_commands::profile_set,
// User plugins
tauri_commands::plugins_list,
tauri_commands::plugins_set_enabled,
tauri_commands::plugins_open_folder,
// Wake-word trainer wizard
tauri_commands::wake_trainer_status,
tauri_commands::wake_trainer_defaults,
tauri_commands::wake_trainer_start,
tauri_commands::wake_trainer_record_sample,
tauri_commands::wake_trainer_finish,
tauri_commands::wake_trainer_cancel,
tauri_commands::wake_trainer_list_models,
tauri_commands::wake_trainer_delete_model,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View file

@ -57,4 +57,12 @@ pub use memory::*;
// Profile switching
mod profile;
pub use profile::*;
pub use profile::*;
// User-installed plugins (APP_CONFIG_DIR/plugins/<name>/)
mod plugins;
pub use plugins::*;
// Wake-word training wizard
mod wake_trainer;
pub use wake_trainer::*;

View file

@ -0,0 +1,47 @@
//! Tauri commands for the plugin system. GUI uses these to render
//! `/plugins` page: list installed plugins, toggle enabled state, reveal the
//! plugins folder in Explorer, and open the docs page.
use serde::Serialize;
use std::path::PathBuf;
#[derive(Serialize)]
pub struct PluginEntry {
pub name: String,
pub path: String,
pub enabled: bool,
pub command_count: usize,
pub error: Option<String>,
}
#[tauri::command]
pub fn plugins_list() -> Vec<PluginEntry> {
jarvis_core::plugins::list()
.into_iter()
.map(|p| PluginEntry {
name: p.name,
path: p.path.display().to_string(),
enabled: p.enabled,
command_count: p.command_count,
error: p.error,
})
.collect()
}
#[tauri::command]
pub fn plugins_set_enabled(name: String, enabled: bool) -> Result<(), String> {
jarvis_core::plugins::set_enabled(&name, enabled)
}
#[tauri::command]
pub fn plugins_open_folder() -> Result<String, String> {
let dir: PathBuf = jarvis_core::plugins::plugins_dir()?;
// Open the folder in Explorer (Windows-only — fall back to printing path).
#[cfg(target_os = "windows")]
{
let _ = std::process::Command::new("explorer.exe")
.arg(&dir)
.spawn();
}
Ok(dir.display().to_string())
}

View file

@ -0,0 +1,50 @@
//! Tauri commands for the wake-word trainer wizard. The GUI drives the
//! whole flow over these — see `frontend/src/routes/wake-trainer/index.svelte`.
//!
//! Key safety note: pv_recorder claims the mic exclusively, so the wizard
//! refuses to enter recording mode while jarvis-app.exe is running. The frontend
//! also checks this beforehand and prompts the user to stop the daemon.
use jarvis_core::wake_trainer::{
self, TrainedModelInfo, TrainerStatus, DEFAULT_SAMPLE_SECONDS, DEFAULT_TARGET_SAMPLES,
};
#[tauri::command]
pub fn wake_trainer_status() -> TrainerStatus {
wake_trainer::status()
}
#[tauri::command]
pub fn wake_trainer_defaults() -> (f32, u8) {
(DEFAULT_SAMPLE_SECONDS, DEFAULT_TARGET_SAMPLES)
}
#[tauri::command]
pub fn wake_trainer_start(name: String, target_samples: u8) -> Result<(), String> {
wake_trainer::start(&name, target_samples)
}
#[tauri::command]
pub fn wake_trainer_record_sample(seconds: f32) -> Result<usize, String> {
wake_trainer::record_sample(seconds)
}
#[tauri::command]
pub fn wake_trainer_finish(threshold: f32) -> Result<String, String> {
wake_trainer::finish(threshold).map(|p| p.display().to_string())
}
#[tauri::command]
pub fn wake_trainer_cancel() {
wake_trainer::cancel()
}
#[tauri::command]
pub fn wake_trainer_list_models() -> Vec<TrainedModelInfo> {
wake_trainer::list_models()
}
#[tauri::command]
pub fn wake_trainer_delete_model(name: String) -> Result<(), String> {
wake_trainer::delete_model(&name)
}