mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 21:30:39 -07:00
Refactor audio generation components and improve debugging capabilities
- Introduced useGenerationForm hook to streamline audio generation form handling, including validation and model download management. - Updated FloatingGenerateBox and GenerationForm components to utilize the new hook, enhancing code organization and reducing duplication. - Replaced console logging with a debug utility for better logging control during audio playback and generation processes. - Improved error handling in HistoryTable and MainEditor components by integrating toast notifications for user feedback. - Adjusted audio recording duration limits across various components for consistency.
This commit is contained in:
@@ -8,6 +8,7 @@ import { Slider } from '@/components/ui/slider';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { isTauri } from '@/lib/tauri';
|
||||
import { formatAudioDuration } from '@/lib/utils/audio';
|
||||
import { debug } from '@/lib/utils/debug';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
|
||||
export function AudioPlayer() {
|
||||
@@ -49,28 +50,17 @@ export function AudioPlayer() {
|
||||
|
||||
// Determine if we should use native playback
|
||||
const useNativePlayback = useMemo(() => {
|
||||
console.log('useNativePlayback memo:', {
|
||||
isTauri: isTauri(),
|
||||
profileId,
|
||||
profileChannels,
|
||||
channels,
|
||||
});
|
||||
|
||||
if (!isTauri() || !profileChannels || !channels) {
|
||||
console.log('useNativePlayback: false - missing requirements');
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
const shouldUseNative = assignedChannels.some(
|
||||
(ch) => ch.device_ids.length > 0 && !ch.is_default,
|
||||
);
|
||||
|
||||
console.log('useNativePlayback result:', shouldUseNative);
|
||||
return shouldUseNative;
|
||||
}, [profileChannels, channels, profileId]);
|
||||
|
||||
@@ -91,11 +81,11 @@ export function AudioPlayer() {
|
||||
}
|
||||
|
||||
if (wavesurferRef.current) {
|
||||
console.log('WaveSurfer already initialized, skipping');
|
||||
debug.log('WaveSurfer already initialized, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Creating NEW WaveSurfer instance');
|
||||
debug.log('Creating NEW WaveSurfer instance');
|
||||
|
||||
// Wait for container to be properly rendered
|
||||
const initWaveSurfer = () => {
|
||||
@@ -121,7 +111,7 @@ export function AudioPlayer() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Initializing WaveSurfer...', {
|
||||
debug.log('Initializing WaveSurfer...', {
|
||||
container,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
@@ -154,9 +144,9 @@ export function AudioPlayer() {
|
||||
});
|
||||
|
||||
wavesurferRef.current = wavesurfer;
|
||||
console.log('WaveSurfer created successfully');
|
||||
debug.log('WaveSurfer created successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to create WaveSurfer:', error);
|
||||
debug.error('Failed to create WaveSurfer:', error);
|
||||
setError(
|
||||
`Failed to initialize waveform: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
@@ -178,8 +168,8 @@ export function AudioPlayer() {
|
||||
loadingRef.current = false;
|
||||
setIsLoading(false);
|
||||
setError(null);
|
||||
console.log('Audio ready, duration:', dur);
|
||||
console.log('Waveform should be visible now');
|
||||
debug.log('Audio ready, duration:', dur);
|
||||
debug.log('Waveform should be visible now');
|
||||
|
||||
// Ensure volume is set
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
@@ -191,7 +181,7 @@ export function AudioPlayer() {
|
||||
if (mediaElement && !isUsingNativePlaybackRef.current) {
|
||||
mediaElement.volume = currentVolume;
|
||||
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
|
||||
@@ -199,7 +189,7 @@ export function AudioPlayer() {
|
||||
const currentAudioUrl = usePlayerStore.getState().audioUrl;
|
||||
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)
|
||||
let runtimeProfileChannels = null;
|
||||
@@ -208,18 +198,18 @@ export function AudioPlayer() {
|
||||
if (isTauri() && currentProfileId) {
|
||||
try {
|
||||
runtimeProfileChannels = await apiClient.getProfileChannels(currentProfileId);
|
||||
console.log('Runtime profileChannels:', runtimeProfileChannels);
|
||||
debug.log('Runtime profileChannels:', runtimeProfileChannels);
|
||||
|
||||
if (runtimeProfileChannels && runtimeProfileChannels.channel_ids.length > 0) {
|
||||
runtimeChannels = await apiClient.listChannels();
|
||||
console.log('Runtime channels:', runtimeChannels);
|
||||
debug.log('Runtime channels:', runtimeChannels);
|
||||
}
|
||||
} 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(),
|
||||
currentAudioUrl,
|
||||
currentProfileId,
|
||||
@@ -234,15 +224,15 @@ export function AudioPlayer() {
|
||||
runtimeProfileChannels &&
|
||||
runtimeChannels
|
||||
) {
|
||||
console.log('Attempting native audio playback...');
|
||||
debug.log('Attempting native audio playback...');
|
||||
|
||||
// Stop any existing native playback first
|
||||
if (isUsingNativePlaybackRef.current) {
|
||||
try {
|
||||
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) {
|
||||
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) =>
|
||||
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
|
||||
const shouldUseNative = assignedChannels.some(
|
||||
(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) {
|
||||
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
|
||||
isUsingNativePlaybackRef.current = false;
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
@@ -268,7 +258,7 @@ export function AudioPlayer() {
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
mediaElement.volume = currentVolume;
|
||||
mediaElement.muted = false;
|
||||
console.log(
|
||||
debug.log(
|
||||
'WaveSurfer unmuted for normal playback - volume:',
|
||||
mediaElement.volume,
|
||||
'muted:',
|
||||
@@ -277,23 +267,23 @@ export function AudioPlayer() {
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
console.log('Fetching audio data from:', currentAudioUrl);
|
||||
debug.log('Fetching audio data from:', currentAudioUrl);
|
||||
// Fetch audio data
|
||||
const response = await fetch(currentAudioUrl);
|
||||
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
|
||||
console.log('Invoking play_audio_to_devices...');
|
||||
debug.log('Invoking play_audio_to_devices...');
|
||||
try {
|
||||
const result = await invoke('play_audio_to_devices', {
|
||||
audioData: Array.from(audioData),
|
||||
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
|
||||
isUsingNativePlaybackRef.current = true;
|
||||
@@ -304,7 +294,7 @@ export function AudioPlayer() {
|
||||
if (mediaElement) {
|
||||
mediaElement.volume = 0;
|
||||
mediaElement.muted = true;
|
||||
console.log(
|
||||
debug.log(
|
||||
'WaveSurfer muted for native playback - volume:',
|
||||
mediaElement.volume,
|
||||
'muted:',
|
||||
@@ -314,22 +304,22 @@ export function AudioPlayer() {
|
||||
|
||||
// Start WaveSurfer playback for visualization (muted)
|
||||
wavesurfer.play().catch((error) => {
|
||||
console.error('Failed to start WaveSurfer visualization:', error);
|
||||
debug.error('Failed to start WaveSurfer visualization:', error);
|
||||
});
|
||||
|
||||
setIsPlaying(true);
|
||||
console.log('Auto-playing via native audio routing - SUCCESS');
|
||||
debug.log('Auto-playing via native audio routing - SUCCESS');
|
||||
return;
|
||||
} catch (invokeError) {
|
||||
console.error('play_audio_to_devices invoke failed:', invokeError);
|
||||
debug.error('play_audio_to_devices invoke failed:', invokeError);
|
||||
throw invokeError;
|
||||
}
|
||||
} else {
|
||||
console.log('No device IDs found, falling back to WaveSurfer');
|
||||
debug.log('No device IDs found, falling back to WaveSurfer');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
debug.error(
|
||||
'Native playback failed during auto-play, falling back to WaveSurfer:',
|
||||
error,
|
||||
);
|
||||
@@ -340,7 +330,7 @@ export function AudioPlayer() {
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
mediaElement.volume = currentVolume;
|
||||
mediaElement.muted = false;
|
||||
console.log(
|
||||
debug.log(
|
||||
'WaveSurfer unmuted after native playback failure - volume:',
|
||||
mediaElement.volume,
|
||||
'muted:',
|
||||
@@ -350,7 +340,7 @@ export function AudioPlayer() {
|
||||
// Fall through to WaveSurfer playback
|
||||
}
|
||||
} else {
|
||||
console.log('Not using native playback, using WaveSurfer');
|
||||
debug.log('Not using native playback, using WaveSurfer');
|
||||
// Reset native playback flag and unmute WaveSurfer
|
||||
isUsingNativePlaybackRef.current = false;
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
@@ -358,7 +348,7 @@ export function AudioPlayer() {
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
mediaElement.volume = currentVolume;
|
||||
mediaElement.muted = false;
|
||||
console.log(
|
||||
debug.log(
|
||||
'WaveSurfer unmuted for normal playback - volume:',
|
||||
mediaElement.volume,
|
||||
'muted:',
|
||||
@@ -371,7 +361,7 @@ export function AudioPlayer() {
|
||||
// Use a small delay to ensure audio element is fully ready
|
||||
setTimeout(() => {
|
||||
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)
|
||||
});
|
||||
}, 100);
|
||||
@@ -388,13 +378,13 @@ export function AudioPlayer() {
|
||||
if (isUsingNativePlaybackRef.current) {
|
||||
mediaElement.volume = 0;
|
||||
mediaElement.muted = true;
|
||||
console.log('Playing (native mode) - WaveSurfer muted for visualization only');
|
||||
debug.log('Playing (native mode) - WaveSurfer muted for visualization only');
|
||||
} else {
|
||||
// Ensure WaveSurfer is unmuted for normal playback
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
mediaElement.volume = currentVolume;
|
||||
mediaElement.muted = false;
|
||||
console.log(
|
||||
debug.log(
|
||||
'Playing (normal mode) - volume:',
|
||||
mediaElement.volume,
|
||||
'muted:',
|
||||
@@ -417,7 +407,7 @@ export function AudioPlayer() {
|
||||
|
||||
// Handle errors
|
||||
wavesurfer.on('error', (error) => {
|
||||
console.error('WaveSurfer error:', error);
|
||||
debug.error('WaveSurfer error:', error);
|
||||
setIsLoading(false);
|
||||
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
|
||||
if (audioUrl) {
|
||||
console.log('WaveSurfer ready, loading audio:', audioUrl);
|
||||
debug.log('WaveSurfer ready, loading audio:', audioUrl);
|
||||
loadingRef.current = true;
|
||||
setIsLoading(true);
|
||||
// Stop any current playback before loading new audio
|
||||
@@ -442,11 +432,11 @@ export function AudioPlayer() {
|
||||
wavesurfer
|
||||
.load(audioUrl)
|
||||
.then(() => {
|
||||
console.log('Audio loaded into WaveSurfer');
|
||||
debug.log('Audio loaded into WaveSurfer');
|
||||
loadingRef.current = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load audio into WaveSurfer:', error);
|
||||
debug.error('Failed to load audio into WaveSurfer:', error);
|
||||
loadingRef.current = false;
|
||||
setIsLoading(false);
|
||||
setError(
|
||||
@@ -471,12 +461,12 @@ export function AudioPlayer() {
|
||||
});
|
||||
|
||||
return () => {
|
||||
console.log('Cleaning up WaveSurfer initialization effect');
|
||||
debug.log('Cleaning up WaveSurfer initialization effect');
|
||||
if (rafId1) cancelAnimationFrame(rafId1);
|
||||
if (rafId2) cancelAnimationFrame(rafId2);
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (wavesurferRef.current) {
|
||||
console.log('Destroying WaveSurfer instance');
|
||||
debug.log('Destroying WaveSurfer instance');
|
||||
try {
|
||||
const mediaElement = wavesurferRef.current.getMediaElement();
|
||||
if (mediaElement) {
|
||||
@@ -485,7 +475,7 @@ export function AudioPlayer() {
|
||||
}
|
||||
wavesurferRef.current.destroy();
|
||||
} catch (error) {
|
||||
console.error('Error destroying WaveSurfer:', error);
|
||||
debug.error('Error destroying WaveSurfer:', error);
|
||||
}
|
||||
wavesurferRef.current = null;
|
||||
}
|
||||
@@ -517,9 +507,9 @@ export function AudioPlayer() {
|
||||
(async () => {
|
||||
try {
|
||||
await invoke('stop_audio_playback');
|
||||
console.log('Stopped native audio playback');
|
||||
debug.log('Stopped native audio playback');
|
||||
} 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
|
||||
// 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
|
||||
try {
|
||||
// First pause if playing
|
||||
if (wavesurfer.isPlaying()) {
|
||||
console.log('Pausing current playback');
|
||||
debug.log('Pausing current playback');
|
||||
wavesurfer.pause();
|
||||
}
|
||||
|
||||
// Stop the media element explicitly
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
if (mediaElement) {
|
||||
console.log('Stopping media element');
|
||||
debug.log('Stopping media element');
|
||||
mediaElement.pause();
|
||||
mediaElement.currentTime = 0;
|
||||
mediaElement.src = '';
|
||||
}
|
||||
|
||||
// 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();
|
||||
} catch (error) {
|
||||
console.error('Error stopping previous audio:', error);
|
||||
debug.error('Error stopping previous audio:', error);
|
||||
// Continue anyway to load new audio
|
||||
}
|
||||
|
||||
@@ -575,16 +565,16 @@ export function AudioPlayer() {
|
||||
setDuration(0);
|
||||
|
||||
// Load new audio
|
||||
console.log('Starting new audio load for:', audioUrl);
|
||||
debug.log('Starting new audio load for:', audioUrl);
|
||||
wavesurfer
|
||||
.load(audioUrl)
|
||||
.then(() => {
|
||||
console.log('Audio load promise resolved');
|
||||
debug.log('Audio load promise resolved');
|
||||
// Don't set loading to false here - wait for 'ready' event
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load audio:', error);
|
||||
console.error('Audio URL:', audioUrl);
|
||||
debug.error('Failed to load audio:', error);
|
||||
debug.error('Audio URL:', audioUrl);
|
||||
loadingRef.current = false;
|
||||
setIsLoading(false);
|
||||
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) {
|
||||
// Only auto-play if audio is ready
|
||||
wavesurferRef.current.play().catch((error) => {
|
||||
console.error('Failed to play:', error);
|
||||
debug.error('Failed to play:', error);
|
||||
setIsPlaying(false);
|
||||
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
});
|
||||
@@ -619,11 +609,11 @@ export function AudioPlayer() {
|
||||
if (isUsingNativePlaybackRef.current) {
|
||||
mediaElement.volume = 0;
|
||||
mediaElement.muted = true;
|
||||
console.log('Volume sync: Using native playback, keeping WaveSurfer muted');
|
||||
debug.log('Volume sync: Using native playback, keeping WaveSurfer muted');
|
||||
} else {
|
||||
mediaElement.volume = volume;
|
||||
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
|
||||
console.log('Restarting current audio from beginning');
|
||||
debug.log('Restarting current audio from beginning');
|
||||
wavesurfer.seekTo(0);
|
||||
wavesurfer.play().catch((error) => {
|
||||
console.error('Failed to play after restart:', error);
|
||||
debug.error('Failed to play after restart:', error);
|
||||
setIsPlaying(false);
|
||||
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)
|
||||
// When using native playback, WaveSurfer is muted but still controls visualization
|
||||
if (!wavesurferRef.current) {
|
||||
console.error('WaveSurfer not initialized');
|
||||
debug.error('WaveSurfer not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if audio is loaded
|
||||
if (duration === 0 && !isLoading) {
|
||||
console.error('Audio not loaded yet');
|
||||
debug.error('Audio not loaded yet');
|
||||
setError('Audio not loaded. Please wait...');
|
||||
return;
|
||||
}
|
||||
@@ -686,9 +676,9 @@ export function AudioPlayer() {
|
||||
// Pause: stop native playback and pause WaveSurfer visualization
|
||||
try {
|
||||
await invoke('stop_audio_playback');
|
||||
console.log('Stopped native audio playback');
|
||||
debug.log('Stopped native audio playback');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop native playback:', error);
|
||||
debug.error('Failed to stop native playback:', error);
|
||||
}
|
||||
wavesurferRef.current.pause();
|
||||
return;
|
||||
@@ -701,7 +691,7 @@ export function AudioPlayer() {
|
||||
await invoke('stop_audio_playback');
|
||||
} catch (_error) {
|
||||
// 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
|
||||
@@ -733,7 +723,7 @@ export function AudioPlayer() {
|
||||
|
||||
// Start WaveSurfer for visualization (muted)
|
||||
wavesurferRef.current.play().catch((error) => {
|
||||
console.error('Failed to start WaveSurfer visualization:', error);
|
||||
debug.error('Failed to start WaveSurfer visualization:', error);
|
||||
setIsPlaying(false);
|
||||
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
});
|
||||
@@ -741,7 +731,7 @@ export function AudioPlayer() {
|
||||
return;
|
||||
}
|
||||
} 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
|
||||
isUsingNativePlaybackRef.current = false;
|
||||
}
|
||||
@@ -761,7 +751,7 @@ export function AudioPlayer() {
|
||||
}
|
||||
|
||||
wavesurferRef.current.play().catch((error) => {
|
||||
console.error('Failed to play:', error);
|
||||
debug.error('Failed to play:', error);
|
||||
setIsPlaying(false);
|
||||
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
});
|
||||
@@ -782,7 +772,7 @@ export function AudioPlayer() {
|
||||
// Stop any native playback
|
||||
if (isUsingNativePlaybackRef.current && isTauri()) {
|
||||
invoke('stop_audio_playback').catch((error) => {
|
||||
console.error('Failed to stop native playback:', error);
|
||||
debug.error('Failed to stop native playback:', error);
|
||||
});
|
||||
}
|
||||
// Stop WaveSurfer
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Loader2, Sparkles } from 'lucide-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 { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
||||
import {
|
||||
@@ -14,24 +11,11 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { LANGUAGE_CODES, LANGUAGE_OPTIONS, type LanguageCode } from '@/lib/constants/languages';
|
||||
import { useGeneration } from '@/lib/hooks/useGeneration';
|
||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
||||
import { LANGUAGE_OPTIONS } from '@/lib/constants/languages';
|
||||
import { useGenerationForm } from '@/lib/hooks/useGenerationForm';
|
||||
import { useProfile } from '@/lib/hooks/useProfiles';
|
||||
import { useGenerationStore } from '@/stores/generationStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
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 {
|
||||
isPlayerOpen: boolean;
|
||||
}
|
||||
@@ -39,27 +23,12 @@ interface FloatingGenerateBoxProps {
|
||||
export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps) {
|
||||
const selectedProfileId = useUIStore((state) => state.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 containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useModelDownloadToast({
|
||||
modelName: downloadingModelName || '',
|
||||
displayName: downloadingDisplayName || '',
|
||||
enabled: !!downloadingModelName,
|
||||
});
|
||||
|
||||
const form = useForm<GenerationFormValues>({
|
||||
resolver: zodResolver(generationSchema),
|
||||
defaultValues: {
|
||||
text: '',
|
||||
language: 'en',
|
||||
modelSize: '1.7B',
|
||||
const { form, handleSubmit, isPending } = useGenerationForm({
|
||||
onSuccess: () => {
|
||||
setIsExpanded(false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,62 +62,8 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
||||
};
|
||||
}, [isExpanded]);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
async function onSubmit(data: Parameters<typeof handleSubmit>[0]) {
|
||||
await handleSubmit(data, selectedProfileId);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -168,7 +83,6 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
||||
<div className="flex gap-2">
|
||||
<motion.div
|
||||
className="flex-1"
|
||||
// animate={{ marginBottom: isExpanded ? '0.75rem' : '0' }}
|
||||
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||||
>
|
||||
<FormField
|
||||
@@ -202,11 +116,11 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
||||
|
||||
<Button
|
||||
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"
|
||||
size="icon"
|
||||
>
|
||||
{generation.isPending ? (
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-4 w-4" />
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
@@ -23,118 +19,19 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { LANGUAGE_CODES, LANGUAGE_OPTIONS, type LanguageCode } from '@/lib/constants/languages';
|
||||
import { useGeneration } from '@/lib/hooks/useGeneration';
|
||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
||||
import { LANGUAGE_OPTIONS } from '@/lib/constants/languages';
|
||||
import { useGenerationForm } from '@/lib/hooks/useGenerationForm';
|
||||
import { useProfile } from '@/lib/hooks/useProfiles';
|
||||
import { useGenerationStore } from '@/stores/generationStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
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() {
|
||||
const selectedProfileId = useUIStore((state) => state.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
|
||||
useModelDownloadToast({
|
||||
modelName: downloadingModelName || '',
|
||||
displayName: downloadingDisplayName || '',
|
||||
enabled: !!downloadingModelName,
|
||||
});
|
||||
const { form, handleSubmit, isPending } = useGenerationForm();
|
||||
|
||||
const form = useForm<GenerationFormValues>({
|
||||
resolver: zodResolver(generationSchema),
|
||||
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);
|
||||
}
|
||||
async function onSubmit(data: Parameters<typeof handleSubmit>[0]) {
|
||||
await handleSubmit(data, selectedProfileId);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -276,9 +173,9 @@ export function GenerationForm() {
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={generation.isPending || !selectedProfileId}
|
||||
disabled={isPending || !selectedProfileId}
|
||||
>
|
||||
{generation.isPending ? (
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Generating...
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
import {
|
||||
@@ -41,6 +42,7 @@ export function HistoryTable() {
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const limit = 20;
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: historyData, isLoading } = useHistory({
|
||||
limit,
|
||||
@@ -86,7 +88,11 @@ export function HistoryTable() {
|
||||
{ generationId, text },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
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) {
|
||||
// Validate file extension
|
||||
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;
|
||||
}
|
||||
setSelectedFile(file);
|
||||
@@ -129,10 +143,17 @@ export function HistoryTable() {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
alert(data.message || 'Generation imported successfully');
|
||||
toast({
|
||||
title: 'Generation imported',
|
||||
description: data.message || 'Generation imported successfully',
|
||||
});
|
||||
},
|
||||
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 (
|
||||
<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 ? (
|
||||
<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...
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { ProfileList } from '@/components/VoiceProfiles/ProfileList';
|
||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
import { useImportProfile } from '@/lib/hooks/useProfiles';
|
||||
@@ -27,6 +28,7 @@ export function MainEditor() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleImportClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
@@ -36,7 +38,11 @@ export function MainEditor() {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
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;
|
||||
}
|
||||
setSelectedFile(file);
|
||||
@@ -53,9 +59,17 @@ export function MainEditor() {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
toast({
|
||||
title: 'Profile imported',
|
||||
description: 'Voice profile imported successfully',
|
||||
});
|
||||
},
|
||||
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">
|
||||
{/* Profiles - Top Left */}
|
||||
<div className="shrink-0 flex flex-col">
|
||||
<ProfileList />
|
||||
</div>
|
||||
|
||||
{/* Generator - Bottom Left */}
|
||||
{/* <div className="shrink-0">
|
||||
<GenerationForm />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,45 +42,13 @@ import {
|
||||
import { useSystemAudioCapture } from '@/lib/hooks/useSystemAudioCapture';
|
||||
import { useTranscription } from '@/lib/hooks/useTranscription';
|
||||
import { isTauri } from '@/lib/tauri';
|
||||
import { formatAudioDuration } from '@/lib/utils/audio';
|
||||
import { formatAudioDuration, getAudioDuration } from '@/lib/utils/audio';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import { AudioSampleRecording } from './AudioSampleRecording';
|
||||
import { AudioSampleSystem } from './AudioSampleSystem';
|
||||
import { AudioSampleUpload } from './AudioSampleUpload';
|
||||
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 baseProfileSchema = z.object({
|
||||
@@ -187,7 +155,7 @@ export function ProfileForm() {
|
||||
stopRecording,
|
||||
cancelRecording,
|
||||
} = useAudioRecording({
|
||||
maxDurationSeconds: 30,
|
||||
maxDurationSeconds: 29,
|
||||
onRecordingComplete: (blob, recordedDuration) => {
|
||||
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
||||
type: blob.type || 'audio/webm',
|
||||
@@ -213,7 +181,7 @@ export function ProfileForm() {
|
||||
stopRecording: stopSystemRecording,
|
||||
cancelRecording: cancelSystemRecording,
|
||||
} = useSystemAudioCapture({
|
||||
maxDurationSeconds: 30,
|
||||
maxDurationSeconds: 29,
|
||||
onRecordingComplete: (blob, recordedDuration) => {
|
||||
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
||||
type: blob.type || 'audio/wav',
|
||||
|
||||
@@ -73,7 +73,7 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp
|
||||
stopRecording,
|
||||
cancelRecording,
|
||||
} = useAudioRecording({
|
||||
maxDurationSeconds: 30,
|
||||
maxDurationSeconds: 29,
|
||||
onRecordingComplete: (blob, recordedDuration) => {
|
||||
// Convert blob to File object
|
||||
const file = new File([blob], `recording-${Date.now()}.webm`, {
|
||||
@@ -100,7 +100,7 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp
|
||||
stopRecording: stopSystemRecording,
|
||||
cancelRecording: cancelSystemRecording,
|
||||
} = useSystemAudioCapture({
|
||||
maxDurationSeconds: 30,
|
||||
maxDurationSeconds: 29,
|
||||
onRecordingComplete: (blob, recordedDuration) => {
|
||||
// Convert blob to File object
|
||||
const file = new File([blob], `system-audio-${Date.now()}.wav`, {
|
||||
|
||||
Reference in New Issue
Block a user