feat(presenter): add browser TTS (Web Speech API) + fix German umlauts

- Integrate Web Speech API into usePresenterMode for text-to-speech
- Speech-driven paragraph advancement (falls back to timer if TTS unavailable)
- TTS toggle button (Volume2/VolumeX) in PresenterOverlay
- Chrome keepAlive workaround for long speeches
- Voice selection: prefers premium/neural voices, falls back to any matching lang
- Fix all German umlauts across presenter-script, presenter-faq, i18n, route.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-20 12:11:12 +01:00
parent 3a2567b44d
commit bcbceba31c
9 changed files with 370 additions and 218 deletions

View File

@@ -1,7 +1,7 @@
'use client'
import { motion, AnimatePresence } from 'framer-motion'
import { Play, Pause, Square, SkipForward } from 'lucide-react'
import { Play, Pause, Square, SkipForward, Volume2, VolumeX } from 'lucide-react'
import { Language } from '@/lib/types'
import { PresenterState } from '@/lib/presenter/types'
import { SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation'
@@ -18,6 +18,10 @@ interface PresenterOverlayProps {
onResume: () => void
onStop: () => void
onSkip: () => void
isSpeaking?: boolean
ttsAvailable?: boolean
ttsEnabled?: boolean
onToggleTts?: () => void
}
export default function PresenterOverlay({
@@ -31,6 +35,10 @@ export default function PresenterOverlay({
onResume,
onStop,
onSkip,
isSpeaking,
ttsAvailable,
ttsEnabled = true,
onToggleTts,
}: PresenterOverlayProps) {
const i = t(lang)
const slideName = i.slideNames[currentIndex] || SLIDE_ORDER[currentIndex] || ''
@@ -79,11 +87,32 @@ export default function PresenterOverlay({
{/* Controls */}
<div className="flex items-center gap-1.5">
{/* TTS toggle */}
{ttsAvailable && onToggleTts && (
<button
onClick={onToggleTts}
className={`w-7 h-7 rounded-full flex items-center justify-center transition-colors ${
ttsEnabled
? 'bg-indigo-500/30 hover:bg-indigo-500/50'
: 'bg-white/10 hover:bg-white/20'
}`}
title={lang === 'de'
? (ttsEnabled ? 'Stimme ausschalten' : 'Stimme einschalten')
: (ttsEnabled ? 'Mute voice' : 'Enable voice')}
>
{ttsEnabled ? (
<Volume2 className={`w-3.5 h-3.5 ${isSpeaking ? 'text-indigo-300' : 'text-white/60'}`} />
) : (
<VolumeX className="w-3.5 h-3.5 text-white/40" />
)}
</button>
)}
<button
onClick={onSkip}
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center
hover:bg-white/20 transition-colors"
title={lang === 'de' ? 'Naechste Folie' : 'Next slide'}
title={lang === 'de' ? 'Nächste Folie' : 'Next slide'}
>
<SkipForward className="w-3.5 h-3.5 text-white/60" />
</button>
@@ -138,7 +167,7 @@ export default function PresenterOverlay({
{/* State message */}
{state === 'paused' && (
<p className="text-xs text-yellow-400/60 mt-1">
{lang === 'de' ? 'Pausiert — stellen Sie eine Frage oder druecken Sie Play' : 'Paused — ask a question or press play'}
{lang === 'de' ? 'Pausiert — stellen Sie eine Frage oder drücken Sie Play' : 'Paused — ask a question or press play'}
</p>
)}
{state === 'answering' && (
@@ -148,7 +177,7 @@ export default function PresenterOverlay({
)}
{state === 'resuming' && (
<p className="text-xs text-indigo-400/60 mt-1">
{lang === 'de' ? 'Setze Praesentation fort...' : 'Resuming presentation...'}
{lang === 'de' ? 'Setze Präsentation fort...' : 'Resuming presentation...'}
</p>
)}
</div>