Merge pull request #5 from Snowy7/fix/dev-mode-sidecar

Fix dev mode sidecar and cross-platform HuggingFace cache paths
This commit is contained in:
Jamie Pine
2026-01-29 16:05:00 -08:00
committed by GitHub
4 changed files with 297 additions and 28 deletions
+12 -5
View File
@@ -78,25 +78,32 @@ Thank you for your interest in contributing to Voicebox! This document provides
This creates the SQLite database at `data/voicebox.db`.
5. **Start development servers**
**Terminal 1: Backend server**
Development requires two terminals: one for the Python backend, one for the Tauri app.
**Terminal 1: Backend server** (start this first)
```bash
cd backend
source venv/bin/activate # Activate venv if not already active
bun run dev:server
# Or manually: uvicorn main:app --reload --port 8000
# Or manually: uvicorn main:app --reload --port 17493
```
Backend will be available at `http://localhost:8000`
Backend will be available at `http://localhost:17493`
**Terminal 2: Desktop app**
```bash
bun run dev
```
This will:
- Create a placeholder sidecar binary (for Tauri compilation)
- Start Vite dev server on port 5173
- Launch Tauri window pointing to localhost:5173
- Connect to the Python server you started in Terminal 1
- Enable hot reload
> **Note:** In dev mode, the app connects to your manually-started Python server.
> The bundled server binary is only used in production builds.
**Optional: Web app**
```bash
bun run dev:web
+3 -2
View File
@@ -9,10 +9,11 @@
"landing"
],
"scripts": {
"dev": "cd tauri && bun run tauri dev",
"dev": "bun run setup:dev && cd tauri && bun run tauri dev",
"dev:web": "cd web && bun run dev",
"dev:landing": "cd landing && bun run dev",
"dev:server": "uvicorn backend.main:app --reload --port 8000",
"dev:server": "uvicorn backend.main:app --reload --port 17493",
"setup:dev": "bun run scripts/setup-dev-sidecar.js",
"build": "cd tauri && bun run tauri build",
"build:web": "cd web && bun run build",
"build:landing": "cd landing && bun run build",
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env node
/**
* Creates placeholder sidecar binaries for development mode.
*
* In dev mode, Tauri requires the sidecar binary files to exist at compile time,
* even though developers typically run the Python server manually.
*
* This script creates minimal placeholder binaries that allow Tauri to compile.
* The actual server should be started separately with `bun run dev:server`.
*/
import { existsSync, mkdirSync, writeFileSync, statSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const BINARIES_DIR = join(__dirname, '..', 'tauri', 'src-tauri', 'binaries');
// Minimum size to consider a binary "real" (placeholder is ~256 bytes, real is MBs)
const MIN_REAL_BINARY_SIZE = 10000;
// Get the current platform's target triple
function getTargetTriple() {
try {
const triple = execSync('rustc --print host-tuple', { encoding: 'utf-8' }).trim();
return triple;
} catch {
// Fallback detection
const platform = process.platform;
const arch = process.arch;
if (platform === 'win32') {
return arch === 'x64' ? 'x86_64-pc-windows-msvc' : 'i686-pc-windows-msvc';
} else if (platform === 'darwin') {
return arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin';
} else if (platform === 'linux') {
return arch === 'x64' ? 'x86_64-unknown-linux-gnu' : 'aarch64-unknown-linux-gnu';
}
throw new Error(`Unsupported platform: ${platform}/${arch}`);
}
}
// Create a minimal executable for the platform
function createPlaceholderBinary(targetTriple) {
const isWindows = targetTriple.includes('windows');
const binaryName = `voicebox-server-${targetTriple}${isWindows ? '.exe' : ''}`;
const binaryPath = join(BINARIES_DIR, binaryName);
// Check if real binary already exists (larger than our placeholder)
if (existsSync(binaryPath)) {
try {
const stats = statSync(binaryPath);
if (stats.size > MIN_REAL_BINARY_SIZE) {
console.log(`Real binary already exists: ${binaryName} (${(stats.size / 1024 / 1024).toFixed(1)} MB)`);
return;
}
} catch {
// File exists but can't stat - try to replace it
}
}
// Ensure binaries directory exists
if (!existsSync(BINARIES_DIR)) {
mkdirSync(BINARIES_DIR, { recursive: true });
}
if (isWindows) {
// Create a minimal valid Windows PE executable that exits with code 1
// This is the smallest valid PE that Windows will accept
const minimalPE = Buffer.from([
// DOS Header
0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
// DOS Stub
0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, 0x4C, 0xCD, 0x21, 0x54, 0x68,
0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x63, 0x61, 0x6E, 0x6E, 0x6F,
0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, 0x69, 0x6E, 0x20, 0x44, 0x4F, 0x53, 0x20,
0x6D, 0x6F, 0x64, 0x65, 0x2E, 0x0D, 0x0D, 0x0A, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// PE Signature
0x50, 0x45, 0x00, 0x00,
// COFF Header (x64)
0x64, 0x86, // Machine: AMD64
0x01, 0x00, // NumberOfSections: 1
0x00, 0x00, 0x00, 0x00, // TimeDateStamp
0x00, 0x00, 0x00, 0x00, // PointerToSymbolTable
0x00, 0x00, 0x00, 0x00, // NumberOfSymbols
0xF0, 0x00, // SizeOfOptionalHeader
0x22, 0x00, // Characteristics: EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE
// Optional Header (PE32+)
0x0B, 0x02, // Magic: PE32+
0x00, 0x00, // Linker version
0x00, 0x00, 0x00, 0x00, // SizeOfCode
0x00, 0x00, 0x00, 0x00, // SizeOfInitializedData
0x00, 0x00, 0x00, 0x00, // SizeOfUninitializedData
0x00, 0x10, 0x00, 0x00, // AddressOfEntryPoint
0x00, 0x00, 0x00, 0x00, // BaseOfCode
0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x00, // ImageBase
0x00, 0x10, 0x00, 0x00, // SectionAlignment
0x00, 0x02, 0x00, 0x00, // FileAlignment
0x06, 0x00, 0x00, 0x00, // OS version
0x00, 0x00, 0x00, 0x00, // Image version
0x06, 0x00, 0x00, 0x00, // Subsystem version
0x00, 0x00, 0x00, 0x00, // Win32VersionValue
0x00, 0x20, 0x00, 0x00, // SizeOfImage
0x00, 0x02, 0x00, 0x00, // SizeOfHeaders
0x00, 0x00, 0x00, 0x00, // CheckSum
0x03, 0x00, // Subsystem: CONSOLE
0x60, 0x01, // DllCharacteristics
// Stack/Heap sizes (8 bytes each for PE32+)
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, // LoaderFlags
0x10, 0x00, 0x00, 0x00, // NumberOfRvaAndSizes
]);
// Pad to 512 bytes minimum for valid PE
const paddedPE = Buffer.alloc(512);
minimalPE.copy(paddedPE);
writeFileSync(binaryPath, paddedPE);
} else {
// Create a minimal shell script for Unix-like systems
const script = `#!/bin/sh
echo "[voicebox-server] Dev mode placeholder - start the real server with: bun run dev:server"
exit 1
`;
writeFileSync(binaryPath, script, { mode: 0o755 });
}
console.log(`Created dev placeholder: ${binaryName}`);
}
function main() {
console.log('Setting up development sidecar...');
console.log('');
const targetTriple = getTargetTriple();
console.log(`Platform: ${targetTriple}`);
createPlaceholderBinary(targetTriple);
console.log('');
console.log('Sidecar setup complete.');
console.log('For development, start the Python server in a separate terminal:');
console.log(' bun run dev:server');
console.log('');
}
main();
+126 -21
View File
@@ -178,14 +178,41 @@ async fn start_server(
println!("Data directory: {:?}", data_dir);
println!("Remote mode: {}", remote.unwrap_or(false));
let mut sidecar = app
.shell()
.sidecar("voicebox-server")
.map_err(|e| {
let sidecar_result = app.shell().sidecar("voicebox-server");
let mut sidecar = match sidecar_result {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to get sidecar: {}", e);
eprintln!("This usually means the binary is not bundled correctly or doesn't have execute permissions");
format!("Failed to get sidecar: {}", e)
})?;
// In dev mode, check if the server is already running (started manually)
#[cfg(debug_assertions)]
{
eprintln!("Dev mode: Checking if server is already running on port {}...", SERVER_PORT);
// Try to connect to the server port
use std::net::TcpStream;
if TcpStream::connect_timeout(
&format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(),
std::time::Duration::from_secs(1),
).is_ok() {
println!("Found server already running on port {}", SERVER_PORT);
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
}
eprintln!("");
eprintln!("=================================================================");
eprintln!("DEV MODE: No server found on port {}", SERVER_PORT);
eprintln!("");
eprintln!("Start the Python server in a separate terminal:");
eprintln!(" bun run dev:server");
eprintln!("=================================================================");
eprintln!("");
}
return Err(format!("Failed to start server. In dev mode, run 'bun run dev:server' in a separate terminal."));
}
};
println!("Sidecar command created successfully");
@@ -204,17 +231,47 @@ async fn start_server(
}
println!("Spawning server process...");
let (mut rx, child) = sidecar
.spawn()
.map_err(|e| {
let spawn_result = sidecar.spawn();
let (mut rx, child) = match spawn_result {
Ok(result) => result,
Err(e) => {
eprintln!("Failed to spawn server process: {}", e);
eprintln!("This could be due to:");
eprintln!(" - Missing or corrupted binary");
eprintln!(" - Missing execute permissions");
eprintln!(" - Code signing issues on macOS");
eprintln!(" - Missing dependencies");
format!("Failed to spawn: {}", e)
})?;
// In dev mode, check if a manually-started server is available
#[cfg(debug_assertions)]
{
use std::net::TcpStream;
if TcpStream::connect_timeout(
&format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(),
std::time::Duration::from_secs(1),
).is_ok() {
println!("Found manually-started server on port {}", SERVER_PORT);
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
}
eprintln!("");
eprintln!("=================================================================");
eprintln!("DEV MODE: Server binary failed to start");
eprintln!("");
eprintln!("Start the Python server in a separate terminal:");
eprintln!(" bun run dev:server");
eprintln!("=================================================================");
eprintln!("");
return Err("Dev mode: Start server manually with 'bun run dev:server'".to_string());
}
#[cfg(not(debug_assertions))]
{
eprintln!("This could be due to:");
eprintln!(" - Missing or corrupted binary");
eprintln!(" - Missing execute permissions");
eprintln!(" - Code signing issues on macOS");
eprintln!(" - Missing dependencies");
return Err(format!("Failed to spawn: {}", e));
}
}
};
println!("Server process spawned, waiting for ready signal...");
println!("=================================================================");
@@ -239,6 +296,22 @@ async fn start_server(
eprintln!(" {}", line);
}
}
// In dev mode, check if a manual server came up during the wait
#[cfg(debug_assertions)]
{
use std::net::TcpStream;
if TcpStream::connect_timeout(
&format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(),
std::time::Duration::from_secs(1),
).is_ok() {
// Kill the placeholder process
let _ = state.child.lock().unwrap().take();
println!("Found manually-started server on port {}", SERVER_PORT);
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
}
}
return Err("Server startup timeout - check Console.app for detailed logs".to_string());
}
@@ -273,10 +346,42 @@ async fn start_server(
}
}
Ok(None) => {
eprintln!("Server process ended unexpectedly during startup!");
eprintln!("The server binary may have crashed or exited with an error.");
eprintln!("Check Console.app logs for more details (search for 'voicebox')");
return Err("Server process ended unexpectedly".to_string());
// In dev mode, this is expected when using the placeholder binary
#[cfg(debug_assertions)]
{
use std::net::TcpStream;
eprintln!("Server process ended (dev mode placeholder detected)");
// Check if a manually-started server is available
if TcpStream::connect_timeout(
&format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(),
std::time::Duration::from_secs(1),
).is_ok() {
// Clean up state
let _ = state.child.lock().unwrap().take();
let _ = state.server_pid.lock().unwrap().take();
println!("Found manually-started server on port {}", SERVER_PORT);
return Ok(format!("http://127.0.0.1:{}", SERVER_PORT));
}
eprintln!("");
eprintln!("=================================================================");
eprintln!("DEV MODE: No bundled server binary available");
eprintln!("");
eprintln!("Start the Python server in a separate terminal:");
eprintln!(" bun run dev:server");
eprintln!("=================================================================");
eprintln!("");
return Err("Dev mode: Start server manually with 'bun run dev:server'".to_string());
}
#[cfg(not(debug_assertions))]
{
eprintln!("Server process ended unexpectedly during startup!");
eprintln!("The server binary may have crashed or exited with an error.");
eprintln!("Check Console.app logs for more details (search for 'voicebox')");
return Err("Server process ended unexpectedly".to_string());
}
}
Err(_) => {
// Timeout on this recv, continue loop