Files
breakpilot-lehrer/studio-v2/components/learn/AudioButton.tsx
Benjamin Admin a30f10a467
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 43s
CI / test-go-edu-search (push) Successful in 35s
CI / test-python-klausur (push) Failing after 2m30s
CI / test-python-agent-core (push) Successful in 22s
CI / test-nodejs-website (push) Successful in 26s
Widen AudioButton lang prop to string for multi-language support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 16:19:45 +02:00

94 lines
3.4 KiB
TypeScript

'use client'
import React, { useCallback, useRef, useState } from 'react'
interface AudioButtonProps {
text: string
lang: string
isDark: boolean
size?: 'sm' | 'md' | 'lg'
}
/**
* AudioButton — plays TTS audio for a word or phrase.
*
* Priority: Piper TTS (Thorsten DE / Lessac EN) via backend API.
* Fallback: Browser Web Speech API if Piper is unavailable.
*/
export function AudioButton({ text, lang, isDark, size = 'md' }: AudioButtonProps) {
const [isSpeaking, setIsSpeaking] = useState(false)
const audioRef = useRef<HTMLAudioElement | null>(null)
const speak = useCallback(async () => {
// Stop if already playing
if (isSpeaking) {
audioRef.current?.pause()
window.speechSynthesis?.cancel()
setIsSpeaking(false)
return
}
setIsSpeaking(true)
// Try Piper TTS via backend API first
try {
const url = `/api/vocabulary/tts?text=${encodeURIComponent(text)}&lang=${lang}`
const resp = await fetch(url)
if (resp.ok && resp.headers.get('content-type')?.startsWith('audio')) {
const blob = await resp.blob()
const audioUrl = URL.createObjectURL(blob)
const audio = new Audio(audioUrl)
audioRef.current = audio
audio.onended = () => { setIsSpeaking(false); URL.revokeObjectURL(audioUrl) }
audio.onerror = () => { setIsSpeaking(false); URL.revokeObjectURL(audioUrl) }
await audio.play()
return
}
} catch {
// Piper unavailable — fall through to Web Speech API
}
// Fallback: Browser Web Speech API
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(text)
utterance.lang = lang === 'de' ? 'de-DE' : 'en-GB'
utterance.rate = 0.9
const voices = window.speechSynthesis.getVoices()
const preferred = voices.find((v) =>
v.lang.startsWith(lang === 'de' ? 'de' : 'en') && v.localService
) || voices.find((v) => v.lang.startsWith(lang === 'de' ? 'de' : 'en'))
if (preferred) utterance.voice = preferred
utterance.onend = () => setIsSpeaking(false)
utterance.onerror = () => setIsSpeaking(false)
window.speechSynthesis.speak(utterance)
} else {
setIsSpeaking(false)
}
}, [text, lang, isSpeaking])
const sizeClasses = { sm: 'w-7 h-7', md: 'w-9 h-9', lg: 'w-11 h-11' }
const iconSizes = { sm: 'w-3.5 h-3.5', md: 'w-4 h-4', lg: 'w-5 h-5' }
return (
<button
onClick={speak}
className={`${sizeClasses[size]} rounded-full flex items-center justify-center transition-all ${
isSpeaking
? 'bg-blue-500 text-white animate-pulse'
: isDark
? 'bg-white/10 text-white/60 hover:bg-white/20 hover:text-white'
: 'bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700'
}`}
title={isSpeaking ? 'Stop' : `${lang === 'de' ? 'Deutsch' : 'Englisch'} vorlesen`}
>
<svg className={iconSizes[size]} fill="none" stroke="currentColor" viewBox="0 0 24 24">
{isSpeaking ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0zM10 9v6m4-6v6" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
)}
</svg>
</button>
)
}