fix: GUI max-width on wrapper + visible TTS swap result + warning cleanup
GUI MAX-WIDTH (was still stretched)
Root cause v2: my earlier rule applied max-width to #header and main
INDIVIDUALLY, but they live inside <Container fluid id="wrapper"> 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.
This commit is contained in:
parent
944cfcc891
commit
965441d4db
12 changed files with 110 additions and 37 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -240,7 +240,6 @@ fn main() -> Result<(), String> {
|
|||
version: config::APP_VERSION.map(|s| s.to_string()),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<ChatMessage>,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
use jarvis_core::{config, db, i18n, voices, DB, SettingsManager};
|
||||
|
||||
#[macro_use]
|
||||
extern crate simple_log;
|
||||
|
||||
mod events;
|
||||
|
|
|
|||
|
|
@ -46,8 +46,22 @@ pub fn set_llm_backend(name: String) -> Result<String, String> {
|
|||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_tts_backend(name: String) -> Result<String, String> {
|
||||
pub fn set_tts_backend(name: String) -> Result<TtsSwapResult, String> {
|
||||
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<String, String> {
|
|||
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).
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// `<Container fluid id="wrapper">` 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string>("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<TtsSwapResult>("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}
|
||||
</Notification>
|
||||
<Space h="md" />
|
||||
{/if}
|
||||
|
||||
<Space h="md" />
|
||||
{#if ttsSwapInfo && ttsSwapInfo.fell_back}
|
||||
<Notification
|
||||
title="TTS откатился на {ttsSwapInfo.applied.toUpperCase()}"
|
||||
color="orange"
|
||||
withCloseButton={true}
|
||||
on:close={() => { ttsSwapInfo = null }}
|
||||
>
|
||||
Запросил <strong>{ttsSwapInfo.requested}</strong> — недоступен.
|
||||
{#if ttsSwapInfo.note}<br/>{ttsSwapInfo.note}{/if}
|
||||
</Notification>
|
||||
<Space h="md" />
|
||||
{:else if ttsSwapInfo && !ttsSwapInfo.fell_back}
|
||||
<Notification
|
||||
title="TTS переключён на {ttsSwapInfo.applied.toUpperCase()}"
|
||||
color="teal"
|
||||
withCloseButton={true}
|
||||
on:close={() => { ttsSwapInfo = null }}
|
||||
>
|
||||
Применено сразу — без перезапуска.
|
||||
</Notification>
|
||||
<Space h="md" />
|
||||
{/if}
|
||||
|
||||
<InputWrapper label={t('settings-llm-context') || 'Контекст разговора'}>
|
||||
<Text size="sm" color="gray">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue