vipan-kumar's picture
Initial commit: Audio Deepfake Detector with 8 detectors trained on jay15k
e6a1f55
Raw
History Blame Contribute Delete
8.31 kB
import { motion } from "framer-motion";
import {
ChevronDown,
ChevronUp,
AlertTriangle,
CheckCircle2,
Clock,
Cpu,
Beaker,
Sparkle,
} from "lucide-react";
import { useState } from "react";
import type { ModelResult } from "@/types/inference";
import {
classifyStatus,
cn,
confidenceColor,
formatMs,
formatPercent,
formatParams,
} from "@/lib/utils";
import ConfidenceGauge from "./ConfidenceGauge";
interface Props {
result: ModelResult;
}
const PAPER_IDS = new Set([
"nes2net",
"sonar",
"bicrossmamba_st",
"voiceradar",
"holi_antispoof",
]);
export default function ModelCard({ result }: Props) {
const [open, setOpen] = useState(false);
const isFake = result.prediction === "fake";
const status = classifyStatus(result.status);
const confColor = confidenceColor(result.confidence, result.prediction);
const isPaper = PAPER_IDS.has(result.model_id);
const ribbon = isPaper ? "bg-warn" : "bg-cyber";
const familyLabel = isPaper ? "paper architecture" : "production";
const FamilyIcon = isPaper ? Beaker : Sparkle;
return (
<motion.div
layout
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className={cn(
"panel relative flex h-full flex-col overflow-hidden p-4 pl-5",
isFake ? "shadow-danger" : "shadow-cyber"
)}
>
{/* Family ribbon along the left edge */}
<div className={cn("absolute left-0 top-0 h-full w-1.5", ribbon, "opacity-70")} />
{/* Soft accent glow */}
<div
className={cn(
"pointer-events-none absolute -right-16 -top-16 h-36 w-36 rounded-full blur-3xl",
isFake ? "bg-danger/20" : "bg-cyber/15"
)}
/>
{/* ── Header ────────────────────────────────────────────────── */}
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<span className="font-display text-base font-semibold text-ink truncate block">
{result.display_name}
</span>
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px] text-ink-dim">
<span
className={cn(
"inline-flex items-center gap-1 font-mono uppercase",
isPaper ? "text-warn" : "text-cyber"
)}
>
<FamilyIcon className="h-3 w-3" /> {familyLabel}
</span>
<span className="text-ink-mute">Β·</span>
<span className="truncate">{result.paradigm}</span>
</div>
</div>
<div
className={cn(
"shrink-0 rounded-md border px-2 py-1 font-mono text-xs font-bold",
isFake
? "border-danger/40 bg-danger/10 text-danger"
: "border-cyber/40 bg-cyber/10 text-cyber"
)}
>
{result.prediction.toUpperCase()}
</div>
</div>
{/* ── Status pill row ───────────────────────────────────────── */}
<div className="mt-2 flex flex-wrap items-center gap-1.5">
<span
className={cn(
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider",
status === "live" && "bg-cyber/15 text-cyber",
status === "fallback" && "bg-warn/15 text-warn",
status === "error" && "bg-danger/15 text-danger",
status === "loading" && "bg-surface-alt text-ink-dim"
)}
title={result.status}
>
{status === "live" && <CheckCircle2 className="h-2.5 w-2.5" />}
{status === "fallback" && <AlertTriangle className="h-2.5 w-2.5" />}
{status}
</span>
<span className="inline-flex items-center gap-1 rounded-full border border-line bg-surface-alt/60 px-2 py-0.5 font-mono text-[10px] text-ink-dim">
<Clock className="h-2.5 w-2.5" /> {formatMs(result.inference_time_ms)}
</span>
<span className="inline-flex items-center gap-1 rounded-full border border-line bg-surface-alt/60 px-2 py-0.5 font-mono text-[10px] text-ink-dim">
<Cpu className="h-2.5 w-2.5" /> {formatParams(result.params_k)}
</span>
</div>
{/* ── Gauge + numbers ──────────────────────────────────────── */}
<div className="mt-3 grid grid-cols-[auto,1fr] gap-4">
<ConfidenceGauge value={result.confidence} prediction={result.prediction} />
<div className="flex flex-col justify-center gap-1 font-mono text-[11px]">
<div className="flex items-baseline gap-2">
<span className="text-ink-mute">confidence</span>
<span className={cn("text-base font-bold", confColor)}>
{formatPercent(result.confidence)}
</span>
</div>
{result.eer_reference?.df21 != null && (
<div className="text-ink-dim">
DF21 EER:{" "}
<span className="text-ink">{result.eer_reference.df21}%</span>
</div>
)}
{result.eer_reference?.in_the_wild != null && (
<div className="text-ink-dim">
ITW EER:{" "}
<span className="text-ink">
{result.eer_reference.in_the_wild}%
</span>
</div>
)}
{result.eer_reference?.la21 != null && (
<div className="text-ink-dim">
LA21 EER:{" "}
<span className="text-ink">{result.eer_reference.la21}%</span>
</div>
)}
</div>
</div>
{/* Spacer pushes the details block to the bottom for visual alignment */}
<div className="flex-1" />
{/* ── Status detail strip ──────────────────────────────────── */}
{status === "fallback" && (
<div className="mt-3 flex items-start gap-2 rounded-md border border-warn/30 bg-warn/5 p-2 text-[11px] text-warn">
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0" />
<span>
No checkpoint β€” routed to{" "}
<span className="font-bold">{result.fallback_to ?? "fallback"}</span>.
Drop weights in{" "}
<code className="font-mono">backend/checkpoints/</code> to go live.
</span>
</div>
)}
{/* ── Why? expander ───────────────────────────────────────── */}
<button
onClick={() => setOpen((v) => !v)}
className="mt-3 inline-flex items-center gap-1 self-start font-mono text-[11px] text-ink-dim transition hover:text-cyber"
>
{open ? "hide details" : "details"}
{open ? (
<ChevronUp className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)}
</button>
{open && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
className="mt-2 overflow-hidden border-t border-line pt-2 text-[11px] leading-relaxed text-ink-dim"
>
{result.notes && (
<p className="mb-2 whitespace-normal">{result.notes}</p>
)}
<FeatureGrid features={result.features} />
</motion.div>
)}
</motion.div>
);
}
function FeatureGrid({ features }: { features: Record<string, unknown> }) {
const flat = Object.entries(features).filter(
([, v]) =>
typeof v === "number" || typeof v === "string" || typeof v === "boolean"
);
if (!flat.length) return null;
return (
<div className="grid grid-cols-2 gap-x-3 gap-y-1 font-mono text-[10px]">
{flat.slice(0, 12).map(([k, v]) => (
<div key={k} className="flex justify-between gap-2 truncate">
<span className="text-ink-mute truncate">{k}</span>
<span className="text-ink truncate">{String(v)}</span>
</div>
))}
</div>
);
}