Files
voicebox/backend/build_binary.py
T
Jamie PineandClaude Sonnet 4.5 2542f64e1b Implement dual server binary system (CPU/CUDA)
Problem: The server binary with CUDA support was 2.9GB, causing:
- MSI installer failures in CI (WiX can't handle 3GB files)
- Massive downloads for all users (even those without GPUs)
- Poor user experience

Solution: Build two separate server binaries:
- voicebox-server.exe (CPU-only, ~295MB) - ships with installer
- voicebox-server-cuda.exe (CUDA, ~2.9GB) - optional download

Changes:
- backend/build_binary.py: Added 'variant' parameter for CPU/CUDA builds
- backend/build_cpu.bat: Script to build CPU-only binary
- backend/build_cuda.bat: Script to build CUDA binary
- backend/build_both.bat: Script to build both binaries
- backend/build_cpu.sh: Unix build script for CPU binary
- .github/workflows/release.yml: Build both variants, upload CUDA separately
- tauri/vite.config.ts: Externalize Tauri plugins to fix build
- docs/dual-server-binaries.md: Complete documentation

Results:
- Installer size reduced from 3GB to ~500MB (6x smaller)
- CI builds now succeed (WiX can handle 500MB)
- GPU users can opt-in to download CUDA support
- Better bandwidth usage for CPU-only users

Next steps:
- Frontend implementation to detect GPU and download CUDA binary
- Settings UI to toggle between CPU/CUDA modes

Co-Authored-By: Claude Sonnet 4.5 (1M context) <[email protected]>
2026-01-30 22:51:43 -08:00

127 lines
4.3 KiB
Python

"""
PyInstaller build script for creating standalone Python server binary.
"""
import PyInstaller.__main__
import os
import platform
import sys
from pathlib import Path
def is_apple_silicon():
"""Check if running on Apple Silicon."""
return platform.system() == "Darwin" and platform.machine() == "arm64"
def build_server(variant="cpu"):
"""Build Python server as standalone binary.
Args:
variant: 'cpu' for CPU-only build (~500MB) or 'cuda' for CUDA build (~3GB)
"""
backend_dir = Path(__file__).parent
if variant not in ['cpu', 'cuda']:
raise ValueError(f"Invalid variant: {variant}. Must be 'cpu' or 'cuda'")
# Set binary name based on variant
binary_name = f'voicebox-server-{variant}' if variant == 'cuda' else 'voicebox-server'
print(f"Building {variant.upper()} variant: {binary_name}")
# PyInstaller arguments
args = [
'server.py', # Use server.py as entry point instead of main.py
'--onefile',
'--name', binary_name,
]
# Add local qwen_tts path if specified (for editable installs)
qwen_tts_path = os.getenv('QWEN_TTS_PATH')
if qwen_tts_path and Path(qwen_tts_path).exists():
args.extend(['--paths', str(qwen_tts_path)])
print(f"Using local qwen_tts source from: {qwen_tts_path}")
# Add common hidden imports
args.extend([
'--hidden-import', 'backend',
'--hidden-import', 'backend.main',
'--hidden-import', 'backend.config',
'--hidden-import', 'backend.database',
'--hidden-import', 'backend.models',
'--hidden-import', 'backend.profiles',
'--hidden-import', 'backend.history',
'--hidden-import', 'backend.tts',
'--hidden-import', 'backend.transcribe',
'--hidden-import', 'backend.platform_detect',
'--hidden-import', 'backend.backends',
'--hidden-import', 'backend.backends.pytorch_backend',
'--hidden-import', 'backend.utils.audio',
'--hidden-import', 'backend.utils.cache',
'--hidden-import', 'backend.utils.progress',
'--hidden-import', 'backend.utils.hf_progress',
'--hidden-import', 'backend.utils.validation',
'--hidden-import', 'torch',
'--hidden-import', 'transformers',
'--hidden-import', 'fastapi',
'--hidden-import', 'uvicorn',
'--hidden-import', 'sqlalchemy',
'--hidden-import', 'librosa',
'--hidden-import', 'soundfile',
'--hidden-import', 'qwen_tts',
'--hidden-import', 'qwen_tts.inference',
'--hidden-import', 'qwen_tts.inference.qwen3_tts_model',
'--hidden-import', 'qwen_tts.inference.qwen3_tts_tokenizer',
'--hidden-import', 'qwen_tts.core',
'--hidden-import', 'qwen_tts.cli',
'--copy-metadata', 'qwen-tts',
'--collect-submodules', 'qwen_tts',
'--collect-data', 'qwen_tts',
# Fix for pkg_resources and jaraco namespace packages
'--hidden-import', 'pkg_resources.extern',
'--collect-submodules', 'jaraco',
])
# Add MLX-specific imports if building on Apple Silicon
if is_apple_silicon():
print("Building for Apple Silicon - including MLX dependencies")
args.extend([
'--hidden-import', 'backend.backends.mlx_backend',
'--hidden-import', 'mlx',
'--hidden-import', 'mlx.core',
'--hidden-import', 'mlx.nn',
'--hidden-import', 'mlx_audio',
'--hidden-import', 'mlx_audio.tts',
'--hidden-import', 'mlx_audio.stt',
'--collect-submodules', 'mlx',
'--collect-submodules', 'mlx_audio',
# Collect MLX data files including Metal shader libraries (.metallib)
'--collect-data', 'mlx',
'--collect-data', 'mlx_audio',
])
else:
print("Building for non-Apple Silicon platform - PyTorch only")
args.extend([
'--noconfirm',
'--clean',
])
# Change to backend directory
os.chdir(backend_dir)
# Run PyInstaller
PyInstaller.__main__.run(args)
print(f"\n{'='*60}")
print(f"Build complete: {variant.upper()} variant")
print(f"Binary: {backend_dir / 'dist' / binary_name}")
print(f"{'='*60}\n")
if __name__ == '__main__':
# Accept variant as command line argument
variant = sys.argv[1] if len(sys.argv) > 1 else 'cpu'
build_server(variant)