Spaces:
Sleeping
Sleeping
Download ui/src/components/CompoundCard.tsx from metzgerdev/vocal-semantic-search: direct link, hf CLI and curl.
- Browser
- Download file 7.31 kB
-
https://huggingface.co/spaces/metzgerdev/vocal-semantic-search/resolve/main/ui/src/components/CompoundCard.tsx
- Command line
-
hf download hf://spaces/metzgerdev/vocal-semantic-search/ui/src/components/CompoundCard.tsx
-
curl -L -o CompoundCard.tsx https://huggingface.co/spaces/metzgerdev/vocal-semantic-search/resolve/main/ui/src/components/CompoundCard.tsx
7.31 kB
| import type { CompoundExample, Robustness, TermRow } from "../types"; | |
| const W = 660; | |
| const H = 266; | |
| // Left and bottom carry a tick column plus an axis title. | |
| const PAD = { l: 64, r: 16, t: 16, b: 58 }; | |
| const AXIS = "rgba(9,15,21,.55)"; | |
| /** Recall against description coverage. CLAP is flat; it never read the text. */ | |
| function RecallChart({ | |
| points, | |
| clap, | |
| chance, | |
| }: { | |
| points: { setting: number; names: boolean; recall: number }[]; | |
| clap: number; | |
| chance: number; | |
| }) { | |
| const max = Math.max(clap, ...points.map((p) => p.recall)) * 1.3; | |
| const x = (i: number) => PAD.l + (i * (W - PAD.l - PAD.r)) / (points.length - 1); | |
| const y = (v: number) => H - PAD.b - (v / max) * (H - PAD.t - PAD.b); | |
| const bm = points.map((p, i) => `${i ? "L" : "M"}${x(i)},${y(p.recall)}`).join(" "); | |
| return ( | |
| <figure style={{ margin: "18px 0 0" }}> | |
| <svg viewBox={`0 0 ${W} ${H}`} className="scatter" role="img" | |
| aria-label="recall against description coverage"> | |
| {[0, 0.25, 0.5, 0.75, 1].map((f) => ( | |
| <g key={f}> | |
| <line x1={PAD.l} x2={W - PAD.r} y1={y(max * f)} y2={y(max * f)} | |
| stroke="#ebe8e4" strokeWidth={1} /> | |
| <text x={PAD.l - 8} y={y(max * f) + 3} textAnchor="end" fontSize="10" | |
| fontFamily="Geist Mono, monospace" fill="rgba(9,15,21,.35)"> | |
| {(max * f).toFixed(2)} | |
| </text> | |
| </g> | |
| ))} | |
| <line x1={PAD.l} x2={W - PAD.r} y1={y(chance)} y2={y(chance)} | |
| stroke="rgba(9,15,21,.3)" strokeWidth={1} strokeDasharray="4 4" /> | |
| <text x={W - PAD.r} y={y(chance) - 5} textAnchor="end" fontSize="10" | |
| fontFamily="Inter, sans-serif" fill="rgba(9,15,21,.4)">chance</text> | |
| <path d={`M${x(0)},${y(clap)} L${x(points.length - 1)},${y(clap)}`} | |
| stroke="#090f15" strokeWidth={2.5} fill="none" /> | |
| <path d={bm} stroke="#fa675b" strokeWidth={2.5} fill="none" /> | |
| {points.map((p, i) => ( | |
| <circle key={i} cx={x(i)} cy={y(p.recall)} r={4} fill="#fa675b" /> | |
| ))} | |
| <circle cx={x(0)} cy={y(clap)} r={4} fill="#090f15" /> | |
| <circle cx={x(points.length - 1)} cy={y(clap)} r={4} fill="#090f15" /> | |
| {points.map((p, i) => ( | |
| <text key={`${p.setting}-${p.names}`} x={x(i)} y={H - PAD.b + 20} | |
| textAnchor="middle" fontSize="11" fontFamily="Inter, sans-serif" | |
| fill="rgba(9,15,21,.5)"> | |
| {Math.round(p.setting * 100)}%{p.names ? "" : " β"} | |
| </text> | |
| ))} | |
| <text x={(PAD.l + W - PAD.r) / 2} y={H - 8} textAnchor="middle" fontSize="11" | |
| fontFamily="Inter, sans-serif" fill={AXIS}> | |
| description metadata remaining | |
| </text> | |
| <text transform={`translate(14 ${(PAD.t + H - PAD.b) / 2}) rotate(-90)`} | |
| textAnchor="middle" fontSize="11" fontFamily="Inter, sans-serif" | |
| fill={AXIS}> | |
| recall β share of the 205 matches found | |
| </text> | |
| </svg> | |
| <figcaption className="legend" style={{ marginTop: 10 }}> | |
| <span style={{ width: "100%", color: "var(--ink-50)" }}> | |
| β marks the stages where names are deleted too, not just descriptions | |
| </span> | |
| <span> | |
| <i className="swatch" style={{ background: "#090f15" }} /> | |
| CLAP | |
| </span> | |
| <span> | |
| <i className="swatch" style={{ background: "#fa675b" }} /> | |
| BM25 | |
| </span> | |
| </figcaption> | |
| </figure> | |
| ); | |
| } | |
| /** P@10 per query term, against the prevalence of the label it asks for. */ | |
| function Terms({ rows }: { rows: TermRow[] }) { | |
| return ( | |
| <div className="tablewrap"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>query term</th> | |
| <th>ground-truth</th> | |
| <th>CLAP</th> | |
| <th>BM25</th> | |
| <th>prevalence</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {rows.map((r) => { | |
| const joint = r.truth === "conjunction"; | |
| return ( | |
| <tr key={r.term} | |
| style={{ opacity: r.truth ? 1 : 0.5, | |
| fontWeight: joint ? 600 : undefined, | |
| borderTop: joint ? "1px solid var(--line)" : undefined }}> | |
| <td>{r.term}</td> | |
| <td className={r.truth ? "mono" : undefined}> | |
| {r.truth ?? "no label"} | |
| </td> | |
| <td className="num"> | |
| {r.clap === undefined ? "β" : r.clap.toFixed(2)} | |
| </td> | |
| <td className="num"> | |
| {r.bm25 === undefined ? "β" : r.bm25.toFixed(2)} | |
| </td> | |
| <td className="num"> | |
| {r.prevalence === undefined ? "β" : r.prevalence.toFixed(3)} | |
| </td> | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| ); | |
| } | |
| function Examples({ rows }: { rows: CompoundExample[] }) { | |
| return ( | |
| <div className="tablewrap"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th></th> | |
| <th>voice</th> | |
| <th>gender</th> | |
| <th>age</th> | |
| <th>use case</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {rows.map((e) => ( | |
| <tr key={e.name} style={{ opacity: e.match ? 1 : 0.55 }}> | |
| <td className={`lift ${e.match ? "up" : "down"}`}>{e.match ? "β" : "β"}</td> | |
| <td>{e.name}</td> | |
| <td>{e.gender}</td> | |
| <td>{(e.age ?? "").replace(/_/g, " ")}</td> | |
| <td>{(e.use_case ?? "").replace(/_/g, " ")}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| ); | |
| } | |
| export function CompoundCard({ data }: { data: Robustness["compound"] }) { | |
| if (!data) return null; | |
| return ( | |
| <div className="card"> | |
| <h2>Comparison with BM25 under metadata ablation</h2> | |
| <p className="sub"> | |
| Vector search (CLAP) is compared against text-based BM25 search. Query: β{data.query}β. | |
| BM25 degrades as descriptions are deleted first, then names. | |
| </p> | |
| <h2 style={{ marginTop: 26 }}>Precision@10, term by term</h2> | |
| <p className="sub"> | |
| The conjunction is{" "} | |
| {(data.clap.p10 / data.chance).toFixed(1)}Γ chance. | |
| </p> | |
| <Terms rows={data.terms} /> | |
| <h2 style={{ marginTop: 26 }}>Recall as metadata is deleted</h2> | |
| <RecallChart points={data.coverage} clap={data.clap.recall} chance={data.chance} /> | |
| <p className="note"> | |
| Vector search is stable even when metadata is sparse. | |
| </p> | |
| <h2 style={{ marginTop: 26 }}>What each returns</h2> | |
| <p className="sub">CLAP</p> | |
| <Examples rows={data.examples.clap} /> | |
| <p className="sub" style={{ marginTop: 18 }}>BM25, descriptions deleted</p> | |
| <Examples rows={data.examples.bm25_names} /> | |
| <p className="note"> | |
| With descriptions gone, BM25 returns voices with <em>narrator</em> or{" "} | |
| <em>audiobook</em> in their title. | |
| </p> | |
| <p className="sub" style={{ marginTop: 18 }}> | |
| BM25, descriptions and names deleted | |
| </p> | |
| <Examples rows={data.examples.bm25_nothing} /> | |
| <p className="note"> | |
| Nothing left to match on: every score is zero. | |
| </p> | |
| </div> | |
| ); | |
| } | |