From 47b7e7a65d6fa7eaeeb6e3d42dc9bb0b53779bc3 Mon Sep 17 00:00:00 2001 From: Priler Date: Wed, 7 Jan 2026 23:29:46 +0500 Subject: [PATCH] Tray options implementation + Voice system rewrite + build fixes --- Cargo.lock | 1 + Cargo.toml | 3 +- crates/jarvis-app/src/app.rs | 185 +++-- crates/jarvis-app/src/main.rs | 25 +- crates/jarvis-app/src/tray.rs | 84 ++- crates/jarvis-core/Cargo.toml | 1 + crates/jarvis-core/src/audio.rs | 23 +- crates/jarvis-core/src/commands.rs | 22 +- crates/jarvis-core/src/commands/structs.rs | 6 +- crates/jarvis-core/src/config.rs | 6 +- crates/jarvis-core/src/i18n/locales/en.ftl | 9 +- crates/jarvis-core/src/i18n/locales/ru.ftl | 9 +- crates/jarvis-core/src/i18n/locales/ua.ftl | 10 +- crates/jarvis-core/src/ipc.rs | 2 +- crates/jarvis-core/src/ipc/events.rs | 6 + crates/jarvis-core/src/ipc/server.rs | 8 + crates/jarvis-core/src/lib.rs | 6 +- crates/jarvis-core/src/time.rs | 2 + crates/jarvis-core/src/time/structs.rs | 21 + crates/jarvis-core/src/voices.rs | 234 +++++++ crates/jarvis-core/src/voices/structs.rs | 70 ++ crates/jarvis-gui/capabilities/default.json | 8 +- .../jarvis-gui/gen/schemas/capabilities.json | 2 +- crates/jarvis-gui/src/main.rs | 23 +- crates/jarvis-gui/src/tauri_commands.rs | 10 +- .../jarvis-gui/src/tauri_commands/commands.rs | 22 + .../jarvis-gui/src/tauri_commands/voices.rs | 16 + crates/jarvis-gui/tauri.conf.json | 12 +- frontend/_src/App.svelte | 19 - frontend/_src/Events.svelte | 44 -- frontend/_src/components/Footer.svelte | 73 -- frontend/_src/components/Header.svelte | 28 - frontend/_src/components/Nav.svelte | 5 - .../components/elements/ArcReactor.svelte | 647 ------------------ .../_src/components/elements/HDivider.svelte | 18 - .../_src/components/elements/SearchBar.svelte | 11 - .../_src/components/elements/Stats.svelte | 377 ---------- frontend/_src/css/main.scss | 308 --------- frontend/_src/css/styles.scss | 92 --- frontend/_src/functions.ts | 61 -- frontend/_src/main.ts | 13 - frontend/_src/routes/_module.svelte | 12 - frontend/_src/routes/commands/index.svelte | 23 - frontend/_src/routes/index.svelte | 71 -- frontend/_src/routes/settings/index.svelte | 188 ----- frontend/_src/stores.ts | 34 - frontend/_src/vite-env.d.ts | 2 - frontend/package-lock.json | 241 ++++--- frontend/package.json | 2 +- frontend/src/components/Footer.svelte | 4 +- .../src/components/elements/ArcReactor.svelte | 20 +- .../src/components/elements/SearchBar.svelte | 77 ++- frontend/src/components/elements/Stats.svelte | 6 +- frontend/src/lib/ipc.ts | 107 +-- frontend/src/routes/index.svelte | 6 +- frontend/src/routes/settings/index.svelte | 192 +++++- frontend/src/stores.ts | 2 + ....timestamp-1767798766136-6d509bc794b39.mjs | 40 ++ .../jarvis-howdy/ru}/game_mode.wav | Bin .../jarvis-howdy/ru}/joke1.wav | Bin .../jarvis-howdy/ru}/joke2.wav | Bin .../jarvis-howdy/ru}/joke3.wav | Bin .../jarvis-howdy/ru}/joke4.wav | Bin .../jarvis-howdy/ru}/joke5.wav | Bin .../jarvis-howdy/ru}/not_found.wav | Bin .../jarvis-howdy/ru}/ok1.wav | Bin .../jarvis-howdy/ru}/ok2.wav | Bin .../jarvis-howdy/ru}/ok3.wav | Bin .../jarvis-howdy/ru}/ok4.wav | Bin .../jarvis-howdy/ru}/ready.wav | Bin .../jarvis-howdy/ru/reply1.wav} | Bin .../jarvis-howdy/ru/reply2.wav} | Bin .../jarvis-howdy/ru/reply3.wav} | Bin .../jarvis-howdy/ru}/run.wav | Bin .../jarvis-howdy/ru}/stupid.wav | Bin .../jarvis-howdy/ru}/thanks.wav | Bin .../sound/voices/jarvis-howdy/voice.toml | 27 + .../jarvis-og/ru}/game_mode.wav | Bin .../jarvis-og/ru}/not_found.wav | Bin .../jarvis-og/ru}/off.wav | Bin .../jarvis-og/ru}/ok1.wav | Bin .../jarvis-og/ru}/ok2.wav | Bin .../jarvis-og/ru}/ok3.wav | Bin .../jarvis-og/ru}/ok4.wav | Bin .../jarvis-og/ru/reply1.wav} | Bin .../jarvis-og/ru/reply2.wav} | Bin .../jarvis-og/ru/reply3.wav} | Bin .../jarvis-og/ru}/run.wav | Bin .../jarvis-og/ru}/stupid.wav | Bin .../jarvis-og/ru}/thanks.wav | Bin resources/sound/voices/jarvis-og/voice.toml | 27 + .../jarvis-remaster/ru}/greet1.mp3 | Bin .../jarvis-remaster/ru}/greet_day.mp3 | Bin .../jarvis-remaster/ru}/greet_evening.mp3 | Bin .../jarvis-remaster/ru}/greet_morning.mp3 | Bin .../jarvis-remaster/ru}/greet_night.mp3 | Bin .../jarvis-remaster/ru}/joke1.mp3 | Bin .../jarvis-remaster/ru}/joke2.mp3 | Bin .../jarvis-remaster/ru}/joke3.mp3 | Bin .../jarvis-remaster/ru}/joke4.mp3 | Bin .../jarvis-remaster/ru}/joke5.mp3 | Bin .../jarvis-remaster/ru}/joke6.mp3 | Bin .../jarvis-remaster/ru}/joke7.mp3 | Bin .../jarvis-remaster/ru}/joke8.mp3 | Bin .../jarvis-remaster/ru}/ok1.mp3 | Bin .../jarvis-remaster/ru}/ok2.mp3 | Bin .../jarvis-remaster/ru}/ok3.mp3 | Bin .../jarvis-remaster/ru}/ok4.mp3 | Bin .../jarvis-remaster/ru}/reply1.mp3 | Bin .../jarvis-remaster/ru}/reply2.mp3 | Bin .../jarvis-remaster/ru}/reply3.mp3 | Bin .../jarvis-remaster/ru}/reply4.mp3 | Bin .../jarvis-remaster/ru}/reply5.mp3 | Bin .../jarvis-remaster/ru}/reply6.mp3 | Bin .../jarvis-remaster/ru}/stupid.mp3 | Bin .../jarvis-remaster/ru}/thanks.mp3 | Bin .../sound/voices/jarvis-remaster/voice.toml | 31 + 117 files changed, 1300 insertions(+), 2334 deletions(-) create mode 100644 crates/jarvis-core/src/time.rs create mode 100644 crates/jarvis-core/src/time/structs.rs create mode 100644 crates/jarvis-core/src/voices.rs create mode 100644 crates/jarvis-core/src/voices/structs.rs create mode 100644 crates/jarvis-gui/src/tauri_commands/commands.rs create mode 100644 crates/jarvis-gui/src/tauri_commands/voices.rs delete mode 100644 frontend/_src/App.svelte delete mode 100644 frontend/_src/Events.svelte delete mode 100644 frontend/_src/components/Footer.svelte delete mode 100644 frontend/_src/components/Header.svelte delete mode 100644 frontend/_src/components/Nav.svelte delete mode 100644 frontend/_src/components/elements/ArcReactor.svelte delete mode 100644 frontend/_src/components/elements/HDivider.svelte delete mode 100644 frontend/_src/components/elements/SearchBar.svelte delete mode 100644 frontend/_src/components/elements/Stats.svelte delete mode 100644 frontend/_src/css/main.scss delete mode 100644 frontend/_src/css/styles.scss delete mode 100644 frontend/_src/functions.ts delete mode 100644 frontend/_src/main.ts delete mode 100644 frontend/_src/routes/_module.svelte delete mode 100644 frontend/_src/routes/commands/index.svelte delete mode 100644 frontend/_src/routes/index.svelte delete mode 100644 frontend/_src/routes/settings/index.svelte delete mode 100644 frontend/_src/stores.ts delete mode 100644 frontend/_src/vite-env.d.ts create mode 100644 frontend/vite.config.ts.timestamp-1767798766136-6d509bc794b39.mjs rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/game_mode.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/joke1.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/joke2.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/joke3.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/joke4.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/joke5.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/not_found.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/ok1.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/ok2.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/ok3.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/ok4.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/ready.wav (100%) rename resources/sound/{jarvis-howdy/greet1.wav => voices/jarvis-howdy/ru/reply1.wav} (100%) rename resources/sound/{jarvis-howdy/greet2.wav => voices/jarvis-howdy/ru/reply2.wav} (100%) rename resources/sound/{jarvis-howdy/greet3.wav => voices/jarvis-howdy/ru/reply3.wav} (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/run.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/stupid.wav (100%) rename resources/sound/{jarvis-howdy => voices/jarvis-howdy/ru}/thanks.wav (100%) create mode 100644 resources/sound/voices/jarvis-howdy/voice.toml rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/game_mode.wav (100%) rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/not_found.wav (100%) rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/off.wav (100%) rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/ok1.wav (100%) rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/ok2.wav (100%) rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/ok3.wav (100%) rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/ok4.wav (100%) rename resources/sound/{jarvis-og/greet1.wav => voices/jarvis-og/ru/reply1.wav} (100%) rename resources/sound/{jarvis-og/greet2.wav => voices/jarvis-og/ru/reply2.wav} (100%) rename resources/sound/{jarvis-og/greet3.wav => voices/jarvis-og/ru/reply3.wav} (100%) rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/run.wav (100%) rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/stupid.wav (100%) rename resources/sound/{jarvis-og => voices/jarvis-og/ru}/thanks.wav (100%) create mode 100644 resources/sound/voices/jarvis-og/voice.toml rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/greet1.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/greet_day.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/greet_evening.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/greet_morning.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/greet_night.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/joke1.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/joke2.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/joke3.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/joke4.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/joke5.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/joke6.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/joke7.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/joke8.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/ok1.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/ok2.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/ok3.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/ok4.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/reply1.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/reply2.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/reply3.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/reply4.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/reply5.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/reply6.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/stupid.mp3 (100%) rename resources/sound/{jarvis-remaster => voices/jarvis-remaster/ru}/thanks.mp3 (100%) create mode 100644 resources/sound/voices/jarvis-remaster/voice.toml diff --git a/Cargo.lock b/Cargo.lock index 65ed3c4..1cbd499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3123,6 +3123,7 @@ dependencies = [ name = "jarvis-core" version = "0.1.0" dependencies = [ + "chrono", "fluent", "fluent-bundle", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 4f6eb5c..eaa710e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,4 +39,5 @@ tokio-tungstenite = "0.28.0" futures-util = "0.3" fluent = "0.17.0" fluent-bundle = "0.16.0" -unic-langid = "0.9" \ No newline at end of file +unic-langid = "0.9" +chrono = "0.4" \ No newline at end of file diff --git a/crates/jarvis-app/src/app.rs b/crates/jarvis-app/src/app.rs index 53db821..ea2db38 100644 --- a/crates/jarvis-app/src/app.rs +++ b/crates/jarvis-app/src/app.rs @@ -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) -> Result<(), ()> { // start the loop - main_loop() + main_loop(text_cmd_rx) } -fn main_loop() -> Result<(), ()> { +fn main_loop(text_cmd_rx: Receiver) -> 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 = 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); } diff --git a/crates/jarvis-app/src/main.rs b/crates/jarvis-app/src/main.rs index 30d45c2..3c72bcb 100644 --- a/crates/jarvis-app/src/main.rs +++ b/crates/jarvis-app/src/main.rs @@ -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::(); + + 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(); diff --git a/crates/jarvis-app/src/tray.rs b/crates/jarvis-app/src/tray.rs index 3686477..3f85d61 100644 --- a/crates/jarvis-app/src/tray.rs +++ b/crates/jarvis-app/src/tray.rs @@ -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" +} \ No newline at end of file diff --git a/crates/jarvis-core/Cargo.toml b/crates/jarvis-core/Cargo.toml index fa7469b..ac90755 100644 --- a/crates/jarvis-core/Cargo.toml +++ b/crates/jarvis-core/Cargo.toml @@ -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 } diff --git a/crates/jarvis-core/src/audio.rs b/crates/jarvis-core/src/audio.rs index 3f0a39d..05a6547 100644 --- a/crates/jarvis-core/src/audio.rs +++ b/crates/jarvis-core/src/audio.rs @@ -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 { 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 } } } diff --git a/crates/jarvis-core/src/commands.rs b/crates/jarvis-core/src/commands.rs index c5a230b..9e7cd57 100644 --- a/crates/jarvis-core/src/commands.rs +++ b/crates/jarvis-core/src/commands.rs @@ -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, String> { // collect commands let mut commands: Vec = 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 { - 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) } diff --git a/crates/jarvis-core/src/commands/structs.rs b/crates/jarvis-core/src/commands/structs.rs index f2bda6b..d403d85 100644 --- a/crates/jarvis-core/src/commands/structs.rs +++ b/crates/jarvis-core/src/commands/structs.rs @@ -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, diff --git a/crates/jarvis-core/src/config.rs b/crates/jarvis-core/src/config.rs index 12303d7..b9c8cc4 100644 --- a/crates/jarvis-core/src/config.rs +++ b/crates/jarvis-core/src/config.rs @@ -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. diff --git a/crates/jarvis-core/src/i18n/locales/en.ftl b/crates/jarvis-core/src/i18n/locales/en.ftl index 10f293d..f4adde8 100644 --- a/crates/jarvis-core/src/i18n/locales/en.ftl +++ b/crates/jarvis-core/src/i18n/locales/en.ftl @@ -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 \ No newline at end of file diff --git a/crates/jarvis-core/src/i18n/locales/ru.ftl b/crates/jarvis-core/src/i18n/locales/ru.ftl index a883c66..9ece828 100644 --- a/crates/jarvis-core/src/i18n/locales/ru.ftl +++ b/crates/jarvis-core/src/i18n/locales/ru.ftl @@ -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 = Голоса не найдены \ No newline at end of file diff --git a/crates/jarvis-core/src/i18n/locales/ua.ftl b/crates/jarvis-core/src/i18n/locales/ua.ftl index 3f39b2c..3e3e145 100644 --- a/crates/jarvis-core/src/i18n/locales/ua.ftl +++ b/crates/jarvis-core/src/i18n/locales/ua.ftl @@ -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 = Голоси не знайдено \ No newline at end of file diff --git a/crates/jarvis-core/src/ipc.rs b/crates/jarvis-core/src/ipc.rs index 11cac5c..1768c1d 100644 --- a/crates/jarvis-core/src/ipc.rs +++ b/crates/jarvis-core/src/ipc.rs @@ -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}; \ No newline at end of file +pub use server::{init, send, set_action_handler, start_server, has_clients, IPC_ADDR, IPC_PORT}; \ No newline at end of file diff --git a/crates/jarvis-core/src/ipc/events.rs b/crates/jarvis-core/src/ipc/events.rs index e555b04..e372d40 100644 --- a/crates/jarvis-core/src/ipc/events.rs +++ b/crates/jarvis-core/src/ipc/events.rs @@ -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 }, } \ No newline at end of file diff --git a/crates/jarvis-core/src/ipc/server.rs b/crates/jarvis-core/src/ipc/server.rs index 53d1f7f..6e9072c 100644 --- a/crates/jarvis-core/src/ipc/server.rs +++ b/crates/jarvis-core/src/ipc/server.rs @@ -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 + } } \ No newline at end of file diff --git a/crates/jarvis-core/src/lib.rs b/crates/jarvis-core/src/lib.rs index 00cec74..d9dbebf 100644 --- a/crates/jarvis-core/src/lib.rs +++ b/crates/jarvis-core/src/lib.rs @@ -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 = Lazy::new(|| std::env::current_dir().unwrap()); pub static APP_DIR: Lazy = Lazy::new(|| { @@ -40,7 +44,7 @@ pub static APP_DIR: Lazy = 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 = Lazy::new(|| APP_DIR.clone().join("resources/sound")); +pub static SOUND_DIR: Lazy = Lazy::new(|| APP_DIR.clone().join(config::SOUND_PATH)); pub static APP_DIRS: OnceCell = OnceCell::new(); pub static APP_CONFIG_DIR: OnceCell = OnceCell::new(); pub static APP_LOG_DIR: OnceCell = OnceCell::new(); diff --git a/crates/jarvis-core/src/time.rs b/crates/jarvis-core/src/time.rs new file mode 100644 index 0000000..c392a21 --- /dev/null +++ b/crates/jarvis-core/src/time.rs @@ -0,0 +1,2 @@ +pub mod structs; +pub use structs::*; \ No newline at end of file diff --git a/crates/jarvis-core/src/time/structs.rs b/crates/jarvis-core/src/time/structs.rs new file mode 100644 index 0000000..bfe481d --- /dev/null +++ b/crates/jarvis-core/src/time/structs.rs @@ -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, + } + } +} \ No newline at end of file diff --git a/crates/jarvis-core/src/voices.rs b/crates/jarvis-core/src/voices.rs new file mode 100644 index 0000000..3fe8e85 --- /dev/null +++ b/crates/jarvis-core/src/voices.rs @@ -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> = OnceCell::new(); +static CURRENT_VOICE_ID: OnceCell> = 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::>() + ); + + VOICES.set(voices).map_err(|_| "Voices already initialized")?; + + Ok(()) +} + +pub fn scan_voices() -> Result, 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 { + 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 { + VOICES.get().cloned().unwrap_or_default() +} + +pub fn get_voice(voice_id: &str) -> Option { + VOICES.get()?.iter().find(|v| v.voice.id == voice_id).cloned() +} + +pub fn get_current_voice() -> Option { + 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 { + 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); } \ No newline at end of file diff --git a/crates/jarvis-core/src/voices/structs.rs b/crates/jarvis-core/src/voices/structs.rs new file mode 100644 index 0000000..022d3d1 --- /dev/null +++ b/crates/jarvis-core/src/voices/structs.rs @@ -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, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct VoiceReactions { + // app startup (time-based or generic) + #[serde(default)] + pub greet: Vec, + + #[serde(default)] + pub greet_morning: Vec, + #[serde(default)] + pub greet_day: Vec, + #[serde(default)] + pub greet_evening: Vec, + #[serde(default)] + pub greet_night: Vec, + + // wake word detected + #[serde(default)] + pub reply: Vec, + + // command executed + #[serde(default)] + pub ok: Vec, + + // command not found + #[serde(default)] + pub not_found: Vec, + + // thank you + #[serde(default)] + pub thanks: Vec, + + // error + #[serde(default)] + pub error: Vec, + + // shutdown + #[serde(default)] + pub goodbye: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub enum Reaction { + Greet, // app startup + Reply, // wake word detected + Ok, // command executed + NotFound, + Thanks, + Error, + Goodbye, +} diff --git a/crates/jarvis-gui/capabilities/default.json b/crates/jarvis-gui/capabilities/default.json index 9205ee1..1cbe2ee 100644 --- a/crates/jarvis-gui/capabilities/default.json +++ b/crates/jarvis-gui/capabilities/default.json @@ -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" ] } diff --git a/crates/jarvis-gui/gen/schemas/capabilities.json b/crates/jarvis-gui/gen/schemas/capabilities.json index fe71707..79f69cd 100644 --- a/crates/jarvis-gui/gen/schemas/capabilities.json +++ b/crates/jarvis-gui/gen/schemas/capabilities.json @@ -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/**"]}]}} \ No newline at end of file +{"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"]}} \ No newline at end of file diff --git a/crates/jarvis-gui/src/main.rs b/crates/jarvis-gui/src/main.rs index f6655c2..0d16504 100644 --- a/crates/jarvis-gui/src/main.rs +++ b/crates/jarvis-gui/src/main.rs @@ -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"); diff --git a/crates/jarvis-gui/src/tauri_commands.rs b/crates/jarvis-gui/src/tauri_commands.rs index dbc1faa..c6e757e 100644 --- a/crates/jarvis-gui/src/tauri_commands.rs +++ b/crates/jarvis-gui/src/tauri_commands.rs @@ -29,4 +29,12 @@ pub use stt::*; // import i18n commands mod i18n; -pub use i18n::*; \ No newline at end of file +pub use i18n::*; + +// import commands commands xD +mod commands; +pub use commands::*; + +// import voices commands +mod voices; +pub use voices::*; \ No newline at end of file diff --git a/crates/jarvis-gui/src/tauri_commands/commands.rs b/crates/jarvis-gui/src/tauri_commands/commands.rs new file mode 100644 index 0000000..f2bfb9b --- /dev/null +++ b/crates/jarvis-gui/src/tauri_commands/commands.rs @@ -0,0 +1,22 @@ +use jarvis_core::commands::{self, JCommand, JCommandsList}; +use once_cell::sync::Lazy; + +static COMMANDS: Lazy> = 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 { + COMMANDS + .iter() + .flat_map(|list| list.commands.clone()) + .collect() +} \ No newline at end of file diff --git a/crates/jarvis-gui/src/tauri_commands/voices.rs b/crates/jarvis-gui/src/tauri_commands/voices.rs new file mode 100644 index 0000000..81beb66 --- /dev/null +++ b/crates/jarvis-gui/src/tauri_commands/voices.rs @@ -0,0 +1,16 @@ +use jarvis_core::voices::{self, structs::VoiceConfig}; + +#[tauri::command] +pub fn list_voices() -> Vec { + voices::list_voices() +} + +#[tauri::command] +pub fn get_voice(voice_id: String) -> Option { + voices::get_voice(&voice_id) +} + +#[tauri::command] +pub fn preview_voice(voice_id: String) { + voices::play_preview(&voice_id); +} \ No newline at end of file diff --git a/crates/jarvis-gui/tauri.conf.json b/crates/jarvis-gui/tauri.conf.json index db12b8b..d962ab3 100644 --- a/crates/jarvis-gui/tauri.conf.json +++ b/crates/jarvis-gui/tauri.conf.json @@ -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" } } } diff --git a/frontend/_src/App.svelte b/frontend/_src/App.svelte deleted file mode 100644 index 04206ea..0000000 --- a/frontend/_src/App.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/frontend/_src/Events.svelte b/frontend/_src/Events.svelte deleted file mode 100644 index 1159d97..0000000 --- a/frontend/_src/Events.svelte +++ /dev/null @@ -1,44 +0,0 @@ - \ No newline at end of file diff --git a/frontend/_src/components/Footer.svelte b/frontend/_src/components/Footer.svelte deleted file mode 100644 index d199f4c..0000000 --- a/frontend/_src/components/Footer.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/_src/components/Header.svelte b/frontend/_src/components/Header.svelte deleted file mode 100644 index 2028185..0000000 --- a/frontend/_src/components/Header.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - \ No newline at end of file diff --git a/frontend/_src/components/Nav.svelte b/frontend/_src/components/Nav.svelte deleted file mode 100644 index c8f8ccb..0000000 --- a/frontend/_src/components/Nav.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/frontend/_src/components/elements/ArcReactor.svelte b/frontend/_src/components/elements/ArcReactor.svelte deleted file mode 100644 index adf9ee5..0000000 --- a/frontend/_src/components/elements/ArcReactor.svelte +++ /dev/null @@ -1,647 +0,0 @@ - -
-
-
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - \ No newline at end of file diff --git a/frontend/_src/components/elements/HDivider.svelte b/frontend/_src/components/elements/HDivider.svelte deleted file mode 100644 index 5c8cea9..0000000 --- a/frontend/_src/components/elements/HDivider.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - -
- - \ No newline at end of file diff --git a/frontend/_src/components/elements/SearchBar.svelte b/frontend/_src/components/elements/SearchBar.svelte deleted file mode 100644 index 3e48b10..0000000 --- a/frontend/_src/components/elements/SearchBar.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/_src/components/elements/Stats.svelte b/frontend/_src/components/elements/Stats.svelte deleted file mode 100644 index fe98d49..0000000 --- a/frontend/_src/components/elements/Stats.svelte +++ /dev/null @@ -1,377 +0,0 @@ - - -
-
-
-
- Микрофон - {microphone_label} -
-
-
-
-
- Нейросети - {nn_details["ww_engine"]} + {nn_details["stt_engine"]} -
-
-
-
-
- Ресурсы - RAM {resources_ram_usage}mb -
-
-
- - diff --git a/frontend/_src/css/main.scss b/frontend/_src/css/main.scss deleted file mode 100644 index 2cc520d..0000000 --- a/frontend/_src/css/main.scss +++ /dev/null @@ -1,308 +0,0 @@ -$prim-font: "Roboto", sans-serif; -$sec-font: "Roboto Condensed", sans-serif; - -html, body { - overflow-x: hidden; -} - -::-webkit-scrollbar { - width: 15px; -} - -::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); -} - -::-webkit-scrollbar-thumb { - background: -webkit-gradient(linear,left top,left bottom,from(#999),to(#27292F)); - background: linear-gradient(to bottom,#999,#27292F); -} - -::selection { - background: #FF8901!important; /* WebKit/Blink Browsers */ - color: white!important; - text-shadow: 1px 1px 1px rgba(0, 0, 0, .5)!important; -} -::-moz-selection { - background: #FF8901!important; /* WebKit/Blink Browsers */ - color: white!important; - text-shadow: 1px 1px 1px rgba(0, 0, 0, .5)!important; -} - -/* - * HEADER - */ - -#header { - height: 80px; - background-color: #0c1013; - box-shadow: 0 0 19px 2px rgba(0, 0, 0, 0.91); - position: relative; - z-index: 100; - text-align: justify; - - display: flex; - justify-content: space-between; - - .logo { - margin-top: 12px; - - & > a > img { - width: 57px; - display: inline-block; - transition: .5s opacity ease-in; - - &:hover { - opacity: .8; - } - } - - & > div { - display: inline-block; - margin-left: 13px; - margin-top: 8px; - vertical-align: top; - } - - h1 { - color: #ffffff; - font-family: $sec-font; - font-size: 20px; - font-weight: bold; - text-transform: uppercase; - letter-spacing: 0.1px; - margin: 0; - - & > a { - color: white; - text-shadow: 1px 1px 1px rgba(0, 0, 0, .5); - text-decoration: none; - - &:hover { - color: #8AC832; - } - } - } - - h2 { - color: #888; - font-family: $sec-font; - font-size: 14px; - font-weight: bold; - letter-spacing: 0.1px; - margin-top: 2px; - letter-spacing: -0.01px; - } - } - - .top-menu { - vertical-align: top; - margin-top: 18px; - margin-left: 50px; - - & > ul { - margin: 0; - padding: 0; - list-style: none; - - & > li { - display: inline-block; - position: relative; - - &:not(:first-child) { - margin-left: 20px; - } - - & > a { - display: inline-block; - text-decoration: none; - color: #c6c6c6; - font-family: $sec-font; - font-size: 16px; - text-transform: uppercase; - font-weight: bold; - transition: .2s color; - padding: 10px 0; - - & > svg { - height: 20px; - width: 20px; - vertical-align: middle; - margin-bottom: 4px; - margin-right: 3px; - margin-left: 3px; - } - - &:hover, &.active { - color: #8AC832; - - &:after { - -webkit-transform: scaleX(1); - -ms-transform: scaleX(1); - transform: scaleX(1); - -webkit-transform-origin: left; - -ms-transform-origin: left; - transform-origin: left; - } - } - - &:after { - content: ""; - display: block; - height: 3px; - border-radius: 10px; - -webkit-transform: scaleX(0); - -ms-transform: scaleX(0); - transform: scaleX(0); - transition: .4s -webkit-transform; - transition: .4s transform; - -webkit-transform-origin: 100% 0; - -ms-transform-origin: 100% 0; - transform-origin: 100% 0; - background: #8AC832; - opacity: 0.90; - position: absolute; - bottom: 5px; - left: 0; - width: 100%; - z-index: 333; - } - - } - } - } - } -} - -.search { - display: block; - margin: 20px 0; - text-align: center; - - & > form { - position: relative; - - & > input { - width: 380px; - height: 38px; - box-shadow: inset 0 0 5px 1px rgba(0, 0, 0, 0.6); - border: 1px solid rgba(6, 6, 6, 0.99); - background-color: #0f1012; - outline: none; - - color: #D1D1D1; - font-family: $sec-font; - font-size: 17px; - font-weight: 600; - line-height: 70.58px; - padding-left: 20px; - padding-right: 45px; - padding-right: 45px; - - &::placeholder { - color: #676767; - font-weight: 400; - } - - &:focus + button + small { - opacity: .3; - } - } - - & > button { - position: absolute; - padding: 0; - margin: 0; - background: none; - border: none; - right: 35px; - top: 4px; - width: 30px; - height: 30px; - opacity: 1; - transition: .3s opacity; - - &::before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 110%; - height: 110%; - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAbCAYAAABvCO8sAAAC4ElEQVRIiaXWX4hXRRTA8c/etkwy+gNBEVRCmxFl1ouZGlEWFUQZPfVQRLAjKvSylEFBf16EiChJOxhLQY8RtA+lRZtpRv8ohUJQNMiHooKyojZNtoeZy95+/fZ3r+uBy5xz5pzznYGZc2dodHRUB1lVviUYwXkYwk84iK8wGRHb2goNtQDXYxRXdVkV9mFrRDx/osCl2IxrG75PsAv75Z1NyzsdwQosb8R+jXURsbML8AG82rDH8TI+77eyiAAppSVYg9SYXhsRW5rxVU/+Qw3Yd7ip+PrCesB7ImKNvNMDxb05pbR+NuBSvFL0L7EYH7SB+oA/Lrm7i2tTSmlVL3AYbxf9eyzDkROFNaBTWCmfYJhIKc1rAh/DuUW/HUfnCmtAp3FrMefjmRp4Kh4tE+PYO6DO6fKp7Ao9hJeKOZZSWlBhNc4ozidbaqyUr8W4/x+42aSuOYT7KtxZHF/gcEvyvDI+KF/yFW20iPgZk8W8o8KVxZjsn/IfOdbQL8OulNLTHfJ2lHFRhYuKcbB/bKs8kVLanVK6ZkBMfS8vqLCgGL/NEQjX48MB8/UVm1/hz2KceRLAfbh3wHxde2pYbmFnY+EcYVsiYm1LzKVl/KHCN8W4sUPxUxr6j7i7AwxuKOP+ykxLW4bzWxKnyziBRXirjZRSOstMx9lW4Q38XRyPt+R/hOtwF35tgzVqDhX99QpTeK441pWVzya/49OOICmlCzFWzE0RcaRuT0/hj6Jv71qwg7xbxuNKv66BR820uIuxE6edDCmltB1XFPOeiPiL/B+sZQcexgtyk96L+3X42/eAFuM1+YUHGyJiop4f7ol/Ef/Iv5TL8VlZQMiXexBoRH7hjTXcj0TEs8242V5tNxdo8wC9J5/Sfq+25bitEfut/Gp7p7dw7w5reV/e4Yay6oW4pXyD5DC2YmNEHOsXMBuwlo3lWy3v+mpcgnPku/WL3Br3yA+uNyPi+KCC/wJQGMGINsMjCwAAAABJRU5ErkJggg=="); - background-repeat: no-repeat; - background-size: 75%; - background-position: center center; - transition: all .3s; - } - - &:hover { - &::before { - cursor: pointer; - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAbCAYAAABvCO8sAAABN2lDQ1BBZG9iZSBSR0IgKDE5OTgpAAAokZWPv0rDUBSHvxtFxaFWCOLgcCdRUGzVwYxJW4ogWKtDkq1JQ5ViEm6uf/oQjm4dXNx9AidHwUHxCXwDxamDQ4QMBYvf9J3fORzOAaNi152GUYbzWKt205Gu58vZF2aYAoBOmKV2q3UAECdxxBjf7wiA10277jTG+38yH6ZKAyNguxtlIYgK0L/SqQYxBMygn2oQD4CpTto1EE9AqZf7G1AKcv8ASsr1fBBfgNlzPR+MOcAMcl8BTB1da4Bakg7UWe9Uy6plWdLuJkEkjweZjs4zuR+HiUoT1dFRF8jvA2AxH2w3HblWtay99X/+PRHX82Vun0cIQCw9F1lBeKEuf1UYO5PrYsdwGQ7vYXpUZLs3cLcBC7dFtlqF8hY8Dn8AwMZP/fNTP8gAAAAJcEhZcwAACxMAAAsTAQCanBgAAAXRaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0MiA3OS4xNjA5MjQsIDIwMTcvMDcvMTMtMDE6MDY6MzkgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE4IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDQtMjNUMDQ6MzE6NTgrMDU6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTA0LTIzVDA0OjM0OjI3KzA1OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzLTA0LTIzVDA0OjM0OjI3KzA1OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowZDA5NTdiMi0zYmM3LTcxNDItODcyNS01ODA3MjA2NTFlYTIiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDoxM2UwZWYxNi03OGM0LTE2NGMtODc1Mi0xYjY5OTQ1OTczMGMiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo5YjNkZTI4Yy1iOTBmLTNjNDUtYjAwNS1kNTExOTE3ZDhkNzIiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjliM2RlMjhjLWI5MGYtM2M0NS1iMDA1LWQ1MTE5MTdkOGQ3MiIgc3RFdnQ6d2hlbj0iMjAyMy0wNC0yM1QwNDozMTo1OCswNTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDowZDA5NTdiMi0zYmM3LTcxNDItODcyNS01ODA3MjA2NTFlYTIiIHN0RXZ0OndoZW49IjIwMjMtMDQtMjNUMDQ6MzQ6MjcrMDU6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE4IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4Wh528AAAC7UlEQVRIiaXWX4gXVRTA8c9v2jLJ6A8ERVAJbUakTr2YqRFl0QRRRk89FBGYqNDLUgY51PQiRERJWhhLQY8RtA+OFm2mGf2RGqEQDAvyoaigrKhNk+3h3mGnX7/9zbgeGO45555zvvfCvWdub/POpTrI6vilGMVF6OEnHMEXmCyyaldboV4LcCPWYnGXVeEQdhRZ9fypApdhG65v+D7GPhwWdjYt7HQUK7GiEfslNhRZtbcL8EG81rDH8TI+G7SyIqtAXqYp1uGRxvT6Iqu2N+OTvvyHG7DvcEv0DYT1gasiq9YJO/06urflZbpxNuAyvBr1z7EE77eBBoA/irn7o2trXqar+4Ej2Bn177Ecx04V1oBOYZVwgmEiL9N5TeATuDDqGY7PFdaATuP2aM7HMzXwTDweJ8ZxcEids4VT2RX6DV6K5lhepgsSrME50flUS41VwrUY9/8DN5vUNXu4P8Fd0XEAR1uS58XxIeGSr2yjFVn1MyajeWeCa6MxOTjlP3KioV+FfXmZFh3y9sRxUYLLonFkcGyrbM7LdH9eptcNianv5SUJFkTjtzkC4UZ8MGS+vmLzE/wZjXNPA3gI9w2Zr2tPjQgt7HwsnCNse5FV61tirozjDwm+isbNHYqf0dB/xD0dYHBTHA8nZlraclzckjgdxwkswtttpLxMzzPTcXYleBN/R8eTLfkf4gbcjV/bYI2avai/kWAKz0XHhrjy2eR3fNIRJC/TSzEWza1FVh2r29PT+CPqu7sW7CDvxPGk2K9r4HEzLe5y7MVZp0PKy3Q3ronmvUVW/UX4D9ayB4/iBaFJH8QDOvzt+0BL8LrwwoNNRVZN1PMjffEv4h/hl3I1Po0LeEW43MNAo8ILb6zhfqzIqmebcbO92m6N0OYBelc4pYNebStwRyP2W+HVVvYX7t9hLe8JO9wUV70Qt8VvmBzFDmwpsurEoIDZgLVsid8aYddLcQUuEO7WL0JrrIQH11tFVp0cVvBfVZDA+HDoxOQAAAAASUVORK5CYII="); - } - } - } - - & > small { - position: absolute; - font-family: $sec-font; - font-size: 14px; - font-weight: bold; - line-height: 1.7em; - top: 8px; - right: 75px; - background-color: #D1D1D1; - color: #080C0F; - padding: 0 7px; - padding-top: 2px; - border-radius: 4px; - opacity: 0; - transition: opacity .3s; - cursor: default; - } - } - - &.active { - small { - opacity: .3; - } - } -} - -@media (max-width: 1364px) { - #content>.inner>section.materials>header>h1 { - font-size: 26px; - } - - #content>.inner>section.materials>article>.details>h1 { - font-size: 25px; - } - - #content>.inner>section.materials>article>.details>blockquote { - font-size: 17px; - line-height: 24px; - -webkit-line-clamp: 4; - } - - .btn { - font-size: 16px!important; - } - - #paginator>header>h1 { - font-size: 30px!important; - } - - #paginator>.paginator-wrapper>.paginator-box { - transform: scale(1.5); - margin-top: 15px; - } - - #paginator>.paginator-wrapper>small:first-child { - display: none; - } - - #paginator>.paginator-wrapper>small:last-child { - display: none; - } -} \ No newline at end of file diff --git a/frontend/_src/css/styles.scss b/frontend/_src/css/styles.scss deleted file mode 100644 index 1cbb6ab..0000000 --- a/frontend/_src/css/styles.scss +++ /dev/null @@ -1,92 +0,0 @@ -/* - SOME DEFAULT CSS. -*/ -html, body { - margin: 0; - padding: 0; - min-height: 100vh; - min-width: 100vw; - - background-color: #4C5062; - color: white; - - - &.assist-page { - background: rgb(24,123,123); - background: -moz-radial-gradient(circle, rgba(24,123,123,0.4906337535014006) 0%, rgba(13,15,19,1) 64%); - background: -webkit-radial-gradient(circle, rgba(24,123,123,0.4906337535014006) 0%, rgba(13,15,19,1) 64%); - background: radial-gradient(circle, rgba(24,123,123,0.4906337535014006) 0%, rgba(13,15,19,1) 64%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#187b7b",endColorstr="#0d0f13",GradientType=1); - - &.assist-active { - background: rgb(24,81,123); - background: -moz-radial-gradient(circle, rgba(24,81,123,0.6418942577030813) 0%, rgba(13,15,19,1) 64%); - background: -webkit-radial-gradient(circle, rgba(24,81,123,0.6418942577030813) 0%, rgba(13,15,19,1) 64%); - background: radial-gradient(circle, rgba(24,81,123,0.6418942577030813) 0%, rgba(13,15,19,1) 64%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#18517b",endColorstr="#0d0f13",GradientType=1); - } - - } -} - -:root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - - color: #0f0f0f; - background-color: #f6f6f6; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - - -a { - color: #2A9CD0; - text-decoration: none; - font-weight: bold; - - &:hover { - color: #1dabed; - text-decoration: underline; - } -} - -/* - OVERRIDES. -*/ -#wrapper { - padding: 0; - margin: 0; -} - -#header, main { - padding: 0 30px; -} - -select, input { - color: #ccc!important; -} - -.form { - label { - font-weight: bold!important; - color: #8AC832!important; - font-size: 15px!important; - border-bottom: 1px dotted gray; - margin-bottom: 10px!important; - - & + div { - line-height: 1.2em; - } - } - - .svelteui-Tab-label { - font-size: 18px!important; - } -} \ No newline at end of file diff --git a/frontend/_src/functions.ts b/frontend/_src/functions.ts deleted file mode 100644 index 10a7e00..0000000 --- a/frontend/_src/functions.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { invoke } from "@tauri-apps/api/core" -import { is_listening, isListening } from "@/stores" -import { clearInterval, clearTimeout, setInterval, setTimeout } from 'worker-timers'; - -// setInterval(() => { -// (async () => { -// is_listening.set(await invoke("is_listening")); -// })().catch(err => { -// console.error(err); -// }); -// }, 1000); - -export function startListening() { - (async () => { - invoke('start_listening') - .then((message) => { - is_listening.set(true); - }) - .catch((error) => { - is_listening.set(false); - console.error(error); - // alert("Ошибка: " + error); - }) - })().catch(err => { - console.error(err); - }); -} - -export function stopListening(callback: () => void) { - (async () => { - invoke('stop_listening') - .then((message) => { - is_listening.set(false); - if(callback) { - callback(); - } - }) - .catch((error) => { - console.error(error); - }) - })().catch(err => { - console.error(err); - }); -} - -export function capitalizeFirstLetter(string: string) { - return string.charAt(0).toUpperCase() + string.slice(1); -} - -export function showInExplorer(path: any) { - (async () => { - invoke('show_in_folder', {path: path}) - .then((message) => {}) - .catch((error) => { - console.error(error); - // alert("Ошибка: " + error); - }) - })().catch(err => { - console.error(err); - }); -} \ No newline at end of file diff --git a/frontend/_src/main.ts b/frontend/_src/main.ts deleted file mode 100644 index 3fbc72c..0000000 --- a/frontend/_src/main.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Klondike project old CSS file -import "./css/main.scss"; - -// App current CSS file -import "./css/styles.scss"; - -// deploy app -import App from "./App.svelte"; -const app = new App({ - target: document.getElementById("app")!, -}); - -export default app; \ No newline at end of file diff --git a/frontend/_src/routes/_module.svelte b/frontend/_src/routes/_module.svelte deleted file mode 100644 index f307626..0000000 --- a/frontend/_src/routes/_module.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - - -
-
- -
- \ No newline at end of file diff --git a/frontend/_src/routes/commands/index.svelte b/frontend/_src/routes/commands/index.svelte deleted file mode 100644 index d724d7b..0000000 --- a/frontend/_src/routes/commands/index.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - Тут будет список команд + полноценный редактор команд.
- Следите за обновлениями в нашем телеграм канале! -
- -
- bruh -
- - -
\ No newline at end of file diff --git a/frontend/_src/routes/index.svelte b/frontend/_src/routes/index.svelte deleted file mode 100644 index ef8e1a6..0000000 --- a/frontend/_src/routes/index.svelte +++ /dev/null @@ -1,71 +0,0 @@ - - - -{#if !is_listening__val} - -В данный момент ассистент не прослушивает команды.
-Пожалуйста, перейдите в настройки и введите ключ Picovoice. -
- -{:else} - - -{/if} - - -
- - - \ No newline at end of file diff --git a/frontend/_src/routes/settings/index.svelte b/frontend/_src/routes/settings/index.svelte deleted file mode 100644 index 4e14b70..0000000 --- a/frontend/_src/routes/settings/index.svelte +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - Часть функций может работать некорректно.
- Сообщайте обо всех найденных багах в наш телеграм бот. - - -
- - - -{#if settings_saved } - - -{/if} - - - - - - - - - - - - - - - - - - {#if selected_wake_word_engine == "picovoice"} - - - - - Мы ждем официального патча от разработчиков. - - - - - Введите сюда свой ключ Picovoice.
- Он выдается бесплатно при регистрации в Picovoice Console.
-
- - - -
- {/if} - - - - - - В данный момент ChatGPT не поддерживается. Он будет добавлен в ближайших обновлениях. - - - -
-
- - - - - - - - -
\ No newline at end of file diff --git a/frontend/_src/stores.ts b/frontend/_src/stores.ts deleted file mode 100644 index dcdd2fa..0000000 --- a/frontend/_src/stores.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { invoke } from "@tauri-apps/api/core" -import { writable } from 'svelte/store' - -// listen state -export const is_listening = writable(true); -let is_listening__val: boolean; -is_listening.subscribe(value => { - is_listening__val = value; -}); -export function isListening() {return is_listening__val} - -// assistant voice -export const assistant_voice = writable(""); - -(async () => { - assistant_voice.set(await invoke("db_read", {key: "assistant_voice"})); -})().catch(err => { - console.error(err); -}); - -// etc -export let tg_official_link = ""; -export let feedback_link = ""; -export let github_repository_link = ""; -export let log_file_path = ""; - -(async () => { - tg_official_link = await invoke("get_tg_official_link") - feedback_link = await invoke("get_feedback_link") - github_repository_link = await invoke("get_repository_link") - log_file_path = await invoke("get_log_file_path") -})().catch(err => { - console.error(err); -}); \ No newline at end of file diff --git a/frontend/_src/vite-env.d.ts b/frontend/_src/vite-env.d.ts deleted file mode 100644 index 4078e74..0000000 --- a/frontend/_src/vite-env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b2e1d75..91d158c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,7 @@ "@svelteuidev/composables": "^0.15.7", "@svelteuidev/core": "^0.15.7", "@svelteuidev/motion": "^0.15.7", - "@tauri-apps/api": "^2", + "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-fs": "^2", "@tauri-apps/plugin-shell": "^2", @@ -819,9 +819,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", "cpu": [ "arm" ], @@ -833,9 +833,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", "cpu": [ "arm64" ], @@ -847,9 +847,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", "cpu": [ "arm64" ], @@ -861,9 +861,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", "cpu": [ "x64" ], @@ -875,9 +875,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", "cpu": [ "arm64" ], @@ -889,9 +889,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", "cpu": [ "x64" ], @@ -903,9 +903,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", "cpu": [ "arm" ], @@ -917,9 +917,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", "cpu": [ "arm" ], @@ -931,9 +931,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", "cpu": [ "arm64" ], @@ -945,9 +945,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", "cpu": [ "arm64" ], @@ -959,9 +959,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", "cpu": [ "loong64" ], @@ -973,9 +987,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", "cpu": [ "ppc64" ], @@ -987,9 +1015,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", "cpu": [ "riscv64" ], @@ -1001,9 +1029,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", "cpu": [ "riscv64" ], @@ -1015,9 +1043,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", "cpu": [ "s390x" ], @@ -1029,9 +1057,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", "cpu": [ "x64" ], @@ -1043,9 +1071,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", "cpu": [ "x64" ], @@ -1056,10 +1084,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", "cpu": [ "arm64" ], @@ -1071,9 +1113,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", "cpu": [ "arm64" ], @@ -1085,9 +1127,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", "cpu": [ "ia32" ], @@ -1099,9 +1141,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", "cpu": [ "x64" ], @@ -1113,9 +1155,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", "cpu": [ "x64" ], @@ -2784,9 +2826,9 @@ } }, "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "dev": true, "license": "MIT", "dependencies": { @@ -2800,28 +2842,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" } }, @@ -2859,9 +2904,9 @@ } }, "node_modules/sass": { - "version": "1.97.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz", - "integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==", + "version": "1.97.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz", + "integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==", "dev": true, "license": "MIT", "peer": true, @@ -3151,9 +3196,9 @@ } }, "node_modules/undici": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", - "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", "dev": true, "license": "MIT", "engines": { diff --git a/frontend/package.json b/frontend/package.json index 3d7bb48..80643ae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,7 @@ "@svelteuidev/composables": "^0.15.7", "@svelteuidev/core": "^0.15.7", "@svelteuidev/motion": "^0.15.7", - "@tauri-apps/api": "^2", + "@tauri-apps/api": "^2.9.1", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-fs": "^2", "@tauri-apps/plugin-shell": "^2", diff --git a/frontend/src/components/Footer.svelte b/frontend/src/components/Footer.svelte index 7de6982..2241d54 100644 --- a/frontend/src/components/Footer.svelte +++ b/frontend/src/components/Footer.svelte @@ -46,13 +46,13 @@

diff --git a/frontend/src/components/elements/SearchBar.svelte b/frontend/src/components/elements/SearchBar.svelte index 0b57c03..b7a13c0 100644 --- a/frontend/src/components/elements/SearchBar.svelte +++ b/frontend/src/components/elements/SearchBar.svelte @@ -1,22 +1,85 @@ - + + \ No newline at end of file diff --git a/frontend/src/components/elements/Stats.svelte b/frontend/src/components/elements/Stats.svelte index d461dc1..1d795ec 100644 --- a/frontend/src/components/elements/Stats.svelte +++ b/frontend/src/components/elements/Stats.svelte @@ -12,10 +12,10 @@ $: t = (key: string) => translate($translations, key) - let microphoneName = "Загрузка..." + let microphoneName = "" let wakeWordEngine = "Rustpotter" let sttEngine = "Vosk" - let vadInfo = "Snip + ChatGPT" + let vadInfo = "" onMount(async () => { microphoneName = t('stats-loading') @@ -68,7 +68,7 @@
{t('stats-resources')} - {#if jarvisRamUsage }RAM {$jarvisRamUsage}mb{:else}...{/if} + {#if $jarvisRamUsage }RAM {$jarvisRamUsage}mb{:else}...{/if}
diff --git a/frontend/src/lib/ipc.ts b/frontend/src/lib/ipc.ts index 6ece44e..e4e1beb 100644 --- a/frontend/src/lib/ipc.ts +++ b/frontend/src/lib/ipc.ts @@ -1,4 +1,6 @@ import { writable, get } from "svelte/store" +import { invoke } from "@tauri-apps/api/core" +import { getCurrentWindow } from "@tauri-apps/api/window" // ### IPC STORES ### @@ -30,57 +32,32 @@ export function disableIpc() { disconnectIpc() } -export function connectIpc() { - if (!enabled) { - console.log("IPC: Not enabled, skipping connection") - return +export function connectIpc(port: number = 9712) { + if (ws?.readyState === WebSocket.OPEN) return + + ws = new WebSocket(`ws://127.0.0.1:${port}`) + + ws.onopen = () => { + ipcConnected.set(true) + jarvisState.set("idle") + console.log("[IPC] connected") } - if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) { - return + ws.onclose = () => { + ipcConnected.set(false) + console.log("[IPC] disconnected") } - manualDisconnect = false + ws.onerror = (err) => { + console.error("[IPC] error:", err) + } - console.log("IPC: Connecting to", IPC_URL) - - try { - ws = new WebSocket(IPC_URL) - - ws.onopen = () => { - console.log("IPC: Connected") - ipcConnected.set(true) - jarvisState.set("idle") - sendAction("ping") - } - - ws.onclose = (event) => { - console.log("IPC: Disconnected", event.code) - ipcConnected.set(false) - jarvisState.set("disconnected") - ws = null - - if (!manualDisconnect && enabled) { - scheduleReconnect() - } - } - - ws.onerror = () => { - // error is handled in onclose, just suppress console spam - } - - ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data) - handleEvent(data) - } catch (e) { - console.error("IPC: Failed to parse message:", event.data, e) - } - } - } catch (e) { - // suppress errors when server isn't running - if (enabled) { - scheduleReconnect() + ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data) + handleEvent(msg) + } catch (e) { + console.error("[IPC] failed to parse message:", e) } } } @@ -151,6 +128,11 @@ function handleEvent(data: any) { case "pong": // connection verified break + + case "reveal_window": + // bring window to foreground + revealWindow() + break } } @@ -171,4 +153,35 @@ export function stopJarvisApp() { export function reloadCommands() { return sendAction("reload_commands") -} \ No newline at end of file +} + +export function sendIpcMessage(message: object): Promise { + return new Promise((resolve, reject) => { + if (!ws || ws.readyState !== WebSocket.OPEN) { + reject(new Error("IPC not connected")) + return + } + + try { + ws.send(JSON.stringify(message)) + resolve() + } catch (err) { + reject(err) + } + }) +} + +export function sendTextCommand(text: string): boolean { + return sendAction("text_command", { text }) +} + +async function revealWindow() { + try { + const window = getCurrentWindow() + await window.show() + await window.unminimize() + await window.setFocus() + } catch (e) { + console.error("[IPC] Failed to reveal window:", e) + } +} diff --git a/frontend/src/routes/index.svelte b/frontend/src/routes/index.svelte index 1f1d474..56f85aa 100644 --- a/frontend/src/routes/index.svelte +++ b/frontend/src/routes/index.svelte @@ -21,13 +21,17 @@ let processRunning = false let launching = false + let wasRunning = false // track previous state isJarvisRunning.subscribe((value) => { processRunning = value if (value) { enableIpc() - } else { + wasRunning = true + } else if (wasRunning) { + // only disable if it was running before disableIpc() + wasRunning = false } }) diff --git a/frontend/src/routes/settings/index.svelte b/frontend/src/routes/settings/index.svelte index 79d9583..91ebc77 100644 --- a/frontend/src/routes/settings/index.svelte +++ b/frontend/src/routes/settings/index.svelte @@ -35,6 +35,30 @@ $: t = (key: string) => translate($translations, key) + interface VoiceMeta { + id: string + name: string + author: string + languages: string[] + } + + interface VoiceConfig { + voice: VoiceMeta + } + + let availableVoices: VoiceMeta[] = [] + + async function selectVoice(voiceId: string) { + voiceVal = voiceId + + // play preview sound + try { + await invoke("preview_voice", { voiceId }) + } catch (err) { + console.error("Failed to preview voice:", err) + } + } + // ### STATE interface MicrophoneOption { label: string @@ -113,11 +137,20 @@ // ### INIT onMount(async () => { + // load voices + try { + const voices = await invoke("list_voices") + availableVoices = voices.map(v => v.voice) + } catch (err) { + console.error("Failed to load voices:", err) + availableVoices = [] + } + try { // load microphones const mics = await invoke("pv_get_audio_devices") availableMicrophones = [ - { label: "По умолчанию (Система)", value: "-1" }, // system default + { label: t('settings-mic-default'), value: "-1" }, // system default ...mics.map((name, idx) => ({ label: name, value: String(idx) @@ -200,18 +233,42 @@ - +
+ +

{t('settings-voice-desc')}

+ +
+ {#each availableVoices as voice} + + {/each} + + {#if availableVoices.length === 0} +

{t('settings-no-voices')}

+ {/if} +
+
@@ -390,3 +447,112 @@