mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 21:30:39 -07:00
Merge branch 'main' into better-docs
This commit is contained in:
@@ -53,6 +53,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Audio export failing when Tauri save dialog returns object instead of string path
|
||||
|
||||
### Added
|
||||
- **Makefile** - Comprehensive development workflow automation with commands for setup, development, building, testing, and code quality checks
|
||||
- Includes Python version detection and compatibility warnings
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
## What is Voicebox?
|
||||
|
||||
Voicebox is a **local-first voice cloning studio** with DAW-like features for professional voice synthesis. Think of it as the **Ollama for voice** — download models, clone voices, and generate speech entirely on your machine.
|
||||
Voicebox is a **local-first voice cloning studio** with DAW-like features for professional voice synthesis. Think of it as a **local, free and open-source alternative to ElevenLabs** — download models, clone voices, and generate speech entirely on your machine.
|
||||
|
||||
Unlike cloud services that lock your voice data behind subscriptions, Voicebox gives you:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMatchRoute } from '@tanstack/react-router';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Loader2, MessageSquare, Sparkles } from 'lucide-react';
|
||||
import { Loader2, SlidersHorizontal, Sparkles } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
||||
@@ -187,7 +187,7 @@ export function FloatingGenerateBox({
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
className="bg-background/30 backdrop-blur-2xl border border-accent/20 rounded-[2rem] shadow-2xl hover:bg-background/40 hover:border-accent/20 transition-all duration-300 overflow-hidden p-3"
|
||||
className="bg-background/30 backdrop-blur-2xl border border-accent/20 rounded-[2rem] shadow-2xl hover:bg-background/40 hover:border-accent/20 transition-all duration-300 p-3"
|
||||
transition={{ duration: 0.6, ease: 'easeInOut' }}
|
||||
>
|
||||
<Form {...form}>
|
||||
@@ -274,7 +274,7 @@ export function FloatingGenerateBox({
|
||||
field.ref(node);
|
||||
}
|
||||
}}
|
||||
placeholder="Add delivery instructions..."
|
||||
placeholder="e.g. very happy and excited"
|
||||
className="resize-none bg-transparent border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0 outline-none ring-0 rounded-2xl text-sm placeholder:text-muted-foreground/60 w-full"
|
||||
style={{
|
||||
minHeight: isExpanded ? '100px' : '32px',
|
||||
@@ -294,18 +294,27 @@ export function FloatingGenerateBox({
|
||||
</motion.div>
|
||||
|
||||
<div className="relative shrink-0">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending || !selectedProfileId}
|
||||
className="h-10 w-10 rounded-full bg-accent hover:bg-accent/90 hover:scale-105 text-accent-foreground shadow-lg hover:shadow-accent/50 transition-all duration-200"
|
||||
size="icon"
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<div className="group relative">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending || !selectedProfileId}
|
||||
className="h-10 w-10 rounded-full bg-accent hover:bg-accent/90 hover:scale-105 text-accent-foreground shadow-lg hover:shadow-accent/50 transition-all duration-200"
|
||||
size="icon"
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<span className="pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 whitespace-nowrap rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground border border-border opacity-0 transition-opacity group-hover:opacity-100 z-[9999]">
|
||||
{isPending
|
||||
? 'Generating...'
|
||||
: !selectedProfileId
|
||||
? 'Select a voice profile first'
|
||||
: 'Generate speech'}
|
||||
</span>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
@@ -315,20 +324,25 @@ export function FloatingGenerateBox({
|
||||
transition={{ duration: 0.2 }}
|
||||
className="absolute top-0 right-[calc(100%+0.5rem)]"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsInstructMode(!isInstructMode)}
|
||||
className={cn(
|
||||
'h-10 w-10 rounded-full transition-all duration-200',
|
||||
isInstructMode
|
||||
? 'bg-accent text-accent-foreground border border-accent hover:bg-accent/90'
|
||||
: 'bg-card border border-border hover:bg-background/50',
|
||||
)}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="group relative">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsInstructMode(!isInstructMode)}
|
||||
className={cn(
|
||||
'h-10 w-10 rounded-full transition-all duration-200',
|
||||
isInstructMode
|
||||
? 'bg-accent text-accent-foreground border border-accent hover:bg-accent/90'
|
||||
: 'bg-card border border-border hover:bg-background/50',
|
||||
)}
|
||||
>
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 whitespace-nowrap rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground border border-border opacity-0 transition-opacity group-hover:opacity-100 z-[9999]">
|
||||
Fine tune instructions
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -5,7 +5,7 @@ description: "Welcome to Voicebox - the open-source voice synthesis studio"
|
||||
|
||||
## What is Voicebox?
|
||||
|
||||
Voicebox is a **local-first voice cloning studio** with DAW-like features for professional voice synthesis. Think of it as the **Ollama for voice** — download models, clone voices, and generate speech entirely on your machine.
|
||||
Voicebox is a **local-first voice cloning studio** with DAW-like features for professional voice synthesis. Think of it as a **local, free and open-source alternative to ElevenLabs** — download models, clone voices, and generate speech entirely on your machine.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/app-screenshot-1.webp" alt="Voicebox App Screenshot" />
|
||||
|
||||
@@ -0,0 +1,964 @@
|
||||
# TTS Provider Architecture
|
||||
|
||||
**Status:** Planned for v0.1.13
|
||||
**Created:** 2025-01-31
|
||||
**Problem:** GitHub 2GB release limit + poor UX for frequent updates requiring 2.4GB re-downloads
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Split the monolithic backend into modular components:
|
||||
|
||||
1. **Main App** (~150-200MB): Tauri + FastAPI backend + Whisper + UI/profiles/history
|
||||
2. **TTS Providers** (downloadable plugins): Separate executables for model inference
|
||||
|
||||
This architecture solves:
|
||||
|
||||
- ✅ GitHub 2GB release artifact limit
|
||||
- ✅ Frequent app updates without re-downloading large python binaries
|
||||
- ✅ User choice of compute backend (CPU/GPU/Cloud)
|
||||
- ✅ External provider support (OpenAI, custom servers)
|
||||
- ✅ Future extensibility
|
||||
|
||||
---
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Voicebox App (Tauri + Backend) ~150MB │
|
||||
│ ├─ UI Layer (React) │
|
||||
│ ├─ Backend (FastAPI) │
|
||||
│ │ ├─ Voice Profiles │
|
||||
│ │ ├─ Generation History │
|
||||
│ │ ├─ Audio Editing / Stories │
|
||||
│ │ └─ Provider Manager ◄──────────────┐ │
|
||||
│ └─ Whisper (bundled, tiny ~50MB) │ │
|
||||
└─────────────────────────────────────────┼────────────────┘
|
||||
│
|
||||
HTTP/IPC │
|
||||
│
|
||||
┌────────────────────────────────┼─────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐
|
||||
│ TTS Provider: │ │ TTS Provider: │ │ TTS Provider: │
|
||||
│ PyTorch CPU │ │ PyTorch CUDA │ │ MLX (Apple) │
|
||||
│ │ │ │ │ │
|
||||
│ ~300MB │ │ ~2.4GB │ │ ~800MB │
|
||||
│ │ │ │ │ │
|
||||
│ Local inference │ │ GPU inference │ │ Metal inference │
|
||||
└─────────────────┘ └─────────────────┘ └──────────────────┘
|
||||
│ │ │
|
||||
└────────────────────────┴─────────────────────┘
|
||||
│
|
||||
┌─────────────▼──────────────┐
|
||||
│ Future Providers: │
|
||||
│ • Remote Server │
|
||||
│ • OpenAI API │
|
||||
│ • ElevenLabs │
|
||||
│ • Custom Docker Container │
|
||||
└────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
### Current Architecture Issues
|
||||
|
||||
**Monolithic Binary:**
|
||||
|
||||
- CPU version: ~295MB
|
||||
- CUDA version: ~2.37GB
|
||||
- GitHub releases: 2GB file size limit (BLOCKED)
|
||||
- Updates require re-downloading entire binary
|
||||
- Poor UX: update app → restart → download CUDA update → restart again
|
||||
|
||||
**User Pain Points:**
|
||||
|
||||
1. Cannot release CUDA version on GitHub (over 2GB)
|
||||
2. Every app update forces 2.4GB re-download for GPU users
|
||||
3. No flexibility (can't use OpenAI, remote servers, etc.)
|
||||
4. Wastes bandwidth for small bug fixes
|
||||
|
||||
---
|
||||
|
||||
## Solution: Pluggable TTS Providers
|
||||
|
||||
### Component Breakdown
|
||||
|
||||
#### 1. Main App (voicebox.exe / .app / .AppImage)
|
||||
|
||||
**Size:** ~100-150MB
|
||||
|
||||
**Includes:**
|
||||
|
||||
- Tauri runtime + React UI
|
||||
- FastAPI backend (pure Python, no PyTorch)
|
||||
- Whisper model (tiny, ~50MB)
|
||||
- SQLite database
|
||||
- Profile/history/audio editing logic
|
||||
- Provider management system
|
||||
|
||||
**Does NOT include:**
|
||||
|
||||
- PyTorch (CPU or CUDA)
|
||||
- TTS models (Qwen3-TTS)
|
||||
- Heavy ML dependencies
|
||||
|
||||
**Updates frequently:** UI fixes, feature additions, non-ML changes
|
||||
|
||||
---
|
||||
|
||||
#### 2. TTS Provider: PyTorch CPU
|
||||
|
||||
**Binary:** `tts-provider-pytorch-cpu.exe`
|
||||
**Size:** ~200MB
|
||||
|
||||
**Includes:**
|
||||
|
||||
- PyTorch CPU build
|
||||
- Qwen3-TTS package
|
||||
- Transformers
|
||||
- No CUDA libraries
|
||||
|
||||
**Download source:** Cloudflare R2
|
||||
**Updates rarely:** Only when model code changes
|
||||
|
||||
---
|
||||
|
||||
#### 3. TTS Provider: PyTorch CUDA
|
||||
|
||||
**Binary:** `tts-provider-pytorch-cuda.exe`
|
||||
**Size:** ~2.4GB
|
||||
|
||||
**Includes:**
|
||||
|
||||
- PyTorch CUDA build (cu121)
|
||||
- Qwen3-TTS package
|
||||
- CUDA runtime, cuDNN, cuBLAS
|
||||
- Transformers
|
||||
|
||||
**Download source:** Cloudflare R2
|
||||
**Platform:** Windows + Linux (NVIDIA GPU)
|
||||
**Updates rarely:** Only when model code or CUDA version changes
|
||||
|
||||
---
|
||||
|
||||
#### 4. TTS Provider: MLX
|
||||
|
||||
**Binary:** `tts-provider-mlx`
|
||||
**Size:** ~150MB
|
||||
|
||||
**Includes:**
|
||||
|
||||
- MLX framework
|
||||
- MLX-optimized Qwen3-TTS
|
||||
- Metal acceleration
|
||||
|
||||
**Platform:** macOS only (Apple Silicon)
|
||||
**Download source:** Cloudflare R2
|
||||
|
||||
---
|
||||
|
||||
#### 5. TTS Provider: Remote
|
||||
|
||||
**Binary:** None (built-in config)
|
||||
**Size:** 0MB
|
||||
|
||||
**How it works:**
|
||||
|
||||
- User provides URL to their own TTS server
|
||||
- Backend proxies requests to that server
|
||||
- Implements API spec from `EXTERNAL_PROVIDERS.md`
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- AMD GPU users running their own server
|
||||
- Team deployments with shared GPU server
|
||||
- Cloud hosting (Modal, RunPod, Replicate)
|
||||
|
||||
---
|
||||
|
||||
#### 6. TTS Provider: OpenAI
|
||||
|
||||
**Binary:** None (API wrapper)
|
||||
**Size:** 0MB
|
||||
|
||||
**How it works:**
|
||||
|
||||
- User provides OpenAI API key
|
||||
- Backend wraps OpenAI Audio API
|
||||
- Voice profiles map to OpenAI voices
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Zero local compute
|
||||
- Pay-per-use
|
||||
- Instant setup
|
||||
|
||||
---
|
||||
|
||||
## Communication Protocol
|
||||
|
||||
### Provider API Specification
|
||||
|
||||
All TTS providers must implement these endpoints:
|
||||
|
||||
#### POST /tts/generate
|
||||
|
||||
Generate speech from text.
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Hello world!",
|
||||
"voice_prompt": {
|
||||
/* voice prompt object */
|
||||
},
|
||||
"language": "en",
|
||||
"seed": 12345,
|
||||
"model_size": "1.7B"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"audio": "base64-encoded-audio",
|
||||
"sample_rate": 24000,
|
||||
"duration": 2.5
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /tts/create_voice_prompt
|
||||
|
||||
Create voice prompt from reference audio.
|
||||
|
||||
**Request:** (multipart/form-data)
|
||||
|
||||
- `audio`: Audio file
|
||||
- `reference_text`: Transcript
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"voice_prompt": {
|
||||
/* serialized prompt */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /tts/health
|
||||
|
||||
Health check.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"provider": "pytorch-cuda",
|
||||
"version": "1.0.0",
|
||||
"model": "Qwen3-TTS-12Hz-1.7B-Base",
|
||||
"device": "cuda:0"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /tts/status
|
||||
|
||||
Model status.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"model_loaded": true,
|
||||
"model_size": "1.7B",
|
||||
"available_sizes": ["0.6B", "1.7B"],
|
||||
"gpu_available": true,
|
||||
"vram_used_mb": 1234
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### Provider Manager
|
||||
|
||||
**File:** `backend/providers/__init__.py`
|
||||
|
||||
```python
|
||||
class ProviderManager:
|
||||
"""Manages TTS provider lifecycle."""
|
||||
|
||||
def __init__(self):
|
||||
self.active_provider: Optional[Provider] = None
|
||||
self.config = load_provider_config()
|
||||
|
||||
async def start_provider(self, provider_type: str) -> str:
|
||||
"""Start a TTS provider process."""
|
||||
if provider_type == "pytorch-cpu":
|
||||
return await self._start_local_provider("tts-provider-pytorch-cpu.exe")
|
||||
elif provider_type == "pytorch-cuda":
|
||||
return await self._start_local_provider("tts-provider-pytorch-cuda.exe")
|
||||
elif provider_type == "mlx":
|
||||
return await self._start_local_provider("tts-provider-mlx")
|
||||
elif provider_type == "remote":
|
||||
return self.config["remote_url"]
|
||||
elif provider_type == "openai":
|
||||
return None # No subprocess, API wrapper
|
||||
|
||||
async def _start_local_provider(self, binary_name: str) -> str:
|
||||
"""Start local provider subprocess."""
|
||||
provider_path = get_provider_binary_path(binary_name)
|
||||
|
||||
if not provider_path.exists():
|
||||
raise ProviderNotInstalledException(binary_name)
|
||||
|
||||
# Start subprocess on random port
|
||||
port = get_free_port()
|
||||
process = subprocess.Popen([
|
||||
str(provider_path),
|
||||
"--port", str(port),
|
||||
"--data-dir", str(config.get_data_dir())
|
||||
])
|
||||
|
||||
# Wait for provider to be ready
|
||||
await wait_for_provider_health(f"http://localhost:{port}")
|
||||
|
||||
self.active_provider = Provider(process, port)
|
||||
return f"http://localhost:{port}"
|
||||
|
||||
async def stop_provider(self):
|
||||
"""Stop active provider."""
|
||||
if self.active_provider:
|
||||
self.active_provider.process.terminate()
|
||||
self.active_provider = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Provider Abstraction
|
||||
|
||||
**File:** `backend/providers/base.py`
|
||||
|
||||
```python
|
||||
class TTSProvider(ABC):
|
||||
"""Abstract base for TTS providers."""
|
||||
|
||||
@abstractmethod
|
||||
async def generate(
|
||||
self,
|
||||
text: str,
|
||||
voice_prompt: dict,
|
||||
language: str,
|
||||
seed: Optional[int]
|
||||
) -> tuple[np.ndarray, int]:
|
||||
"""Generate speech audio."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_voice_prompt(
|
||||
self,
|
||||
audio_path: str,
|
||||
reference_text: str
|
||||
) -> dict:
|
||||
"""Create voice prompt from reference audio."""
|
||||
pass
|
||||
```
|
||||
|
||||
**File:** `backend/providers/local.py`
|
||||
|
||||
```python
|
||||
class LocalProvider(TTSProvider):
|
||||
"""Provider that communicates with local subprocess via HTTP."""
|
||||
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = base_url
|
||||
self.client = httpx.AsyncClient()
|
||||
|
||||
async def generate(self, text, voice_prompt, language, seed):
|
||||
response = await self.client.post(
|
||||
f"{self.base_url}/tts/generate",
|
||||
json={
|
||||
"text": text,
|
||||
"voice_prompt": voice_prompt,
|
||||
"language": language,
|
||||
"seed": seed
|
||||
}
|
||||
)
|
||||
data = response.json()
|
||||
audio = np.frombuffer(base64.b64decode(data["audio"]), dtype=np.float32)
|
||||
return audio, data["sample_rate"]
|
||||
```
|
||||
|
||||
**File:** `backend/providers/openai.py`
|
||||
|
||||
```python
|
||||
class OpenAIProvider(TTSProvider):
|
||||
"""Provider that wraps OpenAI Audio API."""
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
self.client = OpenAI(api_key=api_key)
|
||||
|
||||
async def generate(self, text, voice_prompt, language, seed):
|
||||
# Map voice_prompt to OpenAI voice name
|
||||
voice = map_profile_to_openai_voice(voice_prompt)
|
||||
|
||||
response = await self.client.audio.speech.create(
|
||||
model="tts-1",
|
||||
voice=voice,
|
||||
input=text
|
||||
)
|
||||
|
||||
# Convert to numpy array
|
||||
audio_data = response.content
|
||||
audio, sr = load_audio_from_bytes(audio_data)
|
||||
return audio, sr
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Provider Installation
|
||||
|
||||
### Download Manager
|
||||
|
||||
**File:** `backend/providers/installer.py`
|
||||
|
||||
```python
|
||||
class ProviderInstaller:
|
||||
"""Handles provider download and installation."""
|
||||
|
||||
async def download_provider(self, provider_type: str):
|
||||
"""Download provider binary from R2."""
|
||||
|
||||
binary_name = {
|
||||
"pytorch-cpu": "tts-provider-pytorch-cpu.exe",
|
||||
"pytorch-cuda": "tts-provider-pytorch-cuda.exe",
|
||||
"mlx": "tts-provider-mlx"
|
||||
}[provider_type]
|
||||
|
||||
download_url = f"https://downloads.voicebox.sh/providers/v{PROVIDER_VERSION}/{binary_name}"
|
||||
|
||||
# Download with progress tracking (reuse existing SSE system)
|
||||
await download_with_progress(
|
||||
url=download_url,
|
||||
destination=get_provider_install_path(binary_name),
|
||||
progress_key=f"provider-{provider_type}"
|
||||
)
|
||||
```
|
||||
|
||||
**Provider Storage Location:**
|
||||
|
||||
- Windows: `%APPDATA%/voicebox/providers/`
|
||||
- macOS: `~/Library/Application Support/voicebox/providers/`
|
||||
- Linux: `~/.local/share/voicebox/providers/`
|
||||
|
||||
---
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### Provider Settings UI
|
||||
|
||||
**Component:** `app/src/components/ServerSettings/ProviderSettings.tsx`
|
||||
|
||||
```tsx
|
||||
export function ProviderSettings() {
|
||||
const [selectedProvider, setSelectedProvider] =
|
||||
useState<ProviderType>("auto");
|
||||
const {data: installedProviders} = useQuery({
|
||||
queryKey: ["providers", "installed"],
|
||||
queryFn: () => apiClient.getInstalledProviders(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>TTS Provider</CardTitle>
|
||||
<CardDescription>Choose how Voicebox generates speech</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RadioGroup
|
||||
value={selectedProvider}
|
||||
onValueChange={setSelectedProvider}
|
||||
>
|
||||
{/* Auto-detect */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="auto" id="auto" />
|
||||
<Label htmlFor="auto">
|
||||
<div className="font-medium">Auto-detect (Recommended)</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Automatically choose the best available provider
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* PyTorch CUDA */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="pytorch-cuda"
|
||||
id="cuda"
|
||||
disabled={!gpuAvailable}
|
||||
/>
|
||||
<Label htmlFor="cuda">
|
||||
<div className="font-medium">PyTorch CUDA (NVIDIA GPU)</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
4-5x faster inference on NVIDIA GPUs
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{!installedProviders?.includes("pytorch-cuda") && gpuAvailable && (
|
||||
<Button
|
||||
onClick={() => downloadProvider("pytorch-cuda")}
|
||||
size="sm"
|
||||
>
|
||||
Download (2.4GB)
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* PyTorch CPU */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="pytorch-cpu" id="cpu" />
|
||||
<Label htmlFor="cpu">
|
||||
<div className="font-medium">PyTorch CPU</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Works on any system, slower inference
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{!installedProviders?.includes("pytorch-cpu") && (
|
||||
<Button onClick={() => downloadProvider("pytorch-cpu")} size="sm">
|
||||
Download (300MB)
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* MLX (macOS only) */}
|
||||
{isMacOS && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="mlx" id="mlx" />
|
||||
<Label htmlFor="mlx">
|
||||
<div className="font-medium">MLX (Apple Silicon)</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Optimized for M1/M2/M3 chips
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{!installedProviders?.includes("mlx") && (
|
||||
<Button onClick={() => downloadProvider("mlx")} size="sm">
|
||||
Download (800MB)
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remote */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="remote" id="remote" />
|
||||
<Label htmlFor="remote">
|
||||
<div className="font-medium">Remote Server</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Connect to your own TTS server
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{selectedProvider === "remote" && (
|
||||
<Input placeholder="http://your-server:8000" className="ml-6" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* OpenAI */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="openai" id="openai" />
|
||||
<Label htmlFor="openai">
|
||||
<div className="font-medium">OpenAI API</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Use OpenAI's TTS API (requires API key)
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{selectedProvider === "openai" && (
|
||||
<Input type="password" placeholder="sk-..." className="ml-6" />
|
||||
)}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
voicebox/
|
||||
├── backend/
|
||||
│ ├── main.py # Main FastAPI app (no TTS code)
|
||||
│ ├── providers/
|
||||
│ │ ├── __init__.py # ProviderManager
|
||||
│ │ ├── base.py # TTSProvider ABC
|
||||
│ │ ├── local.py # LocalProvider (subprocess)
|
||||
│ │ ├── remote.py # RemoteProvider (HTTP)
|
||||
│ │ ├── openai.py # OpenAIProvider (API wrapper)
|
||||
│ │ └── installer.py # Provider download logic
|
||||
│ ├── profiles.py # Voice profile management
|
||||
│ ├── history.py # Generation history
|
||||
│ ├── transcribe.py # Whisper (still bundled)
|
||||
│ └── ... (other backend modules)
|
||||
│
|
||||
├── providers/
|
||||
│ ├── pytorch-cpu/
|
||||
│ │ ├── main.py # FastAPI server for TTS
|
||||
│ │ ├── tts_backend.py # PyTorch TTS logic
|
||||
│ │ ├── requirements.txt # torch (CPU), qwen-tts, transformers
|
||||
│ │ └── build.spec # PyInstaller spec
|
||||
│ │
|
||||
│ ├── pytorch-cuda/
|
||||
│ │ ├── main.py # FastAPI server for TTS
|
||||
│ │ ├── tts_backend.py # PyTorch TTS logic
|
||||
│ │ ├── requirements.txt # torch+cu121, qwen-tts, transformers
|
||||
│ │ └── build.spec # PyInstaller spec
|
||||
│ │
|
||||
│ └── mlx/
|
||||
│ ├── main.py # FastAPI server for TTS
|
||||
│ ├── mlx_backend.py # MLX TTS logic
|
||||
│ ├── requirements.txt # mlx, qwen-tts-mlx
|
||||
│ └── build.spec # PyInstaller spec
|
||||
│
|
||||
├── app/ # Frontend (Tauri + React)
|
||||
│ └── src/
|
||||
│ └── components/
|
||||
│ └── ServerSettings/
|
||||
│ └── ProviderSettings.tsx
|
||||
│
|
||||
└── tauri/
|
||||
└── src-tauri/
|
||||
└── tauri.conf.json # No externalBin for providers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Path
|
||||
|
||||
### Phase 1: Refactor Backend (No User Changes)
|
||||
|
||||
**Goal:** Abstract TTS behind provider interface
|
||||
|
||||
1. Create `backend/providers/` module structure
|
||||
2. Implement `TTSProvider` abstract base class
|
||||
3. Create `LocalProvider` wrapper for current PyTorch code
|
||||
4. Modify `backend/tts.py` to use provider abstraction
|
||||
5. Keep PyTorch bundled in main app
|
||||
|
||||
**Result:** Code is prepared, but user experience unchanged
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Build Provider Binaries
|
||||
|
||||
**Goal:** Create standalone TTS provider executables
|
||||
|
||||
1. Create separate PyInstaller specs for each provider
|
||||
2. Build provider executables:
|
||||
- `tts-provider-pytorch-cpu.exe` (~300MB)
|
||||
- `tts-provider-pytorch-cuda.exe` (~2.4GB)
|
||||
- `tts-provider-mlx` (~800MB, macOS)
|
||||
3. Test subprocess communication
|
||||
4. Upload providers to Cloudflare R2
|
||||
|
||||
**Result:** Provider binaries exist but aren't used yet
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Remove PyTorch from Main App
|
||||
|
||||
**Goal:** Split main app from providers
|
||||
|
||||
1. Exclude PyTorch/Qwen3-TTS from main app PyInstaller spec
|
||||
2. Main app now requires provider download
|
||||
3. Update GitHub CI to build multiple artifacts:
|
||||
- `voicebox-{version}-{platform}.exe` (~150MB)
|
||||
- `tts-provider-pytorch-cpu-{version}.exe`
|
||||
- `tts-provider-pytorch-cuda-{version}.exe`
|
||||
- `tts-provider-mlx-{version}` (macOS)
|
||||
|
||||
**Result:** Main app is small, providers downloaded separately
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Add Provider UI
|
||||
|
||||
**Goal:** User-facing provider management
|
||||
|
||||
1. Create Provider Settings page
|
||||
2. Implement provider download UI
|
||||
3. Add provider status indicators
|
||||
4. Show active provider in UI
|
||||
|
||||
**Result:** Users can choose and download providers
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: External Providers
|
||||
|
||||
**Goal:** Enable remote and cloud providers
|
||||
|
||||
1. Implement `RemoteProvider` (HTTP client)
|
||||
2. Implement `OpenAIProvider` (API wrapper)
|
||||
3. Add provider configuration UI (URLs, API keys)
|
||||
4. Document external provider API spec
|
||||
|
||||
**Result:** Full provider ecosystem
|
||||
|
||||
---
|
||||
|
||||
## Provider Versioning
|
||||
|
||||
### Independent Versioning
|
||||
|
||||
Providers have their own version numbers, independent of the main app:
|
||||
|
||||
- **App version:** `v0.2.0` (frequent updates)
|
||||
- **Provider version:** `v1.0.0` (rare updates)
|
||||
|
||||
### Compatibility Matrix
|
||||
|
||||
**Example:**
|
||||
|
||||
| App Version | Min Provider Version | Max Provider Version |
|
||||
| ----------- | -------------------- | -------------------- |
|
||||
| v0.2.0 | v1.0.0 | v1.x.x |
|
||||
| v0.3.0 | v1.0.0 | v1.x.x |
|
||||
| v0.4.0 | v1.2.0 | v1.x.x |
|
||||
| v1.0.0 | v2.0.0 | v2.x.x |
|
||||
|
||||
**Backend checks compatibility:**
|
||||
|
||||
```python
|
||||
async def check_provider_compatibility(provider_version: str) -> bool:
|
||||
"""Check if provider version is compatible with current app."""
|
||||
min_version = "1.0.0"
|
||||
max_version = "1.999.999"
|
||||
return min_version <= provider_version < max_version
|
||||
```
|
||||
|
||||
**UI shows warning if incompatible:**
|
||||
|
||||
```
|
||||
⚠️ Provider version 0.9.0 is outdated. Update to v1.0.0+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Flows
|
||||
|
||||
### First-Time Setup
|
||||
|
||||
1. User downloads and installs Voicebox (~150MB)
|
||||
2. App launches → detects no TTS provider installed
|
||||
3. Shows setup wizard:
|
||||
|
||||
```
|
||||
Choose your TTS provider:
|
||||
|
||||
[ ] PyTorch CUDA (2.4GB) [Download]
|
||||
✓ Fastest on NVIDIA GPUs
|
||||
✗ Requires NVIDIA GPU
|
||||
|
||||
[●] PyTorch CPU (300MB) [Download]
|
||||
✓ Works on any system
|
||||
✗ Slower inference
|
||||
|
||||
[ ] MLX (800MB) [Download]
|
||||
✓ Fast on Apple Silicon
|
||||
✗ macOS only (M1/M2/M3)
|
||||
|
||||
[ ] Remote Server
|
||||
URL: ___________________
|
||||
|
||||
[ ] OpenAI API
|
||||
API Key: ________________
|
||||
```
|
||||
|
||||
4. User selects provider → downloads with progress bar
|
||||
5. Provider installs to AppData/Application Support
|
||||
6. App starts provider → ready to use
|
||||
|
||||
---
|
||||
|
||||
### App Update Flow (No Provider Change)
|
||||
|
||||
**Scenario:** Bug fix in UI, no backend changes
|
||||
|
||||
1. User gets update notification: "Voicebox v0.2.1 available"
|
||||
2. Downloads update (~150MB, not 2.4GB!)
|
||||
3. Installs and restarts
|
||||
4. **Provider stays the same** (no re-download needed)
|
||||
5. App starts using existing provider
|
||||
|
||||
**User experience:** Fast updates, no multi-GB downloads
|
||||
|
||||
---
|
||||
|
||||
### Provider Update Flow
|
||||
|
||||
**Scenario:** New Qwen3-TTS model version released
|
||||
|
||||
1. User opens Settings → Provider tab
|
||||
2. Sees notification: "Provider update available (v1.1.0)"
|
||||
3. Clicks "Update Provider"
|
||||
4. Downloads new provider binary
|
||||
5. Old provider binary is replaced
|
||||
6. Restart app to use new provider
|
||||
|
||||
**Frequency:** Rare (only when TTS model/backend changes)
|
||||
|
||||
---
|
||||
|
||||
### Switching Providers
|
||||
|
||||
**Scenario:** User upgrades to NVIDIA GPU
|
||||
|
||||
1. User goes to Settings → Provider
|
||||
2. Selects "PyTorch CUDA"
|
||||
3. Clicks "Download" → downloads 2.4GB
|
||||
4. Download completes → restarts app
|
||||
5. App now uses CUDA provider
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
| Benefit | Details |
|
||||
| ----------------------------- | --------------------------------------------------------- |
|
||||
| **GitHub Releases Work** | Main app ~150MB << 2GB limit |
|
||||
| **Fast Updates** | UI/feature updates don't require re-downloading providers |
|
||||
| **User Choice** | CPU, CUDA, MLX, OpenAI, remote server |
|
||||
| **External Provider Support** | Users can run their own TTS servers |
|
||||
| **Bandwidth Savings** | Only download provider once, app updates are small |
|
||||
| **Future-Proof** | Easy to add new providers (ElevenLabs, custom models) |
|
||||
| **Team Deployments** | Multiple users share one remote provider |
|
||||
| **Cloud-Ready** | Works with Modal, Replicate, RunPod, etc. |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
### 1. Provider Versioning
|
||||
|
||||
**Question:** Should providers have independent versions or match app version?
|
||||
|
||||
**Options:**
|
||||
|
||||
- A. Independent (providers: v1.x, app: v0.2.x)
|
||||
- B. Matched (both use v0.2.x)
|
||||
|
||||
**Recommendation:** Independent versioning with compatibility matrix
|
||||
|
||||
---
|
||||
|
||||
### 2. Auto-Update Providers
|
||||
|
||||
**Question:** Should providers auto-update separately from app?
|
||||
|
||||
**Options:**
|
||||
|
||||
- A. Manual updates only (user clicks "Update Provider")
|
||||
- B. Optional auto-update (user can enable)
|
||||
- C. Always auto-update
|
||||
|
||||
**Recommendation:** Optional auto-update (default off)
|
||||
|
||||
---
|
||||
|
||||
### 3. Provider Discovery
|
||||
|
||||
**Question:** How does app find installed providers?
|
||||
|
||||
**Options:**
|
||||
|
||||
- A. Check standard paths in AppData/Application Support
|
||||
- B. Registry (Windows) / plist (macOS)
|
||||
- C. Config file with provider locations
|
||||
|
||||
**Recommendation:** Standard paths + config fallback
|
||||
|
||||
---
|
||||
|
||||
### 4. Fallback Behavior
|
||||
|
||||
**Question:** What if no provider is installed?
|
||||
|
||||
**Options:**
|
||||
|
||||
- A. Show setup wizard on first launch
|
||||
- B. Block app until provider installed
|
||||
- C. Allow app to run in "demo mode" (transcription only)
|
||||
|
||||
**Recommendation:** Setup wizard on first launch
|
||||
|
||||
---
|
||||
|
||||
### 5. Provider Auto-Start
|
||||
|
||||
**Question:** Should provider start automatically with app?
|
||||
|
||||
**Options:**
|
||||
|
||||
- A. Always start selected provider on app launch
|
||||
- B. Start on-demand (when user generates speech)
|
||||
- C. User preference
|
||||
|
||||
**Recommendation:** Auto-start (configurable in settings)
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] **Provider Marketplace:** Built-in directory of community providers
|
||||
- [ ] **Multi-Provider Support:** Use different providers per voice/language
|
||||
- [ ] **Provider Health Monitoring:** Automatic failover if provider crashes
|
||||
- [ ] **Cost Tracking:** Monitor API usage for OpenAI/cloud providers
|
||||
- [ ] **Performance Metrics:** Latency, throughput, VRAM usage dashboards
|
||||
- [ ] **Docker Providers:** Run providers in Docker containers
|
||||
- [ ] **Provider Plugins:** Load custom providers from user scripts
|
||||
|
||||
---
|
||||
|
||||
## Related Documents
|
||||
|
||||
- [EXTERNAL_PROVIDERS.md](./EXTERNAL_PROVIDERS.md) - External provider support plan
|
||||
- [OPENAI_SUPPORT.md](./OPENAI_SUPPORT.md) - OpenAI API compatibility
|
||||
- [github-2gb-limit-issue.md](../github-2gb-limit-issue.md) - Original problem
|
||||
- [r2-setup.md](../r2-setup.md) - Cloudflare R2 configuration
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
If you want to build a custom TTS provider:
|
||||
|
||||
1. Implement the provider API spec (see above)
|
||||
2. Test with Voicebox locally
|
||||
3. Package as executable (PyInstaller, Docker, etc.)
|
||||
4. Share in GitHub Discussions
|
||||
|
||||
**Questions?**
|
||||
|
||||
- GitHub Issues: [voicebox/issues](https://github.com/jamiepine/voicebox/issues)
|
||||
- Discord: Coming soon
|
||||
@@ -239,8 +239,9 @@ export default function Home() {
|
||||
<div className="space-y-6 text-lg text-foreground/80 text-center">
|
||||
<p>
|
||||
Voicebox is a <strong>local-first voice cloning studio</strong> with DAW-like features
|
||||
for professional voice synthesis. Think of it as the <strong>Ollama for voice</strong>{' '}
|
||||
— download models, clone voices, and generate speech entirely on your machine.
|
||||
for professional voice synthesis. Think of it as a{' '}
|
||||
<strong>local, free and open-source alternative to ElevenLabs</strong> — download
|
||||
models, clone voices, and generate speech entirely on your machine.
|
||||
</p>
|
||||
<p>
|
||||
Unlike cloud services that lock your voice data behind subscriptions, Voicebox gives
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::audio_capture::AudioCaptureState;
|
||||
|
||||
pub async fn start_capture(
|
||||
state: &AudioCaptureState,
|
||||
max_duration_secs: u32,
|
||||
) -> Result<(), String> {
|
||||
todo!("implement Linux audio capture")
|
||||
}
|
||||
|
||||
pub async fn stop_capture(state: &AudioCaptureState) -> Result<String, String> {
|
||||
todo!("implement Linux audio capture stop")
|
||||
}
|
||||
|
||||
pub fn is_supported() -> bool {
|
||||
false
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
mod macos;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::*;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows::*;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::*;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
||||
@@ -10,9 +10,20 @@ export const tauriFilesystem: PlatformFilesystem = {
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
const { writeBinaryFile } = await import('@tauri-apps/plugin-fs');
|
||||
let resolvedPath = '';
|
||||
if (typeof filePath === 'string') {
|
||||
resolvedPath = filePath;
|
||||
} else if (filePath && typeof filePath === 'object' && 'path' in filePath) {
|
||||
resolvedPath = (filePath as { path: string }).path;
|
||||
}
|
||||
|
||||
if (!resolvedPath) {
|
||||
throw new Error('Failed to resolve save path');
|
||||
}
|
||||
|
||||
const { writeFile } = await import('@tauri-apps/plugin-fs');
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
await writeBinaryFile(filePath, new Uint8Array(arrayBuffer));
|
||||
await writeFile(resolvedPath, new Uint8Array(arrayBuffer));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to use Tauri dialog, falling back to browser download:', error);
|
||||
|
||||
Reference in New Issue
Block a user