diff --git a/backend/server.py b/backend/server.py index f3348225..d5fb0725 100644 --- a/backend/server.py +++ b/backend/server.py @@ -58,6 +58,12 @@ def disable_watchdog(): """Disable the parent watchdog so the server keeps running after parent exits.""" global _watchdog_disabled _watchdog_disabled = True + # Ignore SIGHUP so the server survives when the parent Tauri process exits. + # On Unix, child processes receive SIGHUP when the parent's session leader + # exits, which would kill the server even though we want it to persist. + if sys.platform != "win32": + import signal + signal.signal(signal.SIGHUP, signal.SIG_IGN) def _start_parent_watchdog(parent_pid, data_dir=None): @@ -130,7 +136,16 @@ def _start_parent_watchdog(parent_pid, data_dir=None): watchdog_logger.info("Watchdog disabled (keep server running), stopping monitor") return if not _is_pid_alive(parent_pid): - watchdog_logger.info(f"Parent process {parent_pid} gone, shutting down server...") + # Parent is gone. Before shutting down, give the app a moment + # to send /watchdog/disable — there is a race where the Tauri + # RunEvent::Exit handler sends the disable request while we are + # mid-iteration (already past the _watchdog_disabled check above). + watchdog_logger.info(f"Parent process {parent_pid} gone, waiting for possible disable request...") + time.sleep(1) + if _watchdog_disabled: + watchdog_logger.info("Watchdog was disabled during grace period, keeping server alive") + return + watchdog_logger.info("Watchdog still enabled after grace period, shutting down server...") if sys.platform == "win32": # sys.exit triggers SystemExit, allowing uvicorn to run # shutdown handlers. os.kill(SIGTERM) on Windows calls diff --git a/backend/voicebox-server.spec b/backend/voicebox-server.spec index feccfae0..4d6b2df0 100644 --- a/backend/voicebox-server.spec +++ b/backend/voicebox-server.spec @@ -1,35 +1,34 @@ # -*- mode: python ; coding: utf-8 -*- from PyInstaller.utils.hooks import collect_data_files from PyInstaller.utils.hooks import collect_submodules +from PyInstaller.utils.hooks import collect_all from PyInstaller.utils.hooks import copy_metadata datas = [] -hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.platform_detect', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'torch', 'transformers', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'qwen_tts', 'qwen_tts.inference', 'qwen_tts.inference.qwen3_tts_model', 'qwen_tts.inference.qwen3_tts_tokenizer', 'qwen_tts.core', 'qwen_tts.cli', 'pkg_resources.extern', 'backend.backends.mlx_backend', 'mlx', 'mlx.core', 'mlx.nn', 'mlx_audio', 'mlx_audio.tts', 'mlx_audio.stt'] +binaries = [] +hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.platform_detect', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'backend.cuda_download', 'backend.effects', 'backend.utils.effects', 'backend.versions', 'pedalboard', 'torch', 'transformers', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'qwen_tts', 'qwen_tts.inference', 'qwen_tts.inference.qwen3_tts_model', 'qwen_tts.inference.qwen3_tts_tokenizer', 'qwen_tts.core', 'qwen_tts.cli', 'pkg_resources.extern', 'backend.backends.mlx_backend', 'mlx', 'mlx.core', 'mlx.nn', 'mlx_audio', 'mlx_audio.tts', 'mlx_audio.stt'] datas += collect_data_files('qwen_tts') -# Use collect_all (not collect_data_files) so native .dylib and .metallib -# files are bundled as binaries, not data. Without this, MLX raises OSError -# when loading Metal shaders inside the PyInstaller bundle. -from PyInstaller.utils.hooks import collect_all as _collect_all -_mlx_datas, _mlx_bins, _mlx_hidden = _collect_all('mlx') -_mlxa_datas, _mlxa_bins, _mlxa_hidden = _collect_all('mlx_audio') -datas += _mlx_datas + _mlxa_datas datas += copy_metadata('qwen-tts') hiddenimports += collect_submodules('qwen_tts') hiddenimports += collect_submodules('jaraco') hiddenimports += collect_submodules('mlx') hiddenimports += collect_submodules('mlx_audio') +tmp_ret = collect_all('mlx') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] +tmp_ret = collect_all('mlx_audio') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] a = Analysis( ['server.py'], pathex=[], - binaries=_mlx_bins + _mlxa_bins, + binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, runtime_hooks=[], - excludes=[], + excludes=['nvidia', 'nvidia.cublas', 'nvidia.cuda_cupti', 'nvidia.cuda_nvrtc', 'nvidia.cuda_runtime', 'nvidia.cudnn', 'nvidia.cufft', 'nvidia.curand', 'nvidia.cusolver', 'nvidia.cusparse', 'nvidia.nccl', 'nvidia.nvjitlink', 'nvidia.nvtx'], noarchive=False, optimize=0, ) diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx index e78d1ca3..203ddf1b 100644 --- a/landing/src/app/page.tsx +++ b/landing/src/app/page.tsx @@ -1,7 +1,6 @@ 'use client'; import { Github, Globe, Languages, MessageSquare, Zap } from 'lucide-react'; -import Image from 'next/image'; import { useEffect, useState } from 'react'; import { ControlUI } from '@/components/ControlUI'; import { Features } from '@/components/Features'; @@ -51,13 +50,11 @@ export default function Home() { 'drop-shadow(0 0 20px hsl(43 60% 50% / 0.4)) drop-shadow(0 0 60px hsl(43 60% 50% / 0.2))', }} > - Voicebox diff --git a/scripts/build-server.sh b/scripts/build-server.sh index a8458418..d9eb72c5 100755 --- a/scripts/build-server.sh +++ b/scripts/build-server.sh @@ -9,12 +9,14 @@ PLATFORM=$(rustc --print host-tuple 2>/dev/null || echo "unknown") echo "Building voicebox-server for platform: $PLATFORM" # Build Python binary +# Resolve PATH to absolute paths before changing directory +export PATH="$(cd "$(dirname "$0")/.." && pwd)/backend/venv/bin:$PATH" cd backend # Check if PyInstaller is installed if ! python -c "import PyInstaller" 2>/dev/null; then echo "Installing PyInstaller..." - pip install pyinstaller + python -m pip install pyinstaller fi # Build binary diff --git a/tauri/src-tauri/Cargo.lock b/tauri/src-tauri/Cargo.lock index 7efd2eac..638746d5 100644 --- a/tauri/src-tauri/Cargo.lock +++ b/tauri/src-tauri/Cargo.lock @@ -5041,7 +5041,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "voicebox" -version = "0.2.0" +version = "0.2.1" dependencies = [ "base64 0.22.1", "core-foundation-sys", diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car index 8f321854..d849532b 100644 Binary files a/tauri/src-tauri/gen/Assets.car and b/tauri/src-tauri/gen/Assets.car differ diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index f6993cfa..f9aa6e36 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -572,6 +572,7 @@ async fn restart_server( #[command] fn set_keep_server_running(state: State<'_, ServerState>, keep_running: bool) { + println!("set_keep_server_running called with: {}", keep_running); *state.keep_running_on_close.lock().unwrap() = keep_running; } @@ -762,6 +763,8 @@ pub fn run() { RunEvent::Exit => { let state = app.state::(); let keep_running = *state.keep_running_on_close.lock().unwrap(); + let has_pid = state.server_pid.lock().unwrap().is_some(); + println!("RunEvent::Exit — keep_running={}, has_pid={}", keep_running, has_pid); if keep_running { // Tell the server to disable its watchdog so it survives @@ -771,9 +774,13 @@ pub fn run() { .timeout(std::time::Duration::from_secs(2)) .build() .unwrap(); - let _ = client + match client .post(&format!("http://127.0.0.1:{}/watchdog/disable", SERVER_PORT)) - .send(); + .send() + { + Ok(resp) => println!("Watchdog disable response: {}", resp.status()), + Err(e) => eprintln!("Failed to disable watchdog: {}", e), + } } else { // Server will self-terminate via parent-pid watchdog when // this process exits. On Unix, also send SIGTERM for diff --git a/tauri/src/platform/lifecycle.ts b/tauri/src/platform/lifecycle.ts index d31ddd52..357f48d3 100644 --- a/tauri/src/platform/lifecycle.ts +++ b/tauri/src/platform/lifecycle.ts @@ -64,6 +64,12 @@ class TauriLifecycle implements PlatformLifecycle { // @ts-expect-error - accessing module-level variable from another module const serverStartedByApp = window.__voiceboxServerStartedByApp ?? false; + console.log( + '[lifecycle] window-close-requested: keepRunning=%s, serverStartedByApp=%s', + keepRunning, + serverStartedByApp, + ); + if (!keepRunning && serverStartedByApp) { // Stop server before closing (only if we started it) try {