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

@@ -324,7 +324,7 @@ export default function ChatFAB({
bg-indigo-600 hover:bg-indigo-500 text-white
flex items-center justify-center shadow-lg shadow-indigo-600/30
transition-colors"
aria-label={lang === 'de' ? 'Investor Agent oeffnen' : 'Open Investor Agent'}
aria-label={lang === 'de' ? 'Investor Agent öffnen' : 'Open Investor Agent'}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />

View File

@@ -209,6 +209,10 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
onResume={presenter.resume}
onStop={presenter.stop}
onSkip={presenter.skipSlide}
isSpeaking={presenter.isSpeaking}
ttsAvailable={presenter.ttsAvailable}
ttsEnabled={presenter.ttsEnabled}
onToggleTts={() => presenter.setTtsEnabled(!presenter.ttsEnabled)}
/>
<AnimatePresence>

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>

View File

@@ -53,11 +53,11 @@ export default function IntroPresenterSlide({ lang, onStartPresenter, isPresenti
transition={{ delay: 0.3, duration: 0.5 }}
>
<h1 className="text-4xl md:text-5xl font-bold mb-3">
<GradientText>{isDE ? 'KI-Praesentator' : 'AI Presenter'}</GradientText>
<GradientText>{isDE ? 'KI-Präsentator' : 'AI Presenter'}</GradientText>
</h1>
<p className="text-lg text-white/60 max-w-lg mx-auto mb-8">
{isDE
? 'Ihr persoenlicher KI-Guide durch das BreakPilot ComplAI Pitch Deck. 15 Minuten, alle Fakten, jederzeit unterbrechbar.'
? 'Ihr persönlicher KI-Guide durch das BreakPilot ComplAI Pitch Deck. 15 Minuten, alle Fakten, jederzeit unterbrechbar.'
: 'Your personal AI guide through the BreakPilot ComplAI pitch deck. 15 minutes, all facts, interruptible at any time.'}
</p>
</motion.div>
@@ -79,12 +79,12 @@ export default function IntroPresenterSlide({ lang, onStartPresenter, isPresenti
{isPresenting ? (
<>
<Pause className="w-5 h-5" />
{isDE ? 'Praesentation laeuft...' : 'Presentation running...'}
{isDE ? 'Präsentation läuft...' : 'Presentation running...'}
</>
) : (
<>
<Play className="w-5 h-5" />
{isDE ? 'Praesentation starten' : 'Start Presentation'}
{isDE ? 'Präsentation starten' : 'Start Presentation'}
</>
)}
</span>
@@ -108,7 +108,7 @@ export default function IntroPresenterSlide({ lang, onStartPresenter, isPresenti
</div>
<div className="flex items-center gap-2">
<span className="px-1.5 py-0.5 bg-white/10 rounded text-xs font-mono">ESC</span>
<span>{isDE ? 'Slide-Uebersicht' : 'Slide Overview'}</span>
<span>{isDE ? 'Slide-Übersicht' : 'Slide Overview'}</span>
</div>
</motion.div>
</div>