diff --git a/crates/jarvis-app/src/app.rs b/crates/jarvis-app/src/app.rs index 032e0e6..c87040d 100644 --- a/crates/jarvis-app/src/app.rs +++ b/crates/jarvis-app/src/app.rs @@ -2,7 +2,6 @@ use std::sync::mpsc::Receiver; use std::time::SystemTime; use jarvis_core::{audio_buffer::AudioRingBuffer, audio_processing, audio_processing::vad::listen_window::{ListenWindow, WindowDecision}, audio_processing::vad::webrtc::WebRtcVad, commands, config, listener, recorder, stt, COMMANDS_LIST, intent, voices, ipc::{self, IpcEvent}, i18n, slots}; -use rand::seq::SliceRandom; use crate::should_stop; diff --git a/crates/jarvis-app/src/main.rs b/crates/jarvis-app/src/main.rs index 2b5b985..3928638 100644 --- a/crates/jarvis-app/src/main.rs +++ b/crates/jarvis-app/src/main.rs @@ -240,7 +240,6 @@ fn main() -> Result<(), String> { version: config::APP_VERSION.map(|s| s.to_string()), }); } - _ => {} } }); diff --git a/crates/jarvis-app/src/tray.rs b/crates/jarvis-app/src/tray.rs index 499d278..dafa014 100644 --- a/crates/jarvis-app/src/tray.rs +++ b/crates/jarvis-app/src/tray.rs @@ -7,10 +7,8 @@ use tray_icon::{ use image; use std::process::Command; -#[cfg(target_os="windows")] -use winit::platform::windows::EventLoopBuilderExtWindows; -use jarvis_core::{config, i18n, voices, ipc::{self, IpcEvent}, SettingsManager}; +use jarvis_core::{i18n, voices, ipc::{self, IpcEvent}, SettingsManager}; const TRAY_ICON_BYTES: &[u8] = include_bytes!("../../../resources/icons/32x32.png"); @@ -29,6 +27,7 @@ pub fn init_blocking(settings: SettingsManager) { .unwrap(); let menu_channel = MenuEvent::receiver(); + info!("Tray initialized."); #[cfg(target_os = "linux")] { @@ -77,8 +76,6 @@ pub fn init_blocking(settings: SettingsManager) { std::thread::sleep(std::time::Duration::from_millis(50)); } } - - info!("Tray initialized."); } fn handle_menu_event(event: &MenuEvent, settings: &SettingsManager, tray_state: &menu::TrayState) { diff --git a/crates/jarvis-app/src/tray/menu.rs b/crates/jarvis-app/src/tray/menu.rs index ed2c158..bfdd5a7 100644 --- a/crates/jarvis-app/src/tray/menu.rs +++ b/crates/jarvis-app/src/tray/menu.rs @@ -1,7 +1,6 @@ use tray_icon::menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu}; use jarvis_core::{i18n, voices, SettingsManager}; -use jarvis_core::config::structs::{WakeWordEngine, NoiseSuppressionBackend}; // RADIO GROUP diff --git a/crates/jarvis-core/src/i18n.rs b/crates/jarvis-core/src/i18n.rs index 0dfa52c..8a78fe1 100644 --- a/crates/jarvis-core/src/i18n.rs +++ b/crates/jarvis-core/src/i18n.rs @@ -1,4 +1,4 @@ -use fluent_bundle::{FluentBundle, FluentResource, FluentArgs, FluentValue}; +use fluent_bundle::{FluentResource, FluentArgs, FluentValue}; use fluent_bundle::concurrent::FluentBundle as ConcurrentFluentBundle; use once_cell::sync::OnceCell; use parking_lot::RwLock; diff --git a/crates/jarvis-core/src/llm/history.rs b/crates/jarvis-core/src/llm/history.rs index ffb5261..89d6176 100644 --- a/crates/jarvis-core/src/llm/history.rs +++ b/crates/jarvis-core/src/llm/history.rs @@ -1,6 +1,6 @@ use super::client::ChatMessage; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; pub struct ConversationHistory { system: Option, diff --git a/crates/jarvis-gui/src/events.rs b/crates/jarvis-gui/src/events.rs index 1d6a424..548d731 100644 --- a/crates/jarvis-gui/src/events.rs +++ b/crates/jarvis-gui/src/events.rs @@ -1,6 +1,9 @@ use tauri::Emitter; // the payload type must implement `Serialize` and `Clone`. +// Kept around as the canonical shape for future event emitter wiring even +// though nothing currently emits one — silence the dead-code lint. +#[allow(dead_code)] #[derive(Clone, serde::Serialize)] pub struct Payload { pub data: String, @@ -16,6 +19,7 @@ pub enum EventTypes { CommandEnd, } +#[allow(dead_code)] impl EventTypes { pub fn get(&self) -> &str { match self { @@ -29,6 +33,7 @@ impl EventTypes { } } +#[allow(dead_code)] pub fn play(phrase: &str, app_handle: &tauri::AppHandle) { app_handle .emit( diff --git a/crates/jarvis-gui/src/main.rs b/crates/jarvis-gui/src/main.rs index 004092f..a66a190 100644 --- a/crates/jarvis-gui/src/main.rs +++ b/crates/jarvis-gui/src/main.rs @@ -3,7 +3,6 @@ use jarvis_core::{config, db, i18n, voices, DB, SettingsManager}; -#[macro_use] extern crate simple_log; mod events; diff --git a/crates/jarvis-gui/src/tauri_commands/backends.rs b/crates/jarvis-gui/src/tauri_commands/backends.rs index 802f7c5..416b997 100644 --- a/crates/jarvis-gui/src/tauri_commands/backends.rs +++ b/crates/jarvis-gui/src/tauri_commands/backends.rs @@ -46,8 +46,22 @@ pub fn set_llm_backend(name: String) -> Result { /// Set TTS backend. Persists to settings DB AND hot-swaps the live process /// (in the GUI process; the running daemon picks it up via IPC SwitchTts). +/// +/// Returns a struct describing what ACTUALLY happened — `requested` is what +/// the user asked for, `applied` is what the engine actually installed. They +/// differ when, e.g., Piper was requested but `piper.exe` isn't on disk; the +/// engine falls back to SAPI to keep speech working, and the GUI shows a +/// warning instead of pretending the swap succeeded. +#[derive(Serialize)] +pub struct TtsSwapResult { + pub requested: String, + pub applied: String, + pub fell_back: bool, + pub note: Option, +} + #[tauri::command] -pub fn set_tts_backend(name: String) -> Result { +pub fn set_tts_backend(name: String) -> Result { let normalized = match name.trim().to_lowercase().as_str() { "" | "auto" => "".to_string(), "sapi" | "piper" | "silero" => name.to_lowercase(), @@ -67,18 +81,34 @@ pub fn set_tts_backend(name: String) -> Result { return Err("settings DB not initialised".into()); } - // 2. Apply RIGHT NOW in this GUI process (best-effort — log on failure - // but don't fail the call, the DB change already happened). - let apply_target = if normalized.is_empty() { "auto" } else { normalized.as_str() }; - if let Err(e) = jarvis_core::tts::swap_to(apply_target) { - log::warn!("TTS hot-swap in GUI failed (DB still updated): {}", e); - } + // 2. Apply RIGHT NOW in this GUI process AND report what actually got + // installed. The fallback path in build_backend() may silently swap + // Piper→SAPI when the Piper binary is missing; without surfacing that + // the user sees "saved" but the active engine label stays SAPI and + // they don't know why. + let requested = if normalized.is_empty() { "auto".to_string() } else { normalized.clone() }; + let applied = match jarvis_core::tts::swap_to(&requested) { + Ok(name) => name.to_string(), + Err(e) => { + log::warn!("TTS hot-swap failed: {}", e); + jarvis_core::tts::backend().name().to_string() + } + }; - // 3. The running jarvis-app daemon is a SEPARATE process with its own - // TTS instance; it picks up the change via the IpcAction::SwitchTts - // message — fired from the Svelte page via sendAction() WebSocket. + let fell_back = applied.eq_ignore_ascii_case("sapi") + && !requested.eq_ignore_ascii_case("sapi") + && requested != "auto"; + let note = if fell_back { + Some(match requested.as_str() { + "piper" => "Piper не установлен. Запусти tools/piper/install.ps1 чтобы скачать бинарь и голос.".into(), + "silero" => "Silero недоступен. Нужен Python + torch + silero_tts.py helper.".into(), + other => format!("'{}' недоступен — откатился на SAPI.", other), + }) + } else { + None + }; - Ok(if normalized.is_empty() { "auto".into() } else { normalized }) + Ok(TtsSwapResult { requested, applied, fell_back, note }) } /// Reset conversation context (clears LLM history turns, keeps system prompt). diff --git a/crates/jarvis-gui/src/tauri_commands/sys.rs b/crates/jarvis-gui/src/tauri_commands/sys.rs index 9bc0b45..d9651fe 100644 --- a/crates/jarvis-gui/src/tauri_commands/sys.rs +++ b/crates/jarvis-gui/src/tauri_commands/sys.rs @@ -2,8 +2,6 @@ use sysinfo::{System, Pid, ProcessRefreshKind, RefreshKind, CpuRefreshKind, Comp use peak_alloc::PeakAlloc; use std::sync::Mutex; use once_cell::sync::Lazy; -use std::process::Command; -use std::env; #[global_allocator] static PEAK_ALLOC: PeakAlloc = PeakAlloc; diff --git a/frontend/src/css/styles.scss b/frontend/src/css/styles.scss index e72001b..d38cc6e 100644 --- a/frontend/src/css/styles.scss +++ b/frontend/src/css/styles.scss @@ -61,22 +61,29 @@ a { margin: 0; } -#header, -main { - padding: 0 30px; - // Cap content width on wide monitors so long labels don't stretch - // across 1900px+ screens. On smaller windows this is a no-op. - max-width: 1280px; - margin: 0 auto; +// Cap the WHOLE app (header + main) to 1280px on wide monitors and centre it, +// so labels in the nav don't sprawl across 1920px+ screens. The wrapper is +// `` from svelteui — fluid keeps it width: 100% +// inside the viewport, then we shrink the inner content here. Smaller windows +// are unaffected (max-width is a ceiling, not a floor). +// +// `!important` is unfortunate but svelteui's Container emits a width:100% +// rule that sometimes outweighs ours under HMR. The override is loud and +// localised — only on the top-level wrapper. +#wrapper { + max-width: 1280px !important; + margin: 0 auto !important; + box-sizing: border-box; +} + +#header, +main { + padding: 0 30px; box-sizing: border-box; } -// The home page's arc reactor + status bar can keep their own internal -// max-width while the page itself can still go full width — the page -// layout below `main` already handles its own centering via flex. .app-container.assist-page { width: 100%; - max-width: 1280px; margin: 0 auto; } diff --git a/frontend/src/routes/settings/index.svelte b/frontend/src/routes/settings/index.svelte index daed11f..aed65a4 100644 --- a/frontend/src/routes/settings/index.svelte +++ b/frontend/src/routes/settings/index.svelte @@ -138,12 +138,30 @@ } } + interface TtsSwapResult { + requested: string + applied: string + fell_back: boolean + note: string | null + } + + let ttsSwapInfo: TtsSwapResult | null = null + async function applyTtsBackend(target: string) { + backendSwapError = "" + ttsSwapInfo = null try { // 1. GUI Tauri command — persists to DB + hot-swaps GUI's TTS. - await invoke("set_tts_backend", { name: target }) + // Returns the actual installed backend (may differ from + // requested if Piper/Silero unavailable → fell back to SAPI). + const res = await invoke("set_tts_backend", { name: target }) + ttsSwapInfo = res // 2. Tell the running daemon (separate process) to switch too. switchDaemonTts(target || "auto") + // 3. Refresh the "Активный движок" chip — without this, the user + // saves Piper, sees "saved!", but the active label stays SAPI + // because we never re-read the live engine. + await refreshActiveBackends() } catch (err) { backendSwapError = String(err) } @@ -453,9 +471,31 @@ > {backendSwapError} + {/if} - + {#if ttsSwapInfo && ttsSwapInfo.fell_back} + { ttsSwapInfo = null }} + > + Запросил {ttsSwapInfo.requested} — недоступен. + {#if ttsSwapInfo.note}
{ttsSwapInfo.note}{/if} +
+ + {:else if ttsSwapInfo && !ttsSwapInfo.fell_back} + { ttsSwapInfo = null }} + > + Применено сразу — без перезапуска. + + + {/if}