Includes all Phase 1 features: - Search-first navigation with auto-complete - Responsive reading interface (desktop/tablet/mobile) - 4 customization presets + full fine-tuning controls - Layered details panel with notes, bookmarks, highlights - Smart offline caching with IndexedDB and auto-sync - Full accessibility (WCAG 2.1 AA) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1120 lines
29 KiB
Markdown
1120 lines
29 KiB
Markdown
# Text-to-Speech (TTS) - Implementation Plan
|
||
|
||
## 📋 Overview
|
||
|
||
Implement a full-featured Text-to-Speech system for the Bible reader, allowing users to listen to Scripture while reading, driving, exercising, or multitasking.
|
||
|
||
**Status:** Planning Phase
|
||
**Priority:** 🔴 High
|
||
**Estimated Time:** 2-3 weeks (80-120 hours)
|
||
**Target Completion:** TBD
|
||
|
||
---
|
||
|
||
## 🎯 Goals & Objectives
|
||
|
||
### Primary Goals
|
||
1. Provide accessible audio playback of Bible content
|
||
2. Support multiple voices and languages
|
||
3. Enable hands-free Bible reading
|
||
4. Improve accessibility for visually impaired users
|
||
5. Allow multitasking while consuming Scripture
|
||
|
||
### User Value Proposition
|
||
- **For visually impaired users**: Full accessibility to Bible content
|
||
- **For commuters**: Listen during driving/transit
|
||
- **For learners**: Audio reinforcement of reading
|
||
- **For multitaskers**: Listen while doing other activities
|
||
- **For language learners**: Hear correct pronunciation
|
||
|
||
---
|
||
|
||
## ✨ Feature Specifications
|
||
|
||
### 1. Core TTS Functionality
|
||
|
||
#### Web Speech API (Free Tier)
|
||
- Uses browser's built-in speech synthesis
|
||
- No API costs
|
||
- Works offline after initial load
|
||
- Variable quality depending on OS/browser
|
||
|
||
#### Premium Voices (Optional - Phase 2)
|
||
- Amazon Polly integration
|
||
- Google Cloud Text-to-Speech
|
||
- Higher quality, more natural voices
|
||
- Multiple accents and styles
|
||
- Costs: ~$4 per 1 million characters
|
||
|
||
### 2. Voice Selection
|
||
|
||
```typescript
|
||
interface Voice {
|
||
id: string
|
||
name: string
|
||
language: string
|
||
gender: 'male' | 'female' | 'neutral'
|
||
quality: 'standard' | 'premium'
|
||
provider: 'browser' | 'polly' | 'google'
|
||
isDefault: boolean
|
||
localeName: string // e.g., "en-US", "es-ES"
|
||
}
|
||
|
||
interface TTSConfig {
|
||
// Voice
|
||
selectedVoiceId: string
|
||
|
||
// Playback
|
||
rate: number // 0.5 - 2.0 (speed)
|
||
pitch: number // 0.5 - 2.0 (pitch)
|
||
volume: number // 0 - 1
|
||
|
||
// Behavior
|
||
autoAdvanceChapter: boolean
|
||
highlightCurrentVerse: boolean
|
||
pauseBetweenVerses: number // 0-2000ms
|
||
pauseBetweenChapters: number // 0-5000ms
|
||
|
||
// Display
|
||
showFloatingPlayer: boolean
|
||
showProgress: boolean
|
||
minimizeWhenPlaying: boolean
|
||
}
|
||
```
|
||
|
||
### 3. Playback Controls
|
||
|
||
#### Player UI Components
|
||
```
|
||
┌─────────────────────────────────────────────────┐
|
||
│ 🔊 Genesis 1:1-31 ⚙️ 📍 │
|
||
├─────────────────────────────────────────────────┤
|
||
│ ⏮ ⏪ ▶️ ⏩ ⏭ 1.0x 🔈│
|
||
├─────────────────────────────────────────────────┤
|
||
│ ━━━━━━━━━●━━━━━━━━━━━━━━━━━━━━ 65% │
|
||
│ Verse 15 of 31 • 2:34 / 6:12 │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Controls:**
|
||
- Play/Pause
|
||
- Previous/Next Verse
|
||
- Previous/Next Chapter
|
||
- Skip Backward 10s / Forward 10s
|
||
- Speed Control (0.5x, 0.75x, 1.0x, 1.25x, 1.5x, 2.0x)
|
||
- Volume Control
|
||
- Settings Menu
|
||
- Pin/Unpin Player
|
||
|
||
#### Keyboard Shortcuts
|
||
```typescript
|
||
const shortcuts = {
|
||
'Space': 'Play/Pause',
|
||
'ArrowLeft': 'Previous verse',
|
||
'ArrowRight': 'Next verse',
|
||
'Shift+ArrowLeft': 'Previous chapter',
|
||
'Shift+ArrowRight': 'Next chapter',
|
||
'ArrowUp': 'Increase speed by 0.25x',
|
||
'ArrowDown': 'Decrease speed by 0.25x',
|
||
'[': 'Skip backward 10s',
|
||
']': 'Skip forward 10s',
|
||
'M': 'Mute/Unmute',
|
||
'Escape': 'Stop playback'
|
||
}
|
||
```
|
||
|
||
### 4. Visual Feedback
|
||
|
||
#### Active Verse Highlighting
|
||
```typescript
|
||
interface HighlightConfig {
|
||
enabled: boolean
|
||
style: 'background' | 'border' | 'underline' | 'bold'
|
||
color: string
|
||
scrollToVerse: boolean
|
||
scrollBehavior: 'auto' | 'smooth'
|
||
centerVerse: boolean
|
||
}
|
||
|
||
// CSS Example
|
||
.verse-playing {
|
||
background-color: rgba(var(--primary-rgb), 0.1);
|
||
border-left: 4px solid var(--primary-color);
|
||
padding-left: 12px;
|
||
margin-left: -16px;
|
||
scroll-margin-top: 100px; /* For scroll-into-view */
|
||
transition: all 0.3s ease;
|
||
}
|
||
```
|
||
|
||
#### Progress Visualization
|
||
- Linear progress bar
|
||
- Circular progress (for floating player)
|
||
- Time elapsed / total time
|
||
- Verse counter (current / total)
|
||
- Visual waveform (optional, premium)
|
||
|
||
### 5. Verse-Level Navigation
|
||
|
||
```typescript
|
||
interface VersePosition {
|
||
book: string
|
||
chapter: number
|
||
verse: number
|
||
verseText: string
|
||
startTime: number // ms from chapter start
|
||
duration: number // ms for this verse
|
||
}
|
||
|
||
class VerseNavigator {
|
||
private verses: VersePosition[]
|
||
private currentIndex: number
|
||
|
||
getCurrentVerse(): VersePosition
|
||
nextVerse(): VersePosition
|
||
previousVerse(): VersePosition
|
||
jumpToVerse(verseNum: number): void
|
||
getProgress(): number // 0-100%
|
||
}
|
||
```
|
||
|
||
### 6. Background Playback
|
||
|
||
#### Service Worker Integration
|
||
```typescript
|
||
// Support for background playback on mobile
|
||
// Uses Media Session API
|
||
|
||
if ('mediaSession' in navigator) {
|
||
navigator.mediaSession.metadata = new MediaMetadata({
|
||
title: 'Genesis 1:1-31',
|
||
artist: 'Bible Reader',
|
||
album: 'Old Testament',
|
||
artwork: [
|
||
{ src: '/icons/bible-96.png', sizes: '96x96', type: 'image/png' },
|
||
{ src: '/icons/bible-256.png', sizes: '256x256', type: 'image/png' }
|
||
]
|
||
})
|
||
|
||
navigator.mediaSession.setActionHandler('play', handlePlay)
|
||
navigator.mediaSession.setActionHandler('pause', handlePause)
|
||
navigator.mediaSession.setActionHandler('previoustrack', handlePreviousVerse)
|
||
navigator.mediaSession.setActionHandler('nexttrack', handleNextVerse)
|
||
navigator.mediaSession.setActionHandler('seekbackward', () => seek(-10))
|
||
navigator.mediaSession.setActionHandler('seekforward', () => seek(10))
|
||
}
|
||
```
|
||
|
||
### 7. Persistent Player Bar
|
||
|
||
#### Floating Player States
|
||
1. **Full Player** - All controls visible
|
||
2. **Mini Player** - Compact view with play/pause and progress
|
||
3. **Hidden** - Only show icon in corner
|
||
4. **Minimized to Tab** - Shows in browser tab title
|
||
|
||
```typescript
|
||
type PlayerSize = 'full' | 'mini' | 'icon' | 'hidden'
|
||
|
||
interface PlayerPosition {
|
||
size: PlayerSize
|
||
position: 'top' | 'bottom'
|
||
sticky: boolean // Stays visible when scrolling
|
||
docked: 'left' | 'center' | 'right'
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🏗️ Technical Implementation
|
||
|
||
### File Structure
|
||
```
|
||
/components/bible-reader/
|
||
├── tts/
|
||
│ ├── TTSProvider.tsx # Context provider
|
||
│ ├── TTSPlayer.tsx # Main player component
|
||
│ ├── TTSControls.tsx # Playback controls
|
||
│ ├── TTSSettings.tsx # Settings panel
|
||
│ ├── VoiceSelector.tsx # Voice selection UI
|
||
│ ├── FloatingPlayer.tsx # Floating/sticky player
|
||
│ ├── VerseHighlighter.tsx # Active verse highlighting
|
||
│ ├── PlaybackProgress.tsx # Progress bar/circle
|
||
│ ├── engines/
|
||
│ │ ├── WebSpeechEngine.ts # Browser TTS
|
||
│ │ ├── PollyEngine.ts # Amazon Polly (Phase 2)
|
||
│ │ └── GoogleTTSEngine.ts # Google TTS (Phase 2)
|
||
│ └── hooks/
|
||
│ ├── useTTS.ts # Main TTS hook
|
||
│ ├── useVoices.ts # Voice management
|
||
│ ├── usePlayback.ts # Playback state
|
||
│ ├── useVerseTracking.ts # Track current verse
|
||
│ └── useMediaSession.ts # Background playback
|
||
└── reader.tsx # Updated main reader
|
||
```
|
||
|
||
### Core TTS Engine (Web Speech API)
|
||
|
||
```typescript
|
||
// engines/WebSpeechEngine.ts
|
||
export class WebSpeechEngine {
|
||
private synth: SpeechSynthesis
|
||
private utterance: SpeechSynthesisUtterance | null = null
|
||
private currentVerseIndex: number = 0
|
||
private verses: string[] = []
|
||
private isPaused: boolean = false
|
||
|
||
constructor() {
|
||
this.synth = window.speechSynthesis
|
||
}
|
||
|
||
async getVoices(): Promise<Voice[]> {
|
||
return new Promise((resolve) => {
|
||
const voices = this.synth.getVoices()
|
||
|
||
if (voices.length > 0) {
|
||
resolve(this.mapVoices(voices))
|
||
} else {
|
||
// Some browsers load voices asynchronously
|
||
this.synth.onvoiceschanged = () => {
|
||
resolve(this.mapVoices(this.synth.getVoices()))
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
private mapVoices(synthVoices: SpeechSynthesisVoice[]): Voice[] {
|
||
return synthVoices.map(v => ({
|
||
id: v.voiceURI,
|
||
name: v.name,
|
||
language: v.lang,
|
||
gender: this.detectGender(v.name),
|
||
quality: 'standard',
|
||
provider: 'browser',
|
||
isDefault: v.default,
|
||
localeName: v.lang
|
||
}))
|
||
}
|
||
|
||
private detectGender(name: string): 'male' | 'female' | 'neutral' {
|
||
const lower = name.toLowerCase()
|
||
if (lower.includes('female') || lower.includes('woman')) return 'female'
|
||
if (lower.includes('male') || lower.includes('man')) return 'male'
|
||
return 'neutral'
|
||
}
|
||
|
||
async speak(
|
||
text: string,
|
||
config: TTSConfig,
|
||
onBoundary?: (charIndex: number) => void,
|
||
onEnd?: () => void
|
||
): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
// Cancel any existing speech
|
||
this.synth.cancel()
|
||
|
||
this.utterance = new SpeechSynthesisUtterance(text)
|
||
|
||
// Find selected voice
|
||
const voices = this.synth.getVoices()
|
||
const voice = voices.find(v => v.voiceURI === config.selectedVoiceId)
|
||
if (voice) {
|
||
this.utterance.voice = voice
|
||
}
|
||
|
||
// Apply settings
|
||
this.utterance.rate = config.rate
|
||
this.utterance.pitch = config.pitch
|
||
this.utterance.volume = config.volume
|
||
|
||
// Event handlers
|
||
this.utterance.onboundary = (event) => {
|
||
if (onBoundary) onBoundary(event.charIndex)
|
||
}
|
||
|
||
this.utterance.onend = () => {
|
||
if (onEnd) onEnd()
|
||
resolve()
|
||
}
|
||
|
||
this.utterance.onerror = (event) => {
|
||
console.error('Speech error:', event)
|
||
reject(event)
|
||
}
|
||
|
||
// Start speaking
|
||
this.synth.speak(this.utterance)
|
||
})
|
||
}
|
||
|
||
pause(): void {
|
||
if (this.synth.speaking && !this.synth.paused) {
|
||
this.synth.pause()
|
||
this.isPaused = true
|
||
}
|
||
}
|
||
|
||
resume(): void {
|
||
if (this.synth.paused) {
|
||
this.synth.resume()
|
||
this.isPaused = false
|
||
}
|
||
}
|
||
|
||
stop(): void {
|
||
this.synth.cancel()
|
||
this.isPaused = false
|
||
}
|
||
|
||
isSpeaking(): boolean {
|
||
return this.synth.speaking && !this.synth.paused
|
||
}
|
||
|
||
isPausedState(): boolean {
|
||
return this.isPaused
|
||
}
|
||
}
|
||
```
|
||
|
||
### TTS Context Provider
|
||
|
||
```typescript
|
||
// TTSProvider.tsx
|
||
interface TTSContextType {
|
||
// State
|
||
isPlaying: boolean
|
||
isPaused: boolean
|
||
currentVerse: VersePosition | null
|
||
config: TTSConfig
|
||
voices: Voice[]
|
||
progress: number // 0-100
|
||
|
||
// Actions
|
||
play(): Promise<void>
|
||
pause(): void
|
||
resume(): void
|
||
stop(): void
|
||
nextVerse(): void
|
||
previousVerse(): void
|
||
nextChapter(): void
|
||
previousChapter(): void
|
||
jumpToVerse(verseNum: number): void
|
||
setSpeed(rate: number): void
|
||
setVoice(voiceId: string): void
|
||
setVolume(volume: number): void
|
||
updateConfig(config: Partial<TTSConfig>): void
|
||
}
|
||
|
||
export const TTSProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||
const [engine] = useState(() => new WebSpeechEngine())
|
||
const [isPlaying, setIsPlaying] = useState(false)
|
||
const [isPaused, setIsPaused] = useState(false)
|
||
const [config, setConfig] = useState<TTSConfig>(loadConfig())
|
||
const [voices, setVoices] = useState<Voice[]>([])
|
||
const [currentVerse, setCurrentVerse] = useState<VersePosition | null>(null)
|
||
const [verses, setVerses] = useState<VersePosition[]>([])
|
||
const [progress, setProgress] = useState(0)
|
||
|
||
// Load voices on mount
|
||
useEffect(() => {
|
||
engine.getVoices().then(setVoices)
|
||
}, [engine])
|
||
|
||
// Save config to localStorage
|
||
useEffect(() => {
|
||
localStorage.setItem('tts-config', JSON.stringify(config))
|
||
}, [config])
|
||
|
||
const play = async () => {
|
||
if (verses.length === 0) return
|
||
|
||
setIsPlaying(true)
|
||
setIsPaused(false)
|
||
|
||
const playVerse = async (index: number) => {
|
||
if (index >= verses.length) {
|
||
if (config.autoAdvanceChapter) {
|
||
// Load next chapter
|
||
await loadNextChapter()
|
||
} else {
|
||
stop()
|
||
}
|
||
return
|
||
}
|
||
|
||
const verse = verses[index]
|
||
setCurrentVerse(verse)
|
||
setProgress((index / verses.length) * 100)
|
||
|
||
try {
|
||
await engine.speak(
|
||
verse.verseText,
|
||
config,
|
||
undefined,
|
||
() => {
|
||
// Verse completed
|
||
if (config.pauseBetweenVerses > 0) {
|
||
setTimeout(() => {
|
||
playVerse(index + 1)
|
||
}, config.pauseBetweenVerses)
|
||
} else {
|
||
playVerse(index + 1)
|
||
}
|
||
}
|
||
)
|
||
} catch (error) {
|
||
console.error('TTS error:', error)
|
||
stop()
|
||
}
|
||
}
|
||
|
||
playVerse(currentVerse ? verses.indexOf(currentVerse) : 0)
|
||
}
|
||
|
||
const pause = () => {
|
||
engine.pause()
|
||
setIsPaused(true)
|
||
}
|
||
|
||
const resume = () => {
|
||
engine.resume()
|
||
setIsPaused(false)
|
||
}
|
||
|
||
const stop = () => {
|
||
engine.stop()
|
||
setIsPlaying(false)
|
||
setIsPaused(false)
|
||
setCurrentVerse(null)
|
||
setProgress(0)
|
||
}
|
||
|
||
const nextVerse = () => {
|
||
if (!currentVerse) return
|
||
const currentIndex = verses.indexOf(currentVerse)
|
||
if (currentIndex < verses.length - 1) {
|
||
const wasPlaying = isPlaying
|
||
stop()
|
||
setCurrentVerse(verses[currentIndex + 1])
|
||
if (wasPlaying) play()
|
||
}
|
||
}
|
||
|
||
const previousVerse = () => {
|
||
if (!currentVerse) return
|
||
const currentIndex = verses.indexOf(currentVerse)
|
||
if (currentIndex > 0) {
|
||
const wasPlaying = isPlaying
|
||
stop()
|
||
setCurrentVerse(verses[currentIndex - 1])
|
||
if (wasPlaying) play()
|
||
}
|
||
}
|
||
|
||
// ... more methods
|
||
|
||
return (
|
||
<TTSContext.Provider value={{
|
||
isPlaying,
|
||
isPaused,
|
||
currentVerse,
|
||
config,
|
||
voices,
|
||
progress,
|
||
play,
|
||
pause,
|
||
resume,
|
||
stop,
|
||
nextVerse,
|
||
previousVerse,
|
||
nextChapter,
|
||
previousChapter,
|
||
jumpToVerse,
|
||
setSpeed,
|
||
setVoice,
|
||
setVolume,
|
||
updateConfig
|
||
}}>
|
||
{children}
|
||
</TTSContext.Provider>
|
||
)
|
||
}
|
||
```
|
||
|
||
### TTS Player UI Component
|
||
|
||
```typescript
|
||
// TTSPlayer.tsx
|
||
export const TTSPlayer: React.FC = () => {
|
||
const {
|
||
isPlaying,
|
||
isPaused,
|
||
currentVerse,
|
||
config,
|
||
progress,
|
||
play,
|
||
pause,
|
||
resume,
|
||
stop,
|
||
nextVerse,
|
||
previousVerse,
|
||
setSpeed
|
||
} = useTTS()
|
||
|
||
const [playerSize, setPlayerSize] = useState<PlayerSize>('full')
|
||
const [isVisible, setIsVisible] = useState(false)
|
||
|
||
// Show player when playback starts
|
||
useEffect(() => {
|
||
if (isPlaying) {
|
||
setIsVisible(true)
|
||
}
|
||
}, [isPlaying])
|
||
|
||
if (!isVisible) return null
|
||
|
||
const handlePlayPause = () => {
|
||
if (isPlaying && !isPaused) {
|
||
pause()
|
||
} else if (isPaused) {
|
||
resume()
|
||
} else {
|
||
play()
|
||
}
|
||
}
|
||
|
||
const speeds = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
|
||
const currentSpeedIndex = speeds.indexOf(config.rate)
|
||
|
||
const cycleSpeed = () => {
|
||
const nextIndex = (currentSpeedIndex + 1) % speeds.length
|
||
setSpeed(speeds[nextIndex])
|
||
}
|
||
|
||
if (playerSize === 'mini') {
|
||
return (
|
||
<Box className="tts-player mini" sx={{
|
||
position: 'fixed',
|
||
bottom: 20,
|
||
right: 20,
|
||
bgcolor: 'background.paper',
|
||
borderRadius: 2,
|
||
boxShadow: 3,
|
||
p: 2,
|
||
minWidth: 200
|
||
}}>
|
||
<Box display="flex" alignItems="center" gap={1}>
|
||
<IconButton size="small" onClick={handlePlayPause}>
|
||
{isPlaying && !isPaused ? <PauseIcon /> : <PlayArrowIcon />}
|
||
</IconButton>
|
||
<Box flex={1}>
|
||
<LinearProgress variant="determinate" value={progress} />
|
||
<Typography variant="caption">
|
||
{currentVerse?.book} {currentVerse?.chapter}:{currentVerse?.verse}
|
||
</Typography>
|
||
</Box>
|
||
<IconButton size="small" onClick={() => setPlayerSize('full')}>
|
||
<ExpandLessIcon />
|
||
</IconButton>
|
||
</Box>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<Card className="tts-player full" sx={{
|
||
position: 'fixed',
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
zIndex: 1300,
|
||
borderRadius: 0,
|
||
boxShadow: 6
|
||
}}>
|
||
<CardContent>
|
||
{/* Header */}
|
||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||
<Box display="flex" alignItems="center" gap={1}>
|
||
<HeadphonesIcon color="primary" />
|
||
<Typography variant="h6">
|
||
{currentVerse?.book} {currentVerse?.chapter}:1-{verses.length}
|
||
</Typography>
|
||
</Box>
|
||
<Box>
|
||
<IconButton size="small" onClick={() => setPlayerSize('mini')}>
|
||
<MinimizeIcon />
|
||
</IconButton>
|
||
<IconButton size="small" onClick={stop}>
|
||
<CloseIcon />
|
||
</IconButton>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Main Controls */}
|
||
<Box display="flex" justifyContent="center" alignItems="center" gap={2} mb={2}>
|
||
<IconButton onClick={previousChapter}>
|
||
<SkipPreviousIcon />
|
||
</IconButton>
|
||
<IconButton onClick={previousVerse}>
|
||
<FastRewindIcon />
|
||
</IconButton>
|
||
<IconButton onClick={handlePlayPause} size="large" color="primary">
|
||
{isPlaying && !isPaused ? <PauseIcon /> : <PlayArrowIcon />}
|
||
</IconButton>
|
||
<IconButton onClick={nextVerse}>
|
||
<FastForwardIcon />
|
||
</IconButton>
|
||
<IconButton onClick={nextChapter}>
|
||
<SkipNextIcon />
|
||
</IconButton>
|
||
</Box>
|
||
|
||
{/* Progress Bar */}
|
||
<Box mb={2}>
|
||
<LinearProgress variant="determinate" value={progress} sx={{ height: 6, borderRadius: 1 }} />
|
||
<Box display="flex" justifyContent="space-between" mt={1}>
|
||
<Typography variant="caption" color="text.secondary">
|
||
Verse {currentVerse?.verse} of {verses.length}
|
||
</Typography>
|
||
<Typography variant="caption" color="text.secondary">
|
||
{Math.round(progress)}%
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Secondary Controls */}
|
||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||
<Button
|
||
size="small"
|
||
startIcon={<SpeedIcon />}
|
||
onClick={cycleSpeed}
|
||
>
|
||
{config.rate}x
|
||
</Button>
|
||
|
||
<Box display="flex" alignItems="center" gap={1}>
|
||
<VolumeUpIcon />
|
||
<Slider
|
||
value={config.volume}
|
||
onChange={(_, value) => setVolume(value as number)}
|
||
min={0}
|
||
max={1}
|
||
step={0.1}
|
||
sx={{ width: 100 }}
|
||
/>
|
||
</Box>
|
||
|
||
<IconButton size="small">
|
||
<SettingsIcon />
|
||
</IconButton>
|
||
</Box>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|
||
```
|
||
|
||
### Verse Highlighting
|
||
|
||
```typescript
|
||
// VerseHighlighter.tsx
|
||
export const VerseHighlighter: React.FC = () => {
|
||
const { currentVerse, config } = useTTS()
|
||
|
||
useEffect(() => {
|
||
if (!currentVerse || !config.highlightCurrentVerse) return
|
||
|
||
const verseElement = document.querySelector(
|
||
`[data-verse="${currentVerse.verse}"]`
|
||
)
|
||
|
||
if (verseElement) {
|
||
// Add playing class
|
||
verseElement.classList.add('verse-playing')
|
||
|
||
// Scroll into view
|
||
if (config.scrollToVerse) {
|
||
verseElement.scrollIntoView({
|
||
behavior: 'smooth',
|
||
block: 'center'
|
||
})
|
||
}
|
||
|
||
// Cleanup
|
||
return () => {
|
||
verseElement.classList.remove('verse-playing')
|
||
}
|
||
}
|
||
}, [currentVerse, config])
|
||
|
||
return null // Pure effect component
|
||
}
|
||
```
|
||
|
||
### Media Session API (Background Playback)
|
||
|
||
```typescript
|
||
// hooks/useMediaSession.ts
|
||
export const useMediaSession = () => {
|
||
const {
|
||
currentVerse,
|
||
isPlaying,
|
||
play,
|
||
pause,
|
||
nextVerse,
|
||
previousVerse
|
||
} = useTTS()
|
||
|
||
useEffect(() => {
|
||
if (!('mediaSession' in navigator)) return
|
||
|
||
if (currentVerse) {
|
||
navigator.mediaSession.metadata = new MediaMetadata({
|
||
title: `${currentVerse.book} ${currentVerse.chapter}:${currentVerse.verse}`,
|
||
artist: 'Bible Reader',
|
||
album: currentVerse.book,
|
||
artwork: [
|
||
{ src: '/icons/bible-96.png', sizes: '96x96', type: 'image/png' },
|
||
{ src: '/icons/bible-192.png', sizes: '192x192', type: 'image/png' },
|
||
{ src: '/icons/bible-512.png', sizes: '512x512', type: 'image/png' }
|
||
]
|
||
})
|
||
}
|
||
|
||
// Set up action handlers
|
||
navigator.mediaSession.setActionHandler('play', () => play())
|
||
navigator.mediaSession.setActionHandler('pause', () => pause())
|
||
navigator.mediaSession.setActionHandler('previoustrack', () => previousVerse())
|
||
navigator.mediaSession.setActionHandler('nexttrack', () => nextVerse())
|
||
|
||
// Update playback state
|
||
navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused'
|
||
|
||
return () => {
|
||
navigator.mediaSession.setActionHandler('play', null)
|
||
navigator.mediaSession.setActionHandler('pause', null)
|
||
navigator.mediaSession.setActionHandler('previoustrack', null)
|
||
navigator.mediaSession.setActionHandler('nexttrack', null)
|
||
}
|
||
}, [currentVerse, isPlaying, play, pause, nextVerse, previousVerse])
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 💾 Data Persistence
|
||
|
||
### LocalStorage Schema
|
||
```typescript
|
||
interface TTSStorage {
|
||
version: number
|
||
config: TTSConfig
|
||
recentVoices: string[] // Voice IDs
|
||
lastPlayedVerse: {
|
||
book: string
|
||
chapter: number
|
||
verse: number
|
||
} | null
|
||
stats: {
|
||
totalListeningTime: number // seconds
|
||
chaptersCompleted: number
|
||
favoriteVoice: string
|
||
}
|
||
}
|
||
|
||
// Key: 'bible-reader:tts'
|
||
```
|
||
|
||
### User Preferences API
|
||
```typescript
|
||
// Add to UserPreference model
|
||
model UserPreference {
|
||
// ... existing fields
|
||
ttsConfig Json?
|
||
ttsVoiceId String?
|
||
ttsRate Float? @default(1.0)
|
||
}
|
||
|
||
// Sync endpoint
|
||
POST /api/user/preferences/tts
|
||
Body: Partial<TTSConfig>
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 API Endpoints
|
||
|
||
### For Premium Voices (Phase 2)
|
||
|
||
```typescript
|
||
// /api/tts/voices
|
||
GET /api/tts/voices
|
||
Response: {
|
||
voices: Voice[]
|
||
providers: {
|
||
browser: Voice[]
|
||
polly: Voice[]
|
||
google: Voice[]
|
||
}
|
||
}
|
||
|
||
// /api/tts/synthesize
|
||
POST /api/tts/synthesize
|
||
Body: {
|
||
text: string
|
||
voiceId: string
|
||
provider: 'polly' | 'google'
|
||
config: TTSConfig
|
||
}
|
||
Response: {
|
||
audioUrl: string // Signed URL to audio file
|
||
duration: number // seconds
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🧪 Testing Strategy
|
||
|
||
### Unit Tests
|
||
```typescript
|
||
// __tests__/tts/web-speech-engine.test.ts
|
||
describe('WebSpeechEngine', () => {
|
||
it('should load available voices', async () => {
|
||
const engine = new WebSpeechEngine()
|
||
const voices = await engine.getVoices()
|
||
expect(voices.length).toBeGreaterThan(0)
|
||
})
|
||
|
||
it('should speak text with config', async () => {
|
||
const engine = new WebSpeechEngine()
|
||
const mockSynth = jest.spyOn(window.speechSynthesis, 'speak')
|
||
|
||
await engine.speak('Test', defaultConfig)
|
||
expect(mockSynth).toHaveBeenCalled()
|
||
})
|
||
|
||
it('should pause and resume correctly', () => {
|
||
const engine = new WebSpeechEngine()
|
||
engine.pause()
|
||
expect(engine.isPausedState()).toBe(true)
|
||
engine.resume()
|
||
expect(engine.isPausedState()).toBe(false)
|
||
})
|
||
})
|
||
```
|
||
|
||
### Integration Tests
|
||
```typescript
|
||
// __tests__/tts/integration.test.tsx
|
||
describe('TTS Integration', () => {
|
||
it('should play verse and highlight it', async () => {
|
||
render(
|
||
<TTSProvider>
|
||
<BibleReader />
|
||
<TTSPlayer />
|
||
</TTSProvider>
|
||
)
|
||
|
||
const playButton = screen.getByRole('button', { name: /play/i })
|
||
fireEvent.click(playButton)
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText(/Genesis 1:1/)).toHaveClass('verse-playing')
|
||
})
|
||
})
|
||
})
|
||
```
|
||
|
||
### Manual Testing Checklist
|
||
- [ ] All voices load correctly
|
||
- [ ] Playback works across browsers (Chrome, Firefox, Safari)
|
||
- [ ] Speed adjustment works (0.5x - 2.0x)
|
||
- [ ] Volume control works
|
||
- [ ] Verse highlighting syncs with audio
|
||
- [ ] Auto-advance to next verse works
|
||
- [ ] Auto-advance to next chapter works
|
||
- [ ] Background playback works on mobile
|
||
- [ ] Media controls in notification shade work
|
||
- [ ] Settings persist across sessions
|
||
- [ ] Player UI is responsive on mobile
|
||
- [ ] Keyboard shortcuts work correctly
|
||
- [ ] No memory leaks during long playback
|
||
|
||
---
|
||
|
||
## 📅 Implementation Timeline
|
||
|
||
### Phase 1: Core TTS (Week 1)
|
||
**Day 1-2: Foundation**
|
||
- [ ] Create TTS context provider
|
||
- [ ] Implement Web Speech API engine
|
||
- [ ] Build voice selector component
|
||
- [ ] Add basic play/pause controls
|
||
|
||
**Day 3-4: Player UI**
|
||
- [ ] Design and build full player UI
|
||
- [ ] Implement mini player mode
|
||
- [ ] Add progress bar and tracking
|
||
- [ ] Create settings panel
|
||
|
||
**Day 5: Verse Navigation**
|
||
- [ ] Implement verse-by-verse playback
|
||
- [ ] Add next/previous verse controls
|
||
- [ ] Build verse position tracking
|
||
- [ ] Test chapter boundaries
|
||
|
||
**Deliverable:** Working TTS with basic controls
|
||
|
||
### Phase 2: Enhanced Features (Week 2)
|
||
**Day 1-2: Visual Feedback**
|
||
- [ ] Implement verse highlighting
|
||
- [ ] Add scroll-to-verse
|
||
- [ ] Create speed controls
|
||
- [ ] Build volume controls
|
||
|
||
**Day 3-4: Advanced Controls**
|
||
- [ ] Add keyboard shortcuts
|
||
- [ ] Implement skip forward/backward
|
||
- [ ] Create chapter navigation
|
||
- [ ] Add auto-advance features
|
||
|
||
**Day 5: Mobile & Background**
|
||
- [ ] Implement Media Session API
|
||
- [ ] Test background playback
|
||
- [ ] Optimize for mobile
|
||
- [ ] Add floating player
|
||
|
||
**Deliverable:** Full-featured TTS system
|
||
|
||
### Phase 3: Premium Voices (Week 3 - Optional)
|
||
**Day 1-2: Backend Integration**
|
||
- [ ] Set up Amazon Polly API
|
||
- [ ] Create synthesis endpoint
|
||
- [ ] Implement audio caching
|
||
- [ ] Build voice preview
|
||
|
||
**Day 3-4: Frontend Integration**
|
||
- [ ] Add premium voice selector
|
||
- [ ] Implement streaming playback
|
||
- [ ] Handle API errors gracefully
|
||
- [ ] Add loading states
|
||
|
||
**Day 5: Polish & Testing**
|
||
- [ ] Test premium voices
|
||
- [ ] Optimize API usage
|
||
- [ ] Add cost monitoring
|
||
- [ ] Documentation
|
||
|
||
**Deliverable:** Premium TTS with high-quality voices
|
||
|
||
---
|
||
|
||
## 🚀 Deployment Plan
|
||
|
||
### Pre-Launch Checklist
|
||
- [ ] All unit tests passing
|
||
- [ ] Integration tests passing
|
||
- [ ] Cross-browser testing complete
|
||
- [ ] Mobile testing complete (iOS + Android)
|
||
- [ ] Accessibility audit passed
|
||
- [ ] Performance benchmarks met
|
||
- [ ] User documentation created
|
||
- [ ] Analytics events configured
|
||
|
||
### Rollout Strategy
|
||
1. **Beta (Week 1)**: 10% of users, Web Speech API only
|
||
2. **Staged (Week 2)**: 50% of users, collect feedback
|
||
3. **Full (Week 3)**: 100% of users
|
||
4. **Premium (Week 4)**: Launch premium voices for subscribers
|
||
|
||
### Browser Support
|
||
- ✅ Chrome 33+ (excellent support)
|
||
- ✅ Edge 14+ (excellent support)
|
||
- ✅ Safari 7+ (good support)
|
||
- ⚠️ Firefox 49+ (limited voices)
|
||
- ❌ IE 11 (not supported)
|
||
|
||
---
|
||
|
||
## 💰 Cost Analysis (Premium Voices)
|
||
|
||
### Amazon Polly Pricing
|
||
- Standard voices: $4.00 per 1 million characters
|
||
- Neural voices: $16.00 per 1 million characters
|
||
- Average Bible: ~4 million characters
|
||
- Cost to read entire Bible: ~$16-64
|
||
|
||
### Optimization Strategies
|
||
1. **Caching**: Cache synthesized audio (reduce repeat costs)
|
||
2. **Compression**: Use MP3 (smaller file sizes)
|
||
3. **Lazy Loading**: Only synthesize on-demand
|
||
4. **Rate Limiting**: Prevent abuse
|
||
5. **Subscription Gating**: Premium voices for paid users only
|
||
|
||
### Expected Monthly Costs
|
||
- 1,000 users × 10 chapters/month × 5,000 chars = 50M chars
|
||
- Cost: $200-800/month (depending on voice quality)
|
||
|
||
---
|
||
|
||
## 📚 Documentation
|
||
|
||
### User Documentation
|
||
- "Getting Started with Text-to-Speech"
|
||
- "Choosing the Right Voice"
|
||
- "Keyboard Shortcuts Guide"
|
||
- "Background Playback on Mobile"
|
||
- "Premium Voices: What's the Difference?"
|
||
|
||
### Developer Documentation
|
||
- TTS Engine Architecture
|
||
- Adding New Voice Providers
|
||
- Custom Voice Integration Guide
|
||
- Performance Optimization Tips
|
||
|
||
---
|
||
|
||
## 🔄 Future Enhancements
|
||
|
||
### Phase 3+ Features
|
||
- [ ] Offline audio download (pre-synthesize chapters)
|
||
- [ ] Playlist creation (custom reading lists)
|
||
- [ ] Sleep timer
|
||
- [ ] Bookmarks/favorites for audio
|
||
- [ ] Sharing audio clips
|
||
- [ ] Customizable voice profiles (pitch + rate presets)
|
||
- [ ] Synchronized multi-device playback
|
||
- [ ] Audio effects (reverb, echo)
|
||
- [ ] Speed training (gradually increase WPM)
|
||
- [ ] Comprehension quizzes after listening
|
||
|
||
---
|
||
|
||
## 📝 Notes & Considerations
|
||
|
||
### Performance
|
||
- Keep player UI lightweight (<50ms render time)
|
||
- Use Web Workers for audio processing (Phase 2)
|
||
- Implement audio buffering for smooth playback
|
||
- Monitor memory usage during long sessions
|
||
|
||
### Accessibility
|
||
- Ensure screen reader compatibility
|
||
- Provide text alternatives for all controls
|
||
- Maintain keyboard accessibility
|
||
- Test with assistive technologies
|
||
|
||
### Legal Considerations
|
||
- Bible text copyright (check version licenses)
|
||
- Voice cloning regulations (only use licensed voices)
|
||
- COPPA compliance (children under 13)
|
||
- GDPR compliance (store user preferences correctly)
|
||
|
||
---
|
||
|
||
**Document Version:** 1.0
|
||
**Last Updated:** 2025-10-13
|
||
**Owner:** Development Team
|
||
**Status:** Ready for Implementation
|