From 965441d4db6c22982dd7274e2d5eb7f7d231cd56 Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Sun, 24 May 2026 23:11:49 +0300 Subject: [PATCH] fix: GUI max-width on wrapper + visible TTS swap result + warning cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GUI MAX-WIDTH (was still stretched) Root cause v2: my earlier rule applied max-width to #header and main INDIVIDUALLY, but they live inside from svelteui which goes full viewport width. Both children inherited that full width from the flex layout. Constraining each child wouldn't help when their PARENT was unconstrained. Fix: max-width:1280px !important on #wrapper itself, with margin:0 auto. Now the whole app (header + main) sits in a centred 1280px column on wide monitors. !important is unfortunate but Container's width:100% sometimes wins under HMR otherwise. VISIBLE TTS SWAP RESULT (was failing silently) User saved 'Piper' in dropdown → green 'Saved!' banner → active engine chip still showed 'sapi'. Root cause: when Piper isn't installed, `tts::build_backend("piper")` falls back to SAPI internally, but `set_tts_backend` just logged a warning and returned a generic string. The GUI couldn't tell the difference between success and silent fallback. Fix: - `set_tts_backend` now returns `TtsSwapResult { requested, applied, fell_back, note }`. The Svelte page reads `applied` (actual installed backend), checks `fell_back`, and shows an ORANGE notification with the concrete fix ("запусти tools/piper/install.ps1 чтобы скачать бинарь") instead of a green 'saved' lie. - Saves now also call `refreshActiveBackends()` so the live chip ("Активный движок: TTS: sapi") updates without page reload. - Green notification when swap succeeded normally. WARNING CLEANUP - jarvis-app/main.rs: removed unreachable `_ => {}` arm (all IpcAction variants are explicit now). - jarvis-app/tray.rs: moved `info!("Tray initialized.")` to BEFORE the Windows message-pump loop (the loop runs forever, so the original position was unreachable). - jarvis-gui/main.rs: dropped unused #[macro_use] on simple_log. - jarvis-gui/events.rs: #[allow(dead_code)] on Payload struct + EventTypes impl + `play` fn — they're intentional API surface for future event-emitter wiring, not actual dead code. - Plus a `cargo fix` pass for misc unused imports. Down from 12 warnings to 1 (npm-build-completion notice — informational). Tests: 140 rust + 115 python (was 104, +11 for new wave59_handlers). Release builds of jarvis-app + jarvis-gui both green. --- crates/jarvis-app/src/app.rs | 1 - crates/jarvis-app/src/main.rs | 1 - crates/jarvis-app/src/tray.rs | 7 +-- crates/jarvis-app/src/tray/menu.rs | 1 - crates/jarvis-core/src/i18n.rs | 2 +- crates/jarvis-core/src/llm/history.rs | 2 +- crates/jarvis-gui/src/events.rs | 5 ++ crates/jarvis-gui/src/main.rs | 1 - .../jarvis-gui/src/tauri_commands/backends.rs | 52 +++++++++++++++---- crates/jarvis-gui/src/tauri_commands/sys.rs | 2 - frontend/src/css/styles.scss | 29 +++++++---- frontend/src/routes/settings/index.svelte | 44 +++++++++++++++- 12 files changed, 110 insertions(+), 37 deletions(-) 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}