Update provider documentation and enhance build configurations

- Clarified the bundling of PyTorch CPU providers for Windows and macOS Intel builds in documentation.
- Improved handling of platform-specific dependencies in the build process, including asyncio support for PyInstaller.
- Updated backend logic to gracefully handle missing dependencies and provide clearer error messages.
- Enhanced progress management to ensure compatibility with PyInstaller's async handling.
- Removed unnecessary exclusions from the build scripts for PyTorch providers to streamline the build process.
This commit is contained in:
Jamie Pine
2026-02-01 23:21:27 -08:00
parent 595747c3d0
commit be30a0ac6b
11 changed files with 133 additions and 95 deletions
+4 -3
View File
@@ -148,9 +148,10 @@ Voicebox uses a modular provider system to support different inference backends.
**Hybrid Provider:**
- `pytorch-cpu` — Can be bundled OR downloaded depending on platform
- **Bundled** with macOS Intel builds (`.dmg` for x64)
- Configured in `.github/workflows/release.yml` with `backend: "pytorch"`
- **Downloaded** on first use for Windows/Linux builds (~300MB)
- **Bundled** with Windows and macOS Intel builds
- macOS Intel: `.dmg` for x64 with `backend: "pytorch"`
- Windows: `.exe` installer with PyTorch CPU included
- **Downloaded** on first use for Linux builds (~300MB)
- Falls back to bundled version if external binary not found
**External-Only Providers:**
+3 -3
View File
@@ -205,14 +205,14 @@ Voicebox uses a modular provider system to support different inference backends:
- **`pytorch-cpu`** — Universal CPU provider (bundled or downloaded)
- Bundled with macOS Intel builds
- Downloaded on first use for Windows/Linux (~300MB)
- Bundled with Windows and macOS Intel builds
- Downloaded on first use for Linux (~300MB)
- **`pytorch-cuda`** — Optional NVIDIA GPU-accelerated provider
- Windows/Linux only (~2.4GB)
- 4-5x faster inference on CUDA-capable GPUs
macOS builds work out of the box with bundled providers. Windows and Linux users download a provider on first launch. The app automatically detects your hardware and recommends the best option. All downloadable providers are distributed via Cloudflare R2 for fast, global delivery.
macOS and Windows builds work out of the box with bundled providers. Linux users download a provider on first launch. The app automatically detects your hardware and recommends the best option. All downloadable providers are distributed via Cloudflare R2 for fast, global delivery.
---
+23 -8
View File
@@ -118,22 +118,37 @@ _stt_backend: Optional[STTBackend] = None
def get_tts_backend() -> TTSBackend:
"""
Get or create TTS backend instance based on platform.
Returns:
TTS backend instance (MLX or PyTorch)
Raises:
ImportError: If required dependencies (mlx or torch) are not available
"""
global _tts_backend
if _tts_backend is None:
backend_type = get_backend_type()
if backend_type == "mlx":
from .mlx_backend import MLXTTSBackend
_tts_backend = MLXTTSBackend()
try:
from .mlx_backend import MLXTTSBackend
_tts_backend = MLXTTSBackend()
except ImportError as e:
raise ImportError(
f"MLX backend dependencies not available. "
f"Please install mlx and mlx_audio or download a provider. Error: {e}"
)
else:
from .pytorch_backend import PyTorchTTSBackend
_tts_backend = PyTorchTTSBackend()
try:
from .pytorch_backend import PyTorchTTSBackend
_tts_backend = PyTorchTTSBackend()
except ImportError as e:
raise ImportError(
f"PyTorch backend dependencies not available. "
f"Please download a TTS provider (pytorch-cpu or pytorch-cuda) from the Downloads page. Error: {e}"
)
return _tts_backend
+29 -5
View File
@@ -59,9 +59,16 @@ def build_server():
# Fix for pkg_resources and jaraco namespace packages
'--hidden-import', 'pkg_resources.extern',
'--collect-submodules', 'jaraco',
# Asyncio and threading support for PyInstaller
'--hidden-import', 'asyncio',
'--hidden-import', 'asyncio.subprocess',
'--hidden-import', 'concurrent.futures',
'--hidden-import', 'concurrent.futures.thread',
])
# Platform-specific TTS backend handling
system = platform.system()
if is_apple_silicon():
print("Building for Apple Silicon - including MLX dependencies (bundled)")
args.extend([
@@ -79,13 +86,30 @@ def build_server():
'--collect-data', 'mlx',
'--collect-data', 'mlx_audio',
])
else:
print("Building for Windows/Linux - excluding PyTorch/Qwen-TTS (providers downloaded separately)")
# Note: PyTorch and Qwen-TTS are NOT included - users will download providers separately
# Only include backend abstraction (no actual TTS implementation)
elif system == "Windows" or (system == "Darwin" and not is_apple_silicon()):
# Windows and Intel macOS: Bundle PyTorch CPU provider
print(f"Building for {system} - including PyTorch CPU provider (bundled)")
args.extend([
'--hidden-import', 'backend.backends',
'--hidden-import', 'backend.backends.pytorch_backend', # Keep for reference, but won't work without PyTorch
'--hidden-import', 'backend.backends.pytorch_backend',
'--hidden-import', 'torch',
'--hidden-import', 'transformers',
'--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',
])
else:
# Linux: No bundled provider - users download providers separately
print("Building for Linux - no bundled provider (users download separately)")
args.extend([
'--hidden-import', 'backend.backends',
'--hidden-import', 'backend.backends.pytorch_backend',
])
args.extend([
+57 -30
View File
@@ -14,7 +14,6 @@ from datetime import datetime
import asyncio
import uvicorn
import argparse
import torch
import tempfile
import io
from pathlib import Path
@@ -23,6 +22,14 @@ import asyncio
import signal
import os
# Optional torch import - not available on all platforms (e.g. Windows/Linux without bundled provider)
try:
import torch
TORCH_AVAILABLE = True
except ImportError:
torch = None # type: ignore
TORCH_AVAILABLE = False
from . import database, models, profiles, history, tts, transcribe, config, export_import, channels, stories, __version__
from .database import get_db, Generation as DBGeneration, VoiceProfile as DBVoiceProfile
from .utils.progress import get_progress_manager
@@ -72,20 +79,32 @@ async def shutdown():
@app.get("/health", response_model=models.HealthResponse)
async def health():
"""Health check endpoint."""
from huggingface_hub import hf_hub_download, constants as hf_constants
from huggingface_hub import constants as hf_constants
from pathlib import Path
import os
tts_model = await tts.get_tts_model_async()
# Try to get TTS model provider, but it may not be available if dependencies aren't installed
tts_model = None
try:
tts_model = await tts.get_tts_model_async()
except ImportError as e:
# Provider dependencies not available (e.g., PyTorch not bundled on this platform)
# This is expected on Windows/Linux builds without a bundled provider
print(f"Provider not available: {e}")
backend_type = get_backend_type()
# Check for GPU availability (CUDA or MPS)
has_cuda = torch.cuda.is_available()
has_mps = hasattr(torch.backends, 'mps') and torch.backends.mps.is_available()
# PyTorch might not be available if no provider is bundled
has_cuda = False
has_mps = False
if TORCH_AVAILABLE and torch is not None:
has_cuda = torch.cuda.is_available()
has_mps = hasattr(torch.backends, 'mps') and torch.backends.mps.is_available()
gpu_available = has_cuda or has_mps
gpu_type = None
if has_cuda:
if has_cuda and torch is not None:
gpu_type = f"CUDA ({torch.cuda.get_device_name(0)})"
elif has_mps:
gpu_type = "MPS (Apple Silicon)"
@@ -93,26 +112,27 @@ async def health():
gpu_type = "Metal (Apple Silicon via MLX)"
vram_used = None
if has_cuda:
if has_cuda and torch is not None:
vram_used = torch.cuda.memory_allocated() / 1024 / 1024 # MB
# Check if model is loaded - use the same logic as model status endpoint
model_loaded = False
model_size = None
try:
# Use the same check as model status endpoint
if tts_model.is_loaded():
model_loaded = True
# Get the actual loaded model size
# Check _current_model_size first (more reliable for actually loaded models)
model_size = getattr(tts_model, '_current_model_size', None)
if not model_size:
# Fallback to model_size attribute (which should be set when model loads)
model_size = getattr(tts_model, 'model_size', None)
except Exception:
# If there's an error checking, assume not loaded
model_loaded = False
model_size = None
if tts_model is not None:
try:
# Use the same check as model status endpoint
if tts_model.is_loaded():
model_loaded = True
# Get the actual loaded model size
# Check _current_model_size first (more reliable for actually loaded models)
model_size = getattr(tts_model, '_current_model_size', None)
if not model_size:
# Fallback to model_size attribute (which should be set when model loads)
model_size = getattr(tts_model, 'model_size', None)
except Exception:
# If there's an error checking, assume not loaded
model_loaded = False
model_size = None
# Check if default model is downloaded (cached)
model_downloaded = None
@@ -1836,11 +1856,12 @@ async def get_active_tasks():
def _get_gpu_status() -> str:
"""Get GPU availability status."""
backend_type = get_backend_type()
if torch.cuda.is_available():
return f"CUDA ({torch.cuda.get_device_name(0)})"
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
return "MPS (Apple Silicon)"
elif backend_type == "mlx":
if TORCH_AVAILABLE and torch is not None:
if torch.cuda.is_available():
return f"CUDA ({torch.cuda.get_device_name(0)})"
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
return "MPS (Apple Silicon)"
if backend_type == "mlx":
return "Metal (Apple Silicon via MLX)"
return "None (CPU only)"
@@ -1879,8 +1900,14 @@ async def shutdown_event():
"""Run on application shutdown."""
print("voicebox API shutting down...")
# Unload models to free memory
tts.unload_tts_model()
transcribe.unload_whisper_model()
try:
tts.unload_tts_model()
except Exception as e:
print(f"Warning: Failed to unload TTS model: {e}")
try:
transcribe.unload_whisper_model()
except Exception as e:
print(f"Warning: Failed to unload Whisper model: {e}")
# ============================================
+4 -7
View File
@@ -171,15 +171,12 @@ class ProviderManager:
machine = platform.machine()
if system == "Darwin" and machine == "arm64":
# Apple Silicon gets MLX
# Apple Silicon gets MLX bundled
installed.append("apple-mlx")
# PyTorch CPU is available on all platforms (check if bundled or downloaded)
# For now, assume it's bundled on macOS Intel, Windows, Linux
# Downloaded binaries will be detected below
if not (system == "Darwin" and machine == "arm64"):
# Non-Apple Silicon systems have PyTorch CPU bundled
elif system == "Windows" or (system == "Darwin" and machine != "arm64"):
# Windows and Intel macOS get PyTorch CPU bundled
installed.append("pytorch-cpu")
# Linux: no bundled provider - users must download
# Check for downloaded providers (Phase 2)
providers_dir = _get_providers_dir()
+10 -4
View File
@@ -49,11 +49,17 @@ class ProgressManager:
queue.put_nowait(progress_data.copy())
except RuntimeError:
# Not in async context (running in background thread)
# Use call_soon_threadsafe to safely put on queue
# Use asyncio.run_coroutine_threadsafe for better PyInstaller compatibility
if self._main_loop and self._main_loop.is_running():
self._main_loop.call_soon_threadsafe(
lambda q=queue, d=progress_data.copy(): q.put_nowait(d) if not q.full() else None
)
async def put_data_async():
try:
queue.put_nowait(progress_data.copy())
except asyncio.QueueFull:
pass # Queue full, drop update
try:
asyncio.run_coroutine_threadsafe(put_data_async(), self._main_loop)
except Exception as e:
logger.warning(f"Failed to schedule progress update: {e}")
else:
logger.debug(f"No main loop available for {model_name}, skipping notification")
except asyncio.QueueFull:
+1 -1
View File
@@ -3,7 +3,7 @@ from PyInstaller.utils.hooks import collect_data_files
from PyInstaller.utils.hooks import collect_submodules
datas = []
hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.platform_detect', 'backend.providers', 'backend.providers.base', 'backend.providers.bundled', 'backend.providers.types', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'pkg_resources.extern', 'backend.backends', 'backend.backends.mlx_backend', 'mlx', 'mlx.core', 'mlx.nn', 'mlx_audio', 'mlx_audio.tts', 'mlx_audio.stt']
hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.platform_detect', 'backend.providers', 'backend.providers.base', 'backend.providers.bundled', 'backend.providers.types', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'pkg_resources.extern', 'asyncio', 'asyncio.subprocess', 'concurrent.futures', 'concurrent.futures.thread', 'backend.backends', 'backend.backends.mlx_backend', 'mlx', 'mlx.core', 'mlx.nn', 'mlx_audio', 'mlx_audio.tts', 'mlx_audio.stt']
datas += collect_data_files('mlx')
datas += collect_data_files('mlx_audio')
hiddenimports += collect_submodules('jaraco')
+1 -17
View File
@@ -55,23 +55,7 @@ def build_provider():
'--hidden-import', 'numpy',
'--hidden-import', 'librosa',
])
# Exclude large unused modules to reduce binary size
args.extend([
'--exclude-module', 'torch.utils.tensorboard',
'--exclude-module', 'tensorboard',
'--exclude-module', 'triton',
'--exclude-module', 'torch._dynamo',
'--exclude-module', 'torch._inductor',
'--exclude-module', 'torch.utils.benchmark',
'--exclude-module', 'IPython',
'--exclude-module', 'matplotlib',
'--exclude-module', 'PIL',
'--exclude-module', 'cv2',
'--exclude-module', 'torchvision',
'--exclude-module', 'torchaudio',
])
args.extend([
'--noconfirm',
'--clean',
+1 -17
View File
@@ -57,23 +57,7 @@ def build_provider():
'--hidden-import', 'numpy',
'--hidden-import', 'librosa',
])
# Exclude large unused modules to reduce binary size
args.extend([
'--exclude-module', 'torch.utils.tensorboard',
'--exclude-module', 'tensorboard',
'--exclude-module', 'triton',
'--exclude-module', 'torch._dynamo',
'--exclude-module', 'torch._inductor',
'--exclude-module', 'torch.utils.benchmark',
'--exclude-module', 'IPython',
'--exclude-module', 'matplotlib',
'--exclude-module', 'PIL',
'--exclude-module', 'cv2',
'--exclude-module', 'torchvision',
'--exclude-module', 'torchaudio',
])
args.extend([
'--noconfirm',
'--clean',
Binary file not shown.