Spaces:
Paused
Paused
| import React, { useRef, useState } from "react"; | |
| import Loader from "./Loader"; | |
| const SILENCE_THRESHOLD = 0.01; | |
| const SILENCE_DURATION = 1500; | |
| export default function VoiceBot() { | |
| const mediaRecorderRef = useRef(null); | |
| const audioChunksRef = useRef([]); | |
| const analyserRef = useRef(null); | |
| const audioContextRef = useRef(null); | |
| const silenceTimerRef = useRef(null); | |
| const streamRef = useRef(null); | |
| const listeningRef = useRef(false); | |
| const [status, setStatus] = useState("idle"); | |
| // idle | listening | thinking | speaking | |
| // ========================= | |
| // START RECORDING | |
| // ========================= | |
| const startRecording = async () => { | |
| setStatus("listening"); | |
| listeningRef.current = true; | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| streamRef.current = stream; | |
| const recorder = new MediaRecorder(stream); | |
| mediaRecorderRef.current = recorder; | |
| audioChunksRef.current = []; | |
| recorder.ondataavailable = (e) => { | |
| audioChunksRef.current.push(e.data); | |
| }; | |
| recorder.onstop = handleStop; | |
| recorder.start(); | |
| setupSilenceDetection(stream); | |
| }; | |
| // ========================= | |
| // SILENCE DETECTION | |
| // ========================= | |
| const setupSilenceDetection = (stream) => { | |
| audioContextRef.current = new AudioContext(); | |
| const source = audioContextRef.current.createMediaStreamSource(stream); | |
| analyserRef.current = audioContextRef.current.createAnalyser(); | |
| analyserRef.current.fftSize = 2048; | |
| source.connect(analyserRef.current); | |
| const data = new Uint8Array(analyserRef.current.fftSize); | |
| const checkSilence = () => { | |
| analyserRef.current.getByteTimeDomainData(data); | |
| let sum = 0; | |
| for (let i = 0; i < data.length; i++) { | |
| const v = (data[i] - 128) / 128; | |
| sum += v * v; | |
| } | |
| const volume = Math.sqrt(sum / data.length); | |
| if (volume < SILENCE_THRESHOLD) { | |
| if (!silenceTimerRef.current) { | |
| silenceTimerRef.current = setTimeout(() => { | |
| stopRecording(); | |
| }, SILENCE_DURATION); | |
| } | |
| } else { | |
| clearTimeout(silenceTimerRef.current); | |
| silenceTimerRef.current = null; | |
| } | |
| // ✅ FIX: use ref, not React state | |
| if (listeningRef.current) { | |
| requestAnimationFrame(checkSilence); | |
| } | |
| }; | |
| checkSilence(); | |
| }; | |
| // ========================= | |
| // STOP RECORDING | |
| // ========================= | |
| const stopRecording = () => { | |
| listeningRef.current = false; | |
| if (silenceTimerRef.current) { | |
| clearTimeout(silenceTimerRef.current); | |
| silenceTimerRef.current = null; | |
| } | |
| if (mediaRecorderRef.current?.state !== "inactive") { | |
| mediaRecorderRef.current.stop(); | |
| } | |
| streamRef.current?.getTracks().forEach(t => t.stop()); | |
| if ( | |
| audioContextRef.current && | |
| audioContextRef.current.state !== "closed" | |
| ) { | |
| audioContextRef.current.close(); | |
| } | |
| }; | |
| // ========================= | |
| // SEND TO BACKEND | |
| // ========================= | |
| const handleStop = async () => { | |
| setStatus("thinking"); | |
| const blob = new Blob(audioChunksRef.current, { | |
| type: "audio/wav", | |
| }); | |
| const formData = new FormData(); | |
| formData.append("audio", blob); | |
| const response = await fetch( | |
| "https://durga-7780-voicebot.hf.space/api/process-audio", | |
| { | |
| method: "POST", | |
| body: formData, | |
| } | |
| ); | |
| const data = await response.json(); | |
| if (!response.ok || !data.audio_url) { | |
| console.error("Backend error:", data); | |
| setStatus("idle"); | |
| return; | |
| } | |
| playBotAudio(data.audio_url); | |
| }; | |
| // ========================= | |
| // PLAY BOT AUDIO | |
| // ========================= | |
| const playBotAudio = (url) => { | |
| setStatus("speaking"); | |
| const audio = new Audio(url + "?t=" + Date.now()); | |
| audio.onended = () => setStatus("idle"); | |
| audio.play(); | |
| }; | |
| // ========================= | |
| // UI | |
| // ========================= | |
| return ( | |
| <div style={styles.container}> | |
| {status === "idle" ? ( | |
| <button onClick={startRecording} style={styles.mic}> | |
| 🎤 | |
| </button> | |
| ) : ( | |
| <Loader /> | |
| )} | |
| </div> | |
| ); | |
| } | |
| const styles = { | |
| container: { | |
| height: "100vh", | |
| background: "#111", | |
| display: "flex", | |
| justifyContent: "center", | |
| alignItems: "center", | |
| }, | |
| mic: { | |
| width: 120, | |
| height: 120, | |
| borderRadius: "50%", | |
| fontSize: 40, | |
| background: "#4CAF50", | |
| color: "#fff", | |
| border: "none", | |
| cursor: "pointer", | |
| }, | |
| }; | |