feat: model management improvements and folder migration

- Add model folder migration with byte-level progress tracking (backend + UI)
- Custom models directory support via VOICEBOX_MODELS_DIR env var passed to sidecar
- Hardcoded model descriptions displayed in model detail cards
- Open model folder button in storage location row
- Remove 'not downloaded' badge from model cards
- Fix server settings scroll offset for audio player
- Fix shell open permission to allow file paths
- Add normalize toggle to generation settings
This commit is contained in:
Jamie Pine
2026-03-13 08:38:20 -07:00
parent 325714bb83
commit 3ea587797f
23 changed files with 627 additions and 50 deletions
Binary file not shown.
Binary file not shown.
+35 -2
View File
@@ -16,6 +16,7 @@ struct ServerState {
child: Mutex<Option<tauri_plugin_shell::process::CommandChild>>,
server_pid: Mutex<Option<u32>>,
keep_running_on_close: Mutex<bool>,
models_dir: Mutex<Option<String>>,
}
#[command]
@@ -23,7 +24,16 @@ async fn start_server(
app: tauri::AppHandle,
state: State<'_, ServerState>,
remote: Option<bool>,
models_dir: Option<String>,
) -> Result<String, String> {
// Store models_dir for use on restart (empty string means reset to default)
if let Some(ref dir) = models_dir {
if dir.is_empty() {
*state.models_dir.lock().unwrap() = None;
} else {
*state.models_dir.lock().unwrap() = Some(dir.clone());
}
}
// Check if server is already running (managed by this app instance)
if state.child.lock().unwrap().is_some() {
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
@@ -274,6 +284,12 @@ async fn start_server(
let port_str = SERVER_PORT.to_string();
let is_remote = remote.unwrap_or(false);
// Resolve the custom models directory from the parameter or stored state
let effective_models_dir = models_dir.or_else(|| state.models_dir.lock().unwrap().clone());
if let Some(ref dir) = effective_models_dir {
println!("Custom models directory: {}", dir);
}
// If CUDA binary exists, launch it directly instead of the bundled sidecar
let spawn_result = if let Some(ref cuda_path) = cuda_binary {
println!("Launching CUDA backend: {:?}", cuda_path);
@@ -282,6 +298,9 @@ async fn start_server(
if is_remote {
cmd = cmd.args(["--host", "0.0.0.0"]);
}
if let Some(ref dir) = effective_models_dir {
cmd = cmd.env("VOICEBOX_MODELS_DIR", dir);
}
cmd.spawn()
} else {
// Use the bundled CPU sidecar
@@ -289,6 +308,9 @@ async fn start_server(
if is_remote {
sidecar = sidecar.args(["--host", "0.0.0.0"]);
}
if let Some(ref dir) = effective_models_dir {
sidecar = sidecar.env("VOICEBOX_MODELS_DIR", dir);
}
println!("Spawning server process...");
sidecar.spawn()
};
@@ -613,9 +635,19 @@ async fn stop_server(state: State<'_, ServerState>) -> Result<(), String> {
async fn restart_server(
app: tauri::AppHandle,
state: State<'_, ServerState>,
models_dir: Option<String>,
) -> Result<String, String> {
println!("restart_server: stopping current server...");
// Update stored models_dir: empty string means reset to default, non-empty means set
if let Some(ref dir) = models_dir {
if dir.is_empty() {
*state.models_dir.lock().unwrap() = None;
} else {
*state.models_dir.lock().unwrap() = Some(dir.clone());
}
}
// Stop the current server
stop_server(state.clone()).await?;
@@ -623,9 +655,9 @@ async fn restart_server(
println!("restart_server: waiting for port release...");
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
// Start server again (will auto-detect CUDA binary)
// Start server again (will auto-detect CUDA binary and use stored models_dir)
println!("restart_server: starting server...");
start_server(app, state, None).await
start_server(app, state, None, None).await
}
#[command]
@@ -686,6 +718,7 @@ pub fn run() {
child: Mutex::new(None),
server_pid: Mutex::new(None),
keep_running_on_close: Mutex::new(false),
models_dir: Mutex::new(None),
})
.manage(audio_capture::AudioCaptureState::new())
.manage(audio_output::AudioOutputState::new())
+1 -1
View File
@@ -56,7 +56,7 @@
},
"plugins": {
"shell": {
"open": true
"open": ".*"
},
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUxRENBQkRBQjdBNTM1OTIKUldTU05hVzMycXZjNGJGcUxmcVVocll2QjdSaTJNdlFxR2M3VDJsMnVvbDdyZGRPMmRlOW9aWTcK",
+9 -4
View File
@@ -5,9 +5,12 @@ import type { PlatformLifecycle } from '@/platform/types';
class TauriLifecycle implements PlatformLifecycle {
onServerReady?: () => void;
async startServer(remote = false): Promise<string> {
async startServer(remote = false, modelsDir?: string | null): Promise<string> {
try {
const result = await invoke<string>('start_server', { remote });
const result = await invoke<string>('start_server', {
remote,
modelsDir: modelsDir ?? undefined,
});
console.log('Server started:', result);
this.onServerReady?.();
return result;
@@ -27,9 +30,11 @@ class TauriLifecycle implements PlatformLifecycle {
}
}
async restartServer(): Promise<string> {
async restartServer(modelsDir?: string | null): Promise<string> {
try {
const result = await invoke<string>('restart_server');
const result = await invoke<string>('restart_server', {
modelsDir: modelsDir ?? undefined,
});
console.log('Server restarted:', result);
this.onServerReady?.();
return result;