# Tauri App Plan Plan for building voicebox as a Tauri 2.0 desktop app with shared frontend code for web deployment. --- ## Project Structure ``` voicebox/ ├── app/ # Shared React frontend (used by both web & desktop) │ ├── src/ │ │ ├── components/ │ │ │ ├── VoiceProfiles/ │ │ │ ├── Generation/ │ │ │ ├── AudioStudio/ │ │ │ ├── History/ │ │ │ └── ServerSettings/ │ │ ├── lib/ │ │ │ ├── api/ # Generated OpenAPI client │ │ │ ├── hooks/ # React Query hooks │ │ │ └── utils/ │ │ ├── types/ │ │ ├── App.tsx │ │ └── main.tsx │ ├── package.json │ ├── tsconfig.json │ ├── vite.config.ts │ └── tailwind.config.ts │ ├── tauri/ # Tauri desktop app │ ├── src/ # Thin wrapper, imports from ../app │ │ └── main.tsx # Entry point that renders App from ../app │ ├── src-tauri/ # Rust backend │ │ ├── src/ │ │ │ └── main.rs │ │ ├── icons/ │ │ ├── binaries/ # Bundled Python server │ │ │ ├── voicebox-server-x86_64-apple-darwin │ │ │ ├── voicebox-server-aarch64-apple-darwin │ │ │ ├── voicebox-server-x86_64-unknown-linux-gnu │ │ │ └── voicebox-server-x86_64-pc-windows-msvc.exe │ │ ├── capabilities/ │ │ │ └── default.json │ │ ├── Cargo.toml │ │ ├── Cargo.lock │ │ ├── tauri.conf.json │ │ └── build.rs │ ├── package.json │ └── vite.config.ts # Points to ../app │ ├── web/ # Web deployment │ ├── src/ │ │ └── main.tsx # Entry point that renders App from ../app │ ├── package.json │ └── vite.config.ts # Points to ../app │ ├── backend/ # Python FastAPI server │ ├── main.py │ ├── models.py │ ├── tts.py │ ├── transcribe.py │ ├── profiles.py │ ├── history.py │ ├── studio.py │ ├── database.py │ ├── utils/ │ ├── requirements.txt │ └── build_binary.py # PyInstaller build script │ ├── scripts/ │ ├── build-server.sh # Build Python server for all platforms │ └── generate-api.sh # Generate OpenAPI client │ ├── package.json # Root workspace config (Bun workspaces) └── bun.lockb # Bun lockfile ``` --- ## Technology Stack ### Desktop App (Tauri 2.0) - **Tauri 2.9.5+** - Latest stable version - **Rust** - Tauri backend - **React + TypeScript** - Frontend (shared with web) - **Vite** - Build tool - **Bun** - Fast package manager and JavaScript runtime ### Shared Frontend - **React 18+** - UI framework - **TypeScript** - Type safety - **Vite** - Build tool and dev server - **Bun** - Package manager (faster than npm/yarn/pnpm) - **React Query** - Server state management - **Zustand** - Client state management - **Tailwind CSS** - Styling - **WaveSurfer.js** - Audio visualization ### Backend Bundling - **PyInstaller** - Bundle Python server as standalone binary - **FastAPI** - Python web framework - **Tauri Sidecar** - Execute bundled Python server --- ## Bundling Python Server with Tauri ### 1. Build Python Server as Standalone Binary **Using PyInstaller:** ```python # backend/build_binary.py import PyInstaller.__main__ import sys import os def build_server(): PyInstaller.__main__.run([ 'main.py', '--onefile', '--name', 'voicebox-server', '--add-data', 'data:data', # Include data files '--hidden-import', 'torch', '--hidden-import', 'transformers', '--hidden-import', 'fastapi', '--collect-all', 'qwen-tts', '--noconfirm', ]) if __name__ == '__main__': build_server() ``` **Build script for all platforms:** ```bash #!/bin/bash # scripts/build-server.sh # Determine platform PLATFORM=$(rustc --print host-tuple) # Build Python binary cd backend python build_binary.py # Rename with platform triple cd dist mv voicebox-server ../src-tauri/binaries/voicebox-server-${PLATFORM} echo "Built voicebox-server-${PLATFORM}" ``` **Platform-specific binaries needed:** - macOS Intel: `voicebox-server-x86_64-apple-darwin` - macOS ARM: `voicebox-server-aarch64-apple-darwin` - Linux: `voicebox-server-x86_64-unknown-linux-gnu` - Windows: `voicebox-server-x86_64-pc-windows-msvc.exe` ### 2. Configure Tauri to Bundle Binary **tauri/src-tauri/tauri.conf.json:** ```json { "bundle": { "identifier": "sh.voicebox.app", "externalBin": [ "binaries/voicebox-server" ], "resources": [ "binaries/*" ] }, "build": { "beforeBuildCommand": "bun run build", "devPath": "http://localhost:5173", "distDir": "../dist" } } ``` **Capabilities (src-tauri/capabilities/default.json):** ```json { "identifier": "default", "description": "Default permissions", "permissions": [ "core:default", "shell:allow-execute", "shell:allow-spawn", "fs:default" ] } ``` ### 3. Launch Python Server from Tauri **tauri/src-tauri/src/main.rs:** ```rust use tauri::{command, Manager}; use tauri_plugin_shell::ShellExt; use std::sync::Mutex; struct ServerState { child: Mutex>, } #[command] async fn start_server(app: tauri::AppHandle, state: tauri::State<'_, ServerState>) -> Result { let sidecar = app.shell() .sidecar("voicebox-server") .map_err(|e| format!("Failed to get sidecar: {}", e))?; let (mut rx, child) = sidecar .spawn() .map_err(|e| format!("Failed to spawn: {}", e))?; // Store child process *state.child.lock().unwrap() = Some(child); // Wait for server to be ready (listen for startup log) tokio::spawn(async move { while let Some(event) = rx.recv().await { if let tauri_plugin_shell::process::CommandEvent::Stdout(line) = event { if String::from_utf8_lossy(&line).contains("Uvicorn running") { break; } } } }); Ok("Server started on http://localhost:8000".to_string()) } #[command] async fn stop_server(state: tauri::State<'_, ServerState>) -> Result<(), String> { if let Some(child) = state.child.lock().unwrap().take() { child.kill().map_err(|e| format!("Failed to kill: {}", e))?; } Ok(()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .manage(ServerState { child: Mutex::new(None), }) .invoke_handler(tauri::generate_handler![start_server, stop_server]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` ### 4. Call from Frontend **app/src/lib/hooks/useServer.ts:** ```typescript import { invoke } from '@tauri-apps/api/core'; import { useState } from 'react'; export function useServer() { const [serverUrl, setServerUrl] = useState('http://localhost:8000'); const [isRunning, setIsRunning] = useState(false); const startServer = async () => { try { const url = await invoke('start_server'); setServerUrl(url); setIsRunning(true); return url; } catch (error) { console.error('Failed to start server:', error); throw error; } }; const stopServer = async () => { try { await invoke('stop_server'); setIsRunning(false); } catch (error) { console.error('Failed to stop server:', error); throw error; } }; return { serverUrl, isRunning, startServer, stopServer }; } ``` --- ## Shared Frontend Approach ### App Package Structure The `app/` directory contains all React code that's shared between desktop and web. **app/vite.config.ts:** ```typescript import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, build: { lib: { entry: path.resolve(__dirname, 'src/main.tsx'), formats: ['es'], }, rollupOptions: { external: ['react', 'react-dom'], }, }, }); ``` ### Tauri Wrapper **tauri/src/main.tsx:** ```typescript import React from 'react'; import ReactDOM from 'react-dom/client'; import App from '../app/src/App'; import '../app/src/index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( ); ``` **tauri/vite.config.ts:** ```typescript import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, '../app/src'), }, }, clearScreen: false, server: { port: 5173, strictPort: true, }, envPrefix: ['VITE_', 'TAURI_'], build: { target: 'es2021', minify: !process.env.TAURI_DEBUG, sourcemap: !!process.env.TAURI_DEBUG, outDir: 'dist', }, }); ``` **tauri/package.json:** ```json { "name": "@voicebox/tauri", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "tauri": "tauri" }, "dependencies": { "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-shell": "^2.0.0", "react": "^18.3.0", "react-dom": "^18.3.0" }, "devDependencies": { "@tauri-apps/cli": "^2.0.0", "@vitejs/plugin-react": "^4.3.0", "typescript": "^5.6.0", "vite": "^5.4.0" } } ``` ### Web Wrapper **web/src/main.tsx:** ```typescript import React from 'react'; import ReactDOM from 'react-dom/client'; import App from '../../app/src/App'; import '../../app/src/index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( ); ``` **web/vite.config.ts:** ```typescript import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, '../app/src'), }, }, build: { outDir: 'dist', }, }); ``` --- ## Development Workflow ### 1. Initial Setup ```bash # Install Bun (if not installed) curl -fsSL https://bun.sh/install | bash # Create Tauri app with official CLI cd voicebox bunx create-tauri-app tauri # Move app code to shared directory mkdir app # Move tauri/src/* to app/src/ # Create web directory mkdir web cd web bunx create-vite . --template react-ts # Setup Bun workspace in root package.json cat > package.json << 'EOF' { "name": "voicebox", "private": true, "workspaces": ["app", "tauri", "web"] } EOF # Install all dependencies bun install ``` ### 2. Development **Terminal 1 - Backend (Python FastAPI):** ```bash cd backend python -m venv venv source venv/bin/activate # or venv\Scripts\activate on Windows pip install -r requirements.txt uvicorn main:app --reload --port 8000 ``` **Terminal 2 - Frontend (Tauri dev mode):** ```bash cd tauri bun run tauri dev ``` This will: 1. Start Vite dev server on port 5173 2. Launch Tauri window pointing to localhost:5173 3. Hot reload on code changes **For web development:** ```bash cd web bun run dev ``` ### 3. Building for Production **Build Python server:** ```bash ./scripts/build-server.sh ``` **Build Tauri app:** ```bash cd tauri bun run tauri build ``` This will: 1. Build React frontend with Vite 2. Bundle Python server binary 3. Create platform-specific installers: - macOS: `.app`, `.dmg` - Windows: `.exe`, `.msi` - Linux: `.deb`, `.AppImage` **Build web app:** ```bash cd web bun run build ``` --- ## Platform-Specific Considerations ### macOS - Need both Intel and ARM builds - Sign and notarize for distribution outside App Store - Request permissions for microphone access (audio recording) **tauri.conf.json additions:** ```json { "bundle": { "macOS": { "minimumSystemVersion": "10.15", "entitlements": "src-tauri/Entitlements.plist" } } } ``` **Entitlements.plist:** ```xml com.apple.security.device.audio-input ``` ### Windows - Build with NSIS or WiX installer - Consider code signing for SmartScreen ### Linux - Provide multiple formats: .deb, .AppImage, .rpm - Consider Flatpak for broader distribution --- ## OpenAPI Client Generation **scripts/generate-api.sh:** ```bash #!/bin/bash # Start backend if not running if ! curl -s http://localhost:8000/openapi.json > /dev/null; then echo "Starting backend..." cd backend uvicorn main:app --port 8000 & BACKEND_PID=$! sleep 5 fi # Download OpenAPI schema curl http://localhost:8000/openapi.json > app/openapi.json # Generate TypeScript client cd app bunx openapi-typescript-codegen \ --input openapi.json \ --output src/lib/api \ --client fetch echo "API client generated in app/src/lib/api" # Kill backend if we started it if [ ! -z "$BACKEND_PID" ]; then kill $BACKEND_PID fi ``` **Add to package.json:** ```json { "scripts": { "generate:api": "./scripts/generate-api.sh" } } ``` --- ## Server Mode Architecture ### Local Mode (Default) 1. Tauri app starts 2. App invokes `start_server` command 3. Rust spawns bundled Python binary as sidecar 4. Frontend connects to `http://localhost:8000` 5. All features work locally ### Remote Mode (One-Click) 1. User clicks "Start Server" on GPU machine 2. Tauri invokes `start_server` with `--host 0.0.0.0` flag 3. Server displays connection URL (e.g., `http://192.168.1.100:8000`) 4. User enters URL in client app 5. Client connects to remote server 6. All API calls go to remote machine **Rust command with args:** ```rust #[command] async fn start_server( app: tauri::AppHandle, state: tauri::State<'_, ServerState>, remote: bool, ) -> Result { let mut sidecar = app.shell().sidecar("voicebox-server") .map_err(|e| format!("Failed to get sidecar: {}", e))?; if remote { sidecar = sidecar.args(["--host", "0.0.0.0"]); } // ... rest of spawn logic } ``` --- ## CI/CD for Multi-Platform Builds **GitHub Actions workflow:** ```yaml name: Build on: push: tags: - 'v*' jobs: build: strategy: matrix: platform: [macos-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Setup Rust uses: dtolnay/rust-toolchain@stable - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: | cd tauri bun install - name: Build Python server run: | cd backend pip install -r requirements.txt pip install pyinstaller python build_binary.py - name: Build Tauri app uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: projectPath: tauri tagName: v__VERSION__ releaseName: 'voicebox v__VERSION__' ``` --- ## Key Decisions ### Why This Structure? 1. **Shared `app/` directory** - Single source of truth for UI code 2. **Thin wrappers** - `tauri/` and `web/` just configure build tools 3. **Sidecar pattern** - Bundle Python server without modifying Tauri core 4. **PyInstaller** - Creates standalone Python binary with all dependencies 5. **Platform-specific binaries** - Tauri automatically selects correct binary per platform ### Benefits - ✅ No code duplication between web and desktop - ✅ Python server bundled - users don't install Python - ✅ Single command to build everything - ✅ Type-safe API calls via OpenAPI generation - ✅ Native performance with Tauri - ✅ Web fallback for unsupported platforms - ✅ Fast development with Bun (20-30x faster installs than npm) ### Tradeoffs - ⚠️ Large bundle size (Python runtime + ML models + Tauri) - ⚠️ Need to build Python binary for each platform - ⚠️ First launch slow (model loading) - ⚠️ Separate web build doesn't include server (requires separate backend deployment) --- ## Next Steps 1. Set up monorepo structure 2. Initialize Tauri app with `bunx create-tauri-app` 3. Create shared `app/` directory 4. Configure Vite to share code 5. Build Python server with PyInstaller 6. Configure Tauri sidecar 7. Test on macOS, Windows, Linux 8. Set up CI/CD for multi-platform builds --- ## Resources - [Tauri 2.0 Documentation](https://v2.tauri.app/) - [Tauri Sidecar Guide](https://v2.tauri.app/develop/sidecar/) - [Bun Documentation](https://bun.sh/docs) - [Bun Workspaces](https://bun.sh/docs/install/workspaces) - [PyInstaller Documentation](https://pyinstaller.org/) - [React Query Documentation](https://tanstack.com/query/latest) - [OpenAPI TypeScript Codegen](https://github.com/ferdikoomen/openapi-typescript-codegen)