/** * useRecording — microphone recording with auto-cleanup via the backend. * * Extracted from App.jsx to reduce its useState/useRef count. */ import { useState, useRef } from 'react'; import { toast } from 'react-hot-toast'; import { cleanAudio as apiCleanAudio } from '../api/system'; export default function useRecording(ingestRefAudio) { const [isRecording, setIsRecording] = useState(false); const [isCleaning, setIsCleaning] = useState(false); const [recordingTime, setRecordingTime] = useState(0); const mediaRecorderRef = useRef(null); const recordingChunksRef = useRef([]); const recordingTimerRef = useRef(null); const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); mediaRecorderRef.current = mediaRecorder; recordingChunksRef.current = []; setRecordingTime(0); mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) recordingChunksRef.current.push(e.data); }; mediaRecorder.onstop = async () => { clearInterval(recordingTimerRef.current); stream.getTracks().forEach(t => t.stop()); const blob = new Blob(recordingChunksRef.current, { type: 'audio/webm' }); if (blob.size < 1000) { toast.error("Recording too short"); return; } // Send to backend for denoising setIsCleaning(true); try { const formData = new FormData(); formData.append("audio", blob, "recording.webm"); const res = await apiCleanAudio(formData); const cleanBlob = await res.blob(); const cleanFilename = res.headers.get("X-Clean-Filename") || "recording_clean.wav"; const cleanFile = new File([cleanBlob], cleanFilename, { type: "audio/wav" }); await ingestRefAudio(cleanFile); toast.success("🎙️ Recording cleaned & loaded!"); } catch (e) { // Fallback: use raw recording without denoising const rawFile = new File([blob], "recording.webm", { type: "audio/webm" }); await ingestRefAudio(rawFile); toast.success("Recording loaded (raw — denoising unavailable)"); } finally { setIsCleaning(false); } }; mediaRecorder.start(250); // Collect chunks every 250ms setIsRecording(true); // Timer const st = Date.now(); recordingTimerRef.current = setInterval(() => { setRecordingTime(((Date.now() - st) / 1000).toFixed(1)); }, 100); } catch (e) { toast.error("Microphone access denied"); } }; const stopRecording = () => { if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { mediaRecorderRef.current.stop(); } setIsRecording(false); }; return { isRecording, isCleaning, recordingTime, startRecording, stopRecording, }; }