mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 21:30:39 -07:00
Refactor TTS provider management and enhance documentation
- Renamed `bundled-mlx` to `apple-mlx` for clarity in provider types. - Updated the ProviderSettings component to reflect the new provider naming. - Improved logging for provider startup and error handling in the backend. - Added scripts for building and installing PyTorch CPU and CUDA providers locally. - Enhanced the documentation to include details on TTS provider architecture and development setup.
This commit is contained in:
+149
-9
@@ -14,16 +14,19 @@ Thank you for your interest in contributing to Voicebox! This document provides
|
||||
### Prerequisites
|
||||
|
||||
- **[Bun](https://bun.sh)** - Fast JavaScript runtime and package manager
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
```
|
||||
|
||||
- **[Python 3.11+](https://python.org)** - For backend development
|
||||
|
||||
```bash
|
||||
python --version # Should be 3.11 or higher
|
||||
```
|
||||
|
||||
- **[Rust](https://rustup.rs)** - For Tauri desktop app (installed automatically by Tauri CLI)
|
||||
|
||||
```bash
|
||||
rustc --version # Check if installed
|
||||
```
|
||||
@@ -37,41 +40,46 @@ Thank you for your interest in contributing to Voicebox! This document provides
|
||||
**Manual setup (required for Windows):**
|
||||
|
||||
1. **Fork and clone the repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/voicebox.git
|
||||
cd voicebox
|
||||
```
|
||||
|
||||
2. **Install JavaScript dependencies**
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
This installs dependencies for:
|
||||
|
||||
- `app/` - Shared React frontend
|
||||
- `tauri/` - Tauri desktop wrapper
|
||||
- `web/` - Web deployment wrapper
|
||||
|
||||
3. **Set up Python backend**
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
|
||||
# Create virtual environment
|
||||
python -m venv venv
|
||||
|
||||
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate # On macOS/Linux
|
||||
# or
|
||||
venv\Scripts\activate # On Windows
|
||||
|
||||
|
||||
# Install Python dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
|
||||
# Install MLX dependencies (Apple Silicon only - for faster inference)
|
||||
# On Apple Silicon, this enables native Metal acceleration
|
||||
if [[ $(uname -m) == "arm64" ]]; then
|
||||
pip install -r requirements-mlx.txt
|
||||
fi
|
||||
|
||||
|
||||
# Install Qwen3-TTS (required for voice synthesis)
|
||||
pip install git+https://github.com/QwenLM/Qwen3-TTS.git
|
||||
```
|
||||
@@ -81,19 +89,24 @@ Thank you for your interest in contributing to Voicebox! This document provides
|
||||
Development requires two terminals: one for the Python backend, one for the Tauri app.
|
||||
|
||||
**Terminal 1: Backend server** (start this first)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
source venv/bin/activate # Activate venv if not already active
|
||||
bun run dev:server
|
||||
# Or manually: uvicorn main:app --reload --port 17493
|
||||
```
|
||||
|
||||
Backend will be available at `http://localhost:17493`
|
||||
|
||||
**Terminal 2: Desktop app**
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Create a placeholder sidecar binary (for Tauri compilation)
|
||||
- Start Vite dev server on port 5173
|
||||
- Launch Tauri window pointing to localhost:5173
|
||||
@@ -104,26 +117,132 @@ Thank you for your interest in contributing to Voicebox! This document provides
|
||||
> The bundled server binary is only used in production builds.
|
||||
|
||||
**Optional: Web app**
|
||||
|
||||
```bash
|
||||
bun run dev:web
|
||||
```
|
||||
|
||||
Web app will be available at `http://localhost:5174`
|
||||
|
||||
### Model Downloads
|
||||
|
||||
Models are automatically downloaded from HuggingFace Hub on first use:
|
||||
|
||||
- **Whisper** (transcription): Auto-downloads on first transcription
|
||||
- **Qwen3-TTS** (voice cloning): Auto-downloads on first generation (~2-4GB)
|
||||
|
||||
First-time usage will be slower due to model downloads, but subsequent runs will use cached models.
|
||||
|
||||
### TTS Provider Development
|
||||
|
||||
Voicebox uses a modular provider system to support different inference backends. Understanding this architecture is important when working on TTS features.
|
||||
|
||||
#### Provider Types
|
||||
|
||||
**Bundled Providers** — Included with the app binary:
|
||||
|
||||
- `apple-mlx` — Bundled with macOS Apple Silicon builds (`.dmg` for aarch64)
|
||||
- Uses MLX for native Metal acceleration
|
||||
- Configured in `.github/workflows/release.yml` with `backend: "mlx"`
|
||||
|
||||
**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)
|
||||
- Falls back to bundled version if external binary not found
|
||||
|
||||
**External-Only Providers:**
|
||||
|
||||
- `pytorch-cuda` — NVIDIA GPU-accelerated provider (~2.4GB)
|
||||
- Windows/Linux only (no NVIDIA GPUs on macOS)
|
||||
- Downloaded on demand, not bundled
|
||||
- Optional for users with CUDA-capable GPUs
|
||||
|
||||
#### Provider Architecture
|
||||
|
||||
```
|
||||
backend/providers/
|
||||
├── __init__.py # ProviderManager - lifecycle management
|
||||
├── base.py # TTSProvider protocol
|
||||
├── bundled.py # BundledProvider - wraps built-in backends
|
||||
├── local.py # LocalProvider - wraps external subprocess
|
||||
├── installer.py # Download and install external providers
|
||||
└── types.py # Provider type definitions
|
||||
|
||||
providers/
|
||||
├── pytorch-cpu/ # External PyTorch CPU provider
|
||||
│ ├── main.py # FastAPI server
|
||||
│ ├── build.py # PyInstaller build script
|
||||
│ └── build_and_install.py # Build and install locally
|
||||
└── pytorch-cuda/ # External PyTorch CUDA provider
|
||||
├── main.py
|
||||
├── build.py
|
||||
└── build_and_install.py
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. **Bundled providers** run in-process within the main backend
|
||||
2. **External providers** run as separate subprocess servers
|
||||
3. **LocalProvider** communicates with external providers via HTTP
|
||||
4. **ProviderManager** handles starting/stopping and health checks
|
||||
|
||||
#### Building Providers Locally
|
||||
|
||||
When developing provider features, you'll need to build and test external providers:
|
||||
|
||||
**Build a single provider:**
|
||||
|
||||
```bash
|
||||
cd providers/pytorch-cpu
|
||||
python build_and_install.py
|
||||
```
|
||||
|
||||
**Build all providers:**
|
||||
|
||||
```bash
|
||||
bun run build:providers
|
||||
```
|
||||
|
||||
This script:
|
||||
|
||||
- Builds the provider binary with PyInstaller
|
||||
- Detects your platform (Windows/macOS/Linux)
|
||||
- Copies to the correct location:
|
||||
- macOS: `~/Library/Application Support/voicebox/providers/`
|
||||
- Windows: `%APPDATA%\voicebox\providers\`
|
||||
- Linux: `~/.local/share/voicebox/providers/`
|
||||
- Sets executable permissions on Unix
|
||||
|
||||
**Testing provider changes:**
|
||||
|
||||
1. Make changes to `providers/pytorch-cpu/main.py`
|
||||
2. Run `bun run build:providers`
|
||||
3. Restart the Voicebox app
|
||||
4. Select the provider in Settings → TTS Provider
|
||||
|
||||
#### Provider Binary Distribution
|
||||
|
||||
For production releases, provider binaries are:
|
||||
|
||||
1. Built by GitHub Actions for all platforms
|
||||
2. Uploaded to Cloudflare R2 at `downloads.voicebox.sh/providers/v{VERSION}/`
|
||||
3. Downloaded on-demand by users based on their platform and GPU
|
||||
|
||||
See `.github/workflows/release.yml` for the build matrix.
|
||||
|
||||
### Building
|
||||
|
||||
**Build everything (recommended):**
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
This automatically:
|
||||
|
||||
1. Builds the Python server binary (`./scripts/build-server.sh`)
|
||||
2. Builds the Tauri desktop app (`cd tauri && bun run tauri build`)
|
||||
|
||||
@@ -132,13 +251,23 @@ Creates platform-specific installers (`.dmg`, `.msi`, `.AppImage`) in `tauri/src
|
||||
**Note:** The build process detects your platform and includes the appropriate backend (MLX for Apple Silicon, PyTorch for others).
|
||||
|
||||
**Build server binary only:**
|
||||
|
||||
```bash
|
||||
bun run build:server
|
||||
# or
|
||||
./scripts/build-server.sh
|
||||
```
|
||||
|
||||
Creates platform-specific binary in `tauri/src-tauri/binaries/`
|
||||
|
||||
**Build provider binaries (for development):**
|
||||
|
||||
```bash
|
||||
bun run build:providers
|
||||
```
|
||||
|
||||
Builds all external provider binaries and installs them to the system provider directory. See [TTS Provider Development](#tts-provider-development) for details.
|
||||
|
||||
**Building with local Qwen3-TTS development version:**
|
||||
|
||||
If you're actively developing or modifying the Qwen3-TTS library, set the `QWEN_TTS_PATH` environment variable to point to your local clone:
|
||||
@@ -151,34 +280,41 @@ bun run build:server
|
||||
This makes PyInstaller use your local qwen-tts version instead of the pip-installed package. Useful when testing changes to the TTS library before they're published to PyPI or when using an editable install (`pip install -e`).
|
||||
|
||||
**Build web app:**
|
||||
|
||||
```bash
|
||||
cd web
|
||||
bun run build
|
||||
```
|
||||
|
||||
Output in `web/dist/`
|
||||
|
||||
### Generate OpenAPI Client
|
||||
|
||||
After starting the backend server:
|
||||
|
||||
```bash
|
||||
./scripts/generate-api.sh
|
||||
```
|
||||
|
||||
This downloads the OpenAPI schema and generates the TypeScript client in `app/src/lib/api/`
|
||||
|
||||
### Convert Assets to Web Formats
|
||||
|
||||
To optimize images and videos for the web, run:
|
||||
|
||||
```bash
|
||||
bun run convert:assets
|
||||
```
|
||||
|
||||
This script:
|
||||
|
||||
- Converts PNG → WebP (better compression, same quality)
|
||||
- Converts MOV → WebM (VP9 codec, smaller file size)
|
||||
- Processes files in `landing/public/` and `docs/public/`
|
||||
- **Deletes original files** after successful conversion
|
||||
|
||||
**Requirements:** Install `webp` and `ffmpeg`:
|
||||
|
||||
```bash
|
||||
brew install webp ffmpeg
|
||||
```
|
||||
@@ -225,6 +361,7 @@ git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
Then create a pull request on GitHub with:
|
||||
|
||||
- Clear description of changes
|
||||
- Screenshots (for UI changes)
|
||||
- Reference to related issues
|
||||
@@ -370,21 +507,23 @@ Currently, testing is primarily manual. When adding tests:
|
||||
Releases are managed by maintainers:
|
||||
|
||||
1. **Bump version using bumpversion:**
|
||||
|
||||
```bash
|
||||
# Install bumpversion (if not already installed)
|
||||
pip install bumpversion
|
||||
|
||||
|
||||
# Bump patch version (0.1.0 -> 0.1.1)
|
||||
bumpversion patch
|
||||
|
||||
|
||||
# Or bump minor version (0.1.0 -> 0.2.0)
|
||||
bumpversion minor
|
||||
|
||||
|
||||
# Or bump major version (0.1.0 -> 1.0.0)
|
||||
bumpversion major
|
||||
```
|
||||
|
||||
|
||||
This automatically:
|
||||
|
||||
- Updates version numbers in all files (`tauri.conf.json`, `Cargo.toml`, all `package.json` files, `backend/main.py`)
|
||||
- Creates a git commit with the version bump
|
||||
- Creates a git tag (e.g., `v0.1.1`, `v0.2.0`)
|
||||
@@ -392,6 +531,7 @@ Releases are managed by maintainers:
|
||||
2. **Update CHANGELOG.md** with release notes
|
||||
|
||||
3. **Push commits and tags:**
|
||||
|
||||
```bash
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
@@ -78,12 +78,12 @@ Download a voice model, clone any voice from a few seconds of audio, and compose
|
||||
|
||||
Voicebox is available now for macOS and Windows.
|
||||
|
||||
| Platform | Download |
|
||||
|----------|----------|
|
||||
| macOS (Apple Silicon) | [voicebox_aarch64.app.tar.gz](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_aarch64.app.tar.gz) |
|
||||
| macOS (Intel) | [voicebox_x64.app.tar.gz](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_x64.app.tar.gz) |
|
||||
| Windows (MSI) | [voicebox_0.1.0_x64_en-US.msi](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_0.1.0_x64_en-US.msi) |
|
||||
| Windows (Setup) | [voicebox_0.1.0_x64-setup.exe](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_0.1.0_x64-setup.exe) |
|
||||
| Platform | Download |
|
||||
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| macOS (Apple Silicon) | [voicebox_aarch64.app.tar.gz](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_aarch64.app.tar.gz) |
|
||||
| macOS (Intel) | [voicebox_x64.app.tar.gz](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_x64.app.tar.gz) |
|
||||
| Windows (MSI) | [voicebox_0.1.0_x64_en-US.msi](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_0.1.0_x64_en-US.msi) |
|
||||
| Windows (Setup) | [voicebox_0.1.0_x64-setup.exe](https://github.com/jamiepine/voicebox/releases/download/v0.1.0/voicebox_0.1.0_x64-setup.exe) |
|
||||
|
||||
> **Linux builds coming soon** — Currently blocked by GitHub runner disk space limitations.
|
||||
|
||||
@@ -176,17 +176,17 @@ Full API documentation available at `http://localhost:8000/docs` when running.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Desktop App | Tauri (Rust) |
|
||||
| Frontend | React, TypeScript, Tailwind CSS |
|
||||
| State | Zustand, React Query |
|
||||
| Backend | FastAPI (Python) |
|
||||
| Voice Model | Qwen3-TTS (PyTorch or MLX) |
|
||||
| Transcription | Whisper (PyTorch or MLX) |
|
||||
| Layer | Technology |
|
||||
| ---------------- | --------------------------------------------------- |
|
||||
| Desktop App | Tauri (Rust) |
|
||||
| Frontend | React, TypeScript, Tailwind CSS |
|
||||
| State | Zustand, React Query |
|
||||
| Backend | FastAPI (Python) |
|
||||
| Voice Model | Qwen3-TTS (PyTorch or MLX) |
|
||||
| Transcription | Whisper (PyTorch or MLX) |
|
||||
| Inference Engine | MLX (Apple Silicon) / PyTorch (Windows/Linux/Intel) |
|
||||
| Database | SQLite |
|
||||
| Audio | WaveSurfer.js, librosa |
|
||||
| Database | SQLite |
|
||||
| Audio | WaveSurfer.js, librosa |
|
||||
|
||||
**Why this stack?**
|
||||
|
||||
@@ -194,6 +194,26 @@ Full API documentation available at `http://localhost:8000/docs` when running.
|
||||
- **FastAPI** — Async Python with automatic OpenAPI schema generation
|
||||
- **Type-safe end-to-end** — Generated TypeScript client from OpenAPI spec
|
||||
|
||||
### TTS Provider Architecture
|
||||
|
||||
Voicebox uses a modular provider system to support different inference backends:
|
||||
|
||||
- **`apple-mlx`** — Bundled with macOS Apple Silicon builds
|
||||
|
||||
- Uses MLX with native Metal acceleration (4-5x faster)
|
||||
- Works out of the box, no download required
|
||||
|
||||
- **`pytorch-cpu`** — Universal CPU provider (bundled or downloaded)
|
||||
|
||||
- Bundled with macOS Intel builds
|
||||
- Downloaded on first use for Windows/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.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
@@ -202,13 +222,13 @@ Voicebox is the beginning of something bigger. Here's what's coming:
|
||||
|
||||
### Coming Soon
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Real-time Synthesis** | Stream audio as it generates, word by word |
|
||||
| **Conversation Mode** | Multi-speaker dialogues with automatic turn-taking |
|
||||
| **Voice Effects** | Pitch shift, reverb, M3GAN-style effects |
|
||||
| **Timeline Editor** | Audio studio with word-level precision editing |
|
||||
| **More Models** | XTTS, Bark, and other open-source voice models |
|
||||
| Feature | Description |
|
||||
| ----------------------- | -------------------------------------------------- |
|
||||
| **Real-time Synthesis** | Stream audio as it generates, word by word |
|
||||
| **Conversation Mode** | Multi-speaker dialogues with automatic turn-taking |
|
||||
| **Voice Effects** | Pitch shift, reverb, M3GAN-style effects |
|
||||
| **Timeline Editor** | Audio studio with word-level precision editing |
|
||||
| **More Models** | XTTS, Bark, and other open-source voice models |
|
||||
|
||||
### Future Vision
|
||||
|
||||
@@ -260,9 +280,10 @@ cd backend && pip install -r requirements.txt && cd ..
|
||||
bun run dev
|
||||
```
|
||||
|
||||
**Prerequisites:** [Bun](https://bun.sh), [Rust](https://rustup.rs), [Python 3.11+](https://python.org).
|
||||
**Prerequisites:** [Bun](https://bun.sh), [Rust](https://rustup.rs), [Python 3.11+](https://python.org).
|
||||
|
||||
**Performance:**
|
||||
|
||||
**Performance:**
|
||||
- **Apple Silicon (M1/M2/M3)**: Uses MLX backend with native Metal acceleration for 4-5x faster inference
|
||||
- **Windows/Linux/Intel Mac**: Uses PyTorch backend (CUDA GPU recommended, CPU supported but slower)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const isMacOS = () => navigator.platform.toLowerCase().includes('mac');
|
||||
|
||||
type ProviderType =
|
||||
| 'auto'
|
||||
| 'bundled-mlx'
|
||||
| 'apple-mlx'
|
||||
| 'bundled-pytorch'
|
||||
| 'pytorch-cpu'
|
||||
| 'pytorch-cuda'
|
||||
@@ -286,14 +286,14 @@ export function ProviderSettings() {
|
||||
{/* MLX bundled (macOS Apple Silicon only) */}
|
||||
{isMacOS() && (
|
||||
<div className="flex items-center space-x-3 py-2">
|
||||
<RadioGroupItem value="bundled-mlx" id="mlx" />
|
||||
<RadioGroupItem value="apple-mlx" id="mlx" />
|
||||
<Label htmlFor="mlx" className="flex-1 cursor-pointer">
|
||||
<div className="font-medium">MLX (Apple Silicon)</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Bundled with the app - optimized for M-series chips
|
||||
</div>
|
||||
</Label>
|
||||
{currentProvider === 'bundled-mlx' && (
|
||||
{currentProvider === 'apple-mlx' && (
|
||||
<Badge variant="outline" className="ml-2">
|
||||
Active
|
||||
</Badge>
|
||||
|
||||
+1
-1
@@ -1514,7 +1514,7 @@ async def list_providers():
|
||||
|
||||
# Get info for all known provider types
|
||||
all_providers = [
|
||||
"bundled-mlx",
|
||||
"apple-mlx",
|
||||
"bundled-pytorch",
|
||||
"pytorch-cpu",
|
||||
"pytorch-cuda",
|
||||
|
||||
@@ -3,6 +3,7 @@ Provider management system for TTS providers.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
@@ -50,7 +51,7 @@ class ProviderManager:
|
||||
Args:
|
||||
provider_type: Type of provider to start
|
||||
"""
|
||||
if provider_type == "bundled-mlx":
|
||||
if provider_type == "apple-mlx":
|
||||
# Use bundled MLX provider
|
||||
self.active_provider = self._get_default_provider()
|
||||
elif provider_type in ["pytorch-cpu", "pytorch-cuda"]:
|
||||
@@ -61,8 +62,15 @@ class ProviderManager:
|
||||
# Find a free port
|
||||
port = self._get_free_port()
|
||||
|
||||
# Start provider subprocess
|
||||
# Start provider subprocess with stdout/stderr capture
|
||||
from ..config import get_data_dir
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info(f"Starting provider {provider_type} on port {port}")
|
||||
logger.info(f"Provider binary: {provider_path}")
|
||||
logger.info(f"Data directory: {get_data_dir()}")
|
||||
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
str(provider_path),
|
||||
@@ -71,16 +79,49 @@ class ProviderManager:
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
|
||||
# Wait for provider to be ready
|
||||
base_url = f"http://127.0.0.1:{port}"
|
||||
await self._wait_for_provider_health(base_url, timeout=30)
|
||||
try:
|
||||
await self._wait_for_provider_health(base_url, timeout=30)
|
||||
except TimeoutError as e:
|
||||
# Capture subprocess output for debugging
|
||||
stdout_lines = []
|
||||
stderr_lines = []
|
||||
|
||||
# Try to read available output
|
||||
import select
|
||||
try:
|
||||
if process.stdout and select.select([process.stdout], [], [], 0)[0]:
|
||||
stdout_lines = process.stdout.readlines()
|
||||
if process.stderr and select.select([process.stderr], [], [], 0)[0]:
|
||||
stderr_lines = process.stderr.readlines()
|
||||
except Exception:
|
||||
# select might not work on all platforms
|
||||
pass
|
||||
|
||||
logger.error(f"Provider failed to start. Stdout: {stdout_lines}")
|
||||
logger.error(f"Provider failed to start. Stderr: {stderr_lines}")
|
||||
|
||||
# Terminate the process
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
|
||||
raise
|
||||
|
||||
# Create LocalProvider instance
|
||||
self.active_provider = LocalProvider(base_url)
|
||||
self._provider_process = process
|
||||
self._provider_port = port
|
||||
|
||||
# Start background task to log subprocess output
|
||||
asyncio.create_task(self._log_subprocess_output(process))
|
||||
else:
|
||||
# No external binary, use bundled provider (if available)
|
||||
if provider_type == "pytorch-cpu":
|
||||
@@ -131,7 +172,7 @@ class ProviderManager:
|
||||
|
||||
if system == "Darwin" and machine == "arm64":
|
||||
# Apple Silicon gets MLX
|
||||
installed.append("bundled-mlx")
|
||||
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
|
||||
@@ -163,7 +204,7 @@ class ProviderManager:
|
||||
Returns:
|
||||
Provider information dictionary
|
||||
"""
|
||||
if provider_type in ["bundled-mlx", "bundled-pytorch"]:
|
||||
if provider_type in ["apple-mlx", "bundled-pytorch"]:
|
||||
return {
|
||||
"type": provider_type,
|
||||
"name": "Bundled Provider",
|
||||
@@ -203,7 +244,7 @@ class ProviderManager:
|
||||
"""Wait for provider to become healthy."""
|
||||
import httpx
|
||||
import asyncio
|
||||
|
||||
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
while True:
|
||||
try:
|
||||
@@ -213,12 +254,32 @@ class ProviderManager:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if asyncio.get_event_loop().time() - start_time > timeout:
|
||||
raise TimeoutError(f"Provider did not become healthy within {timeout} seconds")
|
||||
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def _log_subprocess_output(self, process: subprocess.Popen) -> None:
|
||||
"""Log subprocess stdout and stderr."""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def read_stream(stream, prefix):
|
||||
if stream:
|
||||
loop = asyncio.get_event_loop()
|
||||
while True:
|
||||
line = await loop.run_in_executor(None, stream.readline)
|
||||
if not line:
|
||||
break
|
||||
logger.info(f"{prefix}: {line.rstrip()}")
|
||||
|
||||
await asyncio.gather(
|
||||
read_stream(process.stdout, "Provider stdout"),
|
||||
read_stream(process.stderr, "Provider stderr"),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
|
||||
# Global provider manager instance
|
||||
_provider_manager: Optional[ProviderManager] = None
|
||||
|
||||
@@ -8,7 +8,7 @@ from enum import Enum
|
||||
|
||||
class ProviderType(str, Enum):
|
||||
"""Available provider types."""
|
||||
BUNDLED_MLX = "bundled-mlx"
|
||||
BUNDLED_MLX = "apple-mlx"
|
||||
BUNDLED_PYTORCH = "bundled-pytorch"
|
||||
PYTORCH_CPU = "pytorch-cpu"
|
||||
PYTORCH_CUDA = "pytorch-cuda"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"build:web": "cd web && bun run build",
|
||||
"build:landing": "cd landing && bun run build",
|
||||
"build:release": "./scripts/prepare-release.sh",
|
||||
"build:providers": "python scripts/build-providers-local.py",
|
||||
"generate:api": "./scripts/generate-api.sh",
|
||||
"generate:keys": "cd tauri && bun tauri signer generate -w ~/.tauri/voicebox.key",
|
||||
"build:server": "./scripts/build-server.sh",
|
||||
|
||||
@@ -61,7 +61,6 @@ def build_provider():
|
||||
'--exclude-module', 'torch.utils.tensorboard',
|
||||
'--exclude-module', 'tensorboard',
|
||||
'--exclude-module', 'triton',
|
||||
'--exclude-module', 'torch.distributed',
|
||||
'--exclude-module', 'torch._dynamo',
|
||||
'--exclude-module', 'torch._inductor',
|
||||
'--exclude-module', 'torch.testing',
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Build PyTorch CPU provider and install to local provider directory.
|
||||
"""
|
||||
|
||||
import platform
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from build import build_provider
|
||||
|
||||
|
||||
def get_providers_dir() -> Path:
|
||||
"""Get the directory where providers are stored."""
|
||||
system = platform.system()
|
||||
|
||||
if system == "Windows":
|
||||
appdata = Path.home() / "AppData" / "Roaming"
|
||||
elif system == "Darwin":
|
||||
appdata = Path.home() / "Library" / "Application Support"
|
||||
else: # Linux
|
||||
appdata = Path.home() / ".local" / "share"
|
||||
|
||||
providers_dir = appdata / "voicebox" / "providers"
|
||||
providers_dir.mkdir(parents=True, exist_ok=True)
|
||||
return providers_dir
|
||||
|
||||
|
||||
def main():
|
||||
"""Build and install provider."""
|
||||
provider_dir = Path(__file__).parent
|
||||
|
||||
# Build the provider
|
||||
print("Building PyTorch CPU provider...")
|
||||
build_provider()
|
||||
|
||||
# Determine binary name
|
||||
binary_name = "tts-provider-pytorch-cpu"
|
||||
if platform.system() == "Windows":
|
||||
binary_name += ".exe"
|
||||
|
||||
# Source and destination paths
|
||||
source = provider_dir / "dist" / binary_name
|
||||
destination = get_providers_dir() / binary_name
|
||||
|
||||
# Copy to provider directory
|
||||
print(f"Installing to {destination}...")
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
# Make executable on Unix systems
|
||||
if platform.system() != "Windows":
|
||||
destination.chmod(0o755)
|
||||
|
||||
print(f"✓ Provider installed successfully to {destination}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,48 @@
|
||||
# -*- 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 copy_metadata
|
||||
|
||||
datas = []
|
||||
hiddenimports = ['backend', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.config', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.tasks', 'torch', 'transformers', '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', 'fastapi', 'uvicorn', 'soundfile', 'numpy', 'librosa']
|
||||
datas += collect_data_files('qwen_tts')
|
||||
datas += copy_metadata('qwen-tts')
|
||||
hiddenimports += collect_submodules('qwen_tts')
|
||||
hiddenimports += collect_submodules('jaraco')
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=['/Users/jamespine/Projects/voicebox'],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=['torch.utils.tensorboard', 'tensorboard', 'triton', 'torch._dynamo', 'torch._inductor', 'torch.testing', 'torch.utils.benchmark', 'IPython', 'matplotlib', 'PIL', 'cv2', 'torchvision', 'torchaudio'],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='tts-provider-pytorch-cpu',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
@@ -63,7 +63,6 @@ def build_provider():
|
||||
'--exclude-module', 'torch.utils.tensorboard',
|
||||
'--exclude-module', 'tensorboard',
|
||||
'--exclude-module', 'triton',
|
||||
'--exclude-module', 'torch.distributed',
|
||||
'--exclude-module', 'torch._dynamo',
|
||||
'--exclude-module', 'torch._inductor',
|
||||
'--exclude-module', 'torch.testing',
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Build PyTorch CUDA provider and install to local provider directory.
|
||||
"""
|
||||
|
||||
import platform
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from build import build_provider
|
||||
|
||||
|
||||
def get_providers_dir() -> Path:
|
||||
"""Get the directory where providers are stored."""
|
||||
system = platform.system()
|
||||
|
||||
if system == "Windows":
|
||||
appdata = Path.home() / "AppData" / "Roaming"
|
||||
elif system == "Darwin":
|
||||
appdata = Path.home() / "Library" / "Application Support"
|
||||
else: # Linux
|
||||
appdata = Path.home() / ".local" / "share"
|
||||
|
||||
providers_dir = appdata / "voicebox" / "providers"
|
||||
providers_dir.mkdir(parents=True, exist_ok=True)
|
||||
return providers_dir
|
||||
|
||||
|
||||
def main():
|
||||
"""Build and install provider."""
|
||||
provider_dir = Path(__file__).parent
|
||||
|
||||
# Build the provider
|
||||
print("Building PyTorch CUDA provider...")
|
||||
build_provider()
|
||||
|
||||
# Determine binary name
|
||||
binary_name = "tts-provider-pytorch-cuda"
|
||||
if platform.system() == "Windows":
|
||||
binary_name += ".exe"
|
||||
|
||||
# Source and destination paths
|
||||
source = provider_dir / "dist" / binary_name
|
||||
destination = get_providers_dir() / binary_name
|
||||
|
||||
# Copy to provider directory
|
||||
print(f"Installing to {destination}...")
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
# Make executable on Unix systems
|
||||
if platform.system() != "Windows":
|
||||
destination.chmod(0o755)
|
||||
|
||||
print(f"✓ Provider installed successfully to {destination}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,48 @@
|
||||
# -*- 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 copy_metadata
|
||||
|
||||
datas = []
|
||||
hiddenimports = ['backend', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.config', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.tasks', 'torch', 'torch.cuda', 'torch.backends.cudnn', 'transformers', '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', 'fastapi', 'uvicorn', 'soundfile', 'numpy', 'librosa']
|
||||
datas += collect_data_files('qwen_tts')
|
||||
datas += copy_metadata('qwen-tts')
|
||||
hiddenimports += collect_submodules('qwen_tts')
|
||||
hiddenimports += collect_submodules('jaraco')
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=['/Users/jamespine/Projects/voicebox'],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=['torch.utils.tensorboard', 'tensorboard', 'triton', 'torch._dynamo', 'torch._inductor', 'torch.testing', 'torch.utils.benchmark', 'IPython', 'matplotlib', 'PIL', 'cv2', 'torchvision', 'torchaudio'],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='tts-provider-pytorch-cuda',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build and install all TTS providers locally for development.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
"""Build and install all providers."""
|
||||
project_root = Path(__file__).parent.parent
|
||||
providers_dir = project_root / "providers"
|
||||
|
||||
providers = ["pytorch-cpu", "pytorch-cuda"]
|
||||
|
||||
for provider in providers:
|
||||
provider_path = providers_dir / provider
|
||||
script_path = provider_path / "build_and_install.py"
|
||||
|
||||
if not script_path.exists():
|
||||
print(f"⚠ Skipping {provider}: build_and_install.py not found")
|
||||
continue
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Building and installing {provider}...")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
[sys.executable, str(script_path)],
|
||||
cwd=provider_path,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ Failed to build {provider}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print("✓ All providers built and installed successfully!")
|
||||
print(f"{'=' * 60}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Reference in New Issue
Block a user