Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/src/lib/hooks/useHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ export function useExportGenerationAudio() {
.substring(0, 30)
.replace(/[^a-z0-9]/gi, '-')
.toLowerCase();
const filename = `${safeText}.wav`;
const filename = `${safeText}.mp3`;

await platform.filesystem.saveFile(filename, blob, [
{
name: 'Audio File',
extensions: ['wav'],
extensions: ['mp3'],
},
]);

Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/hooks/useStories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,12 @@ export function useExportStoryAudio() {
.substring(0, 50)
.replace(/[^a-z0-9]/gi, '-')
.toLowerCase();
const filename = `${safeName || 'story'}.wav`;
const filename = `${safeName || 'story'}.mp3`;

await platform.filesystem.saveFile(filename, blob, [
{
name: 'Audio File',
extensions: ['wav'],
extensions: ['mp3'],
},
]);

Expand Down
8 changes: 6 additions & 2 deletions app/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import App from './App';
import './i18n';
import './index.css';
import { queryClient } from './lib/queryClient';
import { PlatformProvider } from './platform/PlatformContext';
import { webPlatform } from './platform/webPlatform';

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
<PlatformProvider platform={webPlatform}>
<App />
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
</PlatformProvider>
</QueryClientProvider>
</React.StrictMode>,
);
111 changes: 111 additions & 0 deletions app/src/platform/webPlatform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Web platform implementation
*
* Voicebox's primary target is the Tauri desktop app; the web build
* (app/ served as a static SPA by the Python backend) had no platform
* implementation after the Tauri refactor, which crashed the UI with
* "usePlatform must be used within PlatformProvider".
*
* This implementation provides browser-safe no-ops for the desktop-only
* features (updater, system audio capture, server lifecycle) and a
* download-based saveFile so audio export keeps working in the browser.
*/

import type {
Platform,
PlatformAudio,
PlatformFilesystem,
PlatformLifecycle,
PlatformMetadata,
PlatformUpdater,
UpdateStatus,
} from './types';

const noop = () => {};

const EMPTY_UPDATE_STATUS: UpdateStatus = {
checking: false,
available: false,
downloading: false,
installing: false,
readyToInstall: false,
};

const webFilesystem: PlatformFilesystem = {
async saveFile(filename: string, blob: Blob) {
// Browser download via object URL — file filters are a desktop concept
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
setTimeout(() => URL.revokeObjectURL(url), 10_000);
},

async openPath(path: string) {
window.open(path, '_blank', 'noopener');
},

async pickDirectory() {
// Browsers can't return a persistent directory path
return null;
},
};

const webUpdater: PlatformUpdater = {
async checkForUpdates() {},
async downloadAndInstall() {},
async restartAndInstall() {},
getStatus: () => ({ ...EMPTY_UPDATE_STATUS }),
subscribe: () => noop,
};

const webAudio: PlatformAudio = {
async isSystemAudioSupported() {
return false;
},
async startSystemAudioCapture() {
throw new Error('System audio capture is not supported in the browser.');
},
async stopSystemAudioCapture() {
throw new Error('System audio capture is not supported in the browser.');
},
async listOutputDevices() {
return [];
},
async playToDevices() {
// Web playback uses the default <audio> element path
},
stopPlayback() {},
};

const webLifecycle: PlatformLifecycle = {
async startServer() {
throw new Error('Not running in Tauri environment');
},
async stopServer() {},
async restartServer() {
throw new Error('Not running in Tauri environment');
},
async setKeepServerRunning() {},
async setBackendOverride() {},
async setupWindowCloseHandler() {},
subscribeToServerLogs: () => noop,
};

const webMetadata: PlatformMetadata = {
async getVersion() {
return 'web';
},
isTauri: false,
};

export const webPlatform: Platform = {
filesystem: webFilesystem,
updater: webUpdater,
audio: webAudio,
lifecycle: webLifecycle,
metadata: webMetadata,
};
13 changes: 13 additions & 0 deletions backend/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ def is_loaded(self) -> bool:
"chatterbox_turbo": "Chatterbox Turbo",
"tada": "TADA",
"kokoro": "Kokoro",
"openvoice": "OpenVoice V2",
}

LLM_ENGINES = {
Expand Down Expand Up @@ -364,6 +365,14 @@ def _get_non_qwen_tts_configs() -> list[ModelConfig]:
size_mb=350,
languages=["en", "es", "fr", "hi", "it", "pt", "ja", "zh"],
),
ModelConfig(
model_name="openvoice-v2",
display_name="OpenVoice V2 (Multi-lingual Voice Clone)",
engine="openvoice",
hf_repo_id="myshell-ai/OpenVoiceV2",
size_mb=350,
languages=["en", "es", "fr", "zh", "ja", "ko"],
),
]


Expand Down Expand Up @@ -704,6 +713,10 @@ def get_tts_backend_for_engine(engine: str) -> TTSBackend:
from .kokoro_backend import KokoroTTSBackend

backend = KokoroTTSBackend()
elif engine == "openvoice":
from .openvoice_backend import OpenVoiceBackend

backend = OpenVoiceBackend()
elif engine == "qwen_custom_voice":
from .qwen_custom_voice_backend import QwenCustomVoiceBackend

Expand Down
Loading