18 KiB
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:
# 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:
#!/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:
{
"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):
{
"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:
use tauri::{command, Manager};
use tauri_plugin_shell::ShellExt;
use std::sync::Mutex;
struct ServerState {
child: Mutex<Option<tauri_plugin_shell::process::CommandChild>>,
}
#[command]
async fn start_server(app: tauri::AppHandle, state: tauri::State<'_, ServerState>) -> Result<String, String> {
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:
import { invoke } from '@tauri-apps/api/core';
import { useState } from 'react';
export function useServer() {
const [serverUrl, setServerUrl] = useState<string>('http://localhost:8000');
const [isRunning, setIsRunning] = useState(false);
const startServer = async () => {
try {
const url = await invoke<string>('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:
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:
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(
<React.StrictMode>
<App />
</React.StrictMode>
);
tauri/vite.config.ts:
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:
{
"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:
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(
<React.StrictMode>
<App />
</React.StrictMode>
);
web/vite.config.ts:
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
# 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):
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):
cd tauri
bun run tauri dev
This will:
- Start Vite dev server on port 5173
- Launch Tauri window pointing to localhost:5173
- Hot reload on code changes
For web development:
cd web
bun run dev
3. Building for Production
Build Python server:
./scripts/build-server.sh
Build Tauri app:
cd tauri
bun run tauri build
This will:
- Build React frontend with Vite
- Bundle Python server binary
- Create platform-specific installers:
- macOS:
.app,.dmg - Windows:
.exe,.msi - Linux:
.deb,.AppImage
- macOS:
Build web app:
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:
{
"bundle": {
"macOS": {
"minimumSystemVersion": "10.15",
"entitlements": "src-tauri/Entitlements.plist"
}
}
}
Entitlements.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
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:
#!/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:
{
"scripts": {
"generate:api": "./scripts/generate-api.sh"
}
}
Server Mode Architecture
Local Mode (Default)
- Tauri app starts
- App invokes
start_servercommand - Rust spawns bundled Python binary as sidecar
- Frontend connects to
http://localhost:8000 - All features work locally
Remote Mode (One-Click)
- User clicks "Start Server" on GPU machine
- Tauri invokes
start_serverwith--host 0.0.0.0flag - Server displays connection URL (e.g.,
http://192.168.1.100:8000) - User enters URL in client app
- Client connects to remote server
- All API calls go to remote machine
Rust command with args:
#[command]
async fn start_server(
app: tauri::AppHandle,
state: tauri::State<'_, ServerState>,
remote: bool,
) -> Result<String, String> {
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:
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?
- Shared
app/directory - Single source of truth for UI code - Thin wrappers -
tauri/andweb/just configure build tools - Sidecar pattern - Bundle Python server without modifying Tauri core
- PyInstaller - Creates standalone Python binary with all dependencies
- 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
- Set up monorepo structure
- Initialize Tauri app with
bunx create-tauri-app - Create shared
app/directory - Configure Vite to share code
- Build Python server with PyInstaller
- Configure Tauri sidecar
- Test on macOS, Windows, Linux
- Set up CI/CD for multi-platform builds