mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 21:30:39 -07:00
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:
+4
-3
@@ -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:**
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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}")
|
||||
|
||||
|
||||
# ============================================
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user