Update development scripts and add macOS icon assets

- Renamed backend development script from `dev:backend` to `dev:server` for clarity.
- Added new macOS icon assets and configuration files for the application.
- Enhanced App component to improve server management during production and development modes.
- Updated ConnectionForm to reset state after successful submission and conditionally render the update button.
- Implemented database initialization on application startup in the backend.
This commit is contained in:
Jamie Pine
2026-01-25 12:15:47 -08:00
parent 7b1e2295ef
commit edbdf0ce3f
19 changed files with 155 additions and 17 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

+36
View File
@@ -0,0 +1,36 @@
{
"fill" : "automatic",
"groups" : [
{
"layers" : [
{
"blend-mode" : "normal",
"glass" : true,
"image-name" : "Voicebox.png",
"name" : "Voicebox",
"position" : {
"scale" : 0.37,
"translation-in-points" : [
-0.12338405356129378,
297.27705195772353
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
+1 -1
View File
@@ -437,7 +437,7 @@ projects
## 💡 Development Workflow
1. **Start backend**: `bun run dev:backend` (or via Tauri)
1. **Start backend**: `bun run dev:server` (or via Tauri)
2. **Start frontend**: `bun run dev` (Tauri) or `bun run dev:web` (web)
3. **Generate API client**: `bun run generate:api` (after backend changes)
4. **Build server binary**: `bun run build:server` (for Tauri bundling)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

+22 -10
View File
@@ -16,45 +16,57 @@ function App() {
const [activeTab, setActiveTab] = useState('profiles');
const [serverReady, setServerReady] = useState(false);
// Setup window close handler and auto-start server when running in Tauri
// Setup window close handler and auto-start server when running in Tauri (production only)
useEffect(() => {
if (!isTauri()) {
return;
}
// Setup window close handler to check setting and stop server if needed
// This works in both dev and prod, but will only stop server if it was started by the app
setupWindowCloseHandler().catch((error) => {
console.error('Failed to setup window close handler:', error);
});
// Auto-start server
// Only auto-start server in production mode
// In dev mode, user runs server separately
if (!import.meta.env?.PROD) {
console.log('Dev mode: Skipping auto-start of server (run it separately)');
setServerReady(true); // Mark as ready so UI doesn't show loading screen
// Mark that server was not started by app (so we don't try to stop it on close)
// @ts-expect-error - adding property to window
window.__voiceboxServerStartedByApp = false;
return;
}
// Auto-start server in production
if (serverStarting) {
return;
}
serverStarting = true;
console.log('Running in Tauri, starting bundled server...');
console.log('Production mode: Starting bundled server...');
startServer(false)
.then(() => {
console.log('Server is ready');
setServerReady(true);
// Mark that we started the server (so we know to stop it on close)
// @ts-expect-error - adding property to window
window.__voiceboxServerStartedByApp = true;
})
.catch((error) => {
console.error('Failed to auto-start server:', error);
serverStarting = false;
// @ts-expect-error - adding property to window
window.__voiceboxServerStartedByApp = false;
});
// Cleanup: stop server on actual unmount (not StrictMode remount)
// Note: Window close is handled separately in Tauri Rust code
return () => {
// In dev mode, React StrictMode causes remounts, so we skip cleanup
// In production, window close event handles server shutdown based on setting
if (import.meta.env?.PROD) {
// Only stop if setting says to stop (handled by window close event)
// This cleanup is mainly for React remounts in dev mode
serverStarting = false;
}
// Window close event handles server shutdown based on setting
serverStarting = false;
};
}, []);
@@ -1,5 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useEffect } from 'react';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -37,8 +38,16 @@ export function ConnectionForm() {
},
});
// Sync form with store when serverUrl changes externally
useEffect(() => {
form.reset({ serverUrl });
}, [serverUrl, form]);
const { isDirty } = form.formState;
function onSubmit(data: ConnectionFormValues) {
setServerUrl(data.serverUrl);
form.reset(data); // Reset form state after successful submission
toast({
title: 'Server URL updated',
description: `Connected to ${data.serverUrl}`,
@@ -68,7 +77,9 @@ export function ConnectionForm() {
)}
/>
<Button type="submit">Update Connection</Button>
{isDirty && (
<Button type="submit">Update Connection</Button>
)}
</form>
</Form>
+8 -2
View File
@@ -62,8 +62,14 @@ export async function setupWindowCloseHandler(): Promise<void> {
const { useServerStore } = await import('@/stores/serverStore');
const keepRunning = useServerStore.getState().keepServerRunningOnClose;
if (!keepRunning) {
// Stop server before closing
// Check if server was started by this app instance
// In dev mode, serverStartedByApp will be false, so we won't try to stop a separately-run server
// We need to access the module-level variable - this is a bit hacky but works
// @ts-expect-error - accessing module-level variable from another module
const serverStartedByApp = window.__voiceboxServerStartedByApp ?? false;
if (!keepRunning && serverStartedByApp) {
// Stop server before closing (only if we started it)
try {
await stopServer();
} catch (error) {
+1
View File
@@ -706,6 +706,7 @@ async def trigger_model_download(request: models.ModelDownloadRequest):
async def startup_event():
"""Run on application startup."""
print("voicebox API starting up...")
database.init_db()
print(f"Database initialized at {database._db_path}")
print(f"GPU available: {torch.cuda.is_available()}")
+1 -1
View File
@@ -10,7 +10,7 @@
"scripts": {
"dev": "cd tauri && bun run tauri dev",
"dev:web": "cd web && bun run dev",
"dev:backend": "source backend/venv/bin/activate && uvicorn backend.main:app --reload --port 8000",
"dev:server": "source backend/venv/bin/activate && uvicorn backend.main:app --reload --port 8000",
"build": "cd tauri && bun run tauri build",
"build:web": "cd web && bun run build",
"generate:api": "./scripts/generate-api.sh",
+10
View File
@@ -0,0 +1,10 @@
<?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>CFBundleIconFile</key>
<string>voicebox</string>
<key>CFBundleIconName</key>
<string>voicebox</string>
</dict>
</plist>
+53
View File
@@ -1,3 +1,56 @@
use std::process::Command;
fn main() {
// Compile macOS Liquid Glass icon
#[cfg(target_os = "macos")]
{
let project_root = env!("CARGO_MANIFEST_DIR");
// AppIcon.icon is in the root, two levels up from src-tauri
let icon_source = format!("{}/../../AppIcon.icon", project_root);
let gen_dir = format!("{}/gen", project_root);
std::fs::create_dir_all(&gen_dir).expect("Failed to create gen directory");
if std::path::Path::new(&icon_source).exists() {
println!("cargo:rerun-if-changed={}", icon_source);
println!("cargo:rerun-if-changed={}/icon.json", icon_source);
println!("cargo:rerun-if-changed={}/Assets", icon_source);
let partial_plist = format!("{}/partial.plist", gen_dir);
let output = Command::new("xcrun")
.args([
"actool",
"--compile", &gen_dir,
"--output-format", "human-readable-text",
"--output-partial-info-plist", &partial_plist,
"--app-icon", "voicebox",
"--include-all-app-icons",
"--target-device", "mac",
"--minimum-deployment-target", "11.0",
"--platform", "macosx",
&icon_source,
])
.output();
match output {
Ok(output) => {
if !output.status.success() {
eprintln!("actool stderr: {}", String::from_utf8_lossy(&output.stderr));
eprintln!("actool stdout: {}", String::from_utf8_lossy(&output.stdout));
panic!("actool failed to compile icon");
}
println!("Successfully compiled icon to {}", gen_dir);
}
Err(e) => {
eprintln!("Failed to execute xcrun actool: {}", e);
eprintln!("Make sure you have Xcode Command Line Tools installed");
panic!("Icon compilation failed");
}
}
} else {
println!("cargo:warning=Icon source not found at {}, skipping icon compilation", icon_source);
}
}
tauri_build::build()
}
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
<?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/>
</plist>
+6 -2
View File
@@ -24,8 +24,12 @@
],
"macOS": {
"frameworks": [],
"minimumSystemVersion": "10.15"
}
"minimumSystemVersion": "11.0",
"infoPlist": "Info.plist"
},
"resources": [
"gen/**/*"
]
},
"app": {
"security": {