vipan-kumar's picture
Initial commit: Audio Deepfake Detector with 8 detectors trained on jay15k
e6a1f55
Raw
History Blame Contribute Delete
3.8 kB
import { Mic, Square } from "lucide-react";
import { useCallback, useEffect } from "react";
import { useAudioRecorder } from "@/hooks/useAudioRecorder";
import { useInferenceStore } from "@/store/inferenceStore";
/** Compact recorder. Playback + delete live in <NowPlaying />, so this
* component only captures audio. UI state is derived from BOTH the
* recorder hook AND the global store so the "recorded" indicator survives
* tab switches that unmount this component. */
export default function MicRecorder() {
const audio = useInferenceStore((s) => s.audio);
const setAudio = useInferenceStore((s) => s.setAudio);
const {
state,
start: startInternal,
stop,
reset,
maxDurationMs,
} = useAudioRecorder(15_000);
const seconds = (state.durationMs / 1000).toFixed(1);
// Push a fresh recording into the global audio source as soon as it lands.
useEffect(() => {
if (state.blob && state.objectUrl) {
setAudio({
kind: "recording",
blob: state.blob,
objectUrl: state.objectUrl,
});
}
}, [state.blob, state.objectUrl, setAudio]);
// If the user picks a different audio source elsewhere, free the recorder.
useEffect(() => {
if (audio?.kind !== "recording" && state.blob) {
reset();
}
}, [audio, state.blob, reset]);
// Wrap start: when the user re-records, immediately clear the previous
// recording from the store so NowPlaying doesn't keep showing a stale
// (and now revoked) blob URL while the new recording is in progress.
const start = useCallback(async () => {
if (audio?.kind === "recording") {
setAudio(null);
}
await startInternal();
}, [audio, setAudio, startInternal]);
// The "recorded" indicator is true whenever either:
// • this component holds a freshly-stopped blob, OR
// • the global store already holds a recording (we may have just
// remounted via a tab switch and lost local state).
const hasRecording = state.blob != null || audio?.kind === "recording";
return (
<div className="panel p-4">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="label">microphone</div>
<div className="mt-1 truncate font-mono text-sm">
{state.isRecording ? (
<span className="text-danger">● rec {seconds} s</span>
) : hasRecording ? (
<span className="text-cyber">
● recorded {state.blob ? `${seconds} s · ` : ""}
play above
</span>
) : (
<span className="text-ink-dim">
15 s max · grants browser mic permission
</span>
)}
</div>
</div>
{state.isRecording ? (
<button onClick={stop} className="btn-danger">
<Square className="h-3.5 w-3.5" /> Stop
</button>
) : (
<button onClick={start} className="btn-primary">
<Mic className="h-3.5 w-3.5" />
{hasRecording ? "Re-record" : "Record"}
</button>
)}
</div>
{/* Recording progress bar (only meaningful while recording) */}
<div className="mt-3 h-1 overflow-hidden rounded-full bg-surface-alt">
<div
className={`h-full transition-all ${
state.isRecording ? "bg-danger/70" : "bg-cyber/40"
}`}
style={{
width: state.isRecording
? `${(state.durationMs / maxDurationMs) * 100}%`
: hasRecording
? "100%"
: "0%",
}}
/>
</div>
{state.error && (
<div className="mt-2 font-mono text-xs text-danger">{state.error}</div>
)}
</div>
);
}