diff --git a/AppIcon.icon/Assets/Voicebox.png b/AppIcon.icon/Assets/Voicebox.png
new file mode 100644
index 00000000..7a630b05
Binary files /dev/null and b/AppIcon.icon/Assets/Voicebox.png differ
diff --git a/AppIcon.icon/icon.json b/AppIcon.icon/icon.json
new file mode 100644
index 00000000..dece1804
--- /dev/null
+++ b/AppIcon.icon/icon.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md
index 0a885860..975b1447 100644
--- a/CURRENT_STATE.md
+++ b/CURRENT_STATE.md
@@ -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)
diff --git a/Icon Exports/Icon-iOS-ClearDark-1024x1024@1x.png b/Icon Exports/Icon-iOS-ClearDark-1024x1024@1x.png
new file mode 100644
index 00000000..bb040146
Binary files /dev/null and b/Icon Exports/Icon-iOS-ClearDark-1024x1024@1x.png differ
diff --git a/Icon Exports/Icon-iOS-ClearLight-1024x1024@1x.png b/Icon Exports/Icon-iOS-ClearLight-1024x1024@1x.png
new file mode 100644
index 00000000..90b06cf2
Binary files /dev/null and b/Icon Exports/Icon-iOS-ClearLight-1024x1024@1x.png differ
diff --git a/Icon Exports/Icon-iOS-Dark-1024x1024@1x.png b/Icon Exports/Icon-iOS-Dark-1024x1024@1x.png
new file mode 100644
index 00000000..abb7034f
Binary files /dev/null and b/Icon Exports/Icon-iOS-Dark-1024x1024@1x.png differ
diff --git a/Icon Exports/Icon-iOS-Default-1024x1024@1x.png b/Icon Exports/Icon-iOS-Default-1024x1024@1x.png
new file mode 100644
index 00000000..c058745c
Binary files /dev/null and b/Icon Exports/Icon-iOS-Default-1024x1024@1x.png differ
diff --git a/Icon Exports/Icon-iOS-TintedDark-1024x1024@1x.png b/Icon Exports/Icon-iOS-TintedDark-1024x1024@1x.png
new file mode 100644
index 00000000..c24218b1
Binary files /dev/null and b/Icon Exports/Icon-iOS-TintedDark-1024x1024@1x.png differ
diff --git a/Icon Exports/Icon-iOS-TintedLight-1024x1024@1x.png b/Icon Exports/Icon-iOS-TintedLight-1024x1024@1x.png
new file mode 100644
index 00000000..912ff991
Binary files /dev/null and b/Icon Exports/Icon-iOS-TintedLight-1024x1024@1x.png differ
diff --git a/app/src/App.tsx b/app/src/App.tsx
index ee0820d0..c3c7cb3c 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -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;
};
}, []);
diff --git a/app/src/components/ServerSettings/ConnectionForm.tsx b/app/src/components/ServerSettings/ConnectionForm.tsx
index 75cfff3e..9ce7d605 100644
--- a/app/src/components/ServerSettings/ConnectionForm.tsx
+++ b/app/src/components/ServerSettings/ConnectionForm.tsx
@@ -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() {
)}
/>
-
+ {isDirty && (
+
+ )}
diff --git a/app/src/lib/tauri.ts b/app/src/lib/tauri.ts
index 39cf7fc7..792f7a7e 100644
--- a/app/src/lib/tauri.ts
+++ b/app/src/lib/tauri.ts
@@ -62,8 +62,14 @@ export async function setupWindowCloseHandler(): Promise {
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) {
diff --git a/backend/main.py b/backend/main.py
index 06c8298c..766357d4 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -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()}")
diff --git a/package.json b/package.json
index 876ad0e3..a3f73730 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/tauri/src-tauri/Info.plist b/tauri/src-tauri/Info.plist
new file mode 100644
index 00000000..f3ca2e7d
--- /dev/null
+++ b/tauri/src-tauri/Info.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ CFBundleIconFile
+ voicebox
+ CFBundleIconName
+ voicebox
+
+
diff --git a/tauri/src-tauri/build.rs b/tauri/src-tauri/build.rs
index d860e1e6..33576712 100644
--- a/tauri/src-tauri/build.rs
+++ b/tauri/src-tauri/build.rs
@@ -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()
}
diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car
new file mode 100644
index 00000000..6dd5f3dd
Binary files /dev/null and b/tauri/src-tauri/gen/Assets.car differ
diff --git a/tauri/src-tauri/gen/partial.plist b/tauri/src-tauri/gen/partial.plist
new file mode 100644
index 00000000..0c67376e
--- /dev/null
+++ b/tauri/src-tauri/gen/partial.plist
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tauri/src-tauri/tauri.conf.json b/tauri/src-tauri/tauri.conf.json
index 88270043..1db2972e 100644
--- a/tauri/src-tauri/tauri.conf.json
+++ b/tauri/src-tauri/tauri.conf.json
@@ -24,8 +24,12 @@
],
"macOS": {
"frameworks": [],
- "minimumSystemVersion": "10.15"
- }
+ "minimumSystemVersion": "11.0",
+ "infoPlist": "Info.plist"
+ },
+ "resources": [
+ "gen/**/*"
+ ]
},
"app": {
"security": {