/** * SttBenchmarkPage — Compare STT v1 (latest_long) vs v2 (Chirp 2). * SysAdmin only. Upload audio, run both engines, compare results. */ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { FaMicrophone, FaUpload, FaPlay, FaStop, FaSpinner } from 'react-icons/fa'; import { useLanguage } from '../../providers/language/LanguageContext'; import { useApiRequest } from '../../hooks/useApi'; import styles from '../admin/Admin.module.css'; interface ModelOption { value: string; label: string } interface BenchmarkResult { api: string; model: string; latencyMs: number; results: { transcript: string; confidence: number; words: number }[]; resultCount: number; location?: string; error?: string; } interface BenchmarkResponse { filename: string; fileSizeBytes: number; language: string; v1: BenchmarkResult | { error: string }; v2: BenchmarkResult | { error: string }; } interface ModelsResponse { v1Models: ModelOption[]; v2Models: ModelOption[]; locations: ModelOption[]; languages: ModelOption[]; } export const SttBenchmarkPage: React.FC = () => { const { t } = useLanguage(); const { request } = useApiRequest(); const [models, setModels] = useState(null); const [language, setLanguage] = useState('de-DE'); const [v1Model, setV1Model] = useState('latest_long'); const [v2Model, setV2Model] = useState('chirp_2'); const [v2Location, setV2Location] = useState('europe-west4'); const [running, setRunning] = useState(false); const [result, setResult] = useState(null); const [recording, setRecording] = useState(false); const [audioBlob, setAudioBlob] = useState(null); const [audioUrl, setAudioUrl] = useState(null); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); const fileInputRef = useRef(null); useEffect(() => { request({ url: '/api/admin/stt-benchmark/models', method: 'get' }) .then((data: any) => setModels(data)) .catch(() => {}); }, []); const _startRecording = useCallback(async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const recorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); chunksRef.current = []; recorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data); }; recorder.onstop = () => { const blob = new Blob(chunksRef.current, { type: 'audio/webm' }); setAudioBlob(blob); setAudioUrl(URL.createObjectURL(blob)); stream.getTracks().forEach(t => t.stop()); }; mediaRecorderRef.current = recorder; recorder.start(); setRecording(true); } catch (err) { console.error('Microphone access denied', err); } }, []); const _stopRecording = useCallback(() => { mediaRecorderRef.current?.stop(); setRecording(false); }, []); const _handleFileSelect = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setAudioBlob(file); setAudioUrl(URL.createObjectURL(file)); }, []); const _runBenchmark = useCallback(async () => { if (!audioBlob) return; setRunning(true); setResult(null); try { const formData = new FormData(); const filename = audioBlob instanceof File ? audioBlob.name : 'recording.webm'; formData.append('file', audioBlob, filename); formData.append('language', language); formData.append('v1Model', v1Model); formData.append('v2Model', v2Model); formData.append('v2Location', v2Location); const resp = await fetch('/api/admin/stt-benchmark/run', { method: 'POST', body: formData, credentials: 'include', }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data: BenchmarkResponse = await resp.json(); setResult(data); } catch (err: any) { console.error('Benchmark failed:', err); } finally { setRunning(false); } }, [audioBlob, language, v1Model, v2Model, v2Location]); const _renderResult = (label: string, r: BenchmarkResult | { error: string }) => { if ('error' in r && r.error) { return (

{label}

{r.error}

); } const res = r as BenchmarkResult; const topTranscript = res.results?.[0]?.transcript || '(no result)'; const topConfidence = res.results?.[0]?.confidence ?? 0; return (

{label}

{t('Modell')}: {res.model}
{t('Latenz')}: {res.latencyMs} ms
{t('Konfidenz')}: {(topConfidence * 100).toFixed(1)}%
{t('Alternativen')}: {res.results?.length || 0}
{res.location &&
{t('Region')}: {res.location}
}
{topTranscript}
{res.results?.length > 1 && (
{t('Weitere Alternativen')} {res.results.slice(1).map((alt, i) => (
[{(alt.confidence * 100).toFixed(1)}%] {alt.transcript}
))}
)}
); }; return (

{t('STT Benchmark')}

{t('Vergleiche Speech-to-Text v1 (latest_long) mit v2 (Chirp 2). Lade eine Audio-Datei hoch oder nimm direkt auf.')}

{!recording ? ( ) : ( )} {audioBlob && ( <> {audioBlob instanceof File ? audioBlob.name : 'recording.webm'} ({(audioBlob.size / 1024).toFixed(0)} KB) {audioUrl &&
{result && (

{t('Ergebnis')}

{result.filename} ({(result.fileSizeBytes / 1024).toFixed(0)} KB) — {result.language}

{_renderResult(`v1 — ${(result.v1 as any).model || v1Model}`, result.v1)} {_renderResult(`v2 — ${(result.v2 as any).model || v2Model}`, result.v2)}
)}
); }; export default SttBenchmarkPage;