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:** **Hybrid Provider:**
- `pytorch-cpu` — Can be bundled OR downloaded depending on platform - `pytorch-cpu` — Can be bundled OR downloaded depending on platform
- **Bundled** with macOS Intel builds (`.dmg` for x64) - **Bundled** with Windows and macOS Intel builds
- Configured in `.github/workflows/release.yml` with `backend: "pytorch"` - macOS Intel: `.dmg` for x64 with `backend: "pytorch"`
- **Downloaded** on first use for Windows/Linux builds (~300MB) - 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 - Falls back to bundled version if external binary not found
**External-Only Providers:** **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) - **`pytorch-cpu`** — Universal CPU provider (bundled or downloaded)
- Bundled with macOS Intel builds - Bundled with Windows and macOS Intel builds
- Downloaded on first use for Windows/Linux (~300MB) - Downloaded on first use for Linux (~300MB)
- **`pytorch-cuda`** — Optional NVIDIA GPU-accelerated provider - **`pytorch-cuda`** — Optional NVIDIA GPU-accelerated provider
- Windows/Linux only (~2.4GB) - Windows/Linux only (~2.4GB)
- 4-5x faster inference on CUDA-capable GPUs - 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.
--- ---
+19 -4
View File
@@ -121,6 +121,9 @@ def get_tts_backend() -> TTSBackend:
Returns: Returns:
TTS backend instance (MLX or PyTorch) TTS backend instance (MLX or PyTorch)
Raises:
ImportError: If required dependencies (mlx or torch) are not available
""" """
global _tts_backend global _tts_backend
@@ -128,11 +131,23 @@ def get_tts_backend() -> TTSBackend:
backend_type = get_backend_type() backend_type = get_backend_type()
if backend_type == "mlx": if backend_type == "mlx":
from .mlx_backend import MLXTTSBackend try:
_tts_backend = MLXTTSBackend() 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: else:
from .pytorch_backend import PyTorchTTSBackend try:
_tts_backend = PyTorchTTSBackend() 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 return _tts_backend
+29 -5
View File
@@ -59,9 +59,16 @@ def build_server():
# Fix for pkg_resources and jaraco namespace packages # Fix for pkg_resources and jaraco namespace packages
'--hidden-import', 'pkg_resources.extern', '--hidden-import', 'pkg_resources.extern',
'--collect-submodules', 'jaraco', '--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 # Platform-specific TTS backend handling
system = platform.system()
if is_apple_silicon(): if is_apple_silicon():
print("Building for Apple Silicon - including MLX dependencies (bundled)") print("Building for Apple Silicon - including MLX dependencies (bundled)")
args.extend([ args.extend([
@@ -79,13 +86,30 @@ def build_server():
'--collect-data', 'mlx', '--collect-data', 'mlx',
'--collect-data', 'mlx_audio', '--collect-data', 'mlx_audio',
]) ])
else: elif system == "Windows" or (system == "Darwin" and not is_apple_silicon()):
print("Building for Windows/Linux - excluding PyTorch/Qwen-TTS (providers downloaded separately)") # Windows and Intel macOS: Bundle PyTorch CPU provider
# Note: PyTorch and Qwen-TTS are NOT included - users will download providers separately print(f"Building for {system} - including PyTorch CPU provider (bundled)")
# Only include backend abstraction (no actual TTS implementation)
args.extend([ args.extend([
'--hidden-import', 'backend.backends', '--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([ args.extend([
+56 -29
View File
@@ -14,7 +14,6 @@ from datetime import datetime
import asyncio import asyncio
import uvicorn import uvicorn
import argparse import argparse
import torch
import tempfile import tempfile
import io import io
from pathlib import Path from pathlib import Path
@@ -23,6 +22,14 @@ import asyncio
import signal import signal
import os 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 . 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 .database import get_db, Generation as DBGeneration, VoiceProfile as DBVoiceProfile
from .utils.progress import get_progress_manager from .utils.progress import get_progress_manager
@@ -72,20 +79,32 @@ async def shutdown():
@app.get("/health", response_model=models.HealthResponse) @app.get("/health", response_model=models.HealthResponse)
async def health(): async def health():
"""Health check endpoint.""" """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 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() backend_type = get_backend_type()
# Check for GPU availability (CUDA or MPS) # Check for GPU availability (CUDA or MPS)
has_cuda = torch.cuda.is_available() # PyTorch might not be available if no provider is bundled
has_mps = hasattr(torch.backends, 'mps') and torch.backends.mps.is_available() 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_available = has_cuda or has_mps
gpu_type = None gpu_type = None
if has_cuda: if has_cuda and torch is not None:
gpu_type = f"CUDA ({torch.cuda.get_device_name(0)})" gpu_type = f"CUDA ({torch.cuda.get_device_name(0)})"
elif has_mps: elif has_mps:
gpu_type = "MPS (Apple Silicon)" gpu_type = "MPS (Apple Silicon)"
@@ -93,26 +112,27 @@ async def health():
gpu_type = "Metal (Apple Silicon via MLX)" gpu_type = "Metal (Apple Silicon via MLX)"
vram_used = None vram_used = None
if has_cuda: if has_cuda and torch is not None:
vram_used = torch.cuda.memory_allocated() / 1024 / 1024 # MB vram_used = torch.cuda.memory_allocated() / 1024 / 1024 # MB
# Check if model is loaded - use the same logic as model status endpoint # Check if model is loaded - use the same logic as model status endpoint
model_loaded = False model_loaded = False
model_size = None model_size = None
try: if tts_model is not None:
# Use the same check as model status endpoint try:
if tts_model.is_loaded(): # Use the same check as model status endpoint
model_loaded = True if tts_model.is_loaded():
# Get the actual loaded model size model_loaded = True
# Check _current_model_size first (more reliable for actually loaded models) # Get the actual loaded model size
model_size = getattr(tts_model, '_current_model_size', None) # Check _current_model_size first (more reliable for actually loaded models)
if not model_size: model_size = getattr(tts_model, '_current_model_size', None)
# Fallback to model_size attribute (which should be set when model loads) if not model_size:
model_size = getattr(tts_model, 'model_size', None) # Fallback to model_size attribute (which should be set when model loads)
except Exception: model_size = getattr(tts_model, 'model_size', None)
# If there's an error checking, assume not loaded except Exception:
model_loaded = False # If there's an error checking, assume not loaded
model_size = None model_loaded = False
model_size = None
# Check if default model is downloaded (cached) # Check if default model is downloaded (cached)
model_downloaded = None model_downloaded = None
@@ -1836,11 +1856,12 @@ async def get_active_tasks():
def _get_gpu_status() -> str: def _get_gpu_status() -> str:
"""Get GPU availability status.""" """Get GPU availability status."""
backend_type = get_backend_type() backend_type = get_backend_type()
if torch.cuda.is_available(): if TORCH_AVAILABLE and torch is not None:
return f"CUDA ({torch.cuda.get_device_name(0)})" if torch.cuda.is_available():
elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available(): return f"CUDA ({torch.cuda.get_device_name(0)})"
return "MPS (Apple Silicon)" elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
elif backend_type == "mlx": return "MPS (Apple Silicon)"
if backend_type == "mlx":
return "Metal (Apple Silicon via MLX)" return "Metal (Apple Silicon via MLX)"
return "None (CPU only)" return "None (CPU only)"
@@ -1879,8 +1900,14 @@ async def shutdown_event():
"""Run on application shutdown.""" """Run on application shutdown."""
print("voicebox API shutting down...") print("voicebox API shutting down...")
# Unload models to free memory # Unload models to free memory
tts.unload_tts_model() try:
transcribe.unload_whisper_model() 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() machine = platform.machine()
if system == "Darwin" and machine == "arm64": if system == "Darwin" and machine == "arm64":
# Apple Silicon gets MLX # Apple Silicon gets MLX bundled
installed.append("apple-mlx") installed.append("apple-mlx")
elif system == "Windows" or (system == "Darwin" and machine != "arm64"):
# PyTorch CPU is available on all platforms (check if bundled or downloaded) # Windows and Intel macOS get PyTorch CPU bundled
# 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
installed.append("pytorch-cpu") installed.append("pytorch-cpu")
# Linux: no bundled provider - users must download
# Check for downloaded providers (Phase 2) # Check for downloaded providers (Phase 2)
providers_dir = _get_providers_dir() providers_dir = _get_providers_dir()
+10 -4
View File
@@ -49,11 +49,17 @@ class ProgressManager:
queue.put_nowait(progress_data.copy()) queue.put_nowait(progress_data.copy())
except RuntimeError: except RuntimeError:
# Not in async context (running in background thread) # 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(): if self._main_loop and self._main_loop.is_running():
self._main_loop.call_soon_threadsafe( async def put_data_async():
lambda q=queue, d=progress_data.copy(): q.put_nowait(d) if not q.full() else None 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: else:
logger.debug(f"No main loop available for {model_name}, skipping notification") logger.debug(f"No main loop available for {model_name}, skipping notification")
except asyncio.QueueFull: 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 from PyInstaller.utils.hooks import collect_submodules
datas = [] 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')
datas += collect_data_files('mlx_audio') datas += collect_data_files('mlx_audio')
hiddenimports += collect_submodules('jaraco') hiddenimports += collect_submodules('jaraco')
-16
View File
@@ -56,22 +56,6 @@ def build_provider():
'--hidden-import', 'librosa', '--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([ args.extend([
'--noconfirm', '--noconfirm',
'--clean', '--clean',
-16
View File
@@ -58,22 +58,6 @@ def build_provider():
'--hidden-import', 'librosa', '--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([ args.extend([
'--noconfirm', '--noconfirm',
'--clean', '--clean',
Binary file not shown.