Initial commit (forked from jamiepine/voicebox)

This commit is contained in:
2026-08-24 19:40:39 -07:00
commit eaef8dd838
677 changed files with 129576 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import App from '@/App';
// Import CSS from app directory using alias so Tailwind can scan the source files
import '@/index.css';
import { PlatformProvider } from '@/platform/PlatformContext';
import { tauriPlatform } from './platform';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 10, // 10 minutes
retry: 1,
refetchOnWindowFocus: false,
},
},
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<PlatformProvider platform={tauriPlatform}>
<App />
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
</PlatformProvider>
</QueryClientProvider>
</React.StrictMode>,
);
+44
View File
@@ -0,0 +1,44 @@
import { invoke } from '@tauri-apps/api/core';
import type { PlatformAudio, AudioDevice } from '@/platform/types';
export const tauriAudio: PlatformAudio = {
async isSystemAudioSupported(): Promise<boolean> {
return await invoke<boolean>('is_system_audio_supported');
},
async startSystemAudioCapture(maxDurationSecs: number): Promise<void> {
await invoke('start_system_audio_capture', {
maxDurationSecs,
});
},
async stopSystemAudioCapture(): Promise<Blob> {
const base64Data = await invoke<string>('stop_system_audio_capture');
// Convert base64 to Blob
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new Blob([bytes], { type: 'audio/wav' });
},
async listOutputDevices(): Promise<AudioDevice[]> {
return await invoke<AudioDevice[]>('list_audio_output_devices');
},
async playToDevices(audioData: Uint8Array, deviceIds: string[]): Promise<void> {
await invoke('play_audio_to_devices', {
audioData: Array.from(audioData),
deviceIds,
});
},
stopPlayback(): void {
invoke('stop_audio_playback').catch((error) => {
console.error('Failed to stop audio playback:', error);
});
},
};
+38
View File
@@ -0,0 +1,38 @@
import type { FileFilter, PlatformFilesystem } from '@/platform/types';
export const tauriFilesystem: PlatformFilesystem = {
async saveFile(filename: string, blob: Blob, filters?: FileFilter[]) {
const { save } = await import('@tauri-apps/plugin-dialog');
const { writeFile } = await import('@tauri-apps/plugin-fs');
const filePath = await save({
defaultPath: filename,
filters: filters || [],
});
if (!filePath) return; // User cancelled the dialog
const resolvedPath =
typeof filePath === 'string' ? filePath : (filePath as { path: string }).path;
if (!resolvedPath) {
throw new Error('Failed to resolve save path from dialog');
}
const arrayBuffer = await blob.arrayBuffer();
await writeFile(resolvedPath, new Uint8Array(arrayBuffer));
},
async openPath(path: string) {
const { open } = await import('@tauri-apps/plugin-shell');
await open(path);
},
async pickDirectory(title: string) {
const { open } = await import('@tauri-apps/plugin-dialog');
const selected = await open({ directory: true, title });
if (!selected) return null;
const dir = typeof selected === 'string' ? selected : (selected as { path: string }).path;
return dir || null;
},
};
+14
View File
@@ -0,0 +1,14 @@
import type { Platform } from '@/platform/types';
import { tauriFilesystem } from './filesystem';
import { tauriUpdater } from './updater';
import { tauriAudio } from './audio';
import { tauriLifecycle } from './lifecycle';
import { tauriMetadata } from './metadata';
export const tauriPlatform: Platform = {
filesystem: tauriFilesystem,
updater: tauriUpdater,
audio: tauriAudio,
lifecycle: tauriLifecycle,
metadata: tauriMetadata,
};
+125
View File
@@ -0,0 +1,125 @@
import { invoke } from '@tauri-apps/api/core';
import { emit, listen } from '@tauri-apps/api/event';
import type { PlatformLifecycle, ServerLogEntry } from '@/platform/types';
class TauriLifecycle implements PlatformLifecycle {
onServerReady?: () => void;
async startServer(remote = false, modelsDir?: string | null): Promise<string> {
try {
const result = await invoke<string>('start_server', {
remote,
modelsDir: modelsDir ?? undefined,
});
console.log('Server started:', result);
this.onServerReady?.();
return result;
} catch (error) {
console.error('Failed to start server:', error);
throw error;
}
}
async stopServer(): Promise<void> {
try {
await invoke('stop_server');
console.log('Server stopped');
} catch (error) {
console.error('Failed to stop server:', error);
throw error;
}
}
async restartServer(modelsDir?: string | null): Promise<string> {
try {
const result = await invoke<string>('restart_server', {
modelsDir: modelsDir ?? undefined,
});
console.log('Server restarted:', result);
this.onServerReady?.();
return result;
} catch (error) {
console.error('Failed to restart server:', error);
throw error;
}
}
async setKeepServerRunning(keepRunning: boolean): Promise<void> {
try {
await invoke('set_keep_server_running', { keepRunning });
} catch (error) {
console.error('Failed to set keep server running setting:', error);
}
}
async setBackendOverride(backend?: string | null): Promise<void> {
try {
await invoke('set_backend_override', { backend: backend ?? undefined });
} catch (error) {
console.error('Failed to set backend override:', error);
throw error;
}
}
async setupWindowCloseHandler(): Promise<void> {
try {
// Listen for window close request from Rust
await listen<null>('window-close-requested', async () => {
// Import store here to avoid circular dependency
const { useServerStore } = await import('@/stores/serverStore');
const keepRunning = useServerStore.getState().keepServerRunningOnClose;
// Check if server was started by this app instance
// @ts-expect-error - accessing module-level variable from another module
const serverStartedByApp = window.__voiceboxServerStartedByApp ?? false;
console.log(
'[lifecycle] window-close-requested: keepRunning=%s, serverStartedByApp=%s',
keepRunning,
serverStartedByApp,
);
if (!keepRunning && serverStartedByApp) {
// Stop server before closing (only if we started it)
try {
await this.stopServer();
} catch (error) {
console.error('Failed to stop server on close:', error);
}
}
// Emit event back to Rust to allow close
await emit('window-close-allowed');
});
} catch (error) {
console.error('Failed to setup window close handler:', error);
}
}
subscribeToServerLogs(callback: (entry: ServerLogEntry) => void): () => void {
let disposed = false;
let unlisten: (() => void) | null = null;
void listen<ServerLogEntry>('server-log', (event) => {
callback(event.payload);
})
.then((fn) => {
if (disposed) {
fn();
return;
}
unlisten = fn;
})
.catch((error) => {
console.error('Failed to subscribe to server logs:', error);
});
return () => {
disposed = true;
unlisten?.();
unlisten = null;
};
}
}
export const tauriLifecycle = new TauriLifecycle();
+14
View File
@@ -0,0 +1,14 @@
import { getVersion } from '@tauri-apps/api/app';
import type { PlatformMetadata } from '@/platform/types';
export const tauriMetadata: PlatformMetadata = {
async getVersion(): Promise<string> {
try {
return await getVersion();
} catch (error) {
console.error('Failed to get version:', error);
return '0.1.0';
}
},
isTauri: true,
};
+170
View File
@@ -0,0 +1,170 @@
import { relaunch } from '@tauri-apps/plugin-process';
import { check, type Update } from '@tauri-apps/plugin-updater';
import type { PlatformUpdater, UpdateStatus } from '@/platform/types';
// Check if we're on Windows (NSIS installer handles restart automatically)
const isWindows = () => {
return navigator.userAgent.includes('Windows');
};
class TauriUpdater implements PlatformUpdater {
private status: UpdateStatus = {
checking: false,
available: false,
downloading: false,
installing: false,
readyToInstall: false,
};
private update: Update | null = null;
private subscribers: Set<(status: UpdateStatus) => void> = new Set();
private notifySubscribers() {
this.subscribers.forEach((callback) => callback(this.status));
}
subscribe(callback: (status: UpdateStatus) => void): () => void {
this.subscribers.add(callback);
// Immediately call with current status
callback(this.status);
return () => {
this.subscribers.delete(callback);
};
}
getStatus(): UpdateStatus {
return { ...this.status };
}
async checkForUpdates(): Promise<void> {
try {
this.status = { ...this.status, checking: true, error: undefined };
this.notifySubscribers();
const foundUpdate = await check();
if (foundUpdate?.available) {
this.update = foundUpdate;
this.status = {
checking: false,
available: true,
version: foundUpdate.version,
downloading: false,
installing: false,
readyToInstall: false,
};
} else {
this.status = {
checking: false,
available: false,
downloading: false,
installing: false,
readyToInstall: false,
};
}
this.notifySubscribers();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
// Tauri updater throws on 404 / no published release / network errors.
// Treat "no update available" style errors as up-to-date, not failures.
const isNoUpdate = /404|not found|no update|up.to.date/i.test(message);
this.status = {
checking: false,
available: false,
downloading: false,
installing: false,
readyToInstall: false,
error: isNoUpdate ? undefined : message,
};
this.notifySubscribers();
}
}
async downloadAndInstall(): Promise<void> {
if (!this.update) return;
try {
this.status = { ...this.status, downloading: true, error: undefined };
this.notifySubscribers();
let downloadedBytes = 0;
let totalBytes = 0;
await this.update.download((event) => {
switch (event.event) {
case 'Started':
totalBytes = event.data.contentLength || 0;
downloadedBytes = 0;
this.status = {
...this.status,
downloading: true,
totalBytes,
downloadedBytes: 0,
downloadProgress: 0,
};
this.notifySubscribers();
break;
case 'Progress': {
downloadedBytes += event.data.chunkLength;
const progress =
totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : undefined;
this.status = {
...this.status,
downloadedBytes,
downloadProgress: progress,
};
this.notifySubscribers();
break;
}
case 'Finished':
this.status = {
...this.status,
downloading: false,
readyToInstall: true,
downloadProgress: 100,
};
this.notifySubscribers();
break;
}
});
} catch (error) {
this.status = {
...this.status,
downloading: false,
installing: false,
readyToInstall: false,
downloadProgress: undefined,
downloadedBytes: undefined,
totalBytes: undefined,
error: error instanceof Error ? error.message : 'Failed to download update',
};
this.notifySubscribers();
}
}
async restartAndInstall(): Promise<void> {
if (!this.update) return;
try {
this.status = { ...this.status, installing: true, error: undefined };
this.notifySubscribers();
await this.update.install();
// On Windows with NSIS, the installer handles the restart automatically.
// On macOS/Linux, we need to manually relaunch.
if (!isWindows()) {
await relaunch();
}
} catch (error) {
this.status = {
...this.status,
installing: false,
error: error instanceof Error ? error.message : 'Failed to install update',
};
this.notifySubscribers();
}
}
}
export const tauriUpdater = new TauriUpdater();