From ea77b17363424ca4a0a6373547de61f70a3e6a02 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Tue, 9 Dec 2025 09:29:05 +0000 Subject: [PATCH] Implement audio player component with HTML5 audio and download functionality --- .kiro/specs/text-to-audiobook/tasks.md | 2 +- frontend/src/App.tsx | 51 ++++++++++++++----- .../src/components/AudioPlayerSection.tsx | 36 +++++++++++++ 3 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/AudioPlayerSection.tsx diff --git a/.kiro/specs/text-to-audiobook/tasks.md b/.kiro/specs/text-to-audiobook/tasks.md index 6f1031a..45293b0 100644 --- a/.kiro/specs/text-to-audiobook/tasks.md +++ b/.kiro/specs/text-to-audiobook/tasks.md @@ -214,7 +214,7 @@ - Test speed slider enforces min/max bounds - _Requirements: 3.1, 3.3, 4.2, 4.3, 5.3_ -- [ ] 11. Implement audio player component +- [x] 11. Implement audio player component - Create AudioPlayerSection component - Display HTML5 audio element with controls - Add "Download audiobook" button diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 52ba563..458e6d3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,8 +2,9 @@ import { useState } from 'react' import './App.css' import TextInputSection from './components/TextInputSection' import VoiceAssignmentSection from './components/VoiceAssignmentSection' +import AudioPlayerSection from './components/AudioPlayerSection' import type { Role } from './api/tts' -import { detectRoles } from './api/tts' +import { detectRoles, generateAudiobook } from './api/tts' function App() { const [text, setText] = useState('') @@ -15,6 +16,7 @@ function App() { const [speed, setSpeed] = useState(1.0) // Default: 1.0x const [isGenerating, setIsGenerating] = useState(false) const [error, setError] = useState(null) + const [audioUrl, setAudioUrl] = useState(null) const handleTextChange = (newText: string) => { setText(newText) @@ -63,26 +65,46 @@ function App() { setError(null) try { - // TODO: Implement audio generation - console.log('Generating audiobook with:', { + // Convert voice assignments to the format expected by the API + const rolesArray = Array.from(voiceAssignments.entries()).map(([id, voice]) => ({ + id, + voice + })) + + // Generate the audiobook + const audioBlob = await generateAudiobook({ text, - language, - voiceAssignments: Array.from(voiceAssignments.entries()), + lang: language, + roles: rolesArray, speed }) - // Placeholder - will be implemented in later tasks - setTimeout(() => { - setIsGenerating(false) - setPhase('generated') - }, 1000) + // Create a blob URL for the audio + const url = URL.createObjectURL(audioBlob) + setAudioUrl(url) + + // Move to generated phase + setPhase('generated') } catch (err) { setError(err instanceof Error ? err.message : 'Failed to generate audiobook') console.error('Generation error:', err) + } finally { setIsGenerating(false) } } + const handleDownload = () => { + if (!audioUrl) return + + // Create a temporary anchor element to trigger download + const link = document.createElement('a') + link.href = audioUrl + link.download = 'audiobook.wav' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + // Check if all roles have assigned voices const allRolesAssigned = roles.length > 0 && roles.every(role => voiceAssignments.has(role.id)) @@ -122,10 +144,11 @@ function App() { /> )} - {phase === 'generated' && ( -
-

Audio player phase (to be implemented)

-
+ {phase === 'generated' && audioUrl && ( + )} diff --git a/frontend/src/components/AudioPlayerSection.tsx b/frontend/src/components/AudioPlayerSection.tsx new file mode 100644 index 0000000..3978b7d --- /dev/null +++ b/frontend/src/components/AudioPlayerSection.tsx @@ -0,0 +1,36 @@ +interface AudioPlayerSectionProps { + audioUrl: string; + onDownload: () => void; +} + +export default function AudioPlayerSection({ + audioUrl, + onDownload +}: AudioPlayerSectionProps) { + return ( +
+

Your Audiobook is Ready!

+ +
+

Listen to your audiobook

+ +
+ +
+ +
+
+ ); +}