diff --git a/Cargo.lock b/Cargo.lock index f87edc2..3471036 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,7 +261,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" dependencies = [ "atk-sys", - "glib", + "glib 0.18.5", "libc", ] @@ -271,10 +271,10 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -495,7 +495,7 @@ checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ "bitflags 2.10.0", "cairo-sys-rs", - "glib", + "glib 0.18.5", "libc", "once_cell", "thiserror 1.0.69", @@ -507,9 +507,9 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" dependencies = [ - "glib-sys", + "glib-sys 0.18.1", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -653,7 +653,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon", + "target-lexicon 0.12.16", +] + +[[package]] +name = "cfg-expr" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21be0e1ce6cdb2ee7fff840f922fb04ead349e5cfb1e750b769132d44ce04720" +dependencies = [ + "smallvec", + "target-lexicon 0.13.3", ] [[package]] @@ -675,8 +685,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -1017,6 +1029,19 @@ dependencies = [ "syn 2.0.113", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dasp_sample" version = "0.11.0" @@ -1640,7 +1665,7 @@ dependencies = [ "gdk-pixbuf", "gdk-sys", "gio", - "glib", + "glib 0.18.5", "libc", "pango", ] @@ -1653,7 +1678,7 @@ checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" dependencies = [ "gdk-pixbuf-sys", "gio", - "glib", + "glib 0.18.5", "libc", "once_cell", ] @@ -1664,11 +1689,11 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1679,13 +1704,13 @@ checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "pango-sys", "pkg-config", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1695,11 +1720,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" dependencies = [ "gdk-sys", - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "pkg-config", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1711,7 +1736,7 @@ dependencies = [ "gdk", "gdkx11-sys", "gio", - "glib", + "glib 0.18.5", "libc", "x11", ] @@ -1723,9 +1748,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" dependencies = [ "gdk-sys", - "glib-sys", + "glib-sys 0.18.1", "libc", - "system-deps", + "system-deps 6.2.2", "x11", ] @@ -2040,8 +2065,8 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "gio-sys", - "glib", + "gio-sys 0.18.1", + "glib 0.18.5", "libc", "once_cell", "pin-project-lite", @@ -2055,13 +2080,26 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", "winapi", ] +[[package]] +name = "gio-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" +dependencies = [ + "glib-sys 0.21.5", + "gobject-sys 0.21.5", + "libc", + "system-deps 7.0.7", + "windows-sys 0.61.2", +] + [[package]] name = "glam" version = "0.30.9" @@ -2083,10 +2121,10 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-macros 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "memchr", "once_cell", @@ -2094,6 +2132,27 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "glib" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.21.5", + "glib-macros 0.21.5", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", + "libc", + "memchr", + "smallvec", +] + [[package]] name = "glib-macros" version = "0.18.5" @@ -2108,6 +2167,19 @@ dependencies = [ "syn 2.0.113", ] +[[package]] +name = "glib-macros" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.113", +] + [[package]] name = "glib-sys" version = "0.18.1" @@ -2115,7 +2187,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" dependencies = [ "libc", - "system-deps", + "system-deps 6.2.2", +] + +[[package]] +name = "glib-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" +dependencies = [ + "libc", + "system-deps 7.0.7", ] [[package]] @@ -2130,9 +2212,20 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ - "glib-sys", + "glib-sys 0.18.1", "libc", - "system-deps", + "system-deps 6.2.2", +] + +[[package]] +name = "gobject-sys" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" +dependencies = [ + "glib-sys 0.21.5", + "libc", + "system-deps 7.0.7", ] [[package]] @@ -2148,7 +2241,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gio", - "glib", + "glib 0.18.5", "gtk-sys", "gtk3-macros", "libc", @@ -2166,12 +2259,12 @@ dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "pango-sys", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -2208,6 +2301,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.16.1" @@ -2560,6 +2659,25 @@ dependencies = [ "cfb", ] +[[package]] +name = "intent-classifier" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d4532decd2fe3806d9e068b57ceee827c7f1ab7b4ce4b2ed4c8c80679b7eefa" +dependencies = [ + "ahash", + "chrono", + "dashmap", + "regex", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tracing", + "unicode-segmentation", + "uuid", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -2625,15 +2743,17 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" name = "jarvis-app" version = "0.1.0" dependencies = [ - "glib", + "glib 0.21.5", "gtk", "image", "jarvis-core", "log", "once_cell", + "parking_lot", "platform-dirs", "rand 0.8.5", "simple-log", + "tokio", "tray-icon", "winapi", "winit", @@ -2644,6 +2764,7 @@ name = "jarvis-core" version = "0.1.0" dependencies = [ "hound", + "intent-classifier", "kira", "log", "once_cell", @@ -2657,6 +2778,9 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sha2", + "tokio", + "toml 0.9.10+spec-1.1.0", "vosk", ] @@ -2689,7 +2813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" dependencies = [ "bitflags 1.3.2", - "glib", + "glib 0.18.5", "javascriptcore-rs-sys", ] @@ -2699,10 +2823,10 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -2827,7 +2951,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" dependencies = [ - "glib", + "glib 0.18.5", "gtk", "gtk-sys", "libappindicator-sys", @@ -3884,7 +4008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" dependencies = [ "gio", - "glib", + "glib 0.18.5", "libc", "once_cell", "pango-sys", @@ -3896,10 +4020,10 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -4775,8 +4899,8 @@ dependencies = [ "ashpd", "block2 0.6.2", "dispatch2", - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "gtk-sys", "js-sys", "log", @@ -5421,7 +5545,7 @@ checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" dependencies = [ "futures-channel", "gio", - "glib", + "glib 0.18.5", "libc", "soup3-sys", ] @@ -5432,11 +5556,11 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -5728,13 +5852,26 @@ version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ - "cfg-expr", + "cfg-expr 0.15.8", "heck 0.5.0", "pkg-config", "toml 0.8.2", "version-compare", ] +[[package]] +name = "system-deps" +version = "7.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" +dependencies = [ + "cfg-expr 0.20.5", + "heck 0.5.0", + "pkg-config", + "toml 0.9.10+spec-1.1.0", + "version-compare", +] + [[package]] name = "systemstat" version = "0.2.5" @@ -5806,6 +5943,12 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + [[package]] name = "tauri" version = "2.9.5" @@ -6262,13 +6405,26 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", + "tokio-macros", "tracing", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.113", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -6976,10 +7132,10 @@ dependencies = [ "gdk", "gdk-sys", "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "gtk", "gtk-sys", "javascriptcore-rs", @@ -6998,15 +7154,15 @@ dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "gtk-sys", "javascriptcore-rs-sys", "libc", "pkg-config", "soup3-sys", - "system-deps", + "system-deps 6.2.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5ba2266..9063dbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,4 +29,6 @@ pv_recorder = { git = "https://github.com/Priler/pvrecorder" } vosk = "0.3" rustpotter = { git = "https://github.com/Priler/rustpotter" } image = "0.25" -parking_lot = "0.12.5" \ No newline at end of file +parking_lot = "0.12.5" +toml = "0.9.8" +sha2 = "0.10" \ No newline at end of file diff --git a/crates/jarvis-app/Cargo.toml b/crates/jarvis-app/Cargo.toml index 795cce9..fd23a70 100644 --- a/crates/jarvis-app/Cargo.toml +++ b/crates/jarvis-app/Cargo.toml @@ -16,6 +16,9 @@ winit = "0.30" image.workspace = true platform-dirs.workspace = true rand.workspace = true +parking_lot.workspace = true + +tokio = { version = "1", features = ["rt-multi-thread"] } [target.'cfg(windows)'.dependencies.winit] version = "0.30" @@ -26,4 +29,4 @@ winapi = { version = "0.3", features = ["winuser"] } [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18" -glib = "0.18" \ No newline at end of file +glib = "0.21.5" \ No newline at end of file diff --git a/crates/jarvis-app/src/app.rs b/crates/jarvis-app/src/app.rs index 2639a66..0b695e4 100644 --- a/crates/jarvis-app/src/app.rs +++ b/crates/jarvis-app/src/app.rs @@ -1,6 +1,6 @@ use std::time::SystemTime; -use jarvis_core::{audio, commands, config, listener, recorder, stt, COMMANDS_LIST}; +use jarvis_core::{audio, commands, config, listener, recorder, stt, COMMANDS_LIST, intent}; use rand::prelude::*; pub fn start() -> Result<(), ()> { @@ -9,6 +9,7 @@ pub fn start() -> Result<(), ()> { } fn main_loop() -> 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 frame_length: usize = 512; // default for every wake-word engine @@ -34,7 +35,7 @@ fn main_loop() -> Result<(), ()> { // recognize wake-word match listener::data_callback(&frame_buffer) { - Some(keyword_index) => { + Some(_keyword_index) => { // wake-word activated, process further commands // capture current time start = SystemTime::now(); @@ -66,13 +67,18 @@ fn main_loop() -> Result<(), ()> { } 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); + // 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!"); diff --git a/crates/jarvis-app/src/main.rs b/crates/jarvis-app/src/main.rs index 1c94b35..0456757 100644 --- a/crates/jarvis-app/src/main.rs +++ b/crates/jarvis-app/src/main.rs @@ -1,8 +1,9 @@ -use std::path::PathBuf; +use parking_lot::RwLock; +use std::sync::Arc; // include core use jarvis_core::{ - audio, commands, config, db, listener, recorder, stt, + audio, commands, config, db, listener, recorder, stt, intent, APP_CONFIG_DIR, APP_LOG_DIR, COMMANDS_LIST, DB, }; @@ -32,7 +33,8 @@ fn main() -> Result<(), String> { info!("Log directory is: {}", APP_LOG_DIR.get().unwrap().display()); // initialize database (settings) - let _ = DB.set(db::init_settings()); + DB.set(Arc::new(RwLock::new(db::init_settings()))) + .expect("DB already initialized"); // initialize tray // @TODO. macOS currently not supported for tray functionality, @@ -58,7 +60,13 @@ fn main() -> Result<(), String> { // init commands info!("Initializing commands."); - let cmds = commands::parse_commands().unwrap(); + let cmds = match commands::parse_commands() { + Ok(c) => c, + Err(e) => { + warn!("Failed to parse commands: {}. Starting with empty command list.", e); + Vec::new() + } + }; info!("Commands initialized. Count: {}, List: {:?}", cmds.len(), commands::list(&cmds)); COMMANDS_LIST.set(cmds).unwrap(); @@ -73,6 +81,15 @@ fn main() -> Result<(), String> { app::close(1); // cannot continue without wake-word engine } + // init intent-recognition engine + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + rt.block_on(async { + if intent::init(COMMANDS_LIST.get().unwrap()).await.is_err() { + error!("Failed to initialize intent classifier"); + app::close(1); + } + }); + // start the app (in the background thread) std::thread::spawn(|| { let _ = app::start(); diff --git a/crates/jarvis-core/Cargo.toml b/crates/jarvis-core/Cargo.toml index f417925..2eb46a9 100644 --- a/crates/jarvis-core/Cargo.toml +++ b/crates/jarvis-core/Cargo.toml @@ -21,12 +21,16 @@ kira.workspace = true pv_recorder.workspace = true rustpotter.workspace = true parking_lot.workspace = true - +toml.workspace = true +sha2.workspace = true # pv_recorder = { workspace = true, optional = true } vosk = { version = "0.3.1", optional = true } +intent-classifier = { version = "0.1.0", optional = true } # rustpotter = { workspace = true, optional = true } +tokio = { version = "1", features = ["sync"], optional = true } + [features] default = ["jarvis_app"] -jarvis_app = ["vosk"] \ No newline at end of file +jarvis_app = ["vosk", "intent-classifier", "tokio"] \ No newline at end of file diff --git a/crates/jarvis-core/src/commands.rs b/crates/jarvis-core/src/commands.rs index 544f0af..4c573ec 100644 --- a/crates/jarvis-core/src/commands.rs +++ b/crates/jarvis-core/src/commands.rs @@ -12,72 +12,96 @@ use std::process::{Child, Command}; mod structs; pub use structs::*; +use std::collections::HashMap; + use crate::{audio, config}; // @TODO. Allow commands both in yaml and json format. -pub fn parse_commands() -> Result, String> { +pub fn parse_commands() -> Result, String> { // collect commands - let mut commands: Vec = vec![]; + let mut commands: Vec = Vec::new(); - // read commands directories first - if let Ok(cpaths) = fs::read_dir(config::COMMANDS_PATH) { - for cpath in cpaths { - // validate this command, check if required files exists - let _cpath = match cpath { - Ok(entry) => entry.path(), - Err(e) => { - warn!("Failed to read command directory entry: {}", e); - continue; - } - }; - let cc_file = Path::new(&_cpath).join("command.yaml"); + let cmd_dirs = fs::read_dir(config::COMMANDS_PATH) + .map_err(|e| format!("Error reading commands directory: {}", e))?; - if cc_file.exists() { - // try parse config files - let cc_reader = std::fs::File::open(&cc_file).unwrap(); - let cc_yaml: CommandsList; - - // try parse command.yaml - match serde_yaml::from_reader::(cc_reader) { - Ok(parse_result) => { - cc_yaml = parse_result; - } - Err(msg) => { - warn!( - "Can't parse {}, skipping ...\nCommand parse error is: {:?}", - &cc_file.display(), - msg - ); - continue; - } - } - // everything seems to be Ok - commands.push(AssistantCommand { - path: _cpath, - commands: cc_yaml, - }); + for entry in cmd_dirs { + let entry = match entry { + Ok(e) => e, + Err(e) => { + warn!("Failed to read command directory entry: {}", e); + continue; } + }; + + let cmd_path = entry.path(); + let toml_file = cmd_path.join("command.toml"); + + if !toml_file.exists() { + continue; } + + // read and parse TOML + let content = match fs::read_to_string(&toml_file) { + Ok(c) => c, + Err(e) => { + warn!("Failed to read {}: {}", toml_file.display(), e); + continue; + } + }; - if !commands.is_empty() { - Ok(commands) - } else { - error!("No commands were found"); - Err("No commands were found".into()) - } + let file: JCommandsList = match toml::from_str(&content) { + Ok(f) => f, + Err(e) => { + warn!("Failed to parse {}: {}", toml_file.display(), e); + continue; + } + }; + + commands.push(JCommandsList { + path: cmd_path, + commands: file.commands, + }); + } + + if commands.is_empty() { + Err("No commands found".into()) } else { - error!("Error reading commands directory"); - return Err("Error reading commands directory".into()); + info!("Loaded {} commands", commands.len()); + Ok(commands) } } + +// Commands hash generation for cache invalidation (deterministi c) +pub fn commands_hash(commands: &Vec) -> String { + use sha2::{Sha256, Digest}; + + let mut hasher = Sha256::new(); + + // collect all command ids and phrases, sorted + let mut all_ids: Vec<_> = commands.iter() + .flat_map(|ac| ac.commands.iter().map(|c| (&c.id, &c.phrases))) + .collect(); + all_ids.sort_by_key(|(id, _)| *id); + + for (id, phrases) in all_ids { + hasher.update(id.as_bytes()); + for phrase in phrases { + hasher.update(phrase.as_bytes()); + } + } + + format!("{:x}", hasher.finalize()) +} + + // @TODO. NLU or smthng else is required, in order to infer commands with highest accuracy possible. pub fn fetch_command<'a>( phrase: &str, - commands: &'a Vec, -) -> Option<(&'a PathBuf, &'a Config)> { + commands: &'a Vec, +) -> Option<(&'a PathBuf, &'a JCommand)> { // result scmd - let mut result_scmd: Option<(&PathBuf, &Config)> = None; + let mut result_scmd: Option<(&PathBuf, &JCommand)> = None; let mut current_max_ratio = config::CMD_RATIO_THRESHOLD; // convert fetch phrase to sequence @@ -86,7 +110,7 @@ pub fn fetch_command<'a>( // list all the commands for cmd in commands { // list all subcommands - for scmd in &cmd.commands.list { + for scmd in &cmd.commands { // list all phrases in command for cmd_phrase in &scmd.phrases { // convert cmd phrase to sequence @@ -135,18 +159,17 @@ pub fn execute_cli(cmd: &str, args: &Vec) -> std::io::Result { pub fn execute_command( cmd_path: &PathBuf, - cmd_config: &Config, + cmd_config: &JCommand, // app_handle: &tauri::AppHandle, ) -> Result { let sounds_directory = audio::get_sound_directory().unwrap(); - match cmd_config.command.action.as_str() { + match cmd_config.action.as_str() { "voice" => { // VOICE command type let random_cmd_sound = format!( "{}.wav", cmd_config - .voice .sounds .choose(&mut rand::thread_rng()) .unwrap() @@ -158,8 +181,8 @@ pub fn execute_command( } "ahk" => { // AutoHotkey command type - let exe_path_absolute = Path::new(&cmd_config.command.exe_path); - let exe_path_local = Path::new(&cmd_path).join(&cmd_config.command.exe_path); + let exe_path_absolute = Path::new(&cmd_config.exe_path); + let exe_path_local = Path::new(&cmd_path).join(&cmd_config.exe_path); if let Ok(_) = execute_exe( if exe_path_absolute.exists() { @@ -167,12 +190,11 @@ pub fn execute_command( } else { exe_path_local.to_str().unwrap() }, - &cmd_config.command.exe_args, + &cmd_config.exe_args, ) { let random_cmd_sound = format!( "{}.wav", cmd_config - .voice .sounds .choose(&mut rand::thread_rng()) .unwrap() @@ -188,14 +210,13 @@ pub fn execute_command( } "cli" => { // CLI command type - let cli_cmd = &cmd_config.command.cli_cmd; + let cli_cmd = &cmd_config.cli_cmd; - match execute_cli(cli_cmd, &cmd_config.command.cli_args) { + match execute_cli(cli_cmd, &cmd_config.cli_args) { Ok(_) => { let random_cmd_sound = format!( "{}.wav", cmd_config - .voice .sounds .choose(&mut rand::thread_rng()) .unwrap() @@ -216,7 +237,6 @@ pub fn execute_command( let random_cmd_sound = format!( "{}.wav", cmd_config - .voice .sounds .choose(&mut rand::thread_rng()) .unwrap() @@ -232,7 +252,6 @@ pub fn execute_command( let random_cmd_sound = format!( "{}.wav", cmd_config - .voice .sounds .choose(&mut rand::thread_rng()) .unwrap() @@ -249,7 +268,7 @@ pub fn execute_command( } } -pub fn list(from: &[AssistantCommand]) -> Vec { +pub fn list(from: &Vec) -> Vec { let mut out: Vec = vec![]; for x in from.iter() { diff --git a/crates/jarvis-core/src/commands/structs.rs b/crates/jarvis-core/src/commands/structs.rs index fad3b17..f2bda6b 100644 --- a/crates/jarvis-core/src/commands/structs.rs +++ b/crates/jarvis-core/src/commands/structs.rs @@ -1,45 +1,38 @@ -use serde::Deserialize; use std::path::PathBuf; +use serde::Deserialize; -#[derive(Debug)] -pub struct AssistantCommand { +#[derive(Deserialize, Debug)] +pub struct JCommandsList { + #[serde(skip)] pub path: PathBuf, - pub commands: CommandsList, + + pub commands: Vec, } -#[derive(Deserialize, Debug)] -pub struct CommandsList { - pub list: Vec, -} -#[derive(Deserialize, Debug)] -pub struct Config { - pub command: ConfigCommandSection, - pub voice: ConfigVoiceSection, - - pub phrases: Vec, -} - -#[derive(Deserialize, Debug)] -pub struct ConfigCommandSection { +#[derive(Deserialize, Debug, Clone)] +pub struct JCommand { + pub id: String, pub action: String, - + + #[serde(default)] + pub description: String, + #[serde(default)] pub exe_path: String, - + #[serde(default)] pub exe_args: Vec, - + #[serde(default)] pub cli_cmd: String, - + #[serde(default)] pub cli_args: Vec, -} - -#[derive(Deserialize, Debug)] -pub struct ConfigVoiceSection { + #[serde(default)] pub sounds: Vec, + + pub phrases: Vec, } diff --git a/crates/jarvis-core/src/config.rs b/crates/jarvis-core/src/config.rs index 9710b20..c39c5c2 100644 --- a/crates/jarvis-core/src/config.rs +++ b/crates/jarvis-core/src/config.rs @@ -17,6 +17,7 @@ use rustpotter::{ RustpotterConfig, ScoreMode, }; +use crate::IntentRecognitionEngine; use crate::{APP_CONFIG_DIR, APP_DIRS, APP_LOG_DIR}; #[allow(dead_code)] @@ -64,6 +65,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_INTENT_RECOGNITION_ENGINE: IntentRecognitionEngine = IntentRecognitionEngine::IntentClassifier; pub const DEFAULT_SPEECH_TO_TEXT_ENGINE: SpeechToTextEngine = SpeechToTextEngine::Vosk; pub const DEFAULT_VOICE: &str = "jarvis-og"; @@ -130,6 +132,9 @@ 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; + // ETC pub const CMD_RATIO_THRESHOLD: f64 = 65f64; pub const CMS_WAIT_DELAY: std::time::Duration = std::time::Duration::from_secs(15); diff --git a/crates/jarvis-core/src/config/structs.rs b/crates/jarvis-core/src/config/structs.rs index 67b0d49..e83a032 100644 --- a/crates/jarvis-core/src/config/structs.rs +++ b/crates/jarvis-core/src/config/structs.rs @@ -8,6 +8,12 @@ pub enum WakeWordEngine { Porcupine, } +#[derive(Clone, Copy, Serialize, Deserialize, Debug)] +pub enum IntentRecognitionEngine { + IntentClassifier, + Rasa, +} + impl fmt::Display for WakeWordEngine { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) diff --git a/crates/jarvis-core/src/db/structs.rs b/crates/jarvis-core/src/db/structs.rs index 8ca77b0..8a5f47c 100644 --- a/crates/jarvis-core/src/db/structs.rs +++ b/crates/jarvis-core/src/db/structs.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::config::structs::SpeechToTextEngine; use crate::config::structs::WakeWordEngine; +use crate::config::structs::IntentRecognitionEngine; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Settings { @@ -10,6 +11,7 @@ pub struct Settings { pub voice: String, pub wake_word_engine: WakeWordEngine, + pub intent_recognition_engine: IntentRecognitionEngine, pub speech_to_text_engine: SpeechToTextEngine, pub api_keys: ApiKeys, @@ -22,6 +24,7 @@ impl Default for Settings { voice: String::from(""), wake_word_engine: config::DEFAULT_WAKE_WORD_ENGINE, + intent_recognition_engine: config::DEFAULT_INTENT_RECOGNITION_ENGINE, speech_to_text_engine: config::DEFAULT_SPEECH_TO_TEXT_ENGINE, api_keys: ApiKeys { diff --git a/crates/jarvis-core/src/intent.rs b/crates/jarvis-core/src/intent.rs new file mode 100644 index 0000000..79f60d5 --- /dev/null +++ b/crates/jarvis-core/src/intent.rs @@ -0,0 +1,62 @@ +mod intentclassifier; + +use std::path::PathBuf; + +use crate::{JCommandsList, commands::JCommand, config}; +use once_cell::sync::OnceCell; +use crate::config::structs::IntentRecognitionEngine; + +static IRE_TYPE: OnceCell = OnceCell::new(); + +pub async fn init(commands: &Vec) -> Result<(), String> { + if IRE_TYPE.get().is_some() { + return Ok(()); + } // already initialized + + // set default ire type + // @TODO. Make it configurable? + IRE_TYPE.set(config::DEFAULT_INTENT_RECOGNITION_ENGINE).unwrap(); + + // load given recorder + match IRE_TYPE.get().unwrap() { + IntentRecognitionEngine::IntentClassifier => { + info!("Initializing IRE backend."); + intentclassifier::init(&commands).await?; + info!("IRE backend initialized."); + }, + IntentRecognitionEngine::Rasa => todo!(), + } + + Ok(()) +} + +pub async fn classify(text: &str) -> Option<(String, f64)> { + match IRE_TYPE.get()? { + IntentRecognitionEngine::IntentClassifier => { + match intentclassifier::classify(text).await { + Ok(prediction) => { + let confidence = prediction.confidence.value(); + if confidence >= config::INTENT_CLASSIFIER_MIN_CONFIDENCE { + Some((prediction.intent.to_string(), confidence)) + } else { + None + } + } + Err(e) => { + error!("Intent classification error: {}", e); + None + } + } + } + IntentRecognitionEngine::Rasa => todo!(), + } +} + +pub fn get_command_by_intent(commands: &'static Vec, intent_id: &str) -> Option<(&'static PathBuf, &'static JCommand)> { + match IRE_TYPE.get()? { + IntentRecognitionEngine::IntentClassifier => { + intentclassifier::get_command(commands, intent_id) + } + IntentRecognitionEngine::Rasa => todo!(), + } +} \ No newline at end of file diff --git a/crates/jarvis-core/src/intent/intentclassifier.rs b/crates/jarvis-core/src/intent/intentclassifier.rs new file mode 100644 index 0000000..36c376e --- /dev/null +++ b/crates/jarvis-core/src/intent/intentclassifier.rs @@ -0,0 +1,107 @@ +use intent_classifier::{ + IntentClassifier, IntentPrediction, IntentError, + TrainingExample, TrainingSource, IntentId +}; + +use tokio::sync::OnceCell; +use std::path::PathBuf; +use std::fs; + +use crate::commands::{self, JCommand, JCommandsList}; +use crate::{APP_CONFIG_DIR}; + +static CLASSIFIER: OnceCell = OnceCell::const_new(); +// static COMMANDS_MAP: OnceCell> = OnceCell::const_new(); + +const TRAINING_CACHE_FILE: &str = "intent_training.json"; +const COMMANDS_HASH_FILE: &str = "commands_hash.txt"; + +pub async fn init(commands: &Vec) -> Result<(), String> { + // parse commands first + // let commands = commands::parse_commands()?; + let current_hash = commands::commands_hash(&commands); // regen hash for current commands set + + // init classifier + let classifier = IntentClassifier::new().await + .map_err(|e| format!("Failed to init IntentClassifier: {}", e))?; + + // check if we can use cached training data + let config_dir = APP_CONFIG_DIR.get().ok_or("Config dir not set")?; + let hash_path = config_dir.join(COMMANDS_HASH_FILE); + let cache_path = config_dir.join(TRAINING_CACHE_FILE); + + let should_retrain = if hash_path.exists() && cache_path.exists() { + let stored_hash = fs::read_to_string(&hash_path).unwrap_or_default(); + stored_hash.trim() != current_hash + } else { + true + }; + + if should_retrain { + info!("Training intent classifier with {} commands...", commands.len()); + train_classifier(&classifier, &commands).await?; + + // save training data and hash + if let Ok(export) = classifier.export_training_data().await { + let _ = fs::write(&cache_path, export); + let _ = fs::write(&hash_path, ¤t_hash); + info!("Training data cached."); + } + } else { + info!("Loading cached training data..."); + if let Ok(data) = fs::read_to_string(&cache_path) { + classifier.import_training_data(&data).await + .map_err(|e| format!("Failed to import training data: {}", e))?; + } + } + + // store data + CLASSIFIER.set(classifier).map_err(|_| "Classifier already set")?; + // COMMANDS_MAP.set(commands).map_err(|_| "Commands map already set")?; + + Ok(()) +} + +pub async fn classify(text: &str) -> Result { + let classifier = CLASSIFIER.get().expect("IntentClassifier not initialized"); + classifier.predict_intent(text).await +} + +// get command by intent ID +pub fn get_command(commands: &'static Vec, intent_id: &str) -> Option<(&'static PathBuf, &'static JCommand)> { + // let commands = COMMANDS_MAP.get()?; + + for assistant_cmd in commands { + for cmd in &assistant_cmd.commands { + if cmd.id == intent_id { + return Some((&assistant_cmd.path, cmd)); + } + } + } + + None +} + +// based on: https://github.com/ciresnave/intent-classifier/blob/main/examples/basic_usage.rs +async fn train_classifier( + classifier: &IntentClassifier, + commands: &Vec +) -> Result<(), String> { + for assistant_cmd in commands { + for cmd in &assistant_cmd.commands { + for phrase in &cmd.phrases { + let example = TrainingExample { + text: phrase.clone(), + intent: IntentId::from(cmd.id.as_str()), + confidence: 1.0, + source: TrainingSource::Programmatic, + }; + + classifier.add_training_example(example).await + .map_err(|e| format!("Failed to add training example: {}", e))?; + } + } + } + + Ok(()) +} \ No newline at end of file diff --git a/crates/jarvis-core/src/lib.rs b/crates/jarvis-core/src/lib.rs index d588711..4b315e2 100644 --- a/crates/jarvis-core/src/lib.rs +++ b/crates/jarvis-core/src/lib.rs @@ -1,6 +1,6 @@ use once_cell::sync::{Lazy, OnceCell}; use parking_lot::RwLock; -use std::sync::Arc; +use std::{sync::Arc}; use platform_dirs::AppDirs; use std::path::PathBuf; @@ -20,6 +20,9 @@ pub mod recorder; #[cfg(feature = "jarvis_app")] pub mod stt; +#[cfg(feature = "jarvis_app")] +pub mod intent; + // shared statics pub static APP_DIR: Lazy = Lazy::new(|| std::env::current_dir().unwrap()); pub static SOUND_DIR: Lazy = Lazy::new(|| APP_DIR.clone().join("sound")); @@ -27,9 +30,11 @@ pub static APP_DIRS: OnceCell = OnceCell::new(); pub static APP_CONFIG_DIR: OnceCell = OnceCell::new(); pub static APP_LOG_DIR: OnceCell = OnceCell::new(); pub static DB: OnceCell>> = OnceCell::new(); -pub static COMMANDS_LIST: OnceCell> = OnceCell::new(); +pub static COMMANDS_LIST: OnceCell> = OnceCell::new(); // re-exports -pub use commands::AssistantCommand; +pub use commands::JCommandsList; pub use config::structs::*; -pub use db::structs::Settings; \ No newline at end of file +pub use db::structs::Settings; + +// use crate::commands::{JComandsList, JCommand}; \ No newline at end of file diff --git a/crates/jarvis-core/src/listener.rs b/crates/jarvis-core/src/listener.rs index c9aea47..516a0a1 100644 --- a/crates/jarvis-core/src/listener.rs +++ b/crates/jarvis-core/src/listener.rs @@ -25,7 +25,7 @@ pub fn init() -> Result<(), ()> { // store current engine WAKE_WORD_ENGINE - .set(DB.get().unwrap().wake_word_engine) + .set(DB.get().unwrap().read().wake_word_engine) .unwrap(); // load given wake-word engine diff --git a/crates/jarvis-gui/src/tauri_commands/db.rs b/crates/jarvis-gui/src/tauri_commands/db.rs index ce63d5c..a3d2c2b 100644 --- a/crates/jarvis-gui/src/tauri_commands/db.rs +++ b/crates/jarvis-gui/src/tauri_commands/db.rs @@ -9,6 +9,7 @@ pub fn db_read(state: tauri::State<'_, AppState>, key: &str) -> String { "selected_microphone" => settings.microphone.to_string(), "assistant_voice" => settings.voice.clone(), "selected_wake_word_engine" => format!("{:?}", settings.wake_word_engine), + "selected_intent_recognition_engine" => format!("{:?}", settings.intent_recognition_engine), "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(), @@ -41,6 +42,13 @@ pub fn db_write(state: tauri::State<'_, AppState>, key: &str, val: &str) -> bool _ => return false, } } + "selected_intent_recognition_engine" => { + match val.to_lowercase().as_str() { + "intentclassifier" => settings.intent_recognition_engine = jarvis_core::config::structs::IntentRecognitionEngine::IntentClassifier, + "rasa" => settings.intent_recognition_engine = jarvis_core::config::structs::IntentRecognitionEngine::Rasa, + _ => return false, + } + } "api_key__picovoice" => { settings.api_keys.picovoice = val.to_string(); } diff --git a/frontend/src/routes/settings/index.svelte b/frontend/src/routes/settings/index.svelte index 2b81115..0240fae 100644 --- a/frontend/src/routes/settings/index.svelte +++ b/frontend/src/routes/settings/index.svelte @@ -46,6 +46,7 @@ let voiceVal = "" let selectedMicrophone = "" let selectedWakeWordEngine = "" + let selectedIntentRecognitionEngine = "" let apiKeyPicovoice = "" let apiKeyOpenai = "" @@ -71,6 +72,7 @@ invoke("db_write", { key: "assistant_voice", val: voiceVal }), invoke("db_write", { key: "selected_microphone", val: selectedMicrophone }), invoke("db_write", { key: "selected_wake_word_engine", val: selectedWakeWordEngine }), + invoke("db_write", { key: "selected_intent_recognition_engine", val: selectedIntentRecognitionEngine }), invoke("db_write", { key: "api_key__picovoice", val: apiKeyPicovoice }), invoke("db_write", { key: "api_key__openai", val: apiKeyOpenai }) ]) @@ -106,15 +108,17 @@ })) // load settings from db - const [mic, wakeWord, pico, openai] = await Promise.all([ + const [mic, wakeWord, intentReco, pico, openai] = await Promise.all([ invoke("db_read", { key: "selected_microphone" }), invoke("db_read", { key: "selected_wake_word_engine" }), + invoke("db_read", { key: "selected_intent_recognition_engine" }), invoke("db_read", { key: "api_key__picovoice" }), invoke("db_read", { key: "api_key__openai" }) ]) selectedMicrophone = mic selectedWakeWordEngine = wakeWord + selectedIntentRecognitionEngine = intentReco apiKeyPicovoice = pico apiKeyOpenai = openai } catch (err) { @@ -228,6 +232,18 @@ {/if} + + + diff --git a/jarvis-app.zip b/jarvis-app.zip new file mode 100644 index 0000000..2feacd9 Binary files /dev/null and b/jarvis-app.zip differ diff --git a/resources/commands/browser/command.toml b/resources/commands/browser/command.toml new file mode 100644 index 0000000..2479663 --- /dev/null +++ b/resources/commands/browser/command.toml @@ -0,0 +1,34 @@ +[[commands]] +id = "browser_open" +action = "ahk" +exe_path = "ahk/Run browser.exe" +sounds = ["ok1", "ok2", "ok3"] +phrases = [ + "открой браузер", + "открой хром", + "гугл хром", +] + +[[commands]] +id = "browser_close" +action = "ahk" +exe_path = "ahk/Close browser.exe" +sounds = ["ok1", "ok2", "ok3", "ok4"] +phrases = [ + "закрой все браузеры", + "закрой браузер", + "выключи браузер", + "убери браузер", +] + +[[commands]] +id = "open_google" +action = "ahk" +exe_path = "ahk/Run website.exe" +exe_args = ["http://google.com"] +sounds = ["ok1", "ok2", "ok3", "ok4"] +phrases = [ + "открой гугл", + "запусти гугл", + "перейди в гугл", +] \ No newline at end of file diff --git a/resources/commands/browser/command.yaml b/resources/commands/browser/command.yaml deleted file mode 100644 index 57f25ec..0000000 --- a/resources/commands/browser/command.yaml +++ /dev/null @@ -1,76 +0,0 @@ -list: -- command: - action: ahk - exe_path: ahk/Run browser.exe - voice: - sounds: - - ok1 - - ok2 - - ok3 - phrases: - - открой браузер - - открой хром - - гугл хром - -- command: - action: ahk - exe_path: ahk/Close browser.exe - voice: - sounds: - - ok1 - - ok2 - - ok3 - - ok4 - phrases: - - закрой все браузеры - - закрой браузер - - выключи браузер - - убери браузер - -- command: - action: ahk - exe_path: ahk/Run website.exe - exe_args: - - http://google.com - voice: - sounds: - - ok1 - - ok2 - - ok3 - - ok4 - phrases: - - открой гугл - - запусти гугл - - перейди в гугл - -- command: - action: ahk - exe_path: ahk/Run website.exe - exe_args: - - http://youtube.com - voice: - sounds: - - ok1 - - ok2 - - ok3 - - ok4 - phrases: - - открой ютуб - - ютуб - - запусти ютуб - -- command: - action: ahk - exe_path: ahk/Run website.exe - exe_args: - - https://translate.google.com - voice: - sounds: - - ok1 - - ok2 - - ok3 - - ok4 - phrases: - - открой переводчик - - переводчик - - запусти переводчик \ No newline at end of file