diff --git a/app/src/components/VoiceProfiles/ProfileForm.tsx b/app/src/components/VoiceProfiles/ProfileForm.tsx index f67c163f..68a42bf8 100644 --- a/app/src/components/VoiceProfiles/ProfileForm.tsx +++ b/app/src/components/VoiceProfiles/ProfileForm.tsx @@ -46,14 +46,26 @@ import { formatAudioDuration } from '@/lib/utils/audio'; import { isTauri } from '@/lib/tauri'; // Helper function to get audio duration from File -async function getAudioDuration(file: File): Promise { +async function getAudioDuration(file: File & { recordedDuration?: number }): Promise { + // 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); - resolve(audio.duration); + // 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', () => { @@ -123,7 +135,7 @@ export function ProfileForm() { useEffect(() => { if (selectedFile && selectedFile instanceof File) { setIsValidatingAudio(true); - getAudioDuration(selectedFile) + getAudioDuration(selectedFile as File & { recordedDuration?: number }) .then((duration) => { setAudioDuration(duration); if (duration > MAX_AUDIO_DURATION_SECONDS) { @@ -138,10 +150,18 @@ export function ProfileForm() { .catch((error) => { console.error('Failed to get audio duration:', error); setAudioDuration(null); - form.setError('sampleFile', { - type: 'manual', - message: 'Failed to validate audio file. Please try a different file.', - }); + // For recordings, we auto-stop at max duration, so we can skip validation errors + const isRecordedFile = selectedFile.name.startsWith('recording-') || + selectedFile.name.startsWith('system-audio-'); + if (!isRecordedFile) { + form.setError('sampleFile', { + type: 'manual', + message: 'Failed to validate audio file. Please try a different file.', + }); + } else { + // Clear any existing errors for recorded files + form.clearErrors('sampleFile'); + } }) .finally(() => { setIsValidatingAudio(false); @@ -161,10 +181,14 @@ export function ProfileForm() { cancelRecording, } = useAudioRecording({ maxDurationSeconds: 30, - onRecordingComplete: (blob) => { + onRecordingComplete: (blob, recordedDuration) => { const file = new File([blob], `recording-${Date.now()}.webm`, { type: blob.type || 'audio/webm', - }); + }) as File & { recordedDuration?: number }; + // Store the actual recorded duration to bypass metadata reading issues on Windows + if (recordedDuration !== undefined) { + file.recordedDuration = recordedDuration; + } form.setValue('sampleFile', file, { shouldValidate: true }); toast({ title: 'Recording complete', @@ -183,10 +207,14 @@ export function ProfileForm() { cancelRecording: cancelSystemRecording, } = useSystemAudioCapture({ maxDurationSeconds: 30, - onRecordingComplete: (blob) => { + onRecordingComplete: (blob, recordedDuration) => { const file = new File([blob], `system-audio-${Date.now()}.wav`, { type: blob.type || 'audio/wav', - }); + }) as File & { recordedDuration?: number }; + // Store the actual recorded duration to bypass metadata reading issues on Windows + if (recordedDuration !== undefined) { + file.recordedDuration = recordedDuration; + } form.setValue('sampleFile', file, { shouldValidate: true }); toast({ title: 'System audio captured', diff --git a/app/src/components/VoiceProfiles/SampleUpload.tsx b/app/src/components/VoiceProfiles/SampleUpload.tsx index 3cc3afa6..26ca4598 100644 --- a/app/src/components/VoiceProfiles/SampleUpload.tsx +++ b/app/src/components/VoiceProfiles/SampleUpload.tsx @@ -72,11 +72,15 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp cancelRecording, } = useAudioRecording({ maxDurationSeconds: 30, - onRecordingComplete: (blob) => { + onRecordingComplete: (blob, recordedDuration) => { // Convert blob to File object const file = new File([blob], `recording-${Date.now()}.webm`, { type: blob.type || 'audio/webm', - }); + }) as File & { recordedDuration?: number }; + // Store the actual recorded duration to bypass metadata reading issues on Windows + if (recordedDuration !== undefined) { + file.recordedDuration = recordedDuration; + } form.setValue('file', file, { shouldValidate: true }); toast({ title: 'Recording complete', @@ -95,11 +99,15 @@ export function SampleUpload({ profileId, open, onOpenChange }: SampleUploadProp cancelRecording: cancelSystemRecording, } = useSystemAudioCapture({ maxDurationSeconds: 30, - onRecordingComplete: (blob) => { + onRecordingComplete: (blob, recordedDuration) => { // Convert blob to File object const file = new File([blob], `system-audio-${Date.now()}.wav`, { type: blob.type || 'audio/wav', - }); + }) as File & { recordedDuration?: number }; + // Store the actual recorded duration to bypass metadata reading issues on Windows + if (recordedDuration !== undefined) { + file.recordedDuration = recordedDuration; + } form.setValue('file', file, { shouldValidate: true }); toast({ title: 'System audio captured', diff --git a/app/src/lib/hooks/useAudioRecording.ts b/app/src/lib/hooks/useAudioRecording.ts index 91d25801..cf6c2c79 100644 --- a/app/src/lib/hooks/useAudioRecording.ts +++ b/app/src/lib/hooks/useAudioRecording.ts @@ -3,7 +3,7 @@ import { isTauri } from '@/lib/tauri'; interface UseAudioRecordingOptions { maxDurationSeconds?: number; - onRecordingComplete?: (blob: Blob) => void; + onRecordingComplete?: (blob: Blob, duration?: number) => void; } export function useAudioRecording({ @@ -87,7 +87,11 @@ export function useAudioRecording({ mediaRecorder.onstop = () => { const blob = new Blob(chunksRef.current, { type: 'audio/webm' }); - onRecordingComplete?.(blob); + // Pass the actual recorded duration + const recordedDuration = startTimeRef.current + ? (Date.now() - startTimeRef.current) / 1000 + : undefined; + onRecordingComplete?.(blob, recordedDuration); // Stop all tracks streamRef.current?.getTracks().forEach((track) => { diff --git a/app/src/lib/hooks/useSystemAudioCapture.ts b/app/src/lib/hooks/useSystemAudioCapture.ts index 13a22b55..2c498e2f 100644 --- a/app/src/lib/hooks/useSystemAudioCapture.ts +++ b/app/src/lib/hooks/useSystemAudioCapture.ts @@ -4,7 +4,7 @@ import { isTauri } from '@/lib/tauri'; interface UseSystemAudioCaptureOptions { maxDurationSeconds?: number; - onRecordingComplete?: (blob: Blob) => void; + onRecordingComplete?: (blob: Blob, duration?: number) => void; } /** @@ -110,7 +110,11 @@ export function useSystemAudioCapture({ } const blob = new Blob([bytes], { type: 'audio/wav' }); - onRecordingComplete?.(blob); + // Pass the actual recorded duration + const recordedDuration = startTimeRef.current + ? (Date.now() - startTimeRef.current) / 1000 + : undefined; + onRecordingComplete?.(blob, recordedDuration); } catch (err) { const errorMessage = err instanceof Error diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car index 9db1fbf0..c0a2c1f2 100644 Binary files a/tauri/src-tauri/gen/Assets.car and b/tauri/src-tauri/gen/Assets.car differ