Download index.html from KarthikKB1998/CARD-Audio-Captioning: direct link, hf CLI and curl.
- Browser
- Download file 30.6 kB
-
https://huggingface.co/spaces/KarthikKB1998/CARD-Audio-Captioning/resolve/main/index.html
- Command line
-
hf download hf://spaces/KarthikKB1998/CARD-Audio-Captioning/index.html
-
curl -L -o index.html https://huggingface.co/spaces/KarthikKB1998/CARD-Audio-Captioning/resolve/main/index.html
30.6 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>CARD Audio Captioning</title> | |
| <meta name="description" content="Demo of CARD, an encoder-free audio captioning model (IEEE SLT 2026). Listen to a clip and compare the model's caption with human captions."> | |
| <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="431.72 529.91 95.94 95.94" role="img" aria-label="CARD"%3E%3Ctitle%3ECARD%3C/title%3E%3Cg fill="none" stroke-linecap="round" stroke-width="11.99"%3E%3Cpath stroke="%23D07A22" d="M443.72 565.89L443.72 589.87"/%3E%3Cpath stroke="%234543C4" d="M467.7 541.9L467.7 613.86 M491.69 553.9L515.67 553.9 M491.69 577.88L509.67 577.88 M491.69 601.87L497.68 601.87"/%3E%3C/g%3E%3C/svg%3E"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg: #f5f6fb; | |
| --surface: #ffffff; | |
| --surface-2: #f2f4fa; | |
| --text: #0f172a; | |
| --logo-amber: #D07A22; --logo-indigo: #4543C4; | |
| --muted: #5b6478; | |
| --border: #e2e6f0; | |
| --accent: #6366f1; | |
| --accent-2: #8b5cf6; | |
| --accent-3: #ec4899; | |
| --accent-soft: rgba(99, 102, 241, .10); | |
| --accent-line: rgba(99, 102, 241, .45); | |
| --mark: rgba(139, 92, 246, .20); | |
| --wave: #c7cbe0; | |
| --shadow: 0 1px 2px rgba(15, 23, 42, .04), 0 14px 36px rgba(15, 23, 42, .06); | |
| --hero-glow-1: rgba(99, 102, 241, .20); | |
| --hero-glow-2: rgba(236, 72, 153, .12); | |
| color-scheme: light; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root:not([data-theme="light"]) { | |
| --bg: #0a0e1c; --surface: #111729; --surface-2: #161d33; --text: #e7eaf3; --muted: #9aa3b9; | |
| --border: #252d46; --accent-soft: rgba(99, 102, 241, .16); --accent-line: rgba(129, 140, 248, .55); | |
| --logo-amber: #F2A650; --logo-indigo: #A3A6FF; | |
| --mark: rgba(167, 139, 250, .28); --wave: #3a4363; | |
| --shadow: 0 1px 2px rgba(0, 0, 0, .35), 0 14px 36px rgba(0, 0, 0, .35); | |
| --hero-glow-1: rgba(99, 102, 241, .28); --hero-glow-2: rgba(236, 72, 153, .16); | |
| color-scheme: dark; | |
| } | |
| } | |
| :root[data-theme="dark"] { | |
| --bg: #0a0e1c; --surface: #111729; --surface-2: #161d33; --text: #e7eaf3; --muted: #9aa3b9; | |
| --border: #252d46; --accent-soft: rgba(99, 102, 241, .16); --accent-line: rgba(129, 140, 248, .55); | |
| --logo-amber: #F2A650; --logo-indigo: #A3A6FF; | |
| --mark: rgba(167, 139, 250, .28); --wave: #3a4363; | |
| --shadow: 0 1px 2px rgba(0, 0, 0, .35), 0 14px 36px rgba(0, 0, 0, .35); | |
| --hero-glow-1: rgba(99, 102, 241, .28); --hero-glow-2: rgba(236, 72, 153, .16); | |
| color-scheme: dark; | |
| } | |
| * { box-sizing: border-box; } | |
| html { -webkit-text-size-adjust: 100%; } | |
| body { | |
| margin: 0; background: var(--bg); color: var(--text); | |
| font: 15px/1.55 Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| a { color: var(--accent); } | |
| button { font: inherit; color: inherit; } | |
| .wrap { max-width: 1120px; margin: 0 auto; padding: 28px 16px 40px; } | |
| .card { background: var(--surface); border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow); } | |
| /* hero */ | |
| .hero { | |
| position: relative; overflow: hidden; border-radius: 24px; padding: 34px 34px 30px; | |
| border: 1px solid var(--border); box-shadow: var(--shadow); | |
| background: | |
| radial-gradient(720px 280px at 0% 0%, var(--hero-glow-1), transparent 70%), | |
| radial-gradient(620px 260px at 100% 0%, var(--hero-glow-2), transparent 70%), | |
| var(--surface); | |
| } | |
| .hero-top { display: flex; justify-content: space-between; align-items: center; gap: 12px; } | |
| .eyebrow { | |
| display: inline-block; font-size: .7rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; | |
| padding: 4px 10px; border-radius: 999px; color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-line); | |
| } | |
| .theme-btn { | |
| display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 999px; cursor: pointer; | |
| border: 1px solid var(--border); background: var(--surface); color: var(--muted); font-size: .78rem; | |
| } | |
| .theme-btn svg { width: 15px; height: 15px; } | |
| h1 { margin: 16px 0 8px; font-size: clamp(2rem, 5vw, 2.8rem); line-height: 1.05; font-weight: 800; letter-spacing: -.025em; } | |
| .mark { height: .8em; width: auto; margin-right: .3em; vertical-align: -.02em; } | |
| h1 span { background: linear-gradient(90deg, var(--accent), var(--accent-2) 55%, var(--accent-3)); -webkit-background-clip: text; background-clip: text; color: transparent; } | |
| .hero p { max-width: 760px; margin: 0 0 20px; color: var(--muted); font-size: 1.02rem; } | |
| .links { display: flex; flex-wrap: wrap; gap: 10px; } | |
| .pill { | |
| display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px; border-radius: 999px; font-weight: 600; font-size: .9rem; | |
| text-decoration: none; color: var(--text); border: 1px solid var(--border); background: var(--surface); | |
| transition: transform .15s ease, box-shadow .15s ease; | |
| } | |
| .pill svg { width: 16px; height: 16px; } | |
| .pill:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(99, 102, 241, .18); } | |
| .pill.primary { color: #fff; border-color: transparent; background: linear-gradient(90deg, var(--accent), var(--accent-2)); } | |
| .notice { margin: 16px 0 0; padding: 11px 14px; border-radius: 14px; font-size: .9rem; border: 1px solid var(--accent-line); background: var(--accent-soft); } | |
| .notice b { font-weight: 600; } | |
| /* demo */ | |
| .demo { display: grid; grid-template-columns: 5fr 6fr; gap: 20px; margin-top: 26px; align-items: start; } | |
| .section-k { font-size: .74rem; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--muted); margin: 0 2px 10px; } | |
| .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; } | |
| .tile { | |
| position: relative; text-align: left; padding: 14px 14px 14px 42px; border-radius: 14px; cursor: pointer; font-weight: 600; | |
| border: 1px solid var(--border); background: var(--surface); transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; | |
| } | |
| .tile::before { content: ""; position: absolute; left: 14px; top: 50%; width: 16px; height: 16px; margin-top: -8px; border-radius: 50%; border: 2px solid var(--border); } | |
| .tile:hover { transform: translateY(-1px); border-color: var(--accent-line); } | |
| .tile[aria-checked="true"] { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 3px rgba(99, 102, 241, .16); } | |
| .tile[aria-checked="true"]::before { border-color: var(--accent); background: radial-gradient(circle, var(--accent) 45%, transparent 50%); } | |
| .tile:focus-visible, .play:focus-visible, .cta:focus-visible, .theme-btn:focus-visible, canvas:focus-visible { outline: 3px solid var(--accent-line); outline-offset: 2px; } | |
| .player { margin-top: 12px; padding: 14px 16px; } | |
| .player-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } | |
| .player-name { font-weight: 700; } | |
| .player-time { font-family: "JetBrains Mono", ui-monospace, monospace; font-size: .8rem; color: var(--muted); } | |
| .wave-row { display: flex; align-items: center; gap: 12px; } | |
| .play { | |
| flex: 0 0 44px; width: 44px; height: 44px; border-radius: 50%; border: 0; cursor: pointer; color: #fff; | |
| display: grid; place-items: center; background: linear-gradient(135deg, var(--accent), var(--accent-2)); | |
| box-shadow: 0 6px 16px rgba(99, 102, 241, .35); | |
| } | |
| .play svg { width: 18px; height: 18px; } | |
| .play:disabled { opacity: .5; cursor: default; } | |
| canvas { flex: 1; width: 100%; height: 64px; cursor: pointer; border-radius: 8px; } | |
| audio.fallback { width: 100%; margin-top: 8px; } | |
| .cta { | |
| width: 100%; margin-top: 14px; padding: 15px; border: 0; border-radius: 14px; cursor: pointer; color: #fff; font-weight: 700; font-size: 1.02rem; | |
| background: linear-gradient(90deg, var(--accent), var(--accent-2)); box-shadow: 0 10px 24px rgba(99, 102, 241, .28); | |
| transition: transform .15s ease, filter .15s ease; | |
| } | |
| .cta:hover { transform: translateY(-1px); filter: brightness(1.05); } | |
| .cta:disabled { opacity: .55; cursor: default; transform: none; } | |
| .result { position: relative; padding: 22px 24px; min-height: 360px; display: flex; flex-direction: column; } | |
| .result::before { content: ""; position: absolute; left: 0; top: 20px; bottom: 20px; width: 4px; border-radius: 0 4px 4px 0; background: linear-gradient(180deg, var(--accent), var(--accent-3)); } | |
| .k { font-size: .72rem; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--muted); } | |
| .result-name { font-size: 1.35rem; font-weight: 700; margin-top: 6px; } | |
| .src { margin-top: 10px; font-size: .8rem; color: var(--muted); } | |
| .empty-hint { margin-top: auto; padding: 18px; border-radius: 14px; text-align: center; color: var(--muted); border: 1px dashed var(--border); } | |
| .caption { font-size: 1.5rem; line-height: 1.35; font-weight: 700; letter-spacing: -.01em; margin: 8px 0 12px; } | |
| mark { background: var(--mark); color: inherit; border-radius: 5px; padding: 0 4px; } | |
| .chips { display: flex; flex-wrap: wrap; gap: 6px; } | |
| .chip { font-size: .75rem; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); background: var(--surface-2); } | |
| .refs { margin: 8px 0 0; padding-left: 20px; } | |
| .refs li { margin: 4px 0; } | |
| .legend { margin-top: 12px; font-size: .78rem; color: var(--muted); } | |
| .reveal { animation: reveal .35s ease both; } | |
| @keyframes reveal { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } | |
| @media (prefers-reduced-motion: reduce) { .reveal { animation: none; } .tile, .pill, .cta { transition: none; } } | |
| /* how it works */ | |
| .how { margin-top: 30px; } | |
| .pipeline { padding: 18px 20px; } | |
| .steps { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } | |
| .step { flex: 1 1 130px; padding: 10px 12px; border-radius: 12px; border: 1px solid var(--border); background: var(--surface-2); } | |
| .step.accent { border-color: var(--accent-line); background: var(--accent-soft); } | |
| .step-k { font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); } | |
| .step-v { font-weight: 700; margin-top: 2px; } | |
| .arrow { width: 18px; height: 18px; flex: 0 0 18px; color: var(--accent-2); } | |
| .teacher { margin-top: 12px; padding: 10px 12px; border-radius: 12px; font-size: .88rem; color: var(--muted); border: 1px dashed var(--border); } | |
| .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 12px; } | |
| .stat { padding: 16px; } | |
| .stat-v { font-size: 1.75rem; font-weight: 800; letter-spacing: -.02em; background: linear-gradient(90deg, var(--accent), var(--accent-2)); -webkit-background-clip: text; background-clip: text; color: transparent; } | |
| .stat-k { font-size: .82rem; color: var(--muted); margin-top: 2px; line-height: 1.35; } | |
| .note { font-size: .75rem; color: var(--muted); margin: 8px 2px 0; } | |
| details { margin-top: 20px; padding: 0; overflow: hidden; } | |
| summary { cursor: pointer; padding: 14px 18px; font-weight: 600; list-style: none; display: flex; justify-content: space-between; align-items: center; } | |
| summary::-webkit-details-marker { display: none; } | |
| summary::after { content: "+"; font-size: 1.2rem; color: var(--muted); } | |
| details[open] summary::after { content: "-"; } | |
| .bib { position: relative; margin: 0 18px 18px; } | |
| pre { margin: 0; padding: 14px; border-radius: 12px; overflow-x: auto; background: var(--surface-2); border: 1px solid var(--border); font: .8rem/1.5 "JetBrains Mono", ui-monospace, monospace; } | |
| .copy { position: absolute; top: 8px; right: 8px; padding: 4px 10px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); cursor: pointer; font-size: .75rem; } | |
| footer { margin-top: 26px; text-align: center; font-size: .78rem; color: var(--muted); } | |
| @media (max-width: 820px) { | |
| .demo { grid-template-columns: 1fr; } | |
| .stats { grid-template-columns: repeat(2, 1fr); } | |
| .result { min-height: 0; } | |
| } | |
| @media (max-width: 560px) { | |
| .hero { padding: 24px 18px; } | |
| .steps { flex-direction: column; align-items: stretch; } | |
| .step { flex: 0 0 auto; } | |
| .arrow { align-self: center; transform: rotate(90deg); } | |
| .caption { font-size: 1.3rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main class="wrap"> | |
| <header class="hero"> | |
| <div class="hero-top"> | |
| <span class="eyebrow">IEEE SLT 2026</span> | |
| <button class="theme-btn" id="theme-btn" type="button" aria-label="Switch color theme"> | |
| <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a9 9 0 1 0 9 9 7 7 0 0 1-9-9Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg> | |
| <span id="theme-label">Auto</span> | |
| </button> | |
| </div> | |
| <h1><svg class="mark" viewBox="110.74 292.69 144.28 90.69" aria-hidden="true"><g fill="none" stroke-linecap="round" stroke-width="12.37"><path style="stroke: var(--logo-amber)" d="M116.93 323.61V352.46M139.6 314.33V361.74"/><path style="stroke: var(--logo-indigo)" d="M162.27 306.09V369.98M184.94 298.87V377.2M209.68 316.39H248.84M209.68 338.03H238.54M209.68 359.68H228.23"/></g></svg>CARD <span>audio captioning</span></h1> | |
| <p>Cross-component Audio Representation Distillation for Encoder-Free Audio Captioning. Listen to a clip, | |
| reveal the model's caption, and compare it with what human annotators wrote.</p> | |
| <nav class="links" aria-label="Project links"> | |
| <a class="pill primary" href="https://huggingface.co/KarthikKB1998/CARD-Qwen3-4B-AudioCaps" target="_blank" rel="noopener">Model on the Hub</a> | |
| <a class="pill" href="https://colab.research.google.com/github/KarthikKolluriKB/CARD/blob/main/notebooks/CARD_demo.ipynb" target="_blank" rel="noopener"> | |
| <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>Run it live in Colab</a> | |
| <a class="pill" href="https://github.com/KarthikKolluriKB/CARD" target="_blank" rel="noopener">Code on GitHub</a> | |
| </nav> | |
| </header> | |
| <div class="notice" role="note"> | |
| <b>How this page works:</b> the captions below were generated beforehand by the released model | |
| <code>CARD-Qwen3-4B-AudioCaps</code> with the paper's decoding settings, and are identical to its live output | |
| for these clips. To caption your own audio, run the model live in Colab. | |
| </div> | |
| <section class="demo" aria-label="Demo"> | |
| <div> | |
| <div class="section-k" id="pick-label">1. Choose a clip</div> | |
| <div class="tiles" id="tiles" role="radiogroup" aria-labelledby="pick-label"></div> | |
| <div class="card player" id="player"> | |
| <div class="player-head"> | |
| <span class="player-name" id="player-name">No clip selected</span> | |
| <span class="player-time" id="player-time">0:00 / 0:00</span> | |
| </div> | |
| <div class="wave-row"> | |
| <button class="play" id="play" type="button" aria-label="Play" disabled> | |
| <svg id="play-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg> | |
| </button> | |
| <canvas id="wave" tabindex="0" aria-label="Waveform. Click to seek; left and right arrow keys skip."></canvas> | |
| </div> | |
| </div> | |
| <button class="cta" id="reveal" type="button" disabled>Show CARD caption</button> | |
| </div> | |
| <div> | |
| <div class="section-k">2. Compare with the human captions</div> | |
| <div class="card result" id="result" aria-live="polite"></div> | |
| </div> | |
| </section> | |
| <section class="how" aria-label="How CARD works"> | |
| <div class="section-k">How CARD works</div> | |
| <div class="card pipeline"> | |
| <div class="steps" id="steps"></div> | |
| <div class="teacher">Training only: a frozen CLAP-HTSAT teacher is distilled by component, its early stages into the | |
| projector and its later stages into the language model. The teacher is removed, so there is no audio encoder at inference.</div> | |
| </div> | |
| <div class="stats"> | |
| <div class="card stat"><div class="stat-v">55.4</div><div class="stat-k">CIDEr-D on AudioCaps</div></div> | |
| <div class="card stat"><div class="stat-v">13.2 M</div><div class="stat-k">audio interface parameters, 34.4 M with the encoder</div></div> | |
| <div class="card stat"><div class="stat-v">0.4 ms</div><div class="stat-k">audio interface latency, 11.1 ms with the encoder</div></div> | |
| <div class="card stat"><div class="stat-v">0</div><div class="stat-k">audio encoders at inference</div></div> | |
| </div> | |
| <p class="note">Numbers from the paper (Table II and Section IV-B).</p> | |
| </section> | |
| <details class="card"> | |
| <summary>Cite this work</summary> | |
| <div class="bib"> | |
| <button class="copy" id="copy" type="button">Copy</button> | |
| <pre id="bibtex">@inproceedings{kolluri2026card, | |
| title = {{CARD}: Cross-component Audio Representation Distillation for Encoder-Free Audio Captioning}, | |
| author = {Kolluri, Ganesh Pavan Kartikeya Bharadwaj and Zhang, Yuchen and Kampouridis, Michael and Shekhar, Ravi}, | |
| booktitle = {Proceedings of the IEEE Spoken Language Technology Workshop (SLT)}, | |
| year = {2026} | |
| }</pre> | |
| </div> | |
| </details> | |
| <footer>Model and code released under Apache-2.0. Each sample clip keeps its original source and license, shown with its caption.</footer> | |
| </main> | |
| <script> | |
| (function () { | |
| "use strict"; | |
| var STOPWORDS = new Set(["a", "an", "the", "and", "or", "is", "are", "be", "being", "in", "on", "of", "with", "as", "to", | |
| "by", "while", "then", "some", "it", "its", "at", "from", "for", "into", "followed", "there", "this", "that", | |
| "several", "multiple", "times", "time"]); | |
| var ARROW = '<svg class="arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h13m-5-6 6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'; | |
| var PLAY = '<path d="M8 5v14l11-7z" fill="currentColor"/>'; | |
| var PAUSE = '<path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor"/>'; | |
| var $ = function (id) { return document.getElementById(id); }; | |
| var state = { samples: [], index: -1, revealed: false, peaks: null, loadToken: 0 }; | |
| var audio = new Audio(); | |
| audio.preload = "auto"; | |
| var decodeCtx = null; | |
| /* ---------- theme ---------- */ | |
| var THEMES = ["auto", "light", "dark"]; | |
| function applyTheme(t) { | |
| if (t === "auto") document.documentElement.removeAttribute("data-theme"); | |
| else document.documentElement.setAttribute("data-theme", t); | |
| $("theme-label").textContent = t.charAt(0).toUpperCase() + t.slice(1); | |
| requestAnimationFrame(drawWave); | |
| } | |
| var theme = "auto"; | |
| try { theme = localStorage.getItem("card-theme") || "auto"; } catch (e) {} | |
| if (THEMES.indexOf(theme) < 0) theme = "auto"; | |
| applyTheme(theme); | |
| $("theme-btn").addEventListener("click", function () { | |
| theme = THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length]; | |
| try { localStorage.setItem("card-theme", theme); } catch (e) {} | |
| applyTheme(theme); | |
| }); | |
| if (window.matchMedia) { | |
| var mq = window.matchMedia("(prefers-color-scheme: dark)"); | |
| if (mq.addEventListener) mq.addEventListener("change", function () { requestAnimationFrame(drawWave); }); | |
| } | |
| /* ---------- pipeline ---------- */ | |
| var steps = [["Input", "Audio clip", false], ["Front end, no parameters", "Log-Mel spectrogram", false], | |
| ["13.2 M parameters", "Audio projector", true], ["LoRA merged", "Qwen3-4B", true], ["Output", "Caption", false]]; | |
| $("steps").innerHTML = steps.map(function (s, i) { | |
| return (i ? ARROW : "") + '<div class="step' + (s[2] ? " accent" : "") + '"><div class="step-k">' + s[0] + | |
| '</div><div class="step-v">' + s[1] + "</div></div>"; | |
| }).join(""); | |
| /* ---------- helpers ---------- */ | |
| function el(tag, cls, text) { | |
| var n = document.createElement(tag); | |
| if (cls) n.className = cls; | |
| if (text != null) n.textContent = text; | |
| return n; | |
| } | |
| function fmt(t) { | |
| if (!isFinite(t) || t < 0) t = 0; | |
| var m = Math.floor(t / 60), s = Math.floor(t % 60); | |
| return m + ":" + (s < 10 ? "0" : "") + s; | |
| } | |
| function stem(w) { | |
| w = w.toLowerCase(); | |
| return (w.length > 3 && /s$/.test(w) && !/ss$/.test(w)) ? w.slice(0, -1) : w; | |
| } | |
| function highlighted(caption, refs) { | |
| var refWords = new Set(); | |
| refs.forEach(function (r) { (r.match(/[A-Za-z]+/g) || []).forEach(function (w) { refWords.add(stem(w)); }); }); | |
| var frag = document.createDocumentFragment(); | |
| caption.split(/([A-Za-z]+)/).forEach(function (part) { | |
| if (!part) return; | |
| if (/^[A-Za-z]+$/.test(part) && !STOPWORDS.has(part.toLowerCase()) && refWords.has(stem(part))) { | |
| frag.appendChild(el("mark", null, part)); | |
| } else { | |
| frag.appendChild(document.createTextNode(part)); | |
| } | |
| }); | |
| return frag; | |
| } | |
| function sourceLine(s) { | |
| var line = el("div", "src"); | |
| var bits = []; | |
| if (s.dataset) bits.push(document.createTextNode(s.dataset)); | |
| if (s.source) { | |
| if (/^https?:\/\//.test(s.source)) { | |
| var a = el("a", null, "source"); | |
| a.href = s.source; a.target = "_blank"; a.rel = "noopener"; | |
| bits.push(a); | |
| } else { | |
| bits.push(document.createTextNode(s.source)); | |
| } | |
| } | |
| if (s.license) bits.push(document.createTextNode("license: " + s.license)); | |
| bits.forEach(function (b, i) { | |
| if (i) line.appendChild(document.createTextNode(" \u00b7 ")); | |
| line.appendChild(b); | |
| }); | |
| return line; | |
| } | |
| /* ---------- result card ---------- */ | |
| function renderPlaceholder() { | |
| var box = $("result"); | |
| box.textContent = ""; | |
| var s = state.samples[state.index]; | |
| box.appendChild(el("div", "k", "Selected clip")); | |
| box.appendChild(el("div", "result-name", s ? s.name : "No clip selected")); | |
| if (s) box.appendChild(sourceLine(s)); | |
| var hint = el("div", "empty-hint"); | |
| if (s) { | |
| hint.appendChild(document.createTextNode("Press ")); | |
| hint.appendChild(el("b", null, "Show CARD caption")); | |
| hint.appendChild(document.createTextNode(" to see what the model wrote for this clip.")); | |
| } else { | |
| hint.textContent = state.samples.length ? "Choose a clip to begin." : "Sample clips are not installed yet."; | |
| } | |
| box.appendChild(hint); | |
| } | |
| function renderResult() { | |
| var s = state.samples[state.index]; | |
| if (!s) return; | |
| var refs = s.references || []; | |
| var box = $("result"); | |
| box.textContent = ""; | |
| var inner = el("div", "reveal"); | |
| inner.appendChild(el("div", "k", "CARD caption")); | |
| var cap = el("div", "caption"); | |
| cap.appendChild(highlighted(s.caption || s.expected_caption || "", refs)); | |
| inner.appendChild(cap); | |
| var chips = el("div", "chips"); | |
| ["Released model", "Beam search, 4 beams", "Generated beforehand"].forEach(function (c) { chips.appendChild(el("span", "chip", c)); }); | |
| inner.appendChild(chips); | |
| var rk = el("div", "k", "Human reference captions"); | |
| rk.style.marginTop = "18px"; | |
| inner.appendChild(rk); | |
| var ol = el("ol", "refs"); | |
| (refs.length ? refs : ["No reference captions."]).forEach(function (r) { ol.appendChild(el("li", null, r)); }); | |
| inner.appendChild(ol); | |
| var legend = el("div", "legend"); | |
| legend.appendChild(el("mark", null, "highlighted")); | |
| legend.appendChild(document.createTextNode(" words also appear in a human reference.")); | |
| inner.appendChild(legend); | |
| var src = sourceLine(s); | |
| src.insertBefore(document.createTextNode(s.name + " \u00b7 "), src.firstChild); | |
| inner.appendChild(src); | |
| box.appendChild(inner); | |
| } | |
| /* ---------- tiles ---------- */ | |
| function renderTiles() { | |
| var wrap = $("tiles"); | |
| wrap.textContent = ""; | |
| state.samples.forEach(function (s, i) { | |
| var b = el("button", "tile", s.name); | |
| b.type = "button"; | |
| b.setAttribute("role", "radio"); | |
| b.setAttribute("aria-checked", i === state.index ? "true" : "false"); | |
| b.tabIndex = i === state.index ? 0 : -1; | |
| b.addEventListener("click", function () { select(i); }); | |
| b.addEventListener("keydown", function (e) { | |
| var d = (e.key === "ArrowRight" || e.key === "ArrowDown") ? 1 : (e.key === "ArrowLeft" || e.key === "ArrowUp") ? -1 : 0; | |
| if (!d) return; | |
| e.preventDefault(); | |
| var n = (i + d + state.samples.length) % state.samples.length; | |
| select(n); | |
| wrap.children[n].focus(); | |
| }); | |
| wrap.appendChild(b); | |
| }); | |
| } | |
| function select(i) { | |
| if (i === state.index) return; | |
| state.index = i; | |
| state.revealed = false; | |
| renderTiles(); | |
| renderPlaceholder(); | |
| loadClip(state.samples[i]); | |
| $("reveal").disabled = false; | |
| } | |
| /* ---------- player ---------- */ | |
| function setPlayIcon(playing) { | |
| $("play-icon").innerHTML = playing ? PAUSE : PLAY; | |
| $("play").setAttribute("aria-label", playing ? "Pause" : "Play"); | |
| } | |
| function updateTime() { | |
| $("player-time").textContent = fmt(audio.currentTime) + " / " + fmt(audio.duration); | |
| } | |
| function loadClip(s) { | |
| audio.pause(); | |
| setPlayIcon(false); | |
| audio.src = s.url; | |
| audio.load(); | |
| $("player-name").textContent = s.name; | |
| $("play").disabled = false; | |
| updateTime(); | |
| state.peaks = null; | |
| drawWave(); | |
| var token = ++state.loadToken; | |
| fetch(s.url).then(function (r) { | |
| if (!r.ok) throw new Error("HTTP " + r.status); | |
| return r.arrayBuffer(); | |
| }).then(function (buf) { | |
| var Ctx = window.AudioContext || window.webkitAudioContext; | |
| if (!Ctx) throw new Error("no Web Audio"); | |
| decodeCtx = decodeCtx || new Ctx(); | |
| return new Promise(function (res, rej) { | |
| var p = decodeCtx.decodeAudioData(buf, res, rej); | |
| if (p && p.then) p.then(res, rej); | |
| }); | |
| }).then(function (decoded) { | |
| if (token !== state.loadToken) return; | |
| state.peaks = peaks(decoded, 400); | |
| drawWave(); | |
| }).catch(function () { | |
| if (token === state.loadToken) { state.peaks = "flat"; drawWave(); } | |
| }); | |
| } | |
| function peaks(buf, n) { | |
| var ch = buf.getChannelData(0), step = Math.max(1, Math.floor(ch.length / n)), out = new Float32Array(n), max = 0; | |
| for (var i = 0; i < n; i++) { | |
| var m = 0, end = Math.min(ch.length, (i + 1) * step); | |
| for (var j = i * step; j < end; j += 8) { var v = Math.abs(ch[j]); if (v > m) m = v; } | |
| out[i] = m; if (m > max) max = m; | |
| } | |
| if (max > 0) for (var k = 0; k < n; k++) out[k] = out[k] / max; | |
| return out; | |
| } | |
| function drawWave() { | |
| var c = $("wave"), dpr = window.devicePixelRatio || 1; | |
| var w = c.clientWidth, h = c.clientHeight; | |
| if (!w || !h) return; | |
| c.width = Math.round(w * dpr); c.height = Math.round(h * dpr); | |
| var g = c.getContext("2d"); | |
| g.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| g.clearRect(0, 0, w, h); | |
| var css = getComputedStyle(document.documentElement); | |
| var base = css.getPropertyValue("--wave").trim() || "#c7cbe0"; | |
| var grad = g.createLinearGradient(0, 0, w, 0); | |
| grad.addColorStop(0, css.getPropertyValue("--accent").trim() || "#6366f1"); | |
| grad.addColorStop(1, css.getPropertyValue("--accent-3").trim() || "#ec4899"); | |
| var bar = 3, gap = 2, count = Math.max(1, Math.floor(w / (bar + gap))); | |
| var progress = (audio.duration && isFinite(audio.duration)) ? audio.currentTime / audio.duration : 0; | |
| for (var i = 0; i < count; i++) { | |
| var v; | |
| if (state.peaks && state.peaks !== "flat") { | |
| v = state.peaks[Math.floor(i / count * state.peaks.length)]; | |
| } else { | |
| v = state.index < 0 ? 0.06 : 0.12 + 0.05 * Math.sin(i * 0.6); | |
| } | |
| var bh = Math.max(3, v * (h - 6)); | |
| var x = i * (bar + gap), y = (h - bh) / 2; | |
| g.fillStyle = (i / count) < progress ? grad : base; | |
| if (g.roundRect) { g.beginPath(); g.roundRect(x, y, bar, bh, 1.5); g.fill(); } else { g.fillRect(x, y, bar, bh); } | |
| } | |
| } | |
| var raf = 0; | |
| function tick() { drawWave(); updateTime(); if (!audio.paused) raf = requestAnimationFrame(tick); } | |
| audio.addEventListener("play", function () { setPlayIcon(true); cancelAnimationFrame(raf); raf = requestAnimationFrame(tick); }); | |
| audio.addEventListener("pause", function () { setPlayIcon(false); drawWave(); updateTime(); }); | |
| audio.addEventListener("ended", function () { setPlayIcon(false); drawWave(); updateTime(); }); | |
| audio.addEventListener("loadedmetadata", updateTime); | |
| audio.addEventListener("error", function () { | |
| if (state.index >= 0) $("player-time").textContent = "audio unavailable"; | |
| }); | |
| $("play").addEventListener("click", function () { | |
| if (audio.paused) { var p = audio.play(); if (p && p.catch) p.catch(function () {}); } else { audio.pause(); } | |
| }); | |
| function seekTo(ratio) { | |
| if (!audio.duration || !isFinite(audio.duration)) return; | |
| audio.currentTime = Math.min(Math.max(ratio, 0), 1) * audio.duration; | |
| drawWave(); updateTime(); | |
| } | |
| $("wave").addEventListener("pointerdown", function (e) { | |
| var r = e.currentTarget.getBoundingClientRect(); | |
| seekTo((e.clientX - r.left) / r.width); | |
| }); | |
| $("wave").addEventListener("keydown", function (e) { | |
| if (e.key === "ArrowRight" || e.key === "ArrowLeft") { | |
| e.preventDefault(); | |
| audio.currentTime = Math.max(0, audio.currentTime + (e.key === "ArrowRight" ? 1 : -1)); | |
| drawWave(); updateTime(); | |
| } else if (e.key === " " || e.key === "Enter") { | |
| e.preventDefault(); $("play").click(); | |
| } | |
| }); | |
| if (window.ResizeObserver) new ResizeObserver(drawWave).observe($("wave")); | |
| else window.addEventListener("resize", drawWave); | |
| /* ---------- reveal + copy ---------- */ | |
| $("reveal").addEventListener("click", function () { | |
| if (state.index < 0) return; | |
| state.revealed = true; | |
| renderResult(); | |
| }); | |
| $("copy").addEventListener("click", function () { | |
| var btn = this, text = $("bibtex").textContent; | |
| var done = function () { btn.textContent = "Copied"; setTimeout(function () { btn.textContent = "Copy"; }, 1500); }; | |
| try { navigator.clipboard.writeText(text).then(done, function () {}); } catch (e) {} | |
| }); | |
| /* ---------- boot ---------- */ | |
| renderPlaceholder(); | |
| drawWave(); | |
| fetch("samples/samples.json", { cache: "no-cache" }).then(function (r) { | |
| if (!r.ok) throw new Error("HTTP " + r.status); | |
| return r.json(); | |
| }).then(function (data) { | |
| state.samples = (data.samples || []).filter(function (s) { return s.file && (s.caption || s.expected_caption); }) | |
| .map(function (s) { return Object.assign({}, s, { url: "samples/" + encodeURIComponent(s.file) }); }); | |
| renderTiles(); | |
| if (state.samples.length) select(0); else renderPlaceholder(); | |
| }).catch(function () { | |
| renderPlaceholder(); | |
| }); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |