vosk model selection in GUI

This commit is contained in:
Priler 2026-01-05 03:38:04 +05:00
parent 36acacf259
commit cab53abcbe
14 changed files with 323 additions and 40 deletions

View file

@ -7,7 +7,7 @@ repository.workspace = true
edition.workspace = true
[dependencies]
jarvis-core = { path = "../jarvis-core" }
jarvis-core = { path = "../jarvis-core", features = ["intent"] }
once_cell.workspace = true
log.workspace = true
simple-log = "2.4"

View file

@ -64,7 +64,7 @@ pub fn init_dirs() -> Result<(), String> {
*/
pub const DEFAULT_AUDIO_TYPE: AudioType = AudioType::Kira;
pub const DEFAULT_RECORDER_TYPE: RecorderType = RecorderType::PvRecorder;
pub const DEFAULT_WAKE_WORD_ENGINE: WakeWordEngine = WakeWordEngine::Rustpotter;
pub const DEFAULT_WAKE_WORD_ENGINE: WakeWordEngine = WakeWordEngine::Vosk;
pub const DEFAULT_INTENT_RECOGNITION_ENGINE: IntentRecognitionEngine = IntentRecognitionEngine::IntentClassifier;
pub const DEFAULT_SPEECH_TO_TEXT_ENGINE: SpeechToTextEngine = SpeechToTextEngine::Vosk;
@ -121,19 +121,20 @@ pub const RUSTPOTTER_DEFAULT_CONFIG: Lazy<RustpotterConfig> = Lazy::new(|| {
});
// PICOVOICE
pub const COMMANDS_PATH: &str = "commands/";
pub const KEYWORDS_PATH: &str = "picovoice/keywords/";
pub const COMMANDS_PATH: &str = "resources/commands/";
pub const KEYWORDS_PATH: &str = "resources/picovoice/keywords/";
pub const DEFAULT_KEYWORD: &str = "jarvis_windows.ppn";
pub const DEFAULT_SENSITIVITY: f32 = 1.0;
// VOSK
// pub const VOSK_MODEL_PATH: &str = const_concat!(PUBLIC_PATH, "/vosk/model_small");
pub const VOSK_MODELS_PATH: &str = "resources/vosk";
pub const VOSK_MODEL_PATH: &str = "resources/vosk/model_small";
pub const VOSK_FETCH_PHRASE: &str = "джарвис";
pub const VOSK_MODEL_PATH: &str = "vosk/model_small";
pub const VOSK_MIN_RATIO: f64 = 70.0;
// IRE (intents recognition)
pub const INTENT_CLASSIFIER_MIN_CONFIDENCE: f64 = 0.5;
pub const INTENT_CLASSIFIER_MIN_CONFIDENCE: f64 = 0.75;
// ETC
pub const CMD_RATIO_THRESHOLD: f64 = 65f64;

View file

@ -14,6 +14,8 @@ pub struct Settings {
pub intent_recognition_engine: IntentRecognitionEngine,
pub speech_to_text_engine: SpeechToTextEngine,
pub vosk_model: String,
pub api_keys: ApiKeys,
}
@ -27,6 +29,8 @@ impl Default for Settings {
intent_recognition_engine: config::DEFAULT_INTENT_RECOGNITION_ENGINE,
speech_to_text_engine: config::DEFAULT_SPEECH_TO_TEXT_ENGINE,
vosk_model: String::from(""), // auto detect first available
api_keys: ApiKeys {
picovoice: String::from(""),
openai: String::from(""),

View file

@ -23,9 +23,17 @@ pub mod stt;
#[cfg(feature = "intent")]
pub mod intent;
pub mod vosk_models;
// shared statics
pub static APP_DIR: Lazy<PathBuf> = Lazy::new(|| std::env::current_dir().unwrap());
pub static SOUND_DIR: Lazy<PathBuf> = Lazy::new(|| APP_DIR.clone().join("sound"));
// pub static APP_DIR: Lazy<PathBuf> = Lazy::new(|| std::env::current_dir().unwrap());
pub static APP_DIR: Lazy<PathBuf> = Lazy::new(|| {
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| std::env::current_dir().unwrap())
});
pub static SOUND_DIR: Lazy<PathBuf> = Lazy::new(|| APP_DIR.clone().join("resources/sound"));
pub static APP_DIRS: OnceCell<AppDirs> = OnceCell::new();
pub static APP_CONFIG_DIR: OnceCell<PathBuf> = OnceCell::new();
pub static APP_LOG_DIR: OnceCell<PathBuf> = OnceCell::new();

View file

@ -6,6 +6,9 @@ use once_cell::sync::OnceCell;
use crate::config::structs::SpeechToTextEngine;
use crate::vosk_models;
// use vosk_models::{scan_vosk_models, get_model_path, VoskModelInfo};
static STT_TYPE: OnceCell<SpeechToTextEngine> = OnceCell::new();
pub fn init() -> Result<(), ()> {
@ -30,6 +33,7 @@ pub fn init() -> Result<(), ()> {
Ok(())
}
pub fn recognize(data: &[i16], partial: bool) -> Option<String> {
match STT_TYPE.get().unwrap() {
SpeechToTextEngine::Vosk => vosk::recognize(data, partial),

View file

@ -3,18 +3,26 @@ use vosk::{DecodingState, Model, Recognizer};
use std::sync::Mutex;
use crate::config::VOSK_MODEL_PATH;
// use crate::config::VOSK_MODEL_PATH;
use crate::config;
use crate::stt::vosk_models;
use crate::DB;
static MODEL: OnceCell<Model> = OnceCell::new();
static RECOGNIZER: OnceCell<Mutex<Recognizer>> = OnceCell::new();
pub fn init_vosk() {
pub fn init_vosk() -> Result<(), String> {
if RECOGNIZER.get().is_some() {
return;
return Ok(());
} // already initialized
let model = Model::new(VOSK_MODEL_PATH).unwrap();
let mut recognizer = Recognizer::new(&model, 16000.0).unwrap();
let model_path = get_configured_model_path()?;
let model = Model::new(model_path.to_str().unwrap())
.ok_or_else(|| format!("Failed to load Vosk model from: {}", model_path.display()))?;
let mut recognizer = Recognizer::new(&model, 16000.0)
.ok_or("Failed to create Vosk recognizer")?;
recognizer.set_max_alternatives(10);
recognizer.set_words(true);
@ -22,6 +30,8 @@ pub fn init_vosk() {
MODEL.set(model);
RECOGNIZER.set(Mutex::new(recognizer));
Ok(())
}
pub fn recognize(data: &[i16], include_partial: bool) -> Option<String> {
@ -73,6 +83,34 @@ pub fn recognize(data: &[i16], include_partial: bool) -> Option<String> {
}
}
fn get_configured_model_path() -> Result<std::path::PathBuf, String> {
// try to get from settings
if let Some(db) = DB.get() {
let settings = db.read();
if !settings.vosk_model.is_empty() {
if let Some(path) = vosk_models::get_model_path(&settings.vosk_model) {
return Ok(path);
}
warn!("Configured Vosk model '{}' not found, falling back to auto-detect", settings.vosk_model);
}
}
// auto-detect: use first available model
let available = vosk_models::scan_vosk_models();
if let Some(first) = available.first() {
info!("Auto-detected Vosk model: {}", first.name);
return Ok(first.path.clone());
}
// fallback to legacy path
let legacy_path = std::path::Path::new(config::VOSK_MODEL_PATH);
if legacy_path.exists() {
return Ok(legacy_path.to_path_buf());
}
Err("No Vosk models found".into())
}
// pub fn stereo_to_mono(input_data: &[i16]) -> Vec<i16> {
// let mut result = Vec::with_capacity(input_data.len() / 2);
// result.extend(

View file

@ -0,0 +1,113 @@
use std::fs;
use std::path::{Path, PathBuf};
use crate::{APP_DIR, config};
#[derive(Debug, Clone)]
pub struct VoskModelInfo {
pub name: String, // folder name: "vosk-model-small-ru-0.22"
pub path: PathBuf, // full path
pub language: String, // extracted from name: "ru"
pub size: String, // "small", "large", etc.
}
// Scan for available Vosk models
pub fn scan_vosk_models() -> Vec<VoskModelInfo> {
let models_dir = {
APP_DIR.join(config::VOSK_MODELS_PATH)
};
let mut models = Vec::new();
info!("TESTTTTTTTTTTTTT: {}", models_dir.display());
if !models_dir.exists() {
warn!("Vosk models directory not found: {}", models_dir.display());
return models;
}
let entries = match fs::read_dir(models_dir) {
Ok(e) => e,
Err(e) => {
warn!("Failed to read vosk models directory: {}", e);
return models;
}
};
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
let path = entry.path();
// must be a directory
if !path.is_dir() {
continue;
}
// check if it looks like a vosk model (has am/conf/graph folders or similar)
if !is_vosk_model(&path) {
continue;
}
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let (language, size) = parse_model_name(&name);
models.push(VoskModelInfo {
name,
path,
language,
size,
});
}
models.sort_by(|a, b| a.name.cmp(&b.name));
models
}
// Check if directory looks like a Vosk model
fn is_vosk_model(path: &Path) -> bool {
// vosk models typically have these subdirectories
path.join("am").exists() ||
path.join("conf").exists() ||
path.join("graph").exists() ||
path.join("ivector").exists()
}
// Extract language and size from model name
// e.g., "vosk-model-small-ru-0.22" -> ("ru", "small")
fn parse_model_name(name: &str) -> (String, String) {
let parts: Vec<&str> = name.split('-').collect();
let mut language = String::from("unknown");
let mut size = String::from("unknown");
// look for common size indicators
for part in &parts {
match *part {
"small" | "big" | "large" | "lgraph" => size = part.to_string(),
// language codes are usually 2 letters
s if s.len() == 2 && s.chars().all(|c| c.is_alphabetic()) => {
language = s.to_string();
}
_ => {}
}
}
(language, size)
}
// Get model path by name
pub fn get_model_path(model_name: &str) -> Option<PathBuf> {
let path = Path::new(config::VOSK_MODELS_PATH).join(model_name);
if path.exists() && path.is_dir() {
Some(path)
} else {
None
}
}

View file

@ -61,6 +61,9 @@ fn main() {
tauri_commands::get_peak_ram_usage,
tauri_commands::get_cpu_temp,
tauri_commands::get_cpu_usage,
// vosk
tauri_commands::list_vosk_models,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View file

@ -22,3 +22,7 @@ pub use fs::*;
// import SYS commands
mod sys;
pub use sys::*;
// import STT commands
mod stt;
pub use stt::*;

View file

@ -10,6 +10,7 @@ pub fn db_read(state: tauri::State<'_, AppState>, key: &str) -> String {
"assistant_voice" => settings.voice.clone(),
"selected_wake_word_engine" => format!("{:?}", settings.wake_word_engine),
"selected_intent_recognition_engine" => format!("{:?}", settings.intent_recognition_engine),
"selected_vosk_model" => settings.vosk_model.clone(),
"speech_to_text_engine" => format!("{:?}", settings.speech_to_text_engine),
"api_key__picovoice" => settings.api_keys.picovoice.clone(),
"api_key__openai" => settings.api_keys.openai.clone(),
@ -49,6 +50,9 @@ pub fn db_write(state: tauri::State<'_, AppState>, key: &str, val: &str) -> bool
_ => return false,
}
}
"selected_vosk_model" => {
settings.vosk_model = val.to_string();
}
"api_key__picovoice" => {
settings.api_keys.picovoice = val.to_string();
}

View file

@ -0,0 +1,21 @@
use jarvis_core::{vosk_models, DB};
use serde::Serialize;
#[derive(Serialize)]
pub struct VoskModel {
pub name: String,
pub language: String,
pub size: String,
}
#[tauri::command]
pub fn list_vosk_models() -> Vec<VoskModel> {
vosk_models::scan_vosk_models()
.into_iter()
.map(|m| VoskModel {
name: m.name,
language: m.language,
size: m.size,
})
.collect()
}