mirror of
https://github.com/Priler/jarvis.git
synced 2026-05-26 07:08:11 +00:00
Tray options implementation + Voice system rewrite + build fixes
This commit is contained in:
@@ -1,26 +1,27 @@
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use jarvis_core::{audio, audio_processing, commands, config, listener, recorder, stt, COMMANDS_LIST, intent, ipc::{self, IpcEvent}};
|
||||
use jarvis_core::{audio, audio_processing, commands, config, listener, recorder, stt, COMMANDS_LIST, intent, voices, ipc::{self, IpcEvent}};
|
||||
use rand::prelude::*;
|
||||
|
||||
use crate::should_stop;
|
||||
|
||||
pub fn start() -> Result<(), ()> {
|
||||
pub fn start(text_cmd_rx: Receiver<String>) -> Result<(), ()> {
|
||||
// start the loop
|
||||
main_loop()
|
||||
main_loop(text_cmd_rx)
|
||||
}
|
||||
|
||||
fn main_loop() -> Result<(), ()> {
|
||||
fn main_loop(text_cmd_rx: Receiver<String>) -> Result<(), ()> {
|
||||
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
|
||||
let mut start: SystemTime;
|
||||
let sounds_directory = audio::get_sound_directory().unwrap();
|
||||
// let sounds_directory = audio::get_sound_directory().unwrap();
|
||||
let frame_length: usize = 512; // default for every wake-word engine
|
||||
let mut frame_buffer: Vec<i16> = vec![0; frame_length];
|
||||
let mut silence_frames: u32 = 0;
|
||||
|
||||
// play some run phrase
|
||||
// @TODO. Different sounds? Or better make it via commands or upcoming events system.
|
||||
audio::play_sound(&sounds_directory.join("run.wav"));
|
||||
// play some startup phrase
|
||||
// audio::play_sound(&sounds_directory.join("run.wav"));
|
||||
voices::play_greet();
|
||||
|
||||
// start recording
|
||||
match recorder::start_recording() {
|
||||
@@ -39,10 +40,17 @@ fn main_loop() -> Result<(), ()> {
|
||||
// check for stop signal
|
||||
if should_stop() {
|
||||
info!("Stop signal received, shutting down...");
|
||||
voices::play_goodbye();
|
||||
ipc::send(IpcEvent::Stopping);
|
||||
break;
|
||||
}
|
||||
|
||||
// check for text commands
|
||||
if let Ok(text) = text_cmd_rx.try_recv() {
|
||||
process_text_command(&text, &rt);
|
||||
continue 'wake_word;
|
||||
}
|
||||
|
||||
// read from microphone
|
||||
recorder::read_microphone(&mut frame_buffer);
|
||||
|
||||
@@ -70,14 +78,10 @@ fn main_loop() -> Result<(), ()> {
|
||||
start = SystemTime::now();
|
||||
silence_frames = 0;
|
||||
|
||||
// play some greet phrase
|
||||
// play some reply phrase
|
||||
// @TODO. Make it via commands or upcoming events system.
|
||||
audio::play_sound(&sounds_directory.join(format!(
|
||||
"{}.wav",
|
||||
config::ASSISTANT_GREET_PHRASES
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap()
|
||||
)));
|
||||
voices::play_reply();
|
||||
|
||||
|
||||
// notify GUI we're listening
|
||||
ipc::send(IpcEvent::Listening);
|
||||
@@ -125,12 +129,13 @@ fn main_loop() -> Result<(), ()> {
|
||||
info!("Wake word detected during chaining, reactivating...");
|
||||
|
||||
// play greet sound
|
||||
audio::play_sound(&sounds_directory.join(format!(
|
||||
"{}.wav",
|
||||
config::ASSISTANT_GREET_PHRASES
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap()
|
||||
)));
|
||||
// audio::play_sound(&sounds_directory.join(format!(
|
||||
// "{}.wav",
|
||||
// config::ASSISTANT_GREET_PHRASES
|
||||
// .choose(&mut rand::thread_rng())
|
||||
// .unwrap()
|
||||
// )));
|
||||
voices::play_reply();
|
||||
|
||||
// reset timer and continue listening
|
||||
start = SystemTime::now();
|
||||
@@ -152,59 +157,8 @@ fn main_loop() -> Result<(), ()> {
|
||||
continue 'voice_recognition;
|
||||
}
|
||||
|
||||
// infer command (try intent recognition first, fallback to levenshtein)
|
||||
let cmd_result = if let Some((intent_id, confidence)) =
|
||||
rt.block_on(intent::classify(&recognized_voice))
|
||||
{
|
||||
info!("Intent recognized: {} (confidence: {:.2})", intent_id, confidence);
|
||||
intent::get_command_by_intent(COMMANDS_LIST.get().unwrap(), &intent_id)
|
||||
} else {
|
||||
info!("Intent not recognized, trying levenshtein fallback ...");
|
||||
commands::fetch_command(&recognized_voice, COMMANDS_LIST.get().unwrap())
|
||||
};
|
||||
|
||||
if let Some((cmd_path, cmd_config)) = cmd_result {
|
||||
info!("Command found: {:?}", cmd_path);
|
||||
info!("Executing!");
|
||||
|
||||
// execute the command
|
||||
match commands::execute_command(&cmd_path, &cmd_config) {
|
||||
Ok(chain) => {
|
||||
// success
|
||||
info!("Command executed successfully.");
|
||||
|
||||
// notify GUI
|
||||
ipc::send(IpcEvent::CommandExecuted {
|
||||
id: cmd_config.id.clone(),
|
||||
success: true,
|
||||
});
|
||||
|
||||
if chain {
|
||||
// chain commands
|
||||
start = SystemTime::now();
|
||||
} else {
|
||||
// skip, if chaining is not required
|
||||
start = start
|
||||
.checked_sub(core::time::Duration::from_secs(1000))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
continue 'voice_recognition; // continue voice recognition
|
||||
}
|
||||
Err(msg) => {
|
||||
// fail
|
||||
error!("Error executing command: {}", msg);
|
||||
|
||||
ipc::send(IpcEvent::CommandExecuted {
|
||||
id: cmd_config.id.clone(),
|
||||
success: false,
|
||||
});
|
||||
ipc::send(IpcEvent::Error {
|
||||
message: msg.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// execute command (shared executor)
|
||||
execute_command(&recognized_voice, &rt);
|
||||
|
||||
// return to wake-word listening after command execution (no matter successful or not)
|
||||
break 'voice_recognition;
|
||||
@@ -236,10 +190,93 @@ fn main_loop() -> Result<(), ()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
// process text command from GUI
|
||||
fn process_text_command(text: &str, rt: &tokio::runtime::Runtime) {
|
||||
info!("Processing text command: {}", text);
|
||||
|
||||
ipc::send(IpcEvent::SpeechRecognized { text: text.to_string() });
|
||||
|
||||
// filter text same as voice
|
||||
let mut filtered = text.to_lowercase();
|
||||
for tbr in config::ASSISTANT_PHRASES_TBR {
|
||||
filtered = filtered.replace(tbr, "");
|
||||
}
|
||||
let filtered = filtered.trim();
|
||||
|
||||
if filtered.is_empty() {
|
||||
ipc::send(IpcEvent::Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
execute_command(filtered, rt);
|
||||
}
|
||||
|
||||
// shared command execution logic (manual & voice)
|
||||
fn execute_command(text: &str, rt: &tokio::runtime::Runtime) {
|
||||
let commands_list = match COMMANDS_LIST.get() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
ipc::send(IpcEvent::Error { message: "Commands not loaded".to_string() });
|
||||
ipc::send(IpcEvent::Idle);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// let sounds_directory = audio::get_sound_directory().unwrap();
|
||||
|
||||
// try intent recognition first, fallback to levenshtein
|
||||
let cmd_result = if let Some((intent_id, confidence)) =
|
||||
rt.block_on(intent::classify(text))
|
||||
{
|
||||
info!("Intent recognized: {} (confidence: {:.2})", intent_id, confidence);
|
||||
intent::get_command_by_intent(commands_list, &intent_id)
|
||||
} else {
|
||||
info!("Intent not recognized, trying levenshtein fallback...");
|
||||
commands::fetch_command(text, commands_list)
|
||||
};
|
||||
|
||||
if let Some((cmd_path, cmd_config)) = cmd_result {
|
||||
info!("Command found: {:?}", cmd_path);
|
||||
|
||||
match commands::execute_command(&cmd_path, &cmd_config) {
|
||||
Ok(_) => {
|
||||
info!("Command executed successfully");
|
||||
voices::play_ok(); // command executed sound
|
||||
ipc::send(IpcEvent::CommandExecuted {
|
||||
id: cmd_config.id.clone(),
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
Err(msg) => {
|
||||
error!("Error executing command: {}", msg);
|
||||
voices::play_error();
|
||||
ipc::send(IpcEvent::CommandExecuted {
|
||||
id: cmd_config.id.clone(),
|
||||
success: false,
|
||||
});
|
||||
ipc::send(IpcEvent::Error { message: msg.to_string() });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("No command found for: {}", text);
|
||||
// play "not understood" sound
|
||||
// audio::play_sound(&sounds_directory.join("not_understand.wav"));
|
||||
voices::play_not_found();
|
||||
ipc::send(IpcEvent::Error {
|
||||
message: format!("Command not found: {}", text)
|
||||
});
|
||||
}
|
||||
|
||||
ipc::send(IpcEvent::Idle);
|
||||
}
|
||||
|
||||
|
||||
fn keyword_callback(keyword_index: i32) {}
|
||||
|
||||
pub fn close(code: i32) {
|
||||
info!("Closing application.");
|
||||
voices::play_goodbye();
|
||||
ipc::send(IpcEvent::Stopping);
|
||||
std::process::exit(code);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc;
|
||||
|
||||
// include core
|
||||
use jarvis_core::{
|
||||
audio, audio_processing, commands, config, db, listener, recorder, stt, intent,
|
||||
ipc::{self, IpcAction},
|
||||
i18n,
|
||||
i18n, voices,
|
||||
APP_CONFIG_DIR, APP_LOG_DIR, COMMANDS_LIST, DB,
|
||||
};
|
||||
|
||||
@@ -41,6 +42,12 @@ fn main() -> Result<(), String> {
|
||||
DB.set(Arc::new(RwLock::new(db::init_settings())))
|
||||
.expect("DB already initialized");
|
||||
|
||||
// init voices
|
||||
let voice_id = DB.get().unwrap().read().voice.clone();
|
||||
if let Err(e) = voices::init(&voice_id) {
|
||||
warn!("Failed to init voices: {}", e);
|
||||
}
|
||||
|
||||
// init i18n
|
||||
i18n::init(&DB.get().unwrap().read().language);
|
||||
|
||||
@@ -108,7 +115,10 @@ fn main() -> Result<(), String> {
|
||||
info!("Initializing IPC...");
|
||||
ipc::init();
|
||||
|
||||
ipc::set_action_handler(|action| {
|
||||
// channel for text commands (manually written in the GUI)
|
||||
let (text_cmd_tx, text_cmd_rx) = mpsc::channel::<String>();
|
||||
|
||||
ipc::set_action_handler(move |action| {
|
||||
match action {
|
||||
IpcAction::Stop => {
|
||||
info!("Received stop command from GUI");
|
||||
@@ -122,6 +132,15 @@ fn main() -> Result<(), String> {
|
||||
info!("Received mute request: {}", muted);
|
||||
// TODO: implement mute
|
||||
}
|
||||
IpcAction::TextCommand { text } => {
|
||||
info!("Received text command: {}", text);
|
||||
if let Err(e) = text_cmd_tx.send(text) {
|
||||
error!("Failed to send text command to app: {}", e);
|
||||
}
|
||||
}
|
||||
IpcAction::Ping => {
|
||||
// handled internally by server
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
@@ -134,7 +153,7 @@ fn main() -> Result<(), String> {
|
||||
|
||||
// start the app (in the background thread)
|
||||
std::thread::spawn(|| {
|
||||
let _ = app::start();
|
||||
let _ = app::start(text_cmd_rx);
|
||||
});
|
||||
|
||||
tray::init_blocking();
|
||||
|
||||
@@ -6,11 +6,12 @@ use tray_icon::{
|
||||
};
|
||||
use winit::event_loop::{ControlFlow, EventLoopBuilder};
|
||||
use image;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os="windows")]
|
||||
use winit::platform::windows::EventLoopBuilderExtWindows;
|
||||
|
||||
use jarvis_core::{config, i18n};
|
||||
use jarvis_core::{config, i18n, ipc::{self, IpcEvent}};
|
||||
|
||||
const TRAY_ICON_BYTES: &[u8] = include_bytes!("../../../resources/icons/32x32.png");
|
||||
|
||||
@@ -103,8 +104,14 @@ pub fn init_blocking() {
|
||||
fn handle_menu_event(event: &MenuEvent) {
|
||||
match event.id.0.as_str() {
|
||||
"exit" => std::process::exit(0),
|
||||
"restart" => { /* restart logic */ }
|
||||
"settings" => { /* open settings */ }
|
||||
"restart" => {
|
||||
info!("Restarting from tray menu...");
|
||||
restart_app();
|
||||
}
|
||||
"settings" => {
|
||||
info!("Opening settings from tray menu...");
|
||||
open_settings();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -129,3 +136,74 @@ fn load_icon(path: &std::path::Path) -> tray_icon::Icon {
|
||||
};
|
||||
tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
|
||||
}
|
||||
|
||||
fn restart_app() {
|
||||
// get current executable path
|
||||
let exe_path = match std::env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to get executable path: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// spawn new instance
|
||||
match Command::new(&exe_path).spawn() {
|
||||
Ok(_) => {
|
||||
info!("Spawned new instance, exiting current...");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to restart: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_settings() {
|
||||
// check if jarvis-gui is connected via IPC
|
||||
if ipc::has_clients() {
|
||||
// gui is running, send reveal event
|
||||
info!("GUI is connected, sending reveal event");
|
||||
ipc::send(IpcEvent::RevealWindow);
|
||||
} else {
|
||||
// gui not running, launch it
|
||||
info!("GUI not connected, launching jarvis-gui");
|
||||
launch_gui();
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_gui() {
|
||||
let exe_path = match std::env::current_exe() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to get executable path: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// jarvis-gui should be in same directory as jarvis-app
|
||||
let gui_path = exe_path.parent()
|
||||
.map(|p| p.join(get_gui_executable_name()))
|
||||
.unwrap_or_else(|| get_gui_executable_name().into());
|
||||
|
||||
info!("Launching GUI: {:?}", gui_path);
|
||||
|
||||
match Command::new(&gui_path).spawn() {
|
||||
Ok(_) => {
|
||||
info!("Launched jarvis-gui");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to launch jarvis-gui: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_gui_executable_name() -> &'static str {
|
||||
"jarvis-gui.exe"
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn get_gui_executable_name() -> &'static str {
|
||||
"jarvis-gui"
|
||||
}
|
||||
@@ -29,6 +29,7 @@ futures-util = { workspace = true, optional = true }
|
||||
fluent.workspace = true
|
||||
fluent-bundle.workspace = true
|
||||
unic-langid.workspace = true
|
||||
chrono.workspace = true
|
||||
|
||||
# pv_recorder = { workspace = true, optional = true }
|
||||
vosk = { version = "0.3.1", optional = true }
|
||||
|
||||
@@ -57,9 +57,17 @@ pub fn init() -> Result<(), ()> {
|
||||
}
|
||||
|
||||
pub fn play_sound(filename: &PathBuf) {
|
||||
let audio_type = match AUDIO_TYPE.get() {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
warn!("Audio not initialized, cannot play: {}", filename.display());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Playing {}", filename.display());
|
||||
|
||||
match AUDIO_TYPE.get().unwrap() {
|
||||
match audio_type {
|
||||
AudioType::Rodio => {
|
||||
rodio::play_sound(filename, true);
|
||||
}
|
||||
@@ -75,18 +83,11 @@ pub fn get_sound_directory() -> Option<PathBuf> {
|
||||
SOUND_DIR.join(&s.voice)
|
||||
};
|
||||
|
||||
match voice_path.exists() && voice_path.cmp(&SOUND_DIR) != Ordering::Equal {
|
||||
match voice_path.exists() {
|
||||
true => Some(voice_path),
|
||||
_ => {
|
||||
let default_voice_path = SOUND_DIR.join(config::DEFAULT_VOICE);
|
||||
|
||||
match default_voice_path.exists() {
|
||||
true => Some(default_voice_path),
|
||||
_ => {
|
||||
error!("No sounds found. Search path - {:?}", voice_path);
|
||||
None
|
||||
}
|
||||
}
|
||||
error!("No sounds folder found. Search path - {:?}", voice_path);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::APP_DIR;
|
||||
use rand::prelude::*;
|
||||
use seqdiff::ratio;
|
||||
use serde_yaml;
|
||||
// use serde_yaml;
|
||||
use std::path::Path;
|
||||
use std::{fs, fs::File};
|
||||
|
||||
@@ -14,15 +15,16 @@ pub use structs::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{audio, config};
|
||||
use crate::{config};
|
||||
|
||||
// @TODO. Allow commands both in yaml and json format.
|
||||
pub fn parse_commands() -> Result<Vec<JCommandsList>, String> {
|
||||
// collect commands
|
||||
let mut commands: Vec<JCommandsList> = Vec::new();
|
||||
|
||||
let cmd_dirs = fs::read_dir(config::COMMANDS_PATH)
|
||||
.map_err(|e| format!("Error reading commands directory: {}", e))?;
|
||||
let commands_path = APP_DIR.join(config::COMMANDS_PATH);
|
||||
let cmd_dirs = fs::read_dir(&commands_path)
|
||||
.map_err(|e| format!("Error reading commands directory {:?}: {}", commands_path, e))?;
|
||||
|
||||
for entry in cmd_dirs {
|
||||
let entry = match entry {
|
||||
@@ -204,7 +206,7 @@ pub fn execute_command(
|
||||
cmd_config: &JCommand,
|
||||
// app_handle: &tauri::AppHandle,
|
||||
) -> Result<bool, String> {
|
||||
let sounds_directory = audio::get_sound_directory().unwrap();
|
||||
// let sounds_directory = audio::get_sound_directory().unwrap();
|
||||
|
||||
match cmd_config.action.as_str() {
|
||||
"voice" => {
|
||||
@@ -217,7 +219,7 @@ pub fn execute_command(
|
||||
.unwrap()
|
||||
);
|
||||
// events::play(random_cmd_sound, app_handle);
|
||||
audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
@@ -242,7 +244,7 @@ pub fn execute_command(
|
||||
.unwrap()
|
||||
);
|
||||
// events::play(random_cmd_sound, app_handle);
|
||||
audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
|
||||
Ok(true)
|
||||
} else {
|
||||
@@ -264,7 +266,7 @@ pub fn execute_command(
|
||||
.unwrap()
|
||||
);
|
||||
// events::play(random_cmd_sound, app_handle);
|
||||
audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
@@ -284,7 +286,7 @@ pub fn execute_command(
|
||||
.unwrap()
|
||||
);
|
||||
// events::play(random_cmd_sound, app_handle);
|
||||
audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
std::process::exit(0);
|
||||
@@ -299,7 +301,7 @@ pub fn execute_command(
|
||||
.unwrap()
|
||||
);
|
||||
// events::play(random_cmd_sound, app_handle);
|
||||
audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct JCommandsList {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
@@ -11,7 +11,7 @@ pub struct JCommandsList {
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct JCommand {
|
||||
pub id: String,
|
||||
pub action: String,
|
||||
|
||||
@@ -70,7 +70,9 @@ 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;
|
||||
|
||||
pub const DEFAULT_VOICE: &str = "jarvis-og";
|
||||
pub const DEFAULT_VOICE: &str = "jarvis-remaster";
|
||||
pub const SOUND_PATH: &str = "resources/sound"; // extended from SOUND_DIR (resources/sound)
|
||||
pub const VOICES_PATH: &str = "voices"; // extended from SOUND_PATH (resources/sound)
|
||||
|
||||
pub const BUNDLE_IDENTIFIER: &str = "com.priler.jarvis";
|
||||
pub const DB_FILE_NAME: &str = "app.db";
|
||||
@@ -81,7 +83,7 @@ pub const REPOSITORY_LINK: Option<&str> = option_env!("CARGO_PKG_REPOSITORY");
|
||||
pub const TG_OFFICIAL_LINK: Option<&str> = Some("https://t.me/howdyho_official");
|
||||
pub const FEEDBACK_LINK: Option<&str> = Some("https://t.me/jarvis_feedback_bot");
|
||||
pub const SUPPORT_BOOSTY_LINK: Option<&str> = Some("https://boosty.to/howdyho");
|
||||
pub const SUPPORT_PATREON_LINK: Option<&str> = Some("https://www.patreon.com/user?u=22843414");
|
||||
pub const SUPPORT_PATREON_LINK: Option<&str> = Some("https://www.patreon.com/c/priler");
|
||||
|
||||
/*
|
||||
Tray.
|
||||
|
||||
@@ -54,7 +54,9 @@ settings-microphone = Microphone
|
||||
settings-microphone-desc = The assistant will listen to this microphone.
|
||||
settings-mic-default = Default (System)
|
||||
settings-voice = Assistant voice
|
||||
settings-voice-desc = Not all commands work with all sound packs.
|
||||
settings-voice-desc =
|
||||
Not all commands work with all sound packs.
|
||||
Click to listen the preview of sound.
|
||||
settings-wake-word-engine = Wake word engine
|
||||
settings-wake-word-desc = Choose the engine for wake word recognition.
|
||||
settings-stt-engine = Speech recognition
|
||||
@@ -117,3 +119,8 @@ notification-saved = Settings saved!
|
||||
notification-error = Error
|
||||
notification-assistant-started = Assistant started
|
||||
notification-assistant-stopped = Assistant stopped
|
||||
|
||||
# ETC
|
||||
search-error-not-running = Assistant is not running
|
||||
search-error-failed = Failed to execute command
|
||||
settings-no-voices = No voices found
|
||||
@@ -54,7 +54,9 @@ settings-microphone = Микрофон
|
||||
settings-microphone-desc = Его будет слушать ассистент.
|
||||
settings-mic-default = По умолчанию (Система)
|
||||
settings-voice = Голос ассистента
|
||||
settings-voice-desc = Не все команды работают со всеми звуковыми пакетами.
|
||||
settings-voice-desc =
|
||||
Не все команды работают со всеми звуковыми пакетами.
|
||||
Кликните, чтобы прослушать как звучит голос.
|
||||
settings-wake-word-engine = Движок активации
|
||||
settings-wake-word-desc = Выберите нейросеть для распознавания активационной фразы.
|
||||
settings-stt-engine = Распознавание речи
|
||||
@@ -117,3 +119,8 @@ notification-saved = Настройки сохранены!
|
||||
notification-error = Ошибка
|
||||
notification-assistant-started = Ассистент запущен
|
||||
notification-assistant-stopped = Ассистент остановлен
|
||||
|
||||
# ETC
|
||||
search-error-not-running = Ассистент не запущен
|
||||
search-error-failed = Не удалось выполнить команду
|
||||
settings-no-voices = Голоса не найдены
|
||||
@@ -54,7 +54,9 @@ settings-microphone = Мікрофон
|
||||
settings-microphone-desc = Його буде слухати асистент.
|
||||
settings-mic-default = За замовчуванням (Система)
|
||||
settings-voice = Голос асистента
|
||||
settings-voice-desc = Не всі команди працюють з усіма звуковими пакетами.
|
||||
settings-voice-desc =
|
||||
Не всі команди працюють з усіма звуковими пакетами.
|
||||
Натисніть, щоб прослухати як звучить голос.
|
||||
settings-wake-word-engine = Рушій активації
|
||||
settings-wake-word-desc = Виберіть нейромережу для розпізнавання активаційної фрази.
|
||||
settings-stt-engine = Розпізнавання мовлення
|
||||
@@ -117,3 +119,9 @@ notification-saved = Налаштування збережено!
|
||||
notification-error = Помилка
|
||||
notification-assistant-started = Асистент запущено
|
||||
notification-assistant-stopped = Асистент зупинено
|
||||
|
||||
# ETC
|
||||
|
||||
search-error-not-running = Асистент не запущено
|
||||
search-error-failed = Не вдалося виконати команду
|
||||
settings-no-voices = Голоси не знайдено
|
||||
@@ -2,4 +2,4 @@ mod events;
|
||||
mod server;
|
||||
|
||||
pub use events::{IpcAction, IpcEvent};
|
||||
pub use server::{init, send, set_action_handler, start_server, IPC_ADDR, IPC_PORT};
|
||||
pub use server::{init, send, set_action_handler, start_server, has_clients, IPC_ADDR, IPC_PORT};
|
||||
@@ -30,6 +30,9 @@ pub enum IpcEvent {
|
||||
|
||||
// Pong response
|
||||
Pong,
|
||||
|
||||
// request GUI to reveal/focus window
|
||||
RevealWindow,
|
||||
}
|
||||
|
||||
// Actions sent from GUI to jarvis-app
|
||||
@@ -47,4 +50,7 @@ pub enum IpcAction {
|
||||
|
||||
// Mute/unmute listening
|
||||
SetMuted { muted: bool },
|
||||
|
||||
// Execute text command
|
||||
TextCommand { text: String },
|
||||
}
|
||||
@@ -187,4 +187,12 @@ async fn handle_client(
|
||||
}
|
||||
|
||||
info!("IPC: Client disconnected: {}", peer_addr);
|
||||
}
|
||||
|
||||
pub fn has_clients() -> bool {
|
||||
if let Some(tx) = BROADCAST_TX.get() {
|
||||
tx.receiver_count() > 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ use std::path::PathBuf;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod time;
|
||||
|
||||
pub mod audio;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
@@ -32,6 +34,8 @@ pub mod audio_processing;
|
||||
#[cfg(feature = "jarvis_app")]
|
||||
pub mod ipc;
|
||||
|
||||
pub mod voices;
|
||||
|
||||
// shared statics
|
||||
// pub static APP_DIR: Lazy<PathBuf> = Lazy::new(|| std::env::current_dir().unwrap());
|
||||
pub static APP_DIR: Lazy<PathBuf> = Lazy::new(|| {
|
||||
@@ -40,7 +44,7 @@ pub static APP_DIR: Lazy<PathBuf> = Lazy::new(|| {
|
||||
.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 SOUND_DIR: Lazy<PathBuf> = Lazy::new(|| APP_DIR.clone().join(config::SOUND_PATH));
|
||||
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();
|
||||
|
||||
2
crates/jarvis-core/src/time.rs
Normal file
2
crates/jarvis-core/src/time.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod structs;
|
||||
pub use structs::*;
|
||||
21
crates/jarvis-core/src/time/structs.rs
Normal file
21
crates/jarvis-core/src/time/structs.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use chrono::Timelike;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum TimeOfDay {
|
||||
Morning, // 5:00 - 11:59
|
||||
Day, // 12:00 - 16:59
|
||||
Evening, // 17:00 - 21:59
|
||||
Night, // 22:00 - 4:59
|
||||
}
|
||||
|
||||
impl TimeOfDay {
|
||||
pub fn now() -> Self {
|
||||
let hour = chrono::Local::now().hour();
|
||||
match hour {
|
||||
5..=11 => TimeOfDay::Morning,
|
||||
12..=16 => TimeOfDay::Day,
|
||||
17..=21 => TimeOfDay::Evening,
|
||||
_ => TimeOfDay::Night,
|
||||
}
|
||||
}
|
||||
}
|
||||
234
crates/jarvis-core/src/voices.rs
Normal file
234
crates/jarvis-core/src/voices.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use rand::prelude::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::RwLock;
|
||||
// use chrono::Timelike;
|
||||
|
||||
use crate::{DB, SOUND_DIR, audio, config, time};
|
||||
|
||||
pub mod structs;
|
||||
|
||||
static VOICES: OnceCell<Vec<structs::VoiceConfig>> = OnceCell::new();
|
||||
static CURRENT_VOICE_ID: OnceCell<RwLock<String>> = OnceCell::new();
|
||||
|
||||
pub fn init(default_voice: &str) -> Result<(), String> {
|
||||
CURRENT_VOICE_ID.get_or_init(|| RwLock::new(default_voice.to_string()));
|
||||
|
||||
let voices = scan_voices()?;
|
||||
|
||||
if voices.is_empty() {
|
||||
return Err("No voices found".into());
|
||||
}
|
||||
|
||||
info!("Loaded {} voice(s): {:?}",
|
||||
voices.len(),
|
||||
voices.iter().map(|v| &v.voice.id).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
VOICES.set(voices).map_err(|_| "Voices already initialized")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn scan_voices() -> Result<Vec<structs::VoiceConfig>, String> {
|
||||
let voices_dir = SOUND_DIR.join(&config::VOICES_PATH);
|
||||
|
||||
if !voices_dir.exists() {
|
||||
return Err(format!("Voices directory not found: {:?}", voices_dir));
|
||||
}
|
||||
|
||||
let mut voices = Vec::new();
|
||||
|
||||
let entries = fs::read_dir(&voices_dir)
|
||||
.map_err(|e| format!("Failed to read voices directory: {}", e))?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let voice_path = entry.path();
|
||||
if !voice_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let toml_path = voice_path.join("voice.toml");
|
||||
if !toml_path.exists() {
|
||||
warn!("Voice folder {:?} missing voice.toml, skipping", voice_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
match load_voice_config(&toml_path, &voice_path) {
|
||||
Ok(config) => voices.push(config),
|
||||
Err(e) => warn!("Failed to load voice {:?}: {}", voice_path, e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(voices)
|
||||
}
|
||||
|
||||
fn load_voice_config(toml_path: &Path, voice_path: &Path) -> Result<structs::VoiceConfig, String> {
|
||||
let content = fs::read_to_string(toml_path)
|
||||
.map_err(|e| format!("Failed to read voice.toml: {}", e))?;
|
||||
|
||||
let mut config: structs::VoiceConfig = toml::from_str(&content)
|
||||
.map_err(|e| format!("Failed to parse voice.toml: {}", e))?;
|
||||
|
||||
config.path = voice_path.to_path_buf();
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn list_voices() -> Vec<structs::VoiceConfig> {
|
||||
VOICES.get().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_voice(voice_id: &str) -> Option<structs::VoiceConfig> {
|
||||
VOICES.get()?.iter().find(|v| v.voice.id == voice_id).cloned()
|
||||
}
|
||||
|
||||
pub fn get_current_voice() -> Option<structs::VoiceConfig> {
|
||||
let current_id = CURRENT_VOICE_ID.get()?.read().clone();
|
||||
get_voice(¤t_id)
|
||||
}
|
||||
|
||||
pub fn set_current_voice(voice_id: &str) {
|
||||
if let Some(lock) = CURRENT_VOICE_ID.get() {
|
||||
*lock.write() = voice_id.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_language() -> String {
|
||||
DB.get()
|
||||
.map(|db| db.read().language.clone())
|
||||
.unwrap_or_else(|| "ru".to_string())
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn find_sound_file(voice_path: &Path, lang: &str, sound_name: &str) -> Option<PathBuf> {
|
||||
let extensions = ["mp3", "wav", "ogg"];
|
||||
let lang_path = voice_path.join(lang);
|
||||
|
||||
// try language subfolder first
|
||||
for ext in &extensions {
|
||||
let file_path = lang_path.join(format!("{}.{}", sound_name, ext));
|
||||
if file_path.exists() {
|
||||
return Some(file_path);
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to root voice folder
|
||||
for ext in &extensions {
|
||||
let file_path = voice_path.join(format!("{}.{}", sound_name, ext));
|
||||
if file_path.exists() {
|
||||
return Some(file_path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn play_random_from(sounds: &[String]) {
|
||||
if sounds.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let voice = match get_current_voice() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
warn!("No current voice set");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let lang = get_current_language();
|
||||
let sound_name = sounds.choose(&mut rand::thread_rng()).unwrap();
|
||||
|
||||
match find_sound_file(&voice.path, &lang, sound_name) {
|
||||
Some(path) => {
|
||||
debug!("Playing: {:?}", path);
|
||||
audio::play_sound(&path);
|
||||
}
|
||||
None => {
|
||||
warn!("Sound not found: {} (lang: {}, voice: {})", sound_name, lang, voice.voice.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(reaction: structs::Reaction) {
|
||||
let voice = match get_current_voice() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
warn!("No current voice set");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let sounds = match reaction {
|
||||
structs::Reaction::Greet => {
|
||||
// try time specific first
|
||||
let time_specific = match time::TimeOfDay::now() {
|
||||
time::TimeOfDay::Morning => &voice.reactions.greet_morning,
|
||||
time::TimeOfDay::Day => &voice.reactions.greet_day,
|
||||
time::TimeOfDay::Evening => &voice.reactions.greet_evening,
|
||||
time::TimeOfDay::Night => &voice.reactions.greet_night,
|
||||
};
|
||||
|
||||
if time_specific.is_empty() {
|
||||
// fallback to simple run voice (not time specific)
|
||||
&voice.reactions.greet
|
||||
} else {
|
||||
time_specific
|
||||
}
|
||||
}
|
||||
structs::Reaction::Reply => &voice.reactions.reply,
|
||||
structs::Reaction::Ok => &voice.reactions.ok,
|
||||
structs::Reaction::NotFound => &voice.reactions.not_found,
|
||||
structs::Reaction::Thanks => &voice.reactions.thanks,
|
||||
structs::Reaction::Error => &voice.reactions.error,
|
||||
structs::Reaction::Goodbye => &voice.reactions.goodbye,
|
||||
};
|
||||
|
||||
play_random_from(sounds);
|
||||
}
|
||||
|
||||
// Play a preview sound for a specific voice
|
||||
pub fn play_preview(voice_id: &str) {
|
||||
let voice = match get_voice(voice_id) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
warn!("Voice not found for preview: {}", voice_id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let lang = get_current_language();
|
||||
|
||||
// pick from reply or ok sounds for preview
|
||||
let sounds: Vec<&String> = voice.reactions.reply.iter()
|
||||
.chain(voice.reactions.ok.iter())
|
||||
.chain(voice.reactions.greet.iter())
|
||||
.collect();
|
||||
|
||||
if sounds.is_empty() {
|
||||
warn!("No preview sounds for voice: {}", voice_id);
|
||||
return;
|
||||
}
|
||||
|
||||
let sound_name = sounds.choose(&mut rand::thread_rng()).unwrap();
|
||||
|
||||
if let Some(path) = find_sound_file(&voice.path, &lang, sound_name) {
|
||||
debug!("Playing preview: {:?}", path);
|
||||
audio::play_sound(&path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// shortcuts
|
||||
pub fn play_greet() { play(structs::Reaction::Greet); } // app startup
|
||||
pub fn play_reply() { play(structs::Reaction::Reply); } // wake word detected
|
||||
pub fn play_ok() { play(structs::Reaction::Ok); } // command executed
|
||||
pub fn play_not_found() { play(structs::Reaction::NotFound); }
|
||||
pub fn play_thanks() { play(structs::Reaction::Thanks); }
|
||||
pub fn play_error() { play(structs::Reaction::Error); }
|
||||
pub fn play_goodbye() { play(structs::Reaction::Goodbye); }
|
||||
70
crates/jarvis-core/src/voices/structs.rs
Normal file
70
crates/jarvis-core/src/voices/structs.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VoiceConfig {
|
||||
#[serde(skip)]
|
||||
pub path: PathBuf,
|
||||
pub voice: VoiceMeta,
|
||||
pub reactions: VoiceReactions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VoiceMeta {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub author: String,
|
||||
pub languages: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct VoiceReactions {
|
||||
// app startup (time-based or generic)
|
||||
#[serde(default)]
|
||||
pub greet: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub greet_morning: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub greet_day: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub greet_evening: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub greet_night: Vec<String>,
|
||||
|
||||
// wake word detected
|
||||
#[serde(default)]
|
||||
pub reply: Vec<String>,
|
||||
|
||||
// command executed
|
||||
#[serde(default)]
|
||||
pub ok: Vec<String>,
|
||||
|
||||
// command not found
|
||||
#[serde(default)]
|
||||
pub not_found: Vec<String>,
|
||||
|
||||
// thank you
|
||||
#[serde(default)]
|
||||
pub thanks: Vec<String>,
|
||||
|
||||
// error
|
||||
#[serde(default)]
|
||||
pub error: Vec<String>,
|
||||
|
||||
// shutdown
|
||||
#[serde(default)]
|
||||
pub goodbye: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Reaction {
|
||||
Greet, // app startup
|
||||
Reply, // wake word detected
|
||||
Ok, // command executed
|
||||
NotFound,
|
||||
Thanks,
|
||||
Error,
|
||||
Goodbye,
|
||||
}
|
||||
@@ -15,6 +15,12 @@
|
||||
"allow": [
|
||||
"$RESOURCE/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-unminimize",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-close"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capabilities for Jarvis","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","dialog:allow-message","fs:default","fs:allow-read","fs:allow-write",{"identifier":"fs:scope","allow":["$RESOURCE/**"]}]}}
|
||||
{"default":{"identifier":"default","description":"Default capabilities for Jarvis","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","dialog:allow-message","fs:default","fs:allow-read","fs:allow-write",{"identifier":"fs:scope","allow":["$RESOURCE/**"]},"core:window:allow-show","core:window:allow-hide","core:window:allow-set-focus","core:window:allow-unminimize","core:window:allow-minimize","core:window:allow-close"]}}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use jarvis_core::{config, db, i18n, APP_CONFIG_DIR, APP_LOG_DIR, DB};
|
||||
use jarvis_core::{config, db, i18n, voices, APP_CONFIG_DIR, APP_LOG_DIR, DB};
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
@@ -30,6 +30,16 @@ fn main() {
|
||||
// init i18n
|
||||
i18n::init(&settings.language);
|
||||
|
||||
// init voices
|
||||
if let Err(e) = voices::init(&settings.voice) {
|
||||
eprintln!("Failed to init voices: {}", e);
|
||||
}
|
||||
|
||||
// init audio backend
|
||||
if let Err(e) = jarvis_core::audio::init() {
|
||||
eprintln!("Failed to init audio: {:?}", e);
|
||||
}
|
||||
|
||||
// set db
|
||||
DB.set(Arc::new(RwLock::new(settings)))
|
||||
.expect("DB already initialized");
|
||||
@@ -55,6 +65,8 @@ fn main() {
|
||||
tauri_commands::get_author_name,
|
||||
tauri_commands::get_repository_link,
|
||||
tauri_commands::get_tg_official_link,
|
||||
tauri_commands::get_boosty_link,
|
||||
tauri_commands::get_patreon_link,
|
||||
tauri_commands::get_feedback_link,
|
||||
|
||||
// fs
|
||||
@@ -79,6 +91,15 @@ fn main() {
|
||||
tauri_commands::get_current_language,
|
||||
tauri_commands::set_language,
|
||||
tauri_commands::get_supported_languages,
|
||||
|
||||
// commands
|
||||
tauri_commands::get_commands_count,
|
||||
tauri_commands::get_commands_list,
|
||||
|
||||
// voices
|
||||
tauri_commands::list_voices,
|
||||
tauri_commands::get_voice,
|
||||
tauri_commands::preview_voice,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -29,4 +29,12 @@ pub use stt::*;
|
||||
|
||||
// import i18n commands
|
||||
mod i18n;
|
||||
pub use i18n::*;
|
||||
pub use i18n::*;
|
||||
|
||||
// import commands commands xD
|
||||
mod commands;
|
||||
pub use commands::*;
|
||||
|
||||
// import voices commands
|
||||
mod voices;
|
||||
pub use voices::*;
|
||||
22
crates/jarvis-gui/src/tauri_commands/commands.rs
Normal file
22
crates/jarvis-gui/src/tauri_commands/commands.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use jarvis_core::commands::{self, JCommand, JCommandsList};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static COMMANDS: Lazy<Vec<JCommandsList>> = Lazy::new(|| {
|
||||
commands::parse_commands().unwrap_or_default()
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_commands_count() -> usize {
|
||||
COMMANDS
|
||||
.iter()
|
||||
.map(|list| list.commands.len())
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_commands_list() -> Vec<JCommand> {
|
||||
COMMANDS
|
||||
.iter()
|
||||
.flat_map(|list| list.commands.clone())
|
||||
.collect()
|
||||
}
|
||||
16
crates/jarvis-gui/src/tauri_commands/voices.rs
Normal file
16
crates/jarvis-gui/src/tauri_commands/voices.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use jarvis_core::voices::{self, structs::VoiceConfig};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_voices() -> Vec<VoiceConfig> {
|
||||
voices::list_voices()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_voice(voice_id: String) -> Option<VoiceConfig> {
|
||||
voices::get_voice(&voice_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn preview_voice(voice_id: String) {
|
||||
voices::play_preview(&voice_id);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"frontendDist": "../../frontend/dist"
|
||||
"frontendDist": "../../frontend/dist/client"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
@@ -35,11 +35,11 @@
|
||||
],
|
||||
"targets": "all",
|
||||
"resources": {
|
||||
"../../resources/commands": "commands",
|
||||
"../../resources/sound": "sound",
|
||||
"../../resources/rustpotter": "rustpotter",
|
||||
"../../resources/vosk": "vosk",
|
||||
"../../resources/keywords": "keywords"
|
||||
"../../resources/commands": "resources/commands",
|
||||
"../../resources/sound": "resources/sound",
|
||||
"../../resources/rustpotter": "resources/rustpotter",
|
||||
"../../resources/vosk": "resources/vosk",
|
||||
"../../resources/keywords": "resources/keywords"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user