mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 15:15:27 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3370e3b419 | ||
|
|
615bd188a0 | ||
|
|
7208f51eee | ||
|
|
07a91a2381 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.1.4
|
current_version = 0.1.5
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
tag_name = v{new_version}
|
tag_name = v{new_version}
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/app",
|
"name": "@voicebox/app",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tanstack/react-query-devtools": "^5.0.0",
|
"@tanstack/react-query-devtools": "^5.0.0",
|
||||||
|
"@tanstack/react-router": "^1.157.16",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.0.0",
|
"@tauri-apps/plugin-fs": "^2.0.0",
|
||||||
|
|||||||
+9
-77
@@ -1,32 +1,19 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { RouterProvider } from '@tanstack/react-router';
|
||||||
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
||||||
import { AppFrame } from '@/components/AppFrame/AppFrame';
|
|
||||||
import { AudioTab } from '@/components/AudioTab/AudioTab';
|
|
||||||
import { MainEditor } from '@/components/MainEditor/MainEditor';
|
|
||||||
import { ModelsTab } from '@/components/ModelsTab/ModelsTab';
|
|
||||||
import { ServerTab } from '@/components/ServerTab/ServerTab';
|
|
||||||
// import { GenerationForm } from '@/components/Generation/GenerationForm';
|
|
||||||
import ShinyText from '@/components/ShinyText';
|
import ShinyText from '@/components/ShinyText';
|
||||||
import { Sidebar } from '@/components/Sidebar';
|
|
||||||
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
|
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
|
||||||
import { Toaster } from '@/components/ui/toaster';
|
|
||||||
import { VoicesTab } from '@/components/VoicesTab/VoicesTab';
|
|
||||||
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
|
||||||
import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks';
|
|
||||||
import {
|
import {
|
||||||
isMacOS,
|
|
||||||
isTauri,
|
isTauri,
|
||||||
setKeepServerRunning,
|
setKeepServerRunning,
|
||||||
setupWindowCloseHandler,
|
setupWindowCloseHandler,
|
||||||
startServer,
|
startServer,
|
||||||
} from '@/lib/tauri';
|
} from '@/lib/tauri';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
|
import { router } from '@/router';
|
||||||
import { useServerStore } from '@/stores/serverStore';
|
import { useServerStore } from '@/stores/serverStore';
|
||||||
|
|
||||||
// Track if server is starting to prevent duplicate starts
|
|
||||||
let serverStarting = false;
|
|
||||||
|
|
||||||
const LOADING_MESSAGES = [
|
const LOADING_MESSAGES = [
|
||||||
'Warming up tensors...',
|
'Warming up tensors...',
|
||||||
'Calibrating synthesizer engine...',
|
'Calibrating synthesizer engine...',
|
||||||
@@ -51,12 +38,9 @@ const LOADING_MESSAGES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [activeTab, setActiveTab] = useState('main');
|
|
||||||
const [serverReady, setServerReady] = useState(false);
|
const [serverReady, setServerReady] = useState(false);
|
||||||
const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
|
const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
|
||||||
|
const serverStartingRef = useRef(false);
|
||||||
// Monitor active downloads/generations and show toasts for them
|
|
||||||
const activeDownloads = useRestoreActiveTasks();
|
|
||||||
|
|
||||||
// Sync stored setting to Rust on startup
|
// Sync stored setting to Rust on startup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -92,11 +76,11 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-start server in production
|
// Auto-start server in production
|
||||||
if (serverStarting) {
|
if (serverStartingRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
serverStarting = true;
|
serverStartingRef.current = true;
|
||||||
console.log('Production mode: Starting bundled server...');
|
console.log('Production mode: Starting bundled server...');
|
||||||
|
|
||||||
startServer(false)
|
startServer(false)
|
||||||
@@ -106,13 +90,11 @@ function App() {
|
|||||||
useServerStore.getState().setServerUrl(serverUrl);
|
useServerStore.getState().setServerUrl(serverUrl);
|
||||||
setServerReady(true);
|
setServerReady(true);
|
||||||
// Mark that we started the server (so we know to stop it on close)
|
// Mark that we started the server (so we know to stop it on close)
|
||||||
// @ts-expect-error - adding property to window
|
|
||||||
window.__voiceboxServerStartedByApp = true;
|
window.__voiceboxServerStartedByApp = true;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to auto-start server:', error);
|
console.error('Failed to auto-start server:', error);
|
||||||
serverStarting = false;
|
serverStartingRef.current = false;
|
||||||
// @ts-expect-error - adding property to window
|
|
||||||
window.__voiceboxServerStartedByApp = false;
|
window.__voiceboxServerStartedByApp = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,7 +102,7 @@ function App() {
|
|||||||
// Note: Window close is handled separately in Tauri Rust code
|
// Note: Window close is handled separately in Tauri Rust code
|
||||||
return () => {
|
return () => {
|
||||||
// Window close event handles server shutdown based on setting
|
// Window close event handles server shutdown based on setting
|
||||||
serverStarting = false;
|
serverStartingRef.current = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -172,57 +154,7 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <RouterProvider router={router} />;
|
||||||
<AppFrame>
|
|
||||||
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
||||||
<Sidebar activeTab={activeTab} onTabChange={setActiveTab} isMacOS={isMacOS()} />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-20 overflow-hidden flex flex-col">
|
|
||||||
<div className="container mx-auto px-8 max-w-[1800px] h-full overflow-hidden flex flex-col">
|
|
||||||
{activeTab === 'main' && <MainEditor />}
|
|
||||||
{activeTab === 'voices' && <VoicesTab />}
|
|
||||||
{activeTab === 'audio' && <AudioTab />}
|
|
||||||
{activeTab === 'server' && <ServerTab />}
|
|
||||||
{activeTab === 'models' && <ModelsTab />}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Show download toasts for any active downloads (from anywhere) */}
|
|
||||||
{activeDownloads.map((download) => {
|
|
||||||
const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name;
|
|
||||||
return (
|
|
||||||
<DownloadToastRestorer
|
|
||||||
key={download.model_name}
|
|
||||||
modelName={download.model_name}
|
|
||||||
displayName={displayName}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Toaster />
|
|
||||||
</AppFrame>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component that restores a download toast for a specific model.
|
|
||||||
*/
|
|
||||||
function DownloadToastRestorer({
|
|
||||||
modelName,
|
|
||||||
displayName,
|
|
||||||
}: {
|
|
||||||
modelName: string;
|
|
||||||
displayName: string;
|
|
||||||
}) {
|
|
||||||
// Use the download toast hook to restore the toast
|
|
||||||
useModelDownloadToast({
|
|
||||||
modelName,
|
|
||||||
displayName,
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Slider } from '@/components/ui/slider';
|
|||||||
import { apiClient } from '@/lib/api/client';
|
import { apiClient } from '@/lib/api/client';
|
||||||
import { isTauri } from '@/lib/tauri';
|
import { isTauri } from '@/lib/tauri';
|
||||||
import { formatAudioDuration } from '@/lib/utils/audio';
|
import { formatAudioDuration } from '@/lib/utils/audio';
|
||||||
|
import { debug } from '@/lib/utils/debug';
|
||||||
import { usePlayerStore } from '@/stores/playerStore';
|
import { usePlayerStore } from '@/stores/playerStore';
|
||||||
|
|
||||||
export function AudioPlayer() {
|
export function AudioPlayer() {
|
||||||
@@ -49,28 +50,17 @@ export function AudioPlayer() {
|
|||||||
|
|
||||||
// Determine if we should use native playback
|
// Determine if we should use native playback
|
||||||
const useNativePlayback = useMemo(() => {
|
const useNativePlayback = useMemo(() => {
|
||||||
console.log('useNativePlayback memo:', {
|
|
||||||
isTauri: isTauri(),
|
|
||||||
profileId,
|
|
||||||
profileChannels,
|
|
||||||
channels,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isTauri() || !profileChannels || !channels) {
|
if (!isTauri() || !profileChannels || !channels) {
|
||||||
console.log('useNativePlayback: false - missing requirements');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assignedChannels = channels.filter((ch) => profileChannels.channel_ids.includes(ch.id));
|
const assignedChannels = channels.filter((ch) => profileChannels.channel_ids.includes(ch.id));
|
||||||
|
|
||||||
console.log('Assigned channels:', assignedChannels);
|
|
||||||
|
|
||||||
// Use native playback if any assigned channel has non-default devices
|
// Use native playback if any assigned channel has non-default devices
|
||||||
const shouldUseNative = assignedChannels.some(
|
const shouldUseNative = assignedChannels.some(
|
||||||
(ch) => ch.device_ids.length > 0 && !ch.is_default,
|
(ch) => ch.device_ids.length > 0 && !ch.is_default,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('useNativePlayback result:', shouldUseNative);
|
|
||||||
return shouldUseNative;
|
return shouldUseNative;
|
||||||
}, [profileChannels, channels, profileId]);
|
}, [profileChannels, channels, profileId]);
|
||||||
|
|
||||||
@@ -91,11 +81,11 @@ export function AudioPlayer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (wavesurferRef.current) {
|
if (wavesurferRef.current) {
|
||||||
console.log('WaveSurfer already initialized, skipping');
|
debug.log('WaveSurfer already initialized, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Creating NEW WaveSurfer instance');
|
debug.log('Creating NEW WaveSurfer instance');
|
||||||
|
|
||||||
// Wait for container to be properly rendered
|
// Wait for container to be properly rendered
|
||||||
const initWaveSurfer = () => {
|
const initWaveSurfer = () => {
|
||||||
@@ -121,7 +111,7 @@ export function AudioPlayer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Initializing WaveSurfer...', {
|
debug.log('Initializing WaveSurfer...', {
|
||||||
container,
|
container,
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
height: rect.height,
|
height: rect.height,
|
||||||
@@ -154,9 +144,9 @@ export function AudioPlayer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
wavesurferRef.current = wavesurfer;
|
wavesurferRef.current = wavesurfer;
|
||||||
console.log('WaveSurfer created successfully');
|
debug.log('WaveSurfer created successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create WaveSurfer:', error);
|
debug.error('Failed to create WaveSurfer:', error);
|
||||||
setError(
|
setError(
|
||||||
`Failed to initialize waveform: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to initialize waveform: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
);
|
);
|
||||||
@@ -178,8 +168,8 @@ export function AudioPlayer() {
|
|||||||
loadingRef.current = false;
|
loadingRef.current = false;
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
console.log('Audio ready, duration:', dur);
|
debug.log('Audio ready, duration:', dur);
|
||||||
console.log('Waveform should be visible now');
|
debug.log('Waveform should be visible now');
|
||||||
|
|
||||||
// Ensure volume is set
|
// Ensure volume is set
|
||||||
const currentVolume = usePlayerStore.getState().volume;
|
const currentVolume = usePlayerStore.getState().volume;
|
||||||
@@ -191,7 +181,7 @@ export function AudioPlayer() {
|
|||||||
if (mediaElement && !isUsingNativePlaybackRef.current) {
|
if (mediaElement && !isUsingNativePlaybackRef.current) {
|
||||||
mediaElement.volume = currentVolume;
|
mediaElement.volume = currentVolume;
|
||||||
mediaElement.muted = false;
|
mediaElement.muted = false;
|
||||||
console.log('Audio element volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
debug.log('Audio element volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-play when ready - check if we should use native playback
|
// Auto-play when ready - check if we should use native playback
|
||||||
@@ -199,7 +189,7 @@ export function AudioPlayer() {
|
|||||||
const currentAudioUrl = usePlayerStore.getState().audioUrl;
|
const currentAudioUrl = usePlayerStore.getState().audioUrl;
|
||||||
const currentProfileId = usePlayerStore.getState().profileId;
|
const currentProfileId = usePlayerStore.getState().profileId;
|
||||||
|
|
||||||
console.log('Auto-play check - capturing runtime values...');
|
debug.log('Auto-play check - capturing runtime values...');
|
||||||
|
|
||||||
// Fetch profile channels at runtime (not using captured value)
|
// Fetch profile channels at runtime (not using captured value)
|
||||||
let runtimeProfileChannels = null;
|
let runtimeProfileChannels = null;
|
||||||
@@ -208,18 +198,18 @@ export function AudioPlayer() {
|
|||||||
if (isTauri() && currentProfileId) {
|
if (isTauri() && currentProfileId) {
|
||||||
try {
|
try {
|
||||||
runtimeProfileChannels = await apiClient.getProfileChannels(currentProfileId);
|
runtimeProfileChannels = await apiClient.getProfileChannels(currentProfileId);
|
||||||
console.log('Runtime profileChannels:', runtimeProfileChannels);
|
debug.log('Runtime profileChannels:', runtimeProfileChannels);
|
||||||
|
|
||||||
if (runtimeProfileChannels && runtimeProfileChannels.channel_ids.length > 0) {
|
if (runtimeProfileChannels && runtimeProfileChannels.channel_ids.length > 0) {
|
||||||
runtimeChannels = await apiClient.listChannels();
|
runtimeChannels = await apiClient.listChannels();
|
||||||
console.log('Runtime channels:', runtimeChannels);
|
debug.log('Runtime channels:', runtimeChannels);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch runtime channel data:', error);
|
debug.error('Failed to fetch runtime channel data:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Auto-play check:', {
|
debug.log('Auto-play check:', {
|
||||||
isTauri: isTauri(),
|
isTauri: isTauri(),
|
||||||
currentAudioUrl,
|
currentAudioUrl,
|
||||||
currentProfileId,
|
currentProfileId,
|
||||||
@@ -234,15 +224,15 @@ export function AudioPlayer() {
|
|||||||
runtimeProfileChannels &&
|
runtimeProfileChannels &&
|
||||||
runtimeChannels
|
runtimeChannels
|
||||||
) {
|
) {
|
||||||
console.log('Attempting native audio playback...');
|
debug.log('Attempting native audio playback...');
|
||||||
|
|
||||||
// Stop any existing native playback first
|
// Stop any existing native playback first
|
||||||
if (isUsingNativePlaybackRef.current) {
|
if (isUsingNativePlaybackRef.current) {
|
||||||
try {
|
try {
|
||||||
await invoke('stop_audio_playback');
|
await invoke('stop_audio_playback');
|
||||||
console.log('Stopped existing native playback before starting new one');
|
debug.log('Stopped existing native playback before starting new one');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to stop existing playback:', error);
|
debug.error('Failed to stop existing playback:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,16 +241,16 @@ export function AudioPlayer() {
|
|||||||
const assignedChannels = runtimeChannels.filter((ch: any) =>
|
const assignedChannels = runtimeChannels.filter((ch: any) =>
|
||||||
runtimeProfileChannels.channel_ids.includes(ch.id),
|
runtimeProfileChannels.channel_ids.includes(ch.id),
|
||||||
);
|
);
|
||||||
console.log('Assigned channels for playback:', assignedChannels);
|
debug.log('Assigned channels for playback:', assignedChannels);
|
||||||
|
|
||||||
// Check if any assigned channel has non-default devices
|
// Check if any assigned channel has non-default devices
|
||||||
const shouldUseNative = assignedChannels.some(
|
const shouldUseNative = assignedChannels.some(
|
||||||
(ch: any) => ch.device_ids.length > 0 && !ch.is_default,
|
(ch: any) => ch.device_ids.length > 0 && !ch.is_default,
|
||||||
);
|
);
|
||||||
console.log('Should use native playback:', shouldUseNative);
|
debug.log('Should use native playback:', shouldUseNative);
|
||||||
|
|
||||||
if (!shouldUseNative) {
|
if (!shouldUseNative) {
|
||||||
console.log('No custom devices assigned, falling back to WaveSurfer');
|
debug.log('No custom devices assigned, falling back to WaveSurfer');
|
||||||
// Reset native playback flag and unmute WaveSurfer
|
// Reset native playback flag and unmute WaveSurfer
|
||||||
isUsingNativePlaybackRef.current = false;
|
isUsingNativePlaybackRef.current = false;
|
||||||
const mediaElement = wavesurfer.getMediaElement();
|
const mediaElement = wavesurfer.getMediaElement();
|
||||||
@@ -268,7 +258,7 @@ export function AudioPlayer() {
|
|||||||
const currentVolume = usePlayerStore.getState().volume;
|
const currentVolume = usePlayerStore.getState().volume;
|
||||||
mediaElement.volume = currentVolume;
|
mediaElement.volume = currentVolume;
|
||||||
mediaElement.muted = false;
|
mediaElement.muted = false;
|
||||||
console.log(
|
debug.log(
|
||||||
'WaveSurfer unmuted for normal playback - volume:',
|
'WaveSurfer unmuted for normal playback - volume:',
|
||||||
mediaElement.volume,
|
mediaElement.volume,
|
||||||
'muted:',
|
'muted:',
|
||||||
@@ -277,23 +267,23 @@ export function AudioPlayer() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const deviceIds = assignedChannels.flatMap((ch: any) => ch.device_ids);
|
const deviceIds = assignedChannels.flatMap((ch: any) => ch.device_ids);
|
||||||
console.log('Device IDs to play to:', deviceIds);
|
debug.log('Device IDs to play to:', deviceIds);
|
||||||
|
|
||||||
if (deviceIds.length > 0) {
|
if (deviceIds.length > 0) {
|
||||||
console.log('Fetching audio data from:', currentAudioUrl);
|
debug.log('Fetching audio data from:', currentAudioUrl);
|
||||||
// Fetch audio data
|
// Fetch audio data
|
||||||
const response = await fetch(currentAudioUrl);
|
const response = await fetch(currentAudioUrl);
|
||||||
const audioData = new Uint8Array(await response.arrayBuffer());
|
const audioData = new Uint8Array(await response.arrayBuffer());
|
||||||
console.log('Audio data size:', audioData.length);
|
debug.log('Audio data size:', audioData.length);
|
||||||
|
|
||||||
// Play via native audio
|
// Play via native audio
|
||||||
console.log('Invoking play_audio_to_devices...');
|
debug.log('Invoking play_audio_to_devices...');
|
||||||
try {
|
try {
|
||||||
const result = await invoke('play_audio_to_devices', {
|
const result = await invoke('play_audio_to_devices', {
|
||||||
audioData: Array.from(audioData),
|
audioData: Array.from(audioData),
|
||||||
deviceIds: deviceIds,
|
deviceIds: deviceIds,
|
||||||
});
|
});
|
||||||
console.log('play_audio_to_devices completed successfully, result:', result);
|
debug.log('play_audio_to_devices completed successfully, result:', result);
|
||||||
|
|
||||||
// Mark that we're using native playback
|
// Mark that we're using native playback
|
||||||
isUsingNativePlaybackRef.current = true;
|
isUsingNativePlaybackRef.current = true;
|
||||||
@@ -304,7 +294,7 @@ export function AudioPlayer() {
|
|||||||
if (mediaElement) {
|
if (mediaElement) {
|
||||||
mediaElement.volume = 0;
|
mediaElement.volume = 0;
|
||||||
mediaElement.muted = true;
|
mediaElement.muted = true;
|
||||||
console.log(
|
debug.log(
|
||||||
'WaveSurfer muted for native playback - volume:',
|
'WaveSurfer muted for native playback - volume:',
|
||||||
mediaElement.volume,
|
mediaElement.volume,
|
||||||
'muted:',
|
'muted:',
|
||||||
@@ -314,22 +304,22 @@ export function AudioPlayer() {
|
|||||||
|
|
||||||
// Start WaveSurfer playback for visualization (muted)
|
// Start WaveSurfer playback for visualization (muted)
|
||||||
wavesurfer.play().catch((error) => {
|
wavesurfer.play().catch((error) => {
|
||||||
console.error('Failed to start WaveSurfer visualization:', error);
|
debug.error('Failed to start WaveSurfer visualization:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
console.log('Auto-playing via native audio routing - SUCCESS');
|
debug.log('Auto-playing via native audio routing - SUCCESS');
|
||||||
return;
|
return;
|
||||||
} catch (invokeError) {
|
} catch (invokeError) {
|
||||||
console.error('play_audio_to_devices invoke failed:', invokeError);
|
debug.error('play_audio_to_devices invoke failed:', invokeError);
|
||||||
throw invokeError;
|
throw invokeError;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('No device IDs found, falling back to WaveSurfer');
|
debug.log('No device IDs found, falling back to WaveSurfer');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
debug.error(
|
||||||
'Native playback failed during auto-play, falling back to WaveSurfer:',
|
'Native playback failed during auto-play, falling back to WaveSurfer:',
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
@@ -340,7 +330,7 @@ export function AudioPlayer() {
|
|||||||
const currentVolume = usePlayerStore.getState().volume;
|
const currentVolume = usePlayerStore.getState().volume;
|
||||||
mediaElement.volume = currentVolume;
|
mediaElement.volume = currentVolume;
|
||||||
mediaElement.muted = false;
|
mediaElement.muted = false;
|
||||||
console.log(
|
debug.log(
|
||||||
'WaveSurfer unmuted after native playback failure - volume:',
|
'WaveSurfer unmuted after native playback failure - volume:',
|
||||||
mediaElement.volume,
|
mediaElement.volume,
|
||||||
'muted:',
|
'muted:',
|
||||||
@@ -350,7 +340,7 @@ export function AudioPlayer() {
|
|||||||
// Fall through to WaveSurfer playback
|
// Fall through to WaveSurfer playback
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Not using native playback, using WaveSurfer');
|
debug.log('Not using native playback, using WaveSurfer');
|
||||||
// Reset native playback flag and unmute WaveSurfer
|
// Reset native playback flag and unmute WaveSurfer
|
||||||
isUsingNativePlaybackRef.current = false;
|
isUsingNativePlaybackRef.current = false;
|
||||||
const mediaElement = wavesurfer.getMediaElement();
|
const mediaElement = wavesurfer.getMediaElement();
|
||||||
@@ -358,7 +348,7 @@ export function AudioPlayer() {
|
|||||||
const currentVolume = usePlayerStore.getState().volume;
|
const currentVolume = usePlayerStore.getState().volume;
|
||||||
mediaElement.volume = currentVolume;
|
mediaElement.volume = currentVolume;
|
||||||
mediaElement.muted = false;
|
mediaElement.muted = false;
|
||||||
console.log(
|
debug.log(
|
||||||
'WaveSurfer unmuted for normal playback - volume:',
|
'WaveSurfer unmuted for normal playback - volume:',
|
||||||
mediaElement.volume,
|
mediaElement.volume,
|
||||||
'muted:',
|
'muted:',
|
||||||
@@ -371,7 +361,7 @@ export function AudioPlayer() {
|
|||||||
// Use a small delay to ensure audio element is fully ready
|
// Use a small delay to ensure audio element is fully ready
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wavesurfer.play().catch((error) => {
|
wavesurfer.play().catch((error) => {
|
||||||
console.error('Failed to autoplay:', error);
|
debug.error('Failed to autoplay:', error);
|
||||||
// Don't show error for autoplay failures (browser restrictions)
|
// Don't show error for autoplay failures (browser restrictions)
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -388,13 +378,13 @@ export function AudioPlayer() {
|
|||||||
if (isUsingNativePlaybackRef.current) {
|
if (isUsingNativePlaybackRef.current) {
|
||||||
mediaElement.volume = 0;
|
mediaElement.volume = 0;
|
||||||
mediaElement.muted = true;
|
mediaElement.muted = true;
|
||||||
console.log('Playing (native mode) - WaveSurfer muted for visualization only');
|
debug.log('Playing (native mode) - WaveSurfer muted for visualization only');
|
||||||
} else {
|
} else {
|
||||||
// Ensure WaveSurfer is unmuted for normal playback
|
// Ensure WaveSurfer is unmuted for normal playback
|
||||||
const currentVolume = usePlayerStore.getState().volume;
|
const currentVolume = usePlayerStore.getState().volume;
|
||||||
mediaElement.volume = currentVolume;
|
mediaElement.volume = currentVolume;
|
||||||
mediaElement.muted = false;
|
mediaElement.muted = false;
|
||||||
console.log(
|
debug.log(
|
||||||
'Playing (normal mode) - volume:',
|
'Playing (normal mode) - volume:',
|
||||||
mediaElement.volume,
|
mediaElement.volume,
|
||||||
'muted:',
|
'muted:',
|
||||||
@@ -417,7 +407,7 @@ export function AudioPlayer() {
|
|||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
wavesurfer.on('error', (error) => {
|
wavesurfer.on('error', (error) => {
|
||||||
console.error('WaveSurfer error:', error);
|
debug.error('WaveSurfer error:', error);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(`Audio error: ${error instanceof Error ? error.message : String(error)}`);
|
setError(`Audio error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
});
|
});
|
||||||
@@ -432,7 +422,7 @@ export function AudioPlayer() {
|
|||||||
|
|
||||||
// Load audio immediately if audioUrl is already set
|
// Load audio immediately if audioUrl is already set
|
||||||
if (audioUrl) {
|
if (audioUrl) {
|
||||||
console.log('WaveSurfer ready, loading audio:', audioUrl);
|
debug.log('WaveSurfer ready, loading audio:', audioUrl);
|
||||||
loadingRef.current = true;
|
loadingRef.current = true;
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
// Stop any current playback before loading new audio
|
// Stop any current playback before loading new audio
|
||||||
@@ -442,11 +432,11 @@ export function AudioPlayer() {
|
|||||||
wavesurfer
|
wavesurfer
|
||||||
.load(audioUrl)
|
.load(audioUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Audio loaded into WaveSurfer');
|
debug.log('Audio loaded into WaveSurfer');
|
||||||
loadingRef.current = false;
|
loadingRef.current = false;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to load audio into WaveSurfer:', error);
|
debug.error('Failed to load audio into WaveSurfer:', error);
|
||||||
loadingRef.current = false;
|
loadingRef.current = false;
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(
|
setError(
|
||||||
@@ -471,12 +461,12 @@ export function AudioPlayer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log('Cleaning up WaveSurfer initialization effect');
|
debug.log('Cleaning up WaveSurfer initialization effect');
|
||||||
if (rafId1) cancelAnimationFrame(rafId1);
|
if (rafId1) cancelAnimationFrame(rafId1);
|
||||||
if (rafId2) cancelAnimationFrame(rafId2);
|
if (rafId2) cancelAnimationFrame(rafId2);
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
if (wavesurferRef.current) {
|
if (wavesurferRef.current) {
|
||||||
console.log('Destroying WaveSurfer instance');
|
debug.log('Destroying WaveSurfer instance');
|
||||||
try {
|
try {
|
||||||
const mediaElement = wavesurferRef.current.getMediaElement();
|
const mediaElement = wavesurferRef.current.getMediaElement();
|
||||||
if (mediaElement) {
|
if (mediaElement) {
|
||||||
@@ -485,7 +475,7 @@ export function AudioPlayer() {
|
|||||||
}
|
}
|
||||||
wavesurferRef.current.destroy();
|
wavesurferRef.current.destroy();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error destroying WaveSurfer:', error);
|
debug.error('Error destroying WaveSurfer:', error);
|
||||||
}
|
}
|
||||||
wavesurferRef.current = null;
|
wavesurferRef.current = null;
|
||||||
}
|
}
|
||||||
@@ -517,9 +507,9 @@ export function AudioPlayer() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await invoke('stop_audio_playback');
|
await invoke('stop_audio_playback');
|
||||||
console.log('Stopped native audio playback');
|
debug.log('Stopped native audio playback');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to stop native playback:', error);
|
debug.error('Failed to stop native playback:', error);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@@ -537,30 +527,30 @@ export function AudioPlayer() {
|
|||||||
|
|
||||||
// CRITICAL: Force stop any current playback and cancel any pending loads
|
// CRITICAL: Force stop any current playback and cancel any pending loads
|
||||||
// This must happen BEFORE any early returns
|
// This must happen BEFORE any early returns
|
||||||
console.log('Audio URL changed to:', audioUrl);
|
debug.log('Audio URL changed to:', audioUrl);
|
||||||
|
|
||||||
// COMPLETELY stop and destroy the current audio
|
// COMPLETELY stop and destroy the current audio
|
||||||
try {
|
try {
|
||||||
// First pause if playing
|
// First pause if playing
|
||||||
if (wavesurfer.isPlaying()) {
|
if (wavesurfer.isPlaying()) {
|
||||||
console.log('Pausing current playback');
|
debug.log('Pausing current playback');
|
||||||
wavesurfer.pause();
|
wavesurfer.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the media element explicitly
|
// Stop the media element explicitly
|
||||||
const mediaElement = wavesurfer.getMediaElement();
|
const mediaElement = wavesurfer.getMediaElement();
|
||||||
if (mediaElement) {
|
if (mediaElement) {
|
||||||
console.log('Stopping media element');
|
debug.log('Stopping media element');
|
||||||
mediaElement.pause();
|
mediaElement.pause();
|
||||||
mediaElement.currentTime = 0;
|
mediaElement.currentTime = 0;
|
||||||
mediaElement.src = '';
|
mediaElement.src = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use empty() to completely destroy the waveform and media element
|
// Use empty() to completely destroy the waveform and media element
|
||||||
console.log('Calling wavesurfer.empty() to destroy audio');
|
debug.log('Calling wavesurfer.empty() to destroy audio');
|
||||||
wavesurfer.empty();
|
wavesurfer.empty();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error stopping previous audio:', error);
|
debug.error('Error stopping previous audio:', error);
|
||||||
// Continue anyway to load new audio
|
// Continue anyway to load new audio
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,16 +565,16 @@ export function AudioPlayer() {
|
|||||||
setDuration(0);
|
setDuration(0);
|
||||||
|
|
||||||
// Load new audio
|
// Load new audio
|
||||||
console.log('Starting new audio load for:', audioUrl);
|
debug.log('Starting new audio load for:', audioUrl);
|
||||||
wavesurfer
|
wavesurfer
|
||||||
.load(audioUrl)
|
.load(audioUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Audio load promise resolved');
|
debug.log('Audio load promise resolved');
|
||||||
// Don't set loading to false here - wait for 'ready' event
|
// Don't set loading to false here - wait for 'ready' event
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to load audio:', error);
|
debug.error('Failed to load audio:', error);
|
||||||
console.error('Audio URL:', audioUrl);
|
debug.error('Audio URL:', audioUrl);
|
||||||
loadingRef.current = false;
|
loadingRef.current = false;
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(`Failed to load audio: ${error instanceof Error ? error.message : String(error)}`);
|
setError(`Failed to load audio: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
@@ -599,7 +589,7 @@ export function AudioPlayer() {
|
|||||||
if (isPlaying && wavesurferRef.current.isPlaying() === false) {
|
if (isPlaying && wavesurferRef.current.isPlaying() === false) {
|
||||||
// Only auto-play if audio is ready
|
// Only auto-play if audio is ready
|
||||||
wavesurferRef.current.play().catch((error) => {
|
wavesurferRef.current.play().catch((error) => {
|
||||||
console.error('Failed to play:', error);
|
debug.error('Failed to play:', error);
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
});
|
});
|
||||||
@@ -619,11 +609,11 @@ export function AudioPlayer() {
|
|||||||
if (isUsingNativePlaybackRef.current) {
|
if (isUsingNativePlaybackRef.current) {
|
||||||
mediaElement.volume = 0;
|
mediaElement.volume = 0;
|
||||||
mediaElement.muted = true;
|
mediaElement.muted = true;
|
||||||
console.log('Volume sync: Using native playback, keeping WaveSurfer muted');
|
debug.log('Volume sync: Using native playback, keeping WaveSurfer muted');
|
||||||
} else {
|
} else {
|
||||||
mediaElement.volume = volume;
|
mediaElement.volume = volume;
|
||||||
mediaElement.muted = volume === 0;
|
mediaElement.muted = volume === 0;
|
||||||
console.log('Volume synced:', volume, 'muted:', mediaElement.muted);
|
debug.log('Volume synced:', volume, 'muted:', mediaElement.muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -651,10 +641,10 @@ export function AudioPlayer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset to beginning and play
|
// Reset to beginning and play
|
||||||
console.log('Restarting current audio from beginning');
|
debug.log('Restarting current audio from beginning');
|
||||||
wavesurfer.seekTo(0);
|
wavesurfer.seekTo(0);
|
||||||
wavesurfer.play().catch((error) => {
|
wavesurfer.play().catch((error) => {
|
||||||
console.error('Failed to play after restart:', error);
|
debug.error('Failed to play after restart:', error);
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
});
|
});
|
||||||
@@ -669,13 +659,13 @@ export function AudioPlayer() {
|
|||||||
// Standard WaveSurfer playback (works for both normal and native playback modes)
|
// Standard WaveSurfer playback (works for both normal and native playback modes)
|
||||||
// When using native playback, WaveSurfer is muted but still controls visualization
|
// When using native playback, WaveSurfer is muted but still controls visualization
|
||||||
if (!wavesurferRef.current) {
|
if (!wavesurferRef.current) {
|
||||||
console.error('WaveSurfer not initialized');
|
debug.error('WaveSurfer not initialized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if audio is loaded
|
// Check if audio is loaded
|
||||||
if (duration === 0 && !isLoading) {
|
if (duration === 0 && !isLoading) {
|
||||||
console.error('Audio not loaded yet');
|
debug.error('Audio not loaded yet');
|
||||||
setError('Audio not loaded. Please wait...');
|
setError('Audio not loaded. Please wait...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -686,9 +676,9 @@ export function AudioPlayer() {
|
|||||||
// Pause: stop native playback and pause WaveSurfer visualization
|
// Pause: stop native playback and pause WaveSurfer visualization
|
||||||
try {
|
try {
|
||||||
await invoke('stop_audio_playback');
|
await invoke('stop_audio_playback');
|
||||||
console.log('Stopped native audio playback');
|
debug.log('Stopped native audio playback');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to stop native playback:', error);
|
debug.error('Failed to stop native playback:', error);
|
||||||
}
|
}
|
||||||
wavesurferRef.current.pause();
|
wavesurferRef.current.pause();
|
||||||
return;
|
return;
|
||||||
@@ -701,7 +691,7 @@ export function AudioPlayer() {
|
|||||||
await invoke('stop_audio_playback');
|
await invoke('stop_audio_playback');
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// Ignore errors when stopping (might not be playing)
|
// Ignore errors when stopping (might not be playing)
|
||||||
console.log('No existing playback to stop');
|
debug.log('No existing playback to stop');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all device IDs from assigned channels
|
// Collect all device IDs from assigned channels
|
||||||
@@ -733,7 +723,7 @@ export function AudioPlayer() {
|
|||||||
|
|
||||||
// Start WaveSurfer for visualization (muted)
|
// Start WaveSurfer for visualization (muted)
|
||||||
wavesurferRef.current.play().catch((error) => {
|
wavesurferRef.current.play().catch((error) => {
|
||||||
console.error('Failed to start WaveSurfer visualization:', error);
|
debug.error('Failed to start WaveSurfer visualization:', error);
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
});
|
});
|
||||||
@@ -741,7 +731,7 @@ export function AudioPlayer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Native playback failed, falling back to WaveSurfer:', error);
|
debug.error('Native playback failed, falling back to WaveSurfer:', error);
|
||||||
// Fall through to WaveSurfer playback
|
// Fall through to WaveSurfer playback
|
||||||
isUsingNativePlaybackRef.current = false;
|
isUsingNativePlaybackRef.current = false;
|
||||||
}
|
}
|
||||||
@@ -761,7 +751,7 @@ export function AudioPlayer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wavesurferRef.current.play().catch((error) => {
|
wavesurferRef.current.play().catch((error) => {
|
||||||
console.error('Failed to play:', error);
|
debug.error('Failed to play:', error);
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
});
|
});
|
||||||
@@ -782,7 +772,7 @@ export function AudioPlayer() {
|
|||||||
// Stop any native playback
|
// Stop any native playback
|
||||||
if (isUsingNativePlaybackRef.current && isTauri()) {
|
if (isUsingNativePlaybackRef.current && isTauri()) {
|
||||||
invoke('stop_audio_playback').catch((error) => {
|
invoke('stop_audio_playback').catch((error) => {
|
||||||
console.error('Failed to stop native playback:', error);
|
debug.error('Failed to stop native playback:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Stop WaveSurfer
|
// Stop WaveSurfer
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Loader2, Sparkles } from 'lucide-react';
|
import { Loader2, Sparkles } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import * as z from 'zod';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
||||||
import {
|
import {
|
||||||
@@ -14,24 +11,11 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { LANGUAGE_OPTIONS } from '@/lib/constants/languages';
|
||||||
import { apiClient } from '@/lib/api/client';
|
import { useGenerationForm } from '@/lib/hooks/useGenerationForm';
|
||||||
import { LANGUAGE_CODES, LANGUAGE_OPTIONS, type LanguageCode } from '@/lib/constants/languages';
|
|
||||||
import { useGeneration } from '@/lib/hooks/useGeneration';
|
|
||||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
|
||||||
import { useProfile } from '@/lib/hooks/useProfiles';
|
import { useProfile } from '@/lib/hooks/useProfiles';
|
||||||
import { useGenerationStore } from '@/stores/generationStore';
|
|
||||||
import { usePlayerStore } from '@/stores/playerStore';
|
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
|
|
||||||
const generationSchema = z.object({
|
|
||||||
text: z.string().min(1, 'Text is required').max(5000),
|
|
||||||
language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),
|
|
||||||
modelSize: z.enum(['1.7B', '0.6B']).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type GenerationFormValues = z.infer<typeof generationSchema>;
|
|
||||||
|
|
||||||
interface FloatingGenerateBoxProps {
|
interface FloatingGenerateBoxProps {
|
||||||
isPlayerOpen: boolean;
|
isPlayerOpen: boolean;
|
||||||
}
|
}
|
||||||
@@ -39,27 +23,12 @@ interface FloatingGenerateBoxProps {
|
|||||||
export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps) {
|
export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps) {
|
||||||
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
|
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
|
||||||
const { data: selectedProfile } = useProfile(selectedProfileId || '');
|
const { data: selectedProfile } = useProfile(selectedProfileId || '');
|
||||||
const generation = useGeneration();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const setAudio = usePlayerStore((state) => state.setAudio);
|
|
||||||
const setIsGenerating = useGenerationStore((state) => state.setIsGenerating);
|
|
||||||
const [downloadingModelName, setDownloadingModelName] = useState<string | null>(null);
|
|
||||||
const [downloadingDisplayName, setDownloadingDisplayName] = useState<string | null>(null);
|
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useModelDownloadToast({
|
const { form, handleSubmit, isPending } = useGenerationForm({
|
||||||
modelName: downloadingModelName || '',
|
onSuccess: () => {
|
||||||
displayName: downloadingDisplayName || '',
|
setIsExpanded(false);
|
||||||
enabled: !!downloadingModelName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<GenerationFormValues>({
|
|
||||||
resolver: zodResolver(generationSchema),
|
|
||||||
defaultValues: {
|
|
||||||
text: '',
|
|
||||||
language: 'en',
|
|
||||||
modelSize: '1.7B',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,62 +62,8 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
|||||||
};
|
};
|
||||||
}, [isExpanded]);
|
}, [isExpanded]);
|
||||||
|
|
||||||
async function onSubmit(data: GenerationFormValues) {
|
async function onSubmit(data: Parameters<typeof handleSubmit>[0]) {
|
||||||
if (!selectedProfileId) {
|
await handleSubmit(data, selectedProfileId);
|
||||||
toast({
|
|
||||||
title: 'No profile selected',
|
|
||||||
description: 'Please select a voice profile from the cards above.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsGenerating(true);
|
|
||||||
|
|
||||||
const modelName = `qwen-tts-${data.modelSize}`;
|
|
||||||
const displayName = data.modelSize === '1.7B' ? 'Qwen TTS 1.7B' : 'Qwen TTS 0.6B';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const modelStatus = await apiClient.getModelStatus();
|
|
||||||
const model = modelStatus.models.find((m) => m.model_name === modelName);
|
|
||||||
|
|
||||||
if (model && !model.downloaded) {
|
|
||||||
setDownloadingModelName(modelName);
|
|
||||||
setDownloadingDisplayName(displayName);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to check model status:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await generation.mutateAsync({
|
|
||||||
profile_id: selectedProfileId,
|
|
||||||
text: data.text,
|
|
||||||
language: data.language,
|
|
||||||
model_size: data.modelSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Generation complete!',
|
|
||||||
description: `Audio generated (${result.duration.toFixed(2)}s)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const audioUrl = apiClient.getAudioUrl(result.id);
|
|
||||||
setAudio(audioUrl, result.id, selectedProfileId, data.text.substring(0, 50));
|
|
||||||
|
|
||||||
form.reset();
|
|
||||||
setIsExpanded(false);
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
title: 'Generation failed',
|
|
||||||
description: error instanceof Error ? error.message : 'Failed to generate audio',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsGenerating(false);
|
|
||||||
setDownloadingModelName(null);
|
|
||||||
setDownloadingDisplayName(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -168,7 +83,6 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
// animate={{ marginBottom: isExpanded ? '0.75rem' : '0' }}
|
|
||||||
transition={{ duration: 0.3, ease: 'easeOut' }}
|
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -202,11 +116,11 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={generation.isPending || !selectedProfileId}
|
disabled={isPending || !selectedProfileId}
|
||||||
className="h-10 w-10 rounded-full bg-accent hover:bg-accent/90 hover:scale-105 text-accent-foreground shadow-lg hover:shadow-accent/50 shrink-0 transition-all duration-200"
|
className="h-10 w-10 rounded-full bg-accent hover:bg-accent/90 hover:scale-105 text-accent-foreground shadow-lg hover:shadow-accent/50 shrink-0 transition-all duration-200"
|
||||||
size="icon"
|
size="icon"
|
||||||
>
|
>
|
||||||
{generation.isPending ? (
|
{isPending ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Sparkles className="h-4 w-4" />
|
<Sparkles className="h-4 w-4" />
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { Loader2, Mic } from 'lucide-react';
|
import { Loader2, Mic } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import * as z from 'zod';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
@@ -23,118 +19,19 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { LANGUAGE_OPTIONS } from '@/lib/constants/languages';
|
||||||
import { apiClient } from '@/lib/api/client';
|
import { useGenerationForm } from '@/lib/hooks/useGenerationForm';
|
||||||
import { LANGUAGE_CODES, LANGUAGE_OPTIONS, type LanguageCode } from '@/lib/constants/languages';
|
|
||||||
import { useGeneration } from '@/lib/hooks/useGeneration';
|
|
||||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
|
||||||
import { useProfile } from '@/lib/hooks/useProfiles';
|
import { useProfile } from '@/lib/hooks/useProfiles';
|
||||||
import { useGenerationStore } from '@/stores/generationStore';
|
|
||||||
import { usePlayerStore } from '@/stores/playerStore';
|
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
|
|
||||||
const generationSchema = z.object({
|
|
||||||
text: z.string().min(1, 'Text is required').max(5000),
|
|
||||||
language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),
|
|
||||||
seed: z.number().int().optional(),
|
|
||||||
modelSize: z.enum(['1.7B', '0.6B']).optional(),
|
|
||||||
instruct: z.string().max(500).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type GenerationFormValues = z.infer<typeof generationSchema>;
|
|
||||||
|
|
||||||
export function GenerationForm() {
|
export function GenerationForm() {
|
||||||
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
|
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
|
||||||
const { data: selectedProfile } = useProfile(selectedProfileId || '');
|
const { data: selectedProfile } = useProfile(selectedProfileId || '');
|
||||||
const generation = useGeneration();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const setAudio = usePlayerStore((state) => state.setAudio);
|
|
||||||
const setIsGenerating = useGenerationStore((state) => state.setIsGenerating);
|
|
||||||
const [downloadingModelName, setDownloadingModelName] = useState<string | null>(null);
|
|
||||||
const [downloadingDisplayName, setDownloadingDisplayName] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Use the download toast hook to show progress when model is downloading
|
const { form, handleSubmit, isPending } = useGenerationForm();
|
||||||
useModelDownloadToast({
|
|
||||||
modelName: downloadingModelName || '',
|
|
||||||
displayName: downloadingDisplayName || '',
|
|
||||||
enabled: !!downloadingModelName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<GenerationFormValues>({
|
async function onSubmit(data: Parameters<typeof handleSubmit>[0]) {
|
||||||
resolver: zodResolver(generationSchema),
|
await handleSubmit(data, selectedProfileId);
|
||||||
defaultValues: {
|
|
||||||
text: '',
|
|
||||||
language: 'en',
|
|
||||||
seed: undefined,
|
|
||||||
modelSize: '1.7B',
|
|
||||||
instruct: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function onSubmit(data: GenerationFormValues) {
|
|
||||||
if (!selectedProfileId) {
|
|
||||||
toast({
|
|
||||||
title: 'No profile selected',
|
|
||||||
description: 'Please select a voice profile from the cards above.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsGenerating(true);
|
|
||||||
|
|
||||||
// Determine model name and display name
|
|
||||||
const modelName = `qwen-tts-${data.modelSize}`;
|
|
||||||
const displayName = data.modelSize === '1.7B' ? 'Qwen TTS 1.7B' : 'Qwen TTS 0.6B';
|
|
||||||
|
|
||||||
// Check if model is downloaded before starting generation
|
|
||||||
try {
|
|
||||||
const modelStatus = await apiClient.getModelStatus();
|
|
||||||
const model = modelStatus.models.find((m) => m.model_name === modelName);
|
|
||||||
|
|
||||||
if (model && !model.downloaded) {
|
|
||||||
// Model is not downloaded, enable download toast
|
|
||||||
setDownloadingModelName(modelName);
|
|
||||||
setDownloadingDisplayName(displayName);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// If status check fails, continue anyway - generation will handle it
|
|
||||||
console.error('Failed to check model status:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed with generation (which will trigger download if needed)
|
|
||||||
const result = await generation.mutateAsync({
|
|
||||||
profile_id: selectedProfileId,
|
|
||||||
text: data.text,
|
|
||||||
language: data.language,
|
|
||||||
seed: data.seed,
|
|
||||||
model_size: data.modelSize,
|
|
||||||
instruct: data.instruct || undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Generation complete!',
|
|
||||||
description: `Audio generated (${result.duration.toFixed(2)}s)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Autoplay the generated audio
|
|
||||||
const audioUrl = apiClient.getAudioUrl(result.id);
|
|
||||||
setAudio(audioUrl, result.id, selectedProfileId, data.text.substring(0, 50));
|
|
||||||
|
|
||||||
form.reset();
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
title: 'Generation failed',
|
|
||||||
description: error instanceof Error ? error.message : 'Failed to generate audio',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsGenerating(false);
|
|
||||||
// Clear download state after generation completes
|
|
||||||
setDownloadingModelName(null);
|
|
||||||
setDownloadingDisplayName(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -276,9 +173,9 @@ export function GenerationForm() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
disabled={generation.isPending || !selectedProfileId}
|
disabled={isPending || !selectedProfileId}
|
||||||
>
|
>
|
||||||
{generation.isPending ? (
|
{isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Generating...
|
Generating...
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { apiClient } from '@/lib/api/client';
|
import { apiClient } from '@/lib/api/client';
|
||||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||||
import {
|
import {
|
||||||
@@ -41,6 +42,7 @@ export function HistoryTable() {
|
|||||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { data: historyData, isLoading } = useHistory({
|
const { data: historyData, isLoading } = useHistory({
|
||||||
limit,
|
limit,
|
||||||
@@ -86,7 +88,11 @@ export function HistoryTable() {
|
|||||||
{ generationId, text },
|
{ generationId, text },
|
||||||
{
|
{
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
alert(`Failed to download audio: ${error.message}`);
|
toast({
|
||||||
|
title: 'Failed to download audio',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -97,7 +103,11 @@ export function HistoryTable() {
|
|||||||
{ generationId, text },
|
{ generationId, text },
|
||||||
{
|
{
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
alert(`Failed to export generation: ${error.message}`);
|
toast({
|
||||||
|
title: 'Failed to export generation',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -112,7 +122,11 @@ export function HistoryTable() {
|
|||||||
if (file) {
|
if (file) {
|
||||||
// Validate file extension
|
// Validate file extension
|
||||||
if (!file.name.endsWith('.voicebox.zip')) {
|
if (!file.name.endsWith('.voicebox.zip')) {
|
||||||
alert('Please select a valid .voicebox.zip file');
|
toast({
|
||||||
|
title: 'Invalid file type',
|
||||||
|
description: 'Please select a valid .voicebox.zip file',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
@@ -129,10 +143,17 @@ export function HistoryTable() {
|
|||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = '';
|
||||||
}
|
}
|
||||||
alert(data.message || 'Generation imported successfully');
|
toast({
|
||||||
|
title: 'Generation imported',
|
||||||
|
description: data.message || 'Generation imported successfully',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
alert(`Failed to import generation: ${error.message}`);
|
toast({
|
||||||
|
title: 'Failed to import generation',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -148,23 +169,6 @@ export function HistoryTable() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full min-h-0 relative">
|
<div className="flex flex-col h-full min-h-0 relative">
|
||||||
{/* <div className="flex justify-between items-center mb-4 shrink-0">
|
|
||||||
<h2 className="text-2xl font-bold">History</h2>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button variant="outline" onClick={handleImportClick}>
|
|
||||||
<Upload className="mr-2 h-4 w-4" />
|
|
||||||
Import Generation
|
|
||||||
</Button>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept=".voicebox.zip"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{history.length === 0 ? (
|
{history.length === 0 ? (
|
||||||
<div className="text-center py-12 px-5 border-2 border-dashed mb-5 border-muted rounded-md text-muted-foreground flex-1 flex items-center justify-center">
|
<div className="text-center py-12 px-5 border-2 border-dashed mb-5 border-muted rounded-md text-muted-foreground flex-1 flex items-center justify-center">
|
||||||
No voice generations, yet...
|
No voice generations, yet...
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
|
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
|
||||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||||
import { useImportProfile } from '@/lib/hooks/useProfiles';
|
import { useImportProfile } from '@/lib/hooks/useProfiles';
|
||||||
@@ -27,6 +28,7 @@ export function MainEditor() {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const handleImportClick = () => {
|
const handleImportClick = () => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
@@ -36,7 +38,11 @@ export function MainEditor() {
|
|||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
if (!file.name.endsWith('.voicebox.zip')) {
|
if (!file.name.endsWith('.voicebox.zip')) {
|
||||||
alert('Please select a valid .voicebox.zip file');
|
toast({
|
||||||
|
title: 'Invalid file type',
|
||||||
|
description: 'Please select a valid .voicebox.zip file',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
@@ -53,9 +59,17 @@ export function MainEditor() {
|
|||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
fileInputRef.current.value = '';
|
||||||
}
|
}
|
||||||
|
toast({
|
||||||
|
title: 'Profile imported',
|
||||||
|
description: 'Voice profile imported successfully',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
alert(`Failed to import profile: ${error.message}`);
|
toast({
|
||||||
|
title: 'Failed to import profile',
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -102,15 +116,9 @@ export function MainEditor() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* Profiles - Top Left */}
|
|
||||||
<div className="shrink-0 flex flex-col">
|
<div className="shrink-0 flex flex-col">
|
||||||
<ProfileList />
|
<ProfileList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Generator - Bottom Left */}
|
|
||||||
{/* <div className="shrink-0">
|
|
||||||
<GenerationForm />
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Link, useMatchRoute } from '@tanstack/react-router';
|
||||||
import { Box, Loader2, Mic, Server, Speaker, Volume2 } from 'lucide-react';
|
import { Box, Loader2, Mic, Server, Speaker, Volume2 } from 'lucide-react';
|
||||||
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn } from '@/lib/utils/cn';
|
||||||
@@ -5,23 +6,22 @@ import { useGenerationStore } from '@/stores/generationStore';
|
|||||||
import { usePlayerStore } from '@/stores/playerStore';
|
import { usePlayerStore } from '@/stores/playerStore';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
activeTab: string;
|
|
||||||
onTabChange: (tab: string) => void;
|
|
||||||
isMacOS?: boolean;
|
isMacOS?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'main', icon: Volume2, label: 'Generate' },
|
{ id: 'main', path: '/', icon: Volume2, label: 'Generate' },
|
||||||
{ id: 'voices', icon: Mic, label: 'Voices' },
|
{ id: 'voices', path: '/voices', icon: Mic, label: 'Voices' },
|
||||||
{ id: 'audio', icon: Speaker, label: 'Audio' },
|
{ id: 'audio', path: '/audio', icon: Speaker, label: 'Audio' },
|
||||||
{ id: 'models', icon: Box, label: 'Models' },
|
{ id: 'models', path: '/models', icon: Box, label: 'Models' },
|
||||||
{ id: 'server', icon: Server, label: 'Server' },
|
{ id: 'server', path: '/server', icon: Server, label: 'Server' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar({ activeTab, onTabChange, isMacOS }: SidebarProps) {
|
export function Sidebar({ isMacOS }: SidebarProps) {
|
||||||
const isGenerating = useGenerationStore((state) => state.isGenerating);
|
const isGenerating = useGenerationStore((state) => state.isGenerating);
|
||||||
const audioUrl = usePlayerStore((state) => state.audioUrl);
|
const audioUrl = usePlayerStore((state) => state.audioUrl);
|
||||||
const isPlayerVisible = !!audioUrl;
|
const isPlayerVisible = !!audioUrl;
|
||||||
|
const matchRoute = useMatchRoute();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -39,13 +39,16 @@ export function Sidebar({ activeTab, onTabChange, isMacOS }: SidebarProps) {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
const isActive = activeTab === tab.id;
|
// For index route, use exact match; for others, use default matching
|
||||||
|
const isActive =
|
||||||
|
tab.path === '/'
|
||||||
|
? matchRoute({ to: '/', exact: true })
|
||||||
|
: matchRoute({ to: tab.path });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Link
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
type="button"
|
to={tab.path}
|
||||||
onClick={() => onTabChange(tab.id)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-12 h-12 rounded-full flex items-center justify-center transition-all duration-200',
|
'w-12 h-12 rounded-full flex items-center justify-center transition-all duration-200',
|
||||||
'hover:bg-muted/50',
|
'hover:bg-muted/50',
|
||||||
@@ -55,7 +58,7 @@ export function Sidebar({ activeTab, onTabChange, isMacOS }: SidebarProps) {
|
|||||||
aria-label={tab.label}
|
aria-label={tab.label}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5" />
|
<Icon className="h-5 w-5" />
|
||||||
</button>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,45 +42,13 @@ import {
|
|||||||
import { useSystemAudioCapture } from '@/lib/hooks/useSystemAudioCapture';
|
import { useSystemAudioCapture } from '@/lib/hooks/useSystemAudioCapture';
|
||||||
import { useTranscription } from '@/lib/hooks/useTranscription';
|
import { useTranscription } from '@/lib/hooks/useTranscription';
|
||||||
import { isTauri } from '@/lib/tauri';
|
import { isTauri } from '@/lib/tauri';
|
||||||
import { formatAudioDuration } from '@/lib/utils/audio';
|
import { formatAudioDuration, getAudioDuration } from '@/lib/utils/audio';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
import { AudioSampleRecording } from './AudioSampleRecording';
|
import { AudioSampleRecording } from './AudioSampleRecording';
|
||||||
import { AudioSampleSystem } from './AudioSampleSystem';
|
import { AudioSampleSystem } from './AudioSampleSystem';
|
||||||
import { AudioSampleUpload } from './AudioSampleUpload';
|
import { AudioSampleUpload } from './AudioSampleUpload';
|
||||||
import { SampleList } from './SampleList';
|
import { SampleList } from './SampleList';
|
||||||
|
|
||||||
// Helper function to get audio duration from File
|
|
||||||
async function getAudioDuration(file: File & { recordedDuration?: number }): Promise<number> {
|
|
||||||
// If the file has a recordedDuration property (from our recording hooks),
|
|
||||||
// use that instead of trying to read metadata. This fixes issues on Windows
|
|
||||||
// where WebM files from MediaRecorder don't have proper duration metadata.
|
|
||||||
if (file.recordedDuration !== undefined && Number.isFinite(file.recordedDuration)) {
|
|
||||||
return file.recordedDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const audio = new Audio();
|
|
||||||
const url = URL.createObjectURL(file);
|
|
||||||
|
|
||||||
audio.addEventListener('loadedmetadata', () => {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
// Check if duration is valid (not Infinity or NaN)
|
|
||||||
if (Number.isFinite(audio.duration) && audio.duration > 0) {
|
|
||||||
resolve(audio.duration);
|
|
||||||
} else {
|
|
||||||
reject(new Error('Audio file has invalid duration metadata'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
audio.addEventListener('error', () => {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
reject(new Error('Failed to load audio file'));
|
|
||||||
});
|
|
||||||
|
|
||||||
audio.src = url;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_AUDIO_DURATION_SECONDS = 30;
|
const MAX_AUDIO_DURATION_SECONDS = 30;
|
||||||
|
|
||||||
const baseProfileSchema = z.object({
|
const baseProfileSchema = z.object({
|
||||||
@@ -187,7 +155,7 @@ export function ProfileForm() {
|
|||||||
stopRecording,
|
stopRecording,
|
||||||
cancelRecording,
|
cancelRecording,
|
||||||
} = useAudioRecording({
|
} = useAudioRecording({
|
||||||
maxDurationSeconds: 30,
|
maxDurationSeconds: 29,
|
||||||
onRecordingComplete: (blob, recordedDuration) => {
|
onRecordingComplete: (blob, recordedDuration) => {
|
||||||
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
||||||
type: blob.type || 'audio/webm',
|
type: blob.type || 'audio/webm',
|
||||||
@@ -213,7 +181,7 @@ export function ProfileForm() {
|
|||||||
stopRecording: stopSystemRecording,
|
stopRecording: stopSystemRecording,
|
||||||
cancelRecording: cancelSystemRecording,
|
cancelRecording: cancelSystemRecording,
|
||||||
} = useSystemAudioCapture({
|
} = useSystemAudioCapture({
|
||||||
maxDurationSeconds: 30,
|
maxDurationSeconds: 29,
|
||||||
onRecordingComplete: (blob, recordedDuration) => {
|
onRecordingComplete: (blob, recordedDuration) => {
|
||||||
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
||||||
type: blob.type || 'audio/wav',
|
type: blob.type || 'audio/wav',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Mic, Monitor, Upload } from 'lucide-react';
|
import { Mic, Monitor, Upload } from 'lucide-react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -73,7 +73,7 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp
|
|||||||
stopRecording,
|
stopRecording,
|
||||||
cancelRecording,
|
cancelRecording,
|
||||||
} = useAudioRecording({
|
} = useAudioRecording({
|
||||||
maxDurationSeconds: 30,
|
maxDurationSeconds: 29,
|
||||||
onRecordingComplete: (blob, recordedDuration) => {
|
onRecordingComplete: (blob, recordedDuration) => {
|
||||||
// Convert blob to File object
|
// Convert blob to File object
|
||||||
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
||||||
@@ -100,7 +100,7 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp
|
|||||||
stopRecording: stopSystemRecording,
|
stopRecording: stopSystemRecording,
|
||||||
cancelRecording: cancelSystemRecording,
|
cancelRecording: cancelSystemRecording,
|
||||||
} = useSystemAudioCapture({
|
} = useSystemAudioCapture({
|
||||||
maxDurationSeconds: 30,
|
maxDurationSeconds: 29,
|
||||||
onRecordingComplete: (blob, recordedDuration) => {
|
onRecordingComplete: (blob, recordedDuration) => {
|
||||||
// Convert blob to File object
|
// Convert blob to File object
|
||||||
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
||||||
|
|||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
interface Window {
|
||||||
|
__voiceboxServerStartedByApp?: boolean;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ interface UseAudioRecordingOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAudioRecording({
|
export function useAudioRecording({
|
||||||
maxDurationSeconds = 30,
|
maxDurationSeconds = 29,
|
||||||
onRecordingComplete,
|
onRecordingComplete,
|
||||||
}: UseAudioRecordingOptions = {}) {
|
}: UseAudioRecordingOptions = {}) {
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import * as z from 'zod';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { apiClient } from '@/lib/api/client';
|
||||||
|
import { LANGUAGE_CODES, type LanguageCode } from '@/lib/constants/languages';
|
||||||
|
import { useGeneration } from '@/lib/hooks/useGeneration';
|
||||||
|
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
||||||
|
import { useGenerationStore } from '@/stores/generationStore';
|
||||||
|
import { usePlayerStore } from '@/stores/playerStore';
|
||||||
|
|
||||||
|
const generationSchema = z.object({
|
||||||
|
text: z.string().min(1, 'Text is required').max(5000),
|
||||||
|
language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),
|
||||||
|
seed: z.number().int().optional(),
|
||||||
|
modelSize: z.enum(['1.7B', '0.6B']).optional(),
|
||||||
|
instruct: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GenerationFormValues = z.infer<typeof generationSchema>;
|
||||||
|
|
||||||
|
interface UseGenerationFormOptions {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
defaultValues?: Partial<GenerationFormValues>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGenerationForm(options: UseGenerationFormOptions = {}) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const generation = useGeneration();
|
||||||
|
const setAudio = usePlayerStore((state) => state.setAudio);
|
||||||
|
const setIsGenerating = useGenerationStore((state) => state.setIsGenerating);
|
||||||
|
const [downloadingModelName, setDownloadingModelName] = useState<string | null>(null);
|
||||||
|
const [downloadingDisplayName, setDownloadingDisplayName] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useModelDownloadToast({
|
||||||
|
modelName: downloadingModelName || '',
|
||||||
|
displayName: downloadingDisplayName || '',
|
||||||
|
enabled: !!downloadingModelName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<GenerationFormValues>({
|
||||||
|
resolver: zodResolver(generationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
text: '',
|
||||||
|
language: 'en',
|
||||||
|
seed: undefined,
|
||||||
|
modelSize: '1.7B',
|
||||||
|
instruct: '',
|
||||||
|
...options.defaultValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit(
|
||||||
|
data: GenerationFormValues,
|
||||||
|
selectedProfileId: string | null,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!selectedProfileId) {
|
||||||
|
toast({
|
||||||
|
title: 'No profile selected',
|
||||||
|
description: 'Please select a voice profile from the cards above.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsGenerating(true);
|
||||||
|
|
||||||
|
const modelName = `qwen-tts-${data.modelSize}`;
|
||||||
|
const displayName = data.modelSize === '1.7B' ? 'Qwen TTS 1.7B' : 'Qwen TTS 0.6B';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const modelStatus = await apiClient.getModelStatus();
|
||||||
|
const model = modelStatus.models.find((m) => m.model_name === modelName);
|
||||||
|
|
||||||
|
if (model && !model.downloaded) {
|
||||||
|
setDownloadingModelName(modelName);
|
||||||
|
setDownloadingDisplayName(displayName);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check model status:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await generation.mutateAsync({
|
||||||
|
profile_id: selectedProfileId,
|
||||||
|
text: data.text,
|
||||||
|
language: data.language,
|
||||||
|
seed: data.seed,
|
||||||
|
model_size: data.modelSize,
|
||||||
|
instruct: data.instruct || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Generation complete!',
|
||||||
|
description: `Audio generated (${result.duration.toFixed(2)}s)`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const audioUrl = apiClient.getAudioUrl(result.id);
|
||||||
|
setAudio(audioUrl, result.id, selectedProfileId, data.text.substring(0, 50));
|
||||||
|
|
||||||
|
form.reset();
|
||||||
|
options.onSuccess?.();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: 'Generation failed',
|
||||||
|
description: error instanceof Error ? error.message : 'Failed to generate audio',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsGenerating(false);
|
||||||
|
setDownloadingModelName(null);
|
||||||
|
setDownloadingDisplayName(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
handleSubmit,
|
||||||
|
isPending: generation.isPending,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ interface UseSystemAudioCaptureOptions {
|
|||||||
* Uses ScreenCaptureKit on macOS and WASAPI loopback on Windows.
|
* Uses ScreenCaptureKit on macOS and WASAPI loopback on Windows.
|
||||||
*/
|
*/
|
||||||
export function useSystemAudioCapture({
|
export function useSystemAudioCapture({
|
||||||
maxDurationSeconds = 30,
|
maxDurationSeconds = 29,
|
||||||
onRecordingComplete,
|
onRecordingComplete,
|
||||||
}: UseSystemAudioCaptureOptions = {}) {
|
}: UseSystemAudioCaptureOptions = {}) {
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
|
|||||||
@@ -17,6 +17,41 @@ export function formatAudioDuration(seconds: number): string {
|
|||||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get audio duration from a File.
|
||||||
|
* If the file has a recordedDuration property (from recording hooks),
|
||||||
|
* use that instead of trying to read metadata. This fixes issues on Windows
|
||||||
|
* where WebM files from MediaRecorder don't have proper duration metadata.
|
||||||
|
*/
|
||||||
|
export async function getAudioDuration(
|
||||||
|
file: File & { recordedDuration?: number },
|
||||||
|
): Promise<number> {
|
||||||
|
if (file.recordedDuration !== undefined && Number.isFinite(file.recordedDuration)) {
|
||||||
|
return file.recordedDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const audio = new Audio();
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
audio.addEventListener('loadedmetadata', () => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
if (Number.isFinite(audio.duration) && audio.duration > 0) {
|
||||||
|
resolve(audio.duration);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Audio file has invalid duration metadata'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('error', () => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
reject(new Error('Failed to load audio file'));
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert any audio blob to WAV format using Web Audio API.
|
* Convert any audio blob to WAV format using Web Audio API.
|
||||||
* This ensures compatibility without requiring ffmpeg on the backend.
|
* This ensures compatibility without requiring ffmpeg on the backend.
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const DEBUG = import.meta.env.DEV;
|
||||||
|
|
||||||
|
export const debug = {
|
||||||
|
log: (...args: unknown[]) => {
|
||||||
|
if (DEBUG) {
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (...args: unknown[]) => {
|
||||||
|
if (DEBUG) {
|
||||||
|
console.error(...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
warn: (...args: unknown[]) => {
|
||||||
|
if (DEBUG) {
|
||||||
|
console.warn(...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { createRootRoute, createRoute, createRouter, Outlet } from '@tanstack/react-router';
|
||||||
|
import { AppFrame } from '@/components/AppFrame/AppFrame';
|
||||||
|
import { AudioTab } from '@/components/AudioTab/AudioTab';
|
||||||
|
import { MainEditor } from '@/components/MainEditor/MainEditor';
|
||||||
|
import { ModelsTab } from '@/components/ModelsTab/ModelsTab';
|
||||||
|
import { ServerTab } from '@/components/ServerTab/ServerTab';
|
||||||
|
import { Sidebar } from '@/components/Sidebar';
|
||||||
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
|
import { VoicesTab } from '@/components/VoicesTab/VoicesTab';
|
||||||
|
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
||||||
|
import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks';
|
||||||
|
import { isMacOS } from '@/lib/tauri';
|
||||||
|
|
||||||
|
// Root layout component
|
||||||
|
function RootLayout() {
|
||||||
|
// Monitor active downloads/generations and show toasts for them
|
||||||
|
const activeDownloads = useRestoreActiveTasks();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppFrame>
|
||||||
|
<div className="flex flex-1 min-h-0 overflow-hidden">
|
||||||
|
<Sidebar isMacOS={isMacOS()} />
|
||||||
|
|
||||||
|
<main className="flex-1 ml-20 overflow-hidden flex flex-col">
|
||||||
|
<div className="container mx-auto px-8 max-w-[1800px] h-full overflow-hidden flex flex-col">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show download toasts for any active downloads (from anywhere) */}
|
||||||
|
{activeDownloads.map((download) => {
|
||||||
|
const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name;
|
||||||
|
return (
|
||||||
|
<DownloadToastRestorer
|
||||||
|
key={download.model_name}
|
||||||
|
modelName={download.model_name}
|
||||||
|
displayName={displayName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Toaster />
|
||||||
|
</AppFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that restores a download toast for a specific model.
|
||||||
|
*/
|
||||||
|
function DownloadToastRestorer({
|
||||||
|
modelName,
|
||||||
|
displayName,
|
||||||
|
}: {
|
||||||
|
modelName: string;
|
||||||
|
displayName: string;
|
||||||
|
}) {
|
||||||
|
// Use the download toast hook to restore the toast
|
||||||
|
useModelDownloadToast({
|
||||||
|
modelName,
|
||||||
|
displayName,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root route with layout
|
||||||
|
const rootRoute = createRootRoute({
|
||||||
|
component: RootLayout,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Index route (main/generate)
|
||||||
|
const indexRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/',
|
||||||
|
component: MainEditor,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Voices route
|
||||||
|
const voicesRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/voices',
|
||||||
|
component: VoicesTab,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Audio route
|
||||||
|
const audioRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/audio',
|
||||||
|
component: AudioTab,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Models route
|
||||||
|
const modelsRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/models',
|
||||||
|
component: ModelsTab,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Server route
|
||||||
|
const serverRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/server',
|
||||||
|
component: ServerTab,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route tree
|
||||||
|
const routeTree = rootRoute.addChildren([
|
||||||
|
indexRoute,
|
||||||
|
voicesRoute,
|
||||||
|
audioRoute,
|
||||||
|
modelsRoute,
|
||||||
|
serverRoute,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create router
|
||||||
|
export const router = createRouter({ routeTree });
|
||||||
|
|
||||||
|
// Register router for type safety
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -47,7 +47,7 @@ app.add_middleware(
|
|||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
"""Root endpoint."""
|
"""Root endpoint."""
|
||||||
return {"message": "voicebox API", "version": "0.1.4"}
|
return {"message": "voicebox API", "version": "0.1.5"}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health", response_model=models.HealthResponse)
|
@app.get("/health", response_model=models.HealthResponse)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"name": "@voicebox/app",
|
"name": "@voicebox/app",
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tanstack/react-query-devtools": "^5.0.0",
|
"@tanstack/react-query-devtools": "^5.0.0",
|
||||||
|
"@tanstack/react-router": "^1.157.16",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.0.0",
|
"@tauri-apps/plugin-fs": "^2.0.0",
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"name": "@voicebox/landing",
|
"name": "@voicebox/landing",
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
@@ -88,7 +89,7 @@
|
|||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"name": "@voicebox/tauri",
|
"name": "@voicebox/tauri",
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"name": "@voicebox/web",
|
"name": "@voicebox/web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"react": "^18.3.0",
|
"react": "^18.3.0",
|
||||||
@@ -512,6 +513,8 @@
|
|||||||
|
|
||||||
"@tailwindcss/vite": ["@tailwindcss/[email protected]", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
"@tailwindcss/vite": ["@tailwindcss/[email protected]", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||||
|
|
||||||
|
"@tanstack/history": ["@tanstack/[email protected]", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="],
|
||||||
|
|
||||||
"@tanstack/query-core": ["@tanstack/[email protected]", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
"@tanstack/query-core": ["@tanstack/[email protected]", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
||||||
|
|
||||||
"@tanstack/query-devtools": ["@tanstack/[email protected]", "", {}, "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ=="],
|
"@tanstack/query-devtools": ["@tanstack/[email protected]", "", {}, "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ=="],
|
||||||
@@ -520,6 +523,14 @@
|
|||||||
|
|
||||||
"@tanstack/react-query-devtools": ["@tanstack/[email protected]", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="],
|
"@tanstack/react-query-devtools": ["@tanstack/[email protected]", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="],
|
||||||
|
|
||||||
|
"@tanstack/react-router": ["@tanstack/[email protected]", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.157.16", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-xwFQa7S7dhBhm3aJYwU79cITEYgAKSrcL6wokaROIvl2JyIeazn8jueWqUPJzFjv+QF6Q8euKRlKUEyb5q2ymg=="],
|
||||||
|
|
||||||
|
"@tanstack/react-store": ["@tanstack/[email protected]", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
|
||||||
|
|
||||||
|
"@tanstack/router-core": ["@tanstack/[email protected]", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-eJuVgM7KZYTTr4uPorbUzUflmljMVcaX2g6VvhITLnHmg9SBx9RAgtQ1HmT+72mzyIbRSlQ1q0fY/m+of/fosA=="],
|
||||||
|
|
||||||
|
"@tanstack/store": ["@tanstack/[email protected]", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
|
||||||
|
|
||||||
"@tauri-apps/api": ["@tauri-apps/[email protected]", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="],
|
"@tauri-apps/api": ["@tauri-apps/[email protected]", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="],
|
||||||
|
|
||||||
"@tauri-apps/cli": ["@tauri-apps/[email protected]", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="],
|
"@tauri-apps/cli": ["@tauri-apps/[email protected]", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="],
|
||||||
@@ -664,6 +675,8 @@
|
|||||||
|
|
||||||
"convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
"convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"cookie-es": ["[email protected]", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
|
||||||
|
|
||||||
"cross-spawn": ["[email protected]", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["[email protected]", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
"cssesc": ["[email protected]", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
"cssesc": ["[email protected]", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||||
@@ -792,6 +805,8 @@
|
|||||||
|
|
||||||
"is-path-inside": ["[email protected]", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
"is-path-inside": ["[email protected]", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
||||||
|
|
||||||
|
"isbot": ["[email protected]", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="],
|
||||||
|
|
||||||
"isexe": ["[email protected]", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["[email protected]", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
"jiti": ["[email protected]", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
"jiti": ["[email protected]", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||||
@@ -966,6 +981,10 @@
|
|||||||
|
|
||||||
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"seroval": ["[email protected]", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
|
||||||
|
|
||||||
|
"seroval-plugins": ["[email protected]", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="],
|
||||||
|
|
||||||
"sharp": ["[email protected]", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
"sharp": ["[email protected]", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||||
|
|
||||||
"shebang-command": ["[email protected]", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
"shebang-command": ["[email protected]", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
@@ -1002,6 +1021,10 @@
|
|||||||
|
|
||||||
"thenify-all": ["[email protected]", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
"thenify-all": ["[email protected]", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||||
|
|
||||||
|
"tiny-invariant": ["[email protected]", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
|
"tiny-warning": ["[email protected]", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
|
||||||
|
|
||||||
"tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
"to-regex-range": ["[email protected]", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["[email protected]", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/landing",
|
"name": "@voicebox/landing",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"description": "Landing page for voicebox.sh",
|
"description": "Landing page for voicebox.sh",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --bun next dev --turbo",
|
"dev": "bun --bun next dev --turbo",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "voicebox",
|
"name": "voicebox",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"app",
|
"app",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/tauri",
|
"name": "@voicebox/tauri",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Generated
+1
-1
@@ -4840,7 +4840,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "voicebox"
|
name = "voicebox"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "voicebox"
|
name = "voicebox"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
description = "A production-quality desktop app for Qwen3-TTS voice cloning and generation"
|
description = "A production-quality desktop app for Qwen3-TTS voice cloning and generation"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Voicebox",
|
"productName": "Voicebox",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"identifier": "sh.voicebox.app",
|
"identifier": "sh.voicebox.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@voicebox/web",
|
"name": "@voicebox/web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Reference in New Issue
Block a user