vipan-kumar's picture
Initial commit: Audio Deepfake Detector with 8 detectors trained on jay15k
e6a1f55
Raw
History Blame Contribute Delete
4.75 kB
import { useEffect, useState } from "react";
import { listSamples, sampleDownloadUrl } from "@/lib/api";
import type { SampleAudio } from "@/types/inference";
import { useInferenceStore } from "@/store/inferenceStore";
import { cn } from "@/lib/utils";
import { CheckCircle2, Music2 } from "lucide-react";
export default function SampleLibrary() {
const [samples, setSamples] = useState<SampleAudio[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const audio = useInferenceStore((s) => s.audio);
const setAudio = useInferenceStore((s) => s.setAudio);
useEffect(() => {
let alive = true;
listSamples()
.then((r) => {
if (alive) setSamples(r.samples);
})
.catch((e) => {
if (alive)
setError(
e instanceof Error ? e.message : "Could not load sample list."
);
})
.finally(() => alive && setLoading(false));
return () => {
alive = false;
};
}, []);
if (loading) {
return (
<div className="font-mono text-xs text-ink-dim">loading samples…</div>
);
}
if (error) {
return <div className="font-mono text-xs text-danger">{error}</div>;
}
const reals = samples.filter((s) => s.label === "real");
const fakes = samples.filter((s) => s.label === "fake");
return (
<div>
<div className="mb-3 flex items-center justify-between">
<div className="label">sample library</div>
<span className="font-mono text-[10px] text-ink-mute">
{samples.length} clips
</span>
</div>
<SampleGroup title="real" tint="cyber" items={reals} audio={audio} setAudio={setAudio} />
<div className="my-3 border-t border-line/60" />
<SampleGroup title="fake" tint="danger" items={fakes} audio={audio} setAudio={setAudio} />
<div className="mt-3 font-mono text-[10px] text-ink-mute">
Tip: drop your own real-world WAVs into{" "}
<code className="font-mono">data/sample_audios/</code> to add them here.
</div>
</div>
);
}
function SampleGroup({
title,
tint,
items,
audio,
setAudio,
}: {
title: string;
tint: "cyber" | "danger";
items: SampleAudio[];
audio: ReturnType<typeof useInferenceStore.getState>["audio"];
setAudio: ReturnType<typeof useInferenceStore.getState>["setAudio"];
}) {
return (
<div>
<div className="mb-1.5 flex items-center gap-1.5">
<span
className={cn(
"h-1.5 w-1.5 rounded-full",
tint === "danger" ? "bg-danger" : "bg-cyber"
)}
/>
<span className="font-mono text-[10px] uppercase tracking-wider text-ink-dim">
{title} · {items.length}
</span>
</div>
<div className="grid gap-1.5">
{items.map((s) => {
const isSelected =
audio?.kind === "sample" && audio.sampleId === s.sample_id;
return (
<button
key={s.sample_id}
onClick={() =>
setAudio({
kind: "sample",
sampleId: s.sample_id,
url: sampleDownloadUrl(s.sample_id),
})
}
className={cn(
"group flex items-start justify-between gap-2 rounded-md border bg-surface-alt/50 px-2.5 py-2 text-left transition-all duration-150",
"hover:bg-surface-alt hover:translate-x-0.5",
isSelected
? tint === "danger"
? "border-danger/60 bg-danger/10"
: "border-cyber/60 bg-cyber/10"
: "border-line"
)}
>
<div className="flex min-w-0 items-start gap-2">
<Music2
className={cn(
"mt-0.5 h-3.5 w-3.5 shrink-0 transition-transform group-hover:scale-110",
tint === "danger" ? "text-danger" : "text-cyber"
)}
/>
<div className="min-w-0">
<div className="truncate font-mono text-[11px] text-ink">
{s.filename.replace(/\.wav$/, "")}
</div>
<div className="mt-0.5 truncate text-[10px] text-ink-dim">
{s.description}
</div>
</div>
</div>
{isSelected && (
<CheckCircle2
className={cn(
"h-3.5 w-3.5 shrink-0",
tint === "danger" ? "text-danger" : "text-cyber"
)}
/>
)}
</button>
);
})}
</div>
</div>
);
}