mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-02 16:45:15 -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]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Audio export failing when Tauri save dialog returns object instead of string path
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Makefile** - Comprehensive development workflow automation with commands for setup, development, building, testing, and code quality checks
|
- **Makefile** - Comprehensive development workflow automation with commands for setup, development, building, testing, and code quality checks
|
||||||
- Includes Python version detection and compatibility warnings
|
- Includes Python version detection and compatibility warnings
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
## What is Voicebox?
|
## 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:
|
Unlike cloud services that lock your voice data behind subscriptions, Voicebox gives you:
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMatchRoute } from '@tanstack/react-router';
|
import { useMatchRoute } from '@tanstack/react-router';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
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 { useEffect, useRef, useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
||||||
@@ -187,7 +187,7 @@ export function FloatingGenerateBox({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<motion.div
|
<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' }}
|
transition={{ duration: 0.6, ease: 'easeInOut' }}
|
||||||
>
|
>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -274,7 +274,7 @@ export function FloatingGenerateBox({
|
|||||||
field.ref(node);
|
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"
|
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={{
|
style={{
|
||||||
minHeight: isExpanded ? '100px' : '32px',
|
minHeight: isExpanded ? '100px' : '32px',
|
||||||
@@ -294,18 +294,27 @@ export function FloatingGenerateBox({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="relative shrink-0">
|
<div className="relative shrink-0">
|
||||||
<Button
|
<div className="group relative">
|
||||||
type="submit"
|
<Button
|
||||||
disabled={isPending || !selectedProfileId}
|
type="submit"
|
||||||
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"
|
disabled={isPending || !selectedProfileId}
|
||||||
size="icon"
|
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" />
|
{isPending ? (
|
||||||
) : (
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
<Sparkles className="h-4 w-4" />
|
) : (
|
||||||
)}
|
<Sparkles className="h-4 w-4" />
|
||||||
</Button>
|
)}
|
||||||
|
</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>
|
<AnimatePresence>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -315,20 +324,25 @@ export function FloatingGenerateBox({
|
|||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="absolute top-0 right-[calc(100%+0.5rem)]"
|
className="absolute top-0 right-[calc(100%+0.5rem)]"
|
||||||
>
|
>
|
||||||
<Button
|
<div className="group relative">
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
size="icon"
|
variant="ghost"
|
||||||
onClick={() => setIsInstructMode(!isInstructMode)}
|
size="icon"
|
||||||
className={cn(
|
onClick={() => setIsInstructMode(!isInstructMode)}
|
||||||
'h-10 w-10 rounded-full transition-all duration-200',
|
className={cn(
|
||||||
isInstructMode
|
'h-10 w-10 rounded-full transition-all duration-200',
|
||||||
? 'bg-accent text-accent-foreground border border-accent hover:bg-accent/90'
|
isInstructMode
|
||||||
: 'bg-card border border-border hover:bg-background/50',
|
? '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>
|
<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>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description: "Welcome to Voicebox - the open-source voice synthesis studio"
|
|||||||
|
|
||||||
## What is Voicebox?
|
## 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>
|
<Frame>
|
||||||
<img src="/images/app-screenshot-1.webp" alt="Voicebox App Screenshot" />
|
<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">
|
<div className="space-y-6 text-lg text-foreground/80 text-center">
|
||||||
<p>
|
<p>
|
||||||
Voicebox is a <strong>local-first voice cloning studio</strong> with DAW-like features
|
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>{' '}
|
for professional voice synthesis. Think of it as a{' '}
|
||||||
— download models, clone voices, and generate speech entirely on your machine.
|
<strong>local, free and open-source alternative to ElevenLabs</strong> — download
|
||||||
|
models, clone voices, and generate speech entirely on your machine.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Unlike cloud services that lock your voice data behind subscriptions, Voicebox gives
|
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;
|
mod macos;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
mod windows;
|
mod windows;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub use macos::*;
|
pub use macos::*;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use windows::*;
|
pub use windows::*;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub use linux::*;
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,20 @@ export const tauriFilesystem: PlatformFilesystem = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (filePath) {
|
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();
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
await writeBinaryFile(filePath, new Uint8Array(arrayBuffer));
|
await writeFile(resolvedPath, new Uint8Array(arrayBuffer));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to use Tauri dialog, falling back to browser download:', error);
|
console.error('Failed to use Tauri dialog, falling back to browser download:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user