import React, { useEffect, useRef, useState } from 'react'; import { FileAudio, Loader2, Wand2 } from 'lucide-react'; import { api } from '../../lib/api'; import { mediaBlob, mediaObjectURL, parseMediaContent } from '../../lib/media'; import { loadVoiceConfig } from '../../lib/voice'; interface AudioBlockProps { content: string; canEdit: boolean; onCaptionChange: (caption: string) => void; } // Audio block: player plus caption. With the own Whisper server the recording can be turned into text, // which becomes the caption (and so searchable). export const AudioBlock: React.FC = ({ content, canEdit, onCaptionChange }) => { const ref = parseMediaContent(content); const mediaId = ref?.id; const [src, setSrc] = useState(null); const [failed, setFailed] = useState(false); const [caption, setCaption] = useState(ref?.caption || ''); const [canTranscribe, setCanTranscribe] = useState(false); const [transcribing, setTranscribing] = useState(false); const [error, setError] = useState(null); const audioRef = useRef(null); // Recordings from Chrome (WebM) carry no length: jump to the end once so the browser measures it, // otherwise the player shows no total time and seeking does not work const fixDuration = () => { const a = audioRef.current; if (!a || Number.isFinite(a.duration)) return; const reset = () => { a.removeEventListener('timeupdate', reset); a.currentTime = 0; }; a.addEventListener('timeupdate', reset); a.currentTime = 1e101; }; useEffect(() => { setCaption(ref?.caption || ''); }, [ref?.caption]); useEffect(() => { if (!mediaId) return; let alive = true; setFailed(false); mediaObjectURL(mediaId) .then((u) => alive && setSrc(u)) .catch(() => alive && setFailed(true)); return () => { alive = false; }; }, [mediaId]); useEffect(() => { void loadVoiceConfig().then((cfg) => setCanTranscribe(cfg.mode === 'server')); }, []); if (!ref) { return
Audio-Verweis ist beschädigt: {content}
; } const commitCaption = (value: string) => { if (value.trim() !== ref.caption) onCaptionChange(value.trim()); }; const transcribe = async () => { if (!src) return; setTranscribing(true); setError(null); try { const blob = await mediaBlob(ref.id); const text = await api.transcribe(blob); if (text) { setCaption(text); commitCaption(text); } else { setError('Nichts erkannt'); } } catch (err: any) { setError(err?.message || 'Spracherkennung fehlgeschlagen'); } finally { setTranscribing(false); } }; return (
{failed ? (
Aufnahme nicht verfügbar (offline oder gelöscht)
) : src ? (
) : (
)} {canEdit ? ( setCaption(e.target.value)} onBlur={() => commitCaption(caption)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); (e.target as HTMLInputElement).blur(); } }} placeholder="Beschriftung hinzufügen…" className="w-full bg-transparent border-0 p-0 text-xs text-muted placeholder-faint focus:outline-none focus:ring-0" /> ) : ( ref.caption &&

{ref.caption}

)} {error &&

{error}

}
); };