diff --git a/mnemo_cards_admin/src/components/CardVoicesManager.tsx b/mnemo_cards_admin/src/components/CardVoicesManager.tsx index 9b25d32..ff4cd39 100644 --- a/mnemo_cards_admin/src/components/CardVoicesManager.tsx +++ b/mnemo_cards_admin/src/components/CardVoicesManager.tsx @@ -386,7 +386,26 @@ function VoiceItem({ voice, onRemove, disabled }: VoiceItemProps) { const handlePlayPause = () => { if (!audioRef.current) { try { - const audio = new Audio(`data:audio/mpeg;base64,${voice.voiceUrl}`) + // voiceUrl can be either: + // 1. Presigned URL (http/https) - new format + // 2. Base64 data URL (data:audio/...) - legacy format + // 3. UUID - if presigned URL generation failed (shouldn't happen, but handle gracefully) + let audioUrl = voice.voiceUrl + + // If it's a UUID (not a URL and not base64), we can't play it directly + // This shouldn't happen as backend should return presigned URL, but handle it + if (!audioUrl.startsWith('http://') && !audioUrl.startsWith('https://') && !audioUrl.startsWith('data:')) { + // Check if it looks like a UUID + const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + if (uuidPattern.test(audioUrl)) { + toast.error('Audio URL is not available. Please refresh the page.') + return + } + // Otherwise, assume it's legacy base64 without data: prefix + audioUrl = `data:audio/mpeg;base64,${audioUrl}` + } + + const audio = new Audio(audioUrl) audioRef.current = audio audio.onended = () => { @@ -394,9 +413,18 @@ function VoiceItem({ voice, onRemove, disabled }: VoiceItemProps) { audioRef.current = null } - audio.onerror = (e) => { + audio.onerror = (e: Event | string) => { console.error('Audio playback error:', e) - toast.error('Failed to play audio. The file may be corrupted or in an unsupported format.') + let errorMessage = 'Failed to play audio. The file may be corrupted, in an unsupported format, or the URL may have expired.' + + if (e instanceof Event) { + const audioElement = e.target as HTMLAudioElement | null + if (audioElement?.error) { + errorMessage = `Failed to load audio: ${audioElement.error.message || 'Unknown error'}` + } + } + + toast.error(errorMessage) setIsPlaying(false) audioRef.current = null } @@ -407,7 +435,10 @@ function VoiceItem({ voice, onRemove, disabled }: VoiceItemProps) { audio.play().catch((error) => { console.error('Audio play error:', error) - toast.error('Failed to play audio. Please check your browser audio settings.') + const errorMessage = error instanceof Error + ? error.message + : 'Failed to play audio' + toast.error(`Failed to play audio: ${errorMessage}. Please check your browser audio settings.`) setIsPlaying(false) audioRef.current = null }) diff --git a/mnemo_cards_admin/src/components/ui/audio-upload.tsx b/mnemo_cards_admin/src/components/ui/audio-upload.tsx index fa020c6..287d1c6 100644 --- a/mnemo_cards_admin/src/components/ui/audio-upload.tsx +++ b/mnemo_cards_admin/src/components/ui/audio-upload.tsx @@ -173,9 +173,18 @@ export function AudioUpload({ audioRef.current = null } - audio.onerror = (e) => { + audio.onerror = (e: Event | string) => { console.error('Audio playback error:', e) - alert('Failed to play audio. The file may be corrupted or in an unsupported format.') + let errorMessage = 'Failed to play audio. The file may be corrupted, in an unsupported format, or the URL may have expired.' + + if (e instanceof Event) { + const audioElement = e.target as HTMLAudioElement | null + if (audioElement?.error) { + errorMessage = `Failed to load audio: ${audioElement.error.message || 'Unknown error'}` + } + } + + alert(errorMessage) setIsPlaying(false) audioRef.current = null } @@ -186,7 +195,10 @@ export function AudioUpload({ audio.play().catch((error) => { console.error('Audio play error:', error) - alert('Failed to play audio. Please check your browser audio settings.') + const errorMessage = error instanceof Error + ? error.message + : 'Failed to play audio' + alert(`Failed to play audio: ${errorMessage}. Please check your browser audio settings.`) setIsPlaying(false) audioRef.current = null })