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>
29 KiB
29 KiB
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
- Provide accessible audio playback of Bible content
- Support multiple voices and languages
- Enable hands-free Bible reading
- Improve accessibility for visually impaired users
- 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
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
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
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
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
// 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
- Full Player - All controls visible
- Mini Player - Compact view with play/pause and progress
- Hidden - Only show icon in corner
- Minimized to Tab - Shows in browser tab title
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)
// 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
// 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
// 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
// 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)
// 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
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
// 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)
// /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
// __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
// __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
- Beta (Week 1): 10% of users, Web Speech API only
- Staged (Week 2): 50% of users, collect feedback
- Full (Week 3): 100% of users
- 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
- Caching: Cache synthesized audio (reduce repeat costs)
- Compression: Use MP3 (smaller file sizes)
- Lazy Loading: Only synthesize on-demand
- Rate Limiting: Prevent abuse
- 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