import { useCallback, useEffect, useRef, useState } from 'react'; import { useWhisper } from './whisper'; /** * Aufnahme fuer das Diktat. Die Erkennung selbst macht der Whisper-Worker im * Browser (siehe whisper.tsx) - hier geht es nur um Mikrofon, Dauer und das * Weiterreichen der Aufnahme. */ /** Erstes Format, das der Browser aufnehmen kann. */ function pickMimeType(): string { const candidates = ['audio/webm', 'audio/ogg', 'audio/mp4']; for (const type of candidates) { if (MediaRecorder.isTypeSupported?.(type)) return type; } return ''; } type RecorderState = 'idle' | 'recording' | 'transcribing'; interface UseRecorderResult { state: RecorderState; /** Laufende Aufnahmedauer in Sekunden. */ seconds: number; start: () => Promise; stop: () => void; cancel: () => void; } export function useRecorder( onText: (text: string) => void, onError: (message: string) => void ): UseRecorderResult { const { transcribe, status } = useWhisper(); const [state, setState] = useState('idle'); const [seconds, setSeconds] = useState(0); const recorderRef = useRef(null); const chunksRef = useRef([]); const streamRef = useRef(null); const timerRef = useRef(null); const discardRef = useRef(false); const cleanup = useCallback(() => { if (timerRef.current !== null) { window.clearInterval(timerRef.current); timerRef.current = null; } // Mikrofon freigeben, sonst bleibt die Aufnahmeanzeige des Browsers stehen. streamRef.current?.getTracks().forEach((track) => track.stop()); streamRef.current = null; recorderRef.current = null; chunksRef.current = []; setSeconds(0); }, []); useEffect(() => cleanup, [cleanup]); const start = useCallback(async () => { if (state !== 'idle') return; if (status !== 'ready') { onError('Das Spracherkennungsmodell ist noch nicht geladen.'); return; } discardRef.current = false; let stream: MediaStream; try { stream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (err) { const name = (err as DOMException)?.name; onError( name === 'NotAllowedError' ? 'Kein Zugriff aufs Mikrofon. Bitte im Browser erlauben.' : name === 'NotFoundError' ? 'Es wurde kein Mikrofon gefunden.' : 'Die Aufnahme konnte nicht gestartet werden.' ); return; } const mimeType = pickMimeType(); let recorder: MediaRecorder; try { recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined); } catch { stream.getTracks().forEach((track) => track.stop()); onError('Dieser Browser kann kein passendes Audioformat aufnehmen.'); return; } streamRef.current = stream; recorderRef.current = recorder; chunksRef.current = []; recorder.ondataavailable = (event) => { if (event.data.size > 0) chunksRef.current.push(event.data); }; recorder.onstop = async () => { const blob = new Blob(chunksRef.current, { type: recorder.mimeType || 'audio/webm' }); cleanup(); if (discardRef.current) { setState('idle'); return; } if (blob.size < 1024) { setState('idle'); onError('Die Aufnahme war zu kurz.'); return; } setState('transcribing'); try { const { text } = await transcribe(blob); if (text) onText(text); else onError('Es war nichts Verstaendliches zu hoeren.'); } catch (err) { onError(err instanceof Error ? err.message : 'Die Erkennung ist fehlgeschlagen.'); } finally { setState('idle'); } }; recorder.start(); setState('recording'); setSeconds(0); timerRef.current = window.setInterval(() => setSeconds((value) => value + 1), 1000); }, [state, status, cleanup, onError, onText, transcribe]); const stop = useCallback(() => { if (recorderRef.current?.state === 'recording') recorderRef.current.stop(); }, []); const cancel = useCallback(() => { discardRef.current = true; stop(); }, [stop]); return { state, seconds, start, stop, cancel }; }