project restructure with Rust workspaces
This commit is contained in:
parent
3ffbe876f3
commit
09f72622bc
428 changed files with 19372 additions and 6061 deletions
16
crates/jarvis-app/.cargo/config.toml
Normal file
16
crates/jarvis-app/.cargo/config.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Cargo configuration file
|
||||
|
||||
[build]
|
||||
jobs = 4
|
||||
rustflags = [
|
||||
# "-Ctarget-feature=+fp16,+fhm"
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0
|
||||
debug = true
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
lto = true
|
||||
29
crates/jarvis-app/Cargo.toml
Normal file
29
crates/jarvis-app/Cargo.toml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "jarvis-app"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
jarvis-core = { path = "../jarvis-core" }
|
||||
once_cell.workspace = true
|
||||
log.workspace = true
|
||||
simple-log = "2.4"
|
||||
tray-icon = "0.21"
|
||||
winit = "0.30"
|
||||
image.workspace = true
|
||||
platform-dirs.workspace = true
|
||||
rand.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies.winit]
|
||||
version = "0.30"
|
||||
features = []
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gtk = "0.18"
|
||||
glib = "0.18"
|
||||
45
crates/jarvis-app/Makefile.toml
Normal file
45
crates/jarvis-app/Makefile.toml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[tasks.format]
|
||||
install_crate = "rustfmt"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--", "--emit=files"]
|
||||
|
||||
[tasks.clean]
|
||||
command = "cargo"
|
||||
args = ["clean"]
|
||||
|
||||
[tasks.build_debug]
|
||||
command = "cargo"
|
||||
args = ["build"]
|
||||
|
||||
[tasks.run]
|
||||
command = "cargo"
|
||||
args = ["run"]
|
||||
|
||||
[tasks.build_release]
|
||||
command = "cargo"
|
||||
args = ["build", "--release"]
|
||||
dependencies = ["clean"]
|
||||
|
||||
[tasks.test]
|
||||
command = "cargo"
|
||||
args = ["test"]
|
||||
# dependencies = ["clean"]
|
||||
|
||||
[tasks.post_build]
|
||||
script_runner = "python"
|
||||
script_extension = "py"
|
||||
script = { file = "post_build.py" }
|
||||
|
||||
[tasks.debug]
|
||||
dependencies = [
|
||||
"format",
|
||||
"build_debug",
|
||||
"post_build"
|
||||
]
|
||||
|
||||
[tasks.release]
|
||||
dependencies = [
|
||||
"format",
|
||||
"build_release",
|
||||
"post_build"
|
||||
]
|
||||
10
crates/jarvis-app/build.rs
Normal file
10
crates/jarvis-app/build.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
fn main() {
|
||||
// link to Vosk lib
|
||||
// println!("cargo:rustc-link-lib=libvosk.dll");
|
||||
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let lib_path = std::path::Path::new(&manifest_dir)
|
||||
.join("..\\..\\lib\\windows\\amd64");
|
||||
|
||||
println!("cargo:rustc-link-search=native={}", lib_path.display());
|
||||
}
|
||||
92
crates/jarvis-app/src/_main.rs
Normal file
92
crates/jarvis-app/src/_main.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static; // better switch to once_cell ?
|
||||
use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
|
||||
use log::{info};
|
||||
use log::LevelFilter;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// include assistant commands
|
||||
mod assistant_commands;
|
||||
use assistant_commands::AssistantCommand;
|
||||
|
||||
// include vosk
|
||||
mod vosk;
|
||||
|
||||
// include events
|
||||
mod events;
|
||||
|
||||
// include recorder
|
||||
mod recorder;
|
||||
|
||||
// init PickleDb connection
|
||||
lazy_static! {
|
||||
static ref DB: Mutex<PickleDb> = Mutex::new(
|
||||
PickleDb::load(
|
||||
format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME),
|
||||
PickleDbDumpPolicy::AutoDump,
|
||||
SerializationMethod::Json
|
||||
)
|
||||
.unwrap_or_else(|_x: _| {
|
||||
info!("Creating new db file at {} ...", format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME));
|
||||
PickleDb::new(
|
||||
format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME),
|
||||
PickleDbDumpPolicy::AutoDump,
|
||||
SerializationMethod::Json,
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// init vosk
|
||||
vosk::init_vosk();
|
||||
|
||||
// run the app
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
std::fs::create_dir_all(app.path_resolver().app_config_dir().unwrap())?;
|
||||
APP_CONFIG_DIR.lock().unwrap().push_str(app.path_resolver().app_config_dir().unwrap().to_str().unwrap());
|
||||
|
||||
std::fs::create_dir_all(app.path_resolver().app_log_dir().unwrap())?;
|
||||
APP_LOG_DIR.lock().unwrap().push_str(app.path_resolver().app_log_dir().unwrap().to_str().unwrap());
|
||||
|
||||
// log to file
|
||||
let log_file_path = format!("{}/{}", APP_LOG_DIR.lock().unwrap(), config::LOG_FILE_NAME);
|
||||
println!("!!!===============!!!\nLOGGING TO {}\n!!!===============!!!\n", &log_file_path);
|
||||
simple_logging::log_to_file(&log_file_path, LevelFilter::max()).expect("Failed to start logger ... is directory writable?");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// db commands
|
||||
tauri_commands::db_read,
|
||||
tauri_commands::db_write,
|
||||
// recorder commands
|
||||
tauri_commands::pv_get_audio_devices,
|
||||
tauri_commands::pv_get_audio_device_name,
|
||||
// listener commands
|
||||
tauri_commands::start_listening,
|
||||
tauri_commands::stop_listening,
|
||||
tauri_commands::is_listening,
|
||||
// sys commands
|
||||
tauri_commands::get_current_ram_usage,
|
||||
tauri_commands::get_peak_ram_usage,
|
||||
tauri_commands::get_cpu_temp,
|
||||
tauri_commands::get_cpu_usage,
|
||||
// sound commands
|
||||
tauri_commands::play_sound,
|
||||
// fs commands
|
||||
tauri_commands::show_in_folder,
|
||||
// etc commands
|
||||
tauri_commands::get_app_version,
|
||||
tauri_commands::get_author_name,
|
||||
tauri_commands::get_repository_link,
|
||||
tauri_commands::get_tg_official_link,
|
||||
tauri_commands::get_feedback_link,
|
||||
tauri_commands::get_log_file_path
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
130
crates/jarvis-app/src/app.rs
Normal file
130
crates/jarvis-app/src/app.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use jarvis_core::{audio, commands, config, listener, recorder, stt, COMMANDS_LIST};
|
||||
use rand::prelude::*;
|
||||
|
||||
pub fn start() -> Result<(), ()> {
|
||||
// start the loop
|
||||
main_loop()
|
||||
}
|
||||
|
||||
fn main_loop() -> Result<(), ()> {
|
||||
let mut start: SystemTime;
|
||||
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];
|
||||
|
||||
// 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"));
|
||||
|
||||
// start recording
|
||||
match recorder::start_recording() {
|
||||
Ok(_) => info!("Recording started."),
|
||||
Err(_) => {
|
||||
error!("Cannot start recording.");
|
||||
return Err(()); // quit
|
||||
}
|
||||
}
|
||||
|
||||
// the loop
|
||||
'wake_word: loop {
|
||||
// read from microphone
|
||||
recorder::read_microphone(&mut frame_buffer);
|
||||
|
||||
// recognize wake-word
|
||||
match listener::data_callback(&frame_buffer) {
|
||||
Some(keyword_index) => {
|
||||
// wake-word activated, process further commands
|
||||
// capture current time
|
||||
start = SystemTime::now();
|
||||
|
||||
// play some greet 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()
|
||||
)));
|
||||
|
||||
// wait for voice commands
|
||||
'voice_recognition: loop {
|
||||
// read from microphone
|
||||
recorder::read_microphone(&mut frame_buffer);
|
||||
|
||||
// stt part (without partials)
|
||||
if let Some(mut recognized_voice) = stt::recognize(&frame_buffer, false) {
|
||||
// something was recognized
|
||||
info!("Recognized voice: {}", recognized_voice);
|
||||
|
||||
// filter recognized voice
|
||||
// @TODO. Better recognized voice filtration.
|
||||
recognized_voice = recognized_voice.to_lowercase();
|
||||
for tbr in config::ASSISTANT_PHRASES_TBR {
|
||||
recognized_voice = recognized_voice.replace(tbr, "");
|
||||
}
|
||||
recognized_voice = recognized_voice.trim().into();
|
||||
|
||||
// infer command
|
||||
if let Some((cmd_path, cmd_config)) = commands::fetch_command(
|
||||
&recognized_voice,
|
||||
&COMMANDS_LIST.get().unwrap(),
|
||||
) {
|
||||
// some debug info
|
||||
info!("Recognized voice (filtered): {}", recognized_voice);
|
||||
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.");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return to wake-word listening after command execution (no matter successful or not)
|
||||
break 'voice_recognition;
|
||||
}
|
||||
|
||||
// only recognize voice for a certain period of time
|
||||
match start.elapsed() {
|
||||
Ok(elapsed) if elapsed > config::CMS_WAIT_DELAY => {
|
||||
// return to wake-word listening after N seconds
|
||||
break 'voice_recognition;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keyword_callback(keyword_index: i32) {}
|
||||
|
||||
pub fn close(code: i32) {
|
||||
info!("Closing application.");
|
||||
std::process::exit(code);
|
||||
}
|
||||
25
crates/jarvis-app/src/log.rs
Normal file
25
crates/jarvis-app/src/log.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use simple_log::LogConfigBuilder;
|
||||
|
||||
use crate::config;
|
||||
use crate::APP_LOG_DIR;
|
||||
|
||||
pub fn init_logging() -> Result<(), String> {
|
||||
// configure logging
|
||||
let config = LogConfigBuilder::builder()
|
||||
.path(format!(
|
||||
"{}/{}",
|
||||
APP_LOG_DIR.get().unwrap().display(),
|
||||
config::LOG_FILE_NAME
|
||||
))
|
||||
.size(1 * 100)
|
||||
.roll_count(10)
|
||||
.time_format("%Y-%m-%d %H:%M:%S.%f")
|
||||
.level("debug")?
|
||||
.output_file()
|
||||
.output_console()
|
||||
.build();
|
||||
|
||||
simple_log::new(config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
84
crates/jarvis-app/src/main.rs
Normal file
84
crates/jarvis-app/src/main.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
// include core
|
||||
use jarvis_core::{
|
||||
audio, commands, config, db, listener, recorder, stt,
|
||||
APP_CONFIG_DIR, APP_LOG_DIR, COMMANDS_LIST, DB,
|
||||
};
|
||||
|
||||
// include log
|
||||
#[macro_use]
|
||||
extern crate simple_log;
|
||||
mod log;
|
||||
|
||||
// include app
|
||||
mod app;
|
||||
|
||||
// include tray
|
||||
// @TODO. macOS currently not supported for tray functionality.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod tray;
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
// initialize directories
|
||||
config::init_dirs()?;
|
||||
|
||||
// initialize logging
|
||||
log::init_logging()?;
|
||||
|
||||
// log some base info
|
||||
info!("Starting Jarvis v{} ...", config::APP_VERSION.unwrap());
|
||||
info!("Config directory is: {}", APP_CONFIG_DIR.get().unwrap().display());
|
||||
info!("Log directory is: {}", APP_LOG_DIR.get().unwrap().display());
|
||||
|
||||
// initialize database (settings)
|
||||
let _ = DB.set(db::init_settings());
|
||||
|
||||
// initialize tray
|
||||
// @TODO. macOS currently not supported for tray functionality,
|
||||
// due to the separate thread in which tray processing works,
|
||||
// but macOS requires it to be processed in the main thread only
|
||||
// The solution may be to include wake-word detection etc. in the winit event loop. (only for MacOS, though?)
|
||||
//#[cfg(not(target_os = "macos"))]
|
||||
//tray::init();
|
||||
|
||||
// init recorder
|
||||
if recorder::init().is_err() {
|
||||
app::close(1);
|
||||
}
|
||||
|
||||
// init stt engine
|
||||
if stt::init().is_err() {
|
||||
// @TODO. Allow continuing even without STT, if commands is using keywords or smthng?
|
||||
app::close(1); // cannot continue without stt
|
||||
}
|
||||
|
||||
// init tts engine
|
||||
// none for now (Silero-rs coming)
|
||||
|
||||
// init commands
|
||||
info!("Initializing commands.");
|
||||
let cmds = commands::parse_commands().unwrap();
|
||||
info!("Commands initialized. Count: {}, List: {:?}", cmds.len(), commands::list(&cmds));
|
||||
COMMANDS_LIST.set(cmds).unwrap();
|
||||
|
||||
// init audio
|
||||
if audio::init().is_err() {
|
||||
// @TODO. Allow continuing even without audio?
|
||||
app::close(1); // cannot continue without audio
|
||||
}
|
||||
|
||||
// init wake-word engine
|
||||
if listener::init().is_err() {
|
||||
app::close(1); // cannot continue without wake-word engine
|
||||
}
|
||||
|
||||
// start the app (in the background thread)
|
||||
std::thread::spawn(|| {
|
||||
app::start();
|
||||
});
|
||||
|
||||
tray::init_blocking();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
131
crates/jarvis-app/src/tray.rs
Normal file
131
crates/jarvis-app/src/tray.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
mod menu;
|
||||
|
||||
use tray_icon::{
|
||||
menu::{AboutMetadata, Menu, MenuEvent, MenuItem, PredefinedMenuItem},
|
||||
TrayIconBuilder, TrayIconEvent,
|
||||
};
|
||||
use winit::event_loop::{ControlFlow, EventLoopBuilder};
|
||||
use image;
|
||||
|
||||
#[cfg(target_os="windows")]
|
||||
use winit::platform::windows::EventLoopBuilderExtWindows;
|
||||
|
||||
use jarvis_core::config;
|
||||
|
||||
const TRAY_ICON_BYTES: &[u8] = include_bytes!("../../../resources/icons/32x32.png");
|
||||
|
||||
pub fn init_blocking() {
|
||||
// load tray icon
|
||||
//let icon_path = format!("{}/../../resources/icons/{}", env!("CARGO_MANIFEST_DIR"), config::TRAY_ICON);
|
||||
//let icon = load_icon(std::path::Path::new(&icon_path));
|
||||
let icon = load_icon_from_bytes(TRAY_ICON_BYTES);
|
||||
|
||||
// form tray menu
|
||||
let tray_menu = Menu::with_items(&[
|
||||
&MenuItem::new("Перезапуск", true, None),
|
||||
&MenuItem::new("Настройки", true, None),
|
||||
&MenuItem::new("Выход", true, None),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let tray_menu = Menu::with_items(&[
|
||||
&MenuItem::with_id("restart", "Перезапуск", true, None),
|
||||
&MenuItem::with_id("settings", "Настройки", true, None),
|
||||
&MenuItem::with_id("exit", "Выход", true, None),
|
||||
]).unwrap();
|
||||
|
||||
let _tray_icon = TrayIconBuilder::new()
|
||||
.with_menu(Box::new(tray_menu))
|
||||
.with_tooltip(config::TRAY_TOOLTIP)
|
||||
.with_icon(icon)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let menu_channel = MenuEvent::receiver();
|
||||
// let tray_channel = TrayIconEvent::receiver();
|
||||
|
||||
// @TODO: Test on Linux
|
||||
// We need gtk for the tray icon to show up, we need to initialize gtk and create the tray_icon
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
gtk::init().unwrap();
|
||||
glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
|
||||
if let Ok(event) = menu_channel.try_recv() {
|
||||
handle_menu_event(&event);
|
||||
}
|
||||
glib::ControlFlow::Continue
|
||||
});
|
||||
gtk::main();
|
||||
}
|
||||
|
||||
// @TODO: Test on MacOS
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// macOS needs proper run loop - tao or winit on main thread
|
||||
use winit::event_loop::{EventLoop, ControlFlow};
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
event_loop.run(move |_event, elwt| {
|
||||
elwt.set_control_flow(ControlFlow::Wait);
|
||||
if let Ok(event) = menu_channel.try_recv() {
|
||||
handle_menu_event(&event);
|
||||
}
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// simple polling works on Windows
|
||||
loop {
|
||||
if let Ok(event) = menu_channel.try_recv() {
|
||||
handle_menu_event(&event);
|
||||
}
|
||||
|
||||
// pump Windows messages
|
||||
unsafe {
|
||||
let mut msg: winapi::um::winuser::MSG = std::mem::zeroed();
|
||||
while winapi::um::winuser::PeekMessageW(
|
||||
&mut msg,
|
||||
std::ptr::null_mut(),
|
||||
0, 0,
|
||||
winapi::um::winuser::PM_REMOVE
|
||||
) != 0 {
|
||||
winapi::um::winuser::TranslateMessage(&msg);
|
||||
winapi::um::winuser::DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
|
||||
info!("Tray initialized.");
|
||||
}
|
||||
|
||||
fn handle_menu_event(event: &MenuEvent) {
|
||||
match event.id.0.as_str() {
|
||||
"exit" => std::process::exit(0),
|
||||
"restart" => { /* restart logic */ }
|
||||
"settings" => { /* open settings */ }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_icon_from_bytes(bytes: &[u8]) -> tray_icon::Icon {
|
||||
let image = image::load_from_memory(bytes)
|
||||
.expect("Failed to load icon")
|
||||
.into_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
let rgba = image.into_raw();
|
||||
tray_icon::Icon::from_rgba(rgba, width, height).expect("Failed to create icon")
|
||||
}
|
||||
|
||||
fn load_icon(path: &std::path::Path) -> tray_icon::Icon {
|
||||
let (icon_rgba, icon_width, icon_height) = {
|
||||
let image = image::open(path)
|
||||
.expect("Failed to open icon path")
|
||||
.into_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
let rgba = image.into_raw();
|
||||
(rgba, width, height)
|
||||
};
|
||||
tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
|
||||
}
|
||||
15
crates/jarvis-app/src/tray/menu.rs
Normal file
15
crates/jarvis-app/src/tray/menu.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
pub enum TrayMenuItem {
|
||||
Restart,
|
||||
Settings,
|
||||
Exit,
|
||||
}
|
||||
|
||||
impl TrayMenuItem {
|
||||
pub fn label(&self) -> &str {
|
||||
match *self {
|
||||
TrayMenuItem::Restart => "Перезапустить",
|
||||
TrayMenuItem::Settings => "Настройки",
|
||||
TrayMenuItem::Exit => "Выход",
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue