From 0209008d735743f4c5c6ed6dcfae3e55a8fe7c1b Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 31 Jan 2026 01:46:14 -0800 Subject: [PATCH 01/12] disable cuda for 0.1.12 --- .github/workflows/release.yml | 42 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95956a0c..9e65f520 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: tags: - - 'v*' + - "v*" jobs: release: @@ -14,22 +14,22 @@ jobs: fail-fast: false matrix: include: - - platform: 'macos-latest' - args: '--target aarch64-apple-darwin' - python-version: '3.12' - backend: 'mlx' - - platform: 'macos-15-intel' - args: '--target x86_64-apple-darwin' - python-version: '3.12' - backend: 'pytorch' + - platform: "macos-latest" + args: "--target aarch64-apple-darwin" + python-version: "3.12" + backend: "mlx" + - platform: "macos-15-intel" + args: "--target x86_64-apple-darwin" + python-version: "3.12" + backend: "pytorch" # - platform: 'ubuntu-22.04' # args: '' # python-version: '3.12' # backend: 'pytorch' - - platform: 'windows-latest' - args: '' - python-version: '3.12' - backend: 'pytorch' + - platform: "windows-latest" + args: "" + python-version: "3.12" + backend: "pytorch" runs-on: ${{ matrix.platform }} @@ -53,7 +53,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: 'pip' + cache: "pip" - name: Install Python dependencies run: | @@ -66,11 +66,11 @@ jobs: run: | pip install -r backend/requirements-mlx.txt - - name: Install PyTorch with CUDA (Windows only) - if: matrix.platform == 'windows-latest' - run: | - pip install torch --index-url https://download.pytorch.org/whl/cu121 --force-reinstall --no-deps - pip install torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 + # - name: Install PyTorch with CUDA (Windows only) + # if: matrix.platform == 'windows-latest' + # run: | + # pip install torch --index-url https://download.pytorch.org/whl/cu121 --force-reinstall --no-deps + # pip install torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 - name: Build Python server (Linux/macOS) if: matrix.platform != 'windows-latest' @@ -106,7 +106,7 @@ jobs: - name: Rust cache uses: swatinem/rust-cache@v2 with: - workspaces: './tauri/src-tauri -> target' + workspaces: "./tauri/src-tauri -> target" - name: Install dependencies run: bun install @@ -142,7 +142,7 @@ jobs: with: projectPath: tauri tagName: v__VERSION__ - releaseName: 'voicebox v__VERSION__' + releaseName: "voicebox v__VERSION__" releaseBody: | ## What's Changed See the assets below to download and install this version. From 2bc243f93e463d91552d86b4ac3af7d7011bebeb Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 31 Jan 2026 02:09:29 -0800 Subject: [PATCH 02/12] Add TTS Provider Architecture plan Solves GitHub 2GB limit + frequent update UX issues by splitting app into: - Main app (~150MB): UI + backend logic + Whisper - TTS Providers (plugins): Separate downloadable binaries - pytorch-cpu (~300MB) - pytorch-cuda (~2.4GB) - mlx (~800MB, macOS) - remote (connect to external server) - openai (API wrapper) Benefits: - Main app under GitHub 2GB limit - Updates don't require re-downloading providers - User choice of compute backend - External provider support for teams/cloud - Future-proof extensibility --- docs/plans/TTS_PROVIDER_ARCHITECTURE.md | 932 ++++++++++++++++++++++++ 1 file changed, 932 insertions(+) create mode 100644 docs/plans/TTS_PROVIDER_ARCHITECTURE.md diff --git a/docs/plans/TTS_PROVIDER_ARCHITECTURE.md b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md new file mode 100644 index 00000000..37ad40f7 --- /dev/null +++ b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md @@ -0,0 +1,932 @@ +# TTS Provider Architecture + +**Status:** Planned for v0.2.0 +**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 ML models +- ✅ 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:** ~150-200MB + +**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:** ~300MB + +**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:** ~800MB + +**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('auto'); + const { data: installedProviders } = useQuery({ + queryKey: ['providers', 'installed'], + queryFn: () => apiClient.getInstalledProviders() + }); + + return ( + + + TTS Provider + + Choose how Voicebox generates speech + + + + + + {/* Auto-detect */} +
+ + +
+ + {/* PyTorch CUDA */} +
+
+ + +
+ {!installedProviders?.includes('pytorch-cuda') && gpuAvailable && ( + + )} +
+ + {/* PyTorch CPU */} +
+
+ + +
+ {!installedProviders?.includes('pytorch-cpu') && ( + + )} +
+ + {/* MLX (macOS only) */} + {isMacOS && ( +
+
+ + +
+ {!installedProviders?.includes('mlx') && ( + + )} +
+ )} + + {/* Remote */} +
+
+ + +
+ {selectedProvider === 'remote' && ( + + )} +
+ + {/* OpenAI */} +
+
+ + +
+ {selectedProvider === 'openai' && ( + + )} +
+ +
+
+
+ ); +} +``` + +--- + +## 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 From cb541521d2a090fe70dc8dda079a1ccc86e3b027 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 31 Jan 2026 02:11:41 -0800 Subject: [PATCH 03/12] Update TTS Provider Architecture status to v0.1.13 --- docs/plans/TTS_PROVIDER_ARCHITECTURE.md | 344 +++++++++++++----------- 1 file changed, 188 insertions(+), 156 deletions(-) diff --git a/docs/plans/TTS_PROVIDER_ARCHITECTURE.md b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md index 37ad40f7..417c840e 100644 --- a/docs/plans/TTS_PROVIDER_ARCHITECTURE.md +++ b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md @@ -1,6 +1,6 @@ # TTS Provider Architecture -**Status:** Planned for v0.2.0 +**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 @@ -14,6 +14,7 @@ Split the monolithic backend into modular components: 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 ML models - ✅ User choice of compute backend (CPU/GPU/Cloud) @@ -68,6 +69,7 @@ This architecture solves: ### Current Architecture Issues **Monolithic Binary:** + - CPU version: ~295MB - CUDA version: ~2.37GB - GitHub releases: 2GB file size limit (BLOCKED) @@ -75,6 +77,7 @@ This architecture solves: - 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.) @@ -91,6 +94,7 @@ This architecture solves: **Size:** ~150-200MB **Includes:** + - Tauri runtime + React UI - FastAPI backend (pure Python, no PyTorch) - Whisper model (tiny, ~50MB) @@ -99,6 +103,7 @@ This architecture solves: - Provider management system **Does NOT include:** + - PyTorch (CPU or CUDA) - TTS models (Qwen3-TTS) - Heavy ML dependencies @@ -113,6 +118,7 @@ This architecture solves: **Size:** ~300MB **Includes:** + - PyTorch CPU build - Qwen3-TTS package - Transformers @@ -129,6 +135,7 @@ This architecture solves: **Size:** ~2.4GB **Includes:** + - PyTorch CUDA build (cu121) - Qwen3-TTS package - CUDA runtime, cuDNN, cuBLAS @@ -146,6 +153,7 @@ This architecture solves: **Size:** ~800MB **Includes:** + - MLX framework - MLX-optimized Qwen3-TTS - Metal acceleration @@ -161,11 +169,13 @@ This architecture solves: **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) @@ -178,11 +188,13 @@ This architecture solves: **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 @@ -200,22 +212,26 @@ All TTS providers must implement these endpoints: Generate speech from text. **Request:** + ```json { - "text": "Hello world!", - "voice_prompt": { /* voice prompt object */ }, - "language": "en", - "seed": 12345, - "model_size": "1.7B" + "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 + "audio": "base64-encoded-audio", + "sample_rate": 24000, + "duration": 2.5 } ``` @@ -224,13 +240,17 @@ Generate speech from text. Create voice prompt from reference audio. **Request:** (multipart/form-data) + - `audio`: Audio file - `reference_text`: Transcript **Response:** + ```json { - "voice_prompt": { /* serialized prompt */ } + "voice_prompt": { + /* serialized prompt */ + } } ``` @@ -239,13 +259,14 @@ Create voice prompt from reference audio. Health check. **Response:** + ```json { - "status": "healthy", - "provider": "pytorch-cuda", - "version": "1.0.0", - "model": "Qwen3-TTS-12Hz-1.7B-Base", - "device": "cuda:0" + "status": "healthy", + "provider": "pytorch-cuda", + "version": "1.0.0", + "model": "Qwen3-TTS-12Hz-1.7B-Base", + "device": "cuda:0" } ``` @@ -254,13 +275,14 @@ Health check. Model status. **Response:** + ```json { - "model_loaded": true, - "model_size": "1.7B", - "available_sizes": ["0.6B", "1.7B"], - "gpu_available": true, - "vram_used_mb": 1234 + "model_loaded": true, + "model_size": "1.7B", + "available_sizes": ["0.6B", "1.7B"], + "gpu_available": true, + "vram_used_mb": 1234 } ``` @@ -434,6 +456,7 @@ class ProviderInstaller: ``` **Provider Storage Location:** + - Windows: `%APPDATA%/voicebox/providers/` - macOS: `~/Library/Application Support/voicebox/providers/` - Linux: `~/.local/share/voicebox/providers/` @@ -448,133 +471,133 @@ class ProviderInstaller: ```tsx export function ProviderSettings() { - const [selectedProvider, setSelectedProvider] = useState('auto'); - const { data: installedProviders } = useQuery({ - queryKey: ['providers', 'installed'], - queryFn: () => apiClient.getInstalledProviders() - }); + const [selectedProvider, setSelectedProvider] = + useState("auto"); + const {data: installedProviders} = useQuery({ + queryKey: ["providers", "installed"], + queryFn: () => apiClient.getInstalledProviders(), + }); - return ( - - - TTS Provider - - Choose how Voicebox generates speech - - - - + return ( + + + TTS Provider + Choose how Voicebox generates speech + + + + {/* Auto-detect */} +
+ + +
- {/* Auto-detect */} -
- - -
+ {/* PyTorch CUDA */} +
+
+ + +
+ {!installedProviders?.includes("pytorch-cuda") && gpuAvailable && ( + + )} +
- {/* PyTorch CUDA */} -
-
- - -
- {!installedProviders?.includes('pytorch-cuda') && gpuAvailable && ( - - )} -
+ {/* PyTorch CPU */} +
+
+ + +
+ {!installedProviders?.includes("pytorch-cpu") && ( + + )} +
- {/* PyTorch CPU */} -
-
- - -
- {!installedProviders?.includes('pytorch-cpu') && ( - - )} -
+ {/* MLX (macOS only) */} + {isMacOS && ( +
+
+ + +
+ {!installedProviders?.includes("mlx") && ( + + )} +
+ )} - {/* MLX (macOS only) */} - {isMacOS && ( -
-
- - -
- {!installedProviders?.includes('mlx') && ( - - )} -
- )} + {/* Remote */} +
+
+ + +
+ {selectedProvider === "remote" && ( + + )} +
- {/* Remote */} -
-
- - -
- {selectedProvider === 'remote' && ( - - )} -
- - {/* OpenAI */} -
-
- - -
- {selectedProvider === 'openai' && ( - - )} -
- -
-
-
- ); + {/* OpenAI */} +
+
+ + +
+ {selectedProvider === "openai" && ( + + )} +
+
+
+
+ ); } ``` @@ -718,11 +741,11 @@ Providers have their own version numbers, independent of the main app: **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 | +| ----------- | -------------------- | -------------------- | +| 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:** @@ -735,6 +758,7 @@ async def check_provider_compatibility(provider_version: str) -> bool: ``` **UI shows warning if incompatible:** + ``` ⚠️ Provider version 0.9.0 is outdated. Update to v1.0.0+ ``` @@ -748,6 +772,7 @@ async def check_provider_compatibility(provider_version: str) -> bool: 1. User downloads and installs Voicebox (~150MB) 2. App launches → detects no TTS provider installed 3. Shows setup wizard: + ``` Choose your TTS provider: @@ -769,6 +794,7 @@ async def check_provider_compatibility(provider_version: str) -> bool: [ ] 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 @@ -818,16 +844,16 @@ async def check_provider_compatibility(provider_version: str) -> bool: ## 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. | +| 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. | --- @@ -838,6 +864,7 @@ async def check_provider_compatibility(provider_version: str) -> bool: **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) @@ -850,6 +877,7 @@ async def check_provider_compatibility(provider_version: str) -> bool: **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 @@ -863,6 +891,7 @@ async def check_provider_compatibility(provider_version: str) -> bool: **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 @@ -876,6 +905,7 @@ async def check_provider_compatibility(provider_version: str) -> bool: **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) @@ -889,6 +919,7 @@ async def check_provider_compatibility(provider_version: str) -> bool: **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 @@ -928,5 +959,6 @@ If you want to build a custom TTS provider: 4. Share in GitHub Discussions **Questions?** + - GitHub Issues: [voicebox/issues](https://github.com/jamiepine/voicebox/issues) - Discord: Coming soon From e796412c2ca820f1f5915bf9dc009bf330152c85 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 31 Jan 2026 02:13:42 -0800 Subject: [PATCH 04/12] corrections --- docs/plans/TTS_PROVIDER_ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plans/TTS_PROVIDER_ARCHITECTURE.md b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md index 417c840e..4a750279 100644 --- a/docs/plans/TTS_PROVIDER_ARCHITECTURE.md +++ b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md @@ -16,7 +16,7 @@ Split the monolithic backend into modular components: This architecture solves: - ✅ GitHub 2GB release artifact limit -- ✅ Frequent app updates without re-downloading large ML models +- ✅ 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 From e194e95512a912e3a5b9950007a40af3368242db Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 31 Jan 2026 02:14:37 -0800 Subject: [PATCH 05/12] corrections --- docs/plans/TTS_PROVIDER_ARCHITECTURE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plans/TTS_PROVIDER_ARCHITECTURE.md b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md index 4a750279..7fbdf4b3 100644 --- a/docs/plans/TTS_PROVIDER_ARCHITECTURE.md +++ b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md @@ -115,7 +115,7 @@ This architecture solves: #### 2. TTS Provider: PyTorch CPU **Binary:** `tts-provider-pytorch-cpu.exe` -**Size:** ~300MB +**Size:** ~200MB **Includes:** @@ -150,7 +150,7 @@ This architecture solves: #### 4. TTS Provider: MLX **Binary:** `tts-provider-mlx` -**Size:** ~800MB +**Size:** ~150MB **Includes:** From 220333b3bb83d2f540aca409e65413c8ab5e8c1b Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 31 Jan 2026 02:15:45 -0800 Subject: [PATCH 06/12] corrections --- docs/plans/TTS_PROVIDER_ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plans/TTS_PROVIDER_ARCHITECTURE.md b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md index 7fbdf4b3..8d35a7e5 100644 --- a/docs/plans/TTS_PROVIDER_ARCHITECTURE.md +++ b/docs/plans/TTS_PROVIDER_ARCHITECTURE.md @@ -91,7 +91,7 @@ This architecture solves: #### 1. Main App (voicebox.exe / .app / .AppImage) -**Size:** ~150-200MB +**Size:** ~100-150MB **Includes:** From 610f64c762653abb8fa40a34e438eb9ec263ffae Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sat, 31 Jan 2026 07:44:28 -0800 Subject: [PATCH 07/12] fix linux compile --- tauri/src-tauri/src/audio_capture/linux.rs | 16 ++++++++++++++++ tauri/src-tauri/src/audio_capture/mod.rs | 4 ++++ 2 files changed, 20 insertions(+) create mode 100644 tauri/src-tauri/src/audio_capture/linux.rs diff --git a/tauri/src-tauri/src/audio_capture/linux.rs b/tauri/src-tauri/src/audio_capture/linux.rs new file mode 100644 index 00000000..8af26e97 --- /dev/null +++ b/tauri/src-tauri/src/audio_capture/linux.rs @@ -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 { + todo!("implement Linux audio capture stop") +} + +pub fn is_supported() -> bool { + false +} diff --git a/tauri/src-tauri/src/audio_capture/mod.rs b/tauri/src-tauri/src/audio_capture/mod.rs index 7a55c334..a67bf795 100644 --- a/tauri/src-tauri/src/audio_capture/mod.rs +++ b/tauri/src-tauri/src/audio_capture/mod.rs @@ -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}; From b9c858295db5a3f5b39c8236a8de04eb305516bf Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sun, 1 Feb 2026 00:45:47 -0800 Subject: [PATCH 08/12] Update Voicebox description as an alternative to ElevenLabs, rather than Ollama --- README.md | 2 +- docs/overview/introduction.mdx | 2 +- landing/src/app/page.tsx | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0b67840b..575918cf 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/docs/overview/introduction.mdx b/docs/overview/introduction.mdx index 99569a73..21edd4fc 100644 --- a/docs/overview/introduction.mdx +++ b/docs/overview/introduction.mdx @@ -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. Voicebox App Screenshot diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx index 46cf3556..4ba9d8c4 100644 --- a/landing/src/app/page.tsx +++ b/landing/src/app/page.tsx @@ -239,8 +239,9 @@ export default function Home() {

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. + 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 From 04f9880c9ab3f83495c51eda59d1b3fc1447361a Mon Sep 17 00:00:00 2001 From: Reese Wright Date: Mon, 2 Feb 2026 14:26:34 +0000 Subject: [PATCH 09/12] fix audio export path resolution --- tauri/src/platform/filesystem.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tauri/src/platform/filesystem.ts b/tauri/src/platform/filesystem.ts index e37b4d18..fc5083b9 100644 --- a/tauri/src/platform/filesystem.ts +++ b/tauri/src/platform/filesystem.ts @@ -10,9 +10,20 @@ export const tauriFilesystem: PlatformFilesystem = { }); if (filePath) { - const { writeBinaryFile } = await import('@tauri-apps/plugin-fs'); + const resolvedPath = + typeof filePath === 'string' + ? filePath + : filePath && typeof filePath === 'object' && 'path' in filePath + ? (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); From 99fbcca7f495c3759aa8c9b1b18bee4c814ad28a Mon Sep 17 00:00:00 2001 From: Reese Wright Date: Mon, 2 Feb 2026 14:34:39 +0000 Subject: [PATCH 10/12] update CHANGELOG for audio export fix --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47e1dfd0..b7116d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From d40f7d2676b8558de14e6997d564328a10108e07 Mon Sep 17 00:00:00 2001 From: Reese Wright Date: Mon, 2 Feb 2026 14:54:05 +0000 Subject: [PATCH 11/12] refactor: improve path resolution readability --- tauri/src/platform/filesystem.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tauri/src/platform/filesystem.ts b/tauri/src/platform/filesystem.ts index fc5083b9..2292a99a 100644 --- a/tauri/src/platform/filesystem.ts +++ b/tauri/src/platform/filesystem.ts @@ -10,12 +10,12 @@ export const tauriFilesystem: PlatformFilesystem = { }); if (filePath) { - const resolvedPath = - typeof filePath === 'string' - ? filePath - : filePath && typeof filePath === 'object' && 'path' in filePath - ? (filePath as { path: string }).path - : ''; + 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'); From 6f4503b5212a31f3ef453904382ad5ff66867414 Mon Sep 17 00:00:00 2001 From: Sergej Lopatkin Date: Mon, 2 Feb 2026 22:11:04 +0100 Subject: [PATCH 12/12] Enhances floating generate box UX - Adds tooltips on hover for buttons of the generate box - Replaces the message square icon with a sliders icon for the instruction mode toggle. - Adds a tooltip to the instruction mode toggle button. - Updates the placeholder text for the input field. --- .../Generation/FloatingGenerateBox.tsx | 72 +++++++++++-------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/app/src/components/Generation/FloatingGenerateBox.tsx b/app/src/components/Generation/FloatingGenerateBox.tsx index b020a81f..a8d556a6 100644 --- a/app/src/components/Generation/FloatingGenerateBox.tsx +++ b/app/src/components/Generation/FloatingGenerateBox.tsx @@ -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({ }} >

@@ -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({
- +
+ + + {isPending + ? 'Generating...' + : !selectedProfileId + ? 'Select a voice profile first' + : 'Generate speech'} + +
{isExpanded && ( - +
+ + + Fine tune instructions + +
)}