Files
voicebox/tauri/src/platform/lifecycle.ts
T
James Pine 2867421550 feat: CUDA backend swap via binary download and restart
Add the ability to download a CUDA-enabled backend binary (~2.4 GB) and
swap it in via a backend-only restart, solving the #1 user pain point
(19 open 'GPU not detected' issues caused by GitHub's 2 GB asset limit).

Backend:
- cuda_download.py: download from R2 (primary) or GitHub split-parts
  (fallback), SHA-256 verification, atomic writes, progress via SSE
- 4 new endpoints: GET/POST/DELETE /backend/cuda-*, GET cuda-progress
- server.py: --version flag, auto-detect variant from binary name
- build_binary.py: --cuda flag for CUDA PyInstaller builds
- split_binary.py: split large binaries into <2GB GitHub Release assets
- CI workflow for building CUDA binary

Tauri:
- restart_server command (stop -> wait -> start)
- start_server prefers CUDA binary from {data_dir}/backends/ if present
- Version mismatch check: runs --version before launching CUDA binary

Frontend:
- GpuAcceleration component: download, progress, restart, switch, delete
- API client + types for CUDA status and management
- Platform lifecycle: restartServer() on Tauri/Web
- Aggressive 1s health polling during restart for fast reconnection
2026-03-13 00:04:12 -07:00

81 lines
2.5 KiB
TypeScript

import { invoke } from '@tauri-apps/api/core';
import { emit, listen } from '@tauri-apps/api/event';
import type { PlatformLifecycle } from '@/platform/types';
class TauriLifecycle implements PlatformLifecycle {
onServerReady?: () => void;
async startServer(remote = false): Promise<string> {
try {
const result = await invoke<string>('start_server', { remote });
console.log('Server started:', result);
this.onServerReady?.();
return result;
} catch (error) {
console.error('Failed to start server:', error);
throw error;
}
}
async stopServer(): Promise<void> {
try {
await invoke('stop_server');
console.log('Server stopped');
} catch (error) {
console.error('Failed to stop server:', error);
throw error;
}
}
async restartServer(): Promise<string> {
try {
const result = await invoke<string>('restart_server');
console.log('Server restarted:', result);
this.onServerReady?.();
return result;
} catch (error) {
console.error('Failed to restart server:', error);
throw error;
}
}
async setKeepServerRunning(keepRunning: boolean): Promise<void> {
try {
await invoke('set_keep_server_running', { keepRunning });
} catch (error) {
console.error('Failed to set keep server running setting:', error);
}
}
async setupWindowCloseHandler(): Promise<void> {
try {
// Listen for window close request from Rust
await listen<null>('window-close-requested', async () => {
// Import store here to avoid circular dependency
const { useServerStore } = await import('@/stores/serverStore');
const keepRunning = useServerStore.getState().keepServerRunningOnClose;
// Check if server was started by this app instance
// @ts-expect-error - accessing module-level variable from another module
const serverStartedByApp = window.__voiceboxServerStartedByApp ?? false;
if (!keepRunning && serverStartedByApp) {
// Stop server before closing (only if we started it)
try {
await this.stopServer();
} catch (error) {
console.error('Failed to stop server on close:', error);
}
}
// Emit event back to Rust to allow close
await emit('window-close-allowed');
});
} catch (error) {
console.error('Failed to setup window close handler:', error);
}
}
}
export const tauriLifecycle = new TauriLifecycle();