Spaces:
Running
Running
davanstrien HF Staff
Add semantic search (EmbeddingGemma embeddings config, mode=semantic + meaning toggle)
3fa46ca verified | <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Apollo 11 — Air-to-Ground Voice Transcription (Machine)</title> | |
| <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=Courier+Prime:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Sans:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --paper: #FBFAF6; | |
| --ink: #1C1B18; | |
| --pencil: #8A857C; | |
| --stamp: #C8332A; | |
| --nasa-blue: #0B3D91; | |
| --mark: #FFE86B; | |
| --rule: #DCD8CE; | |
| } | |
| * { box-sizing: border-box; } | |
| html { background: #E9E6DE; } | |
| body { | |
| margin: 0; | |
| font-family: "IBM Plex Sans", system-ui, sans-serif; | |
| color: var(--ink); | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| .page { | |
| max-width: 820px; | |
| margin: 24px auto 80px; | |
| background: var(--paper); | |
| padding: 48px 56px 64px; | |
| box-shadow: 0 1px 3px rgba(28,27,24,.18), 0 12px 40px rgba(28,27,24,.10); | |
| } | |
| @media (max-width: 700px) { .page { margin: 0; padding: 28px 20px 48px; } } | |
| .rubric { | |
| font-family: "Courier Prime", monospace; | |
| font-size: 12px; | |
| letter-spacing: .08em; | |
| color: var(--pencil); | |
| display: flex; justify-content: space-between; gap: 12px; | |
| border-bottom: 2px solid var(--ink); | |
| padding-bottom: 8px; | |
| text-transform: uppercase; | |
| } | |
| header { position: relative; margin-bottom: 30px; } | |
| h1 { | |
| font-family: "Courier Prime", monospace; | |
| font-weight: 700; | |
| font-size: clamp(20px, 4vw, 30px); | |
| line-height: 1.25; | |
| text-transform: uppercase; | |
| letter-spacing: .02em; | |
| margin: 22px 0 6px; | |
| max-width: 24ch; | |
| } | |
| .subtitle { | |
| font-family: "Courier Prime", monospace; | |
| font-size: 13px; | |
| color: var(--pencil); | |
| text-transform: uppercase; | |
| letter-spacing: .06em; | |
| margin: 0 0 18px; | |
| } | |
| .stamp { | |
| position: absolute; right: 0; top: 34px; | |
| transform: rotate(-6deg); | |
| font-family: "Courier Prime", monospace; | |
| font-weight: 700; | |
| font-size: 13px; | |
| letter-spacing: .12em; | |
| text-transform: uppercase; | |
| color: var(--stamp); | |
| border: 3px double var(--stamp); | |
| border-radius: 4px; | |
| padding: 6px 10px; | |
| opacity: .85; | |
| pointer-events: none; | |
| text-align: center; | |
| } | |
| @media (max-width: 700px) { .stamp { position: static; display: inline-block; margin-bottom: 10px; } } | |
| .meta { | |
| font-family: "Courier Prime", monospace; | |
| font-size: 13px; | |
| border-top: 1px solid var(--rule); | |
| border-bottom: 1px solid var(--rule); | |
| padding: 10px 0; | |
| display: flex; flex-wrap: wrap; gap: 6px 28px; | |
| } | |
| .meta b { font-weight: 700; } | |
| .meta a { color: var(--nasa-blue); text-decoration: none; } | |
| .meta a:hover, .meta a:focus { text-decoration: underline; } | |
| form { margin: 26px 0 8px; display: flex; gap: 10px; flex-wrap: wrap; } | |
| label.subject { | |
| font-family: "Courier Prime", monospace; | |
| font-weight: 700; font-size: 14px; text-transform: uppercase; | |
| align-self: center; | |
| } | |
| input[type="search"] { | |
| flex: 1; min-width: 200px; | |
| font-family: "Courier Prime", monospace; | |
| font-size: 16px; | |
| padding: 10px 12px; | |
| color: var(--ink); | |
| background: transparent; | |
| border: none; | |
| border-bottom: 2px solid var(--ink); | |
| outline-offset: 4px; | |
| } | |
| input[type="search"]:focus-visible, select:focus-visible, .row:focus-visible { | |
| outline: 2px solid var(--nasa-blue); | |
| } | |
| select { | |
| font-family: "Courier Prime", monospace; | |
| font-size: 14px; | |
| padding: 8px; | |
| background: transparent; | |
| border: 1px solid var(--rule); | |
| color: var(--ink); | |
| max-width: 180px; | |
| } | |
| .count { | |
| font-family: "Courier Prime", monospace; | |
| font-size: 13px; color: var(--pencil); | |
| text-transform: uppercase; letter-spacing: .05em; | |
| margin: 14px 0 10px; | |
| } | |
| .results { border-top: 2px solid var(--ink); } | |
| .row { | |
| display: grid; | |
| grid-template-columns: 34px 92px 72px 44px 1fr; | |
| gap: 10px; | |
| align-items: baseline; | |
| padding: 9px 4px; | |
| border-bottom: 1px solid var(--rule); | |
| font-family: "Courier Prime", monospace; | |
| font-size: 15px; | |
| cursor: pointer; | |
| } | |
| .row:hover { background: rgba(11,61,145,.05); } | |
| @media (max-width: 700px) { | |
| .row { grid-template-columns: 30px 74px 1fr; } | |
| .row .tape, .row .spk { display: none; } | |
| } | |
| .play { | |
| font-size: 13px; | |
| color: var(--nasa-blue); | |
| border: 1px solid var(--nasa-blue); | |
| border-radius: 50%; | |
| width: 24px; height: 24px; | |
| display: inline-flex; align-items: center; justify-content: center; | |
| background: transparent; | |
| cursor: pointer; | |
| padding-left: 2px; | |
| } | |
| .row.playing .play { background: var(--nasa-blue); color: var(--paper); } | |
| .tape { color: var(--pencil); font-size: 13px; } | |
| .get { font-weight: 700; } | |
| .spk { color: var(--stamp); font-weight: 700; } | |
| .txt mark { background: var(--mark); padding: 0 1px; } | |
| .player { | |
| grid-column: 1 / -1; | |
| padding: 6px 0 4px 44px; | |
| } | |
| .player audio { width: 100%; } | |
| .player small { font-family: "IBM Plex Sans"; color: var(--pencil); } | |
| .player a { color: var(--nasa-blue); } | |
| .empty { | |
| font-family: "Courier Prime", monospace; | |
| padding: 28px 4px; color: var(--pencil); | |
| } | |
| footer { | |
| margin-top: 40px; | |
| font-size: 12.5px; color: var(--pencil); | |
| line-height: 1.6; | |
| border-top: 1px solid var(--rule); | |
| padding-top: 14px; | |
| } | |
| footer a { color: var(--nasa-blue); } | |
| @media (prefers-reduced-motion: no-preference) { | |
| .row { transition: background .12s ease; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="page"> | |
| <header> | |
| <div class="rubric"><span>GOSS NET 1 · JULY 1969</span><span>AS-506 / APOLLO 11</span></div> | |
| <div class="stamp">Machine<br>Transcribed<br>2026</div> | |
| <h1>Apollo 11 — Air-to-Ground Voice Transcription</h1> | |
| <p class="subtitle">Diarized by a 0.9B open model in one Hugging Face job · search below, press ▶ to hear the tape</p> | |
| <div class="meta" id="meta"> | |
| <span><b id="stat-hours">—</b> hours of tape</span> | |
| <span><b id="stat-tapes">—</b> reels</span> | |
| <span><b id="stat-segments">—</b> utterances</span> | |
| <span><a href="https://huggingface.co/datasets/davanstrien/apollo-11-diarized" target="_blank" rel="noopener">dataset ↗</a></span> | |
| </div> | |
| </header> | |
| <form id="form" role="search"> | |
| <label class="subject" for="q">Subject:</label> | |
| <input id="q" type="search" placeholder="go for landing · tranquility|eagle · regex ok" autocomplete="off" autofocus> | |
| <select id="mode" aria-label="Search mode"> | |
| <option value="keyword">text match</option> | |
| <option value="semantic">meaning</option> | |
| </select> | |
| <select id="tape" aria-label="Filter by tape"><option value="">all reels</option></select> | |
| </form> | |
| <div class="count" id="count">Standing by.</div> | |
| <div class="results" id="results"></div> | |
| <footer> | |
| Transcripts are unedited machine output from | |
| <a href="https://huggingface.co/OpenMOSS-Team/MOSS-Transcribe-Diarize" target="_blank" rel="noopener">MOSS-Transcribe-Diarize</a> | |
| over scratchy 1969 radio — expect mishearings ("Follow eleven, this is Houston"). | |
| Speaker labels (S01, S02…) are anonymous and reset between ~55-minute clips. | |
| Audio: <a href="https://archive.org/details/Apollo11Audio" target="_blank" rel="noopener">Internet Archive / NASA</a> (public domain). | |
| Generated with a <a href="https://huggingface.co/datasets/uv-scripts/transcription" target="_blank" rel="noopener">uv-scripts recipe</a> | |
| on <a href="https://huggingface.co/docs/hub/jobs" target="_blank" rel="noopener">HF Jobs</a>. | |
| Meaning search uses <a href="https://huggingface.co/datasets/davanstrien/apollo-11-diarized/viewer/embeddings" target="_blank" rel="noopener">EmbeddingGemma embeddings</a> | |
| of every utterance, built with one more Job and an | |
| <a href="https://huggingface.co/datasets/uv-scripts/embeddings" target="_blank" rel="noopener">embeddings recipe</a>. | |
| </footer> | |
| </div> | |
| <script> | |
| const IA = "https://archive.org/download/Apollo11Audio/"; | |
| const $ = (s) => document.querySelector(s); | |
| const results = $("#results"), count = $("#count"), q = $("#q"), tapeSel = $("#tape"), modeSel = $("#mode"); | |
| let currentAudio = null, debounce = null; | |
| const PLACEHOLDERS = { | |
| keyword: "go for landing · tranquility|eagle · regex ok", | |
| semantic: "problems with the radio · describe a moment in plain words", | |
| }; | |
| fetch("api/meta").then(r => r.json()).then(({stats, tapes}) => { | |
| $("#stat-hours").textContent = stats.hours; | |
| $("#stat-tapes").textContent = stats.tapes; | |
| $("#stat-segments").textContent = stats.segments.toLocaleString(); | |
| for (const t of tapes) { | |
| const o = document.createElement("option"); | |
| o.value = t; o.textContent = t; | |
| tapeSel.appendChild(o); | |
| } | |
| }); | |
| function esc(s) { | |
| return s.replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); | |
| } | |
| function highlight(text, query) { | |
| const safe = esc(text); | |
| if (!query) return safe; | |
| try { return safe.replace(new RegExp(`(${query})`, "gi"), "<mark>$1</mark>"); } | |
| catch { return safe; } | |
| } | |
| function stopAudio() { | |
| if (currentAudio) { currentAudio.pause(); currentAudio.remove(); currentAudio = null; } | |
| document.querySelectorAll(".row.playing").forEach(r => r.classList.remove("playing")); | |
| document.querySelectorAll(".player").forEach(p => p.remove()); | |
| } | |
| function render(data, query) { | |
| stopAudio(); | |
| results.innerHTML = ""; | |
| count.textContent = data.total === 0 | |
| ? "No utterances match." | |
| : `${data.total.toLocaleString()} utterances match` + | |
| (data.total > data.shown ? ` — first ${data.shown} shown` : "") + " · press ▶ to hear the tape"; | |
| if (data.total === 0) { | |
| results.innerHTML = `<div class="empty">Try a looser term — or a regex like <b>tranquility|eagle</b>.</div>`; | |
| return; | |
| } | |
| for (const r of data.results) { | |
| const row = document.createElement("div"); | |
| row.className = "row"; row.tabIndex = 0; | |
| row.innerHTML = | |
| `<button class="play" aria-label="Play from ${r.get}">▶</button>` + | |
| `<span class="tape">${esc(r.tape)}</span>` + | |
| `<span class="get">${r.get}</span>` + | |
| `<span class="spk">${esc(r.speaker)}</span>` + | |
| `<span class="txt">${highlight(r.text, query)}</span>`; | |
| const activate = () => { | |
| const wasPlaying = row.classList.contains("playing"); | |
| stopAudio(); | |
| if (wasPlaying) return; | |
| row.classList.add("playing"); | |
| const holder = document.createElement("div"); | |
| holder.className = "player"; | |
| const src = `${IA}${r.tape}.mp3#t=${r.start}`; | |
| holder.innerHTML = | |
| `<audio controls autoplay preload="none" src="${src}"></audio>` + | |
| `<small>reel ${esc(r.tape)} from ${r.get} · ` + | |
| `<a href="https://archive.org/details/Apollo11Audio/${esc(r.tape)}.mp3" target="_blank" rel="noopener">open on archive.org ↗</a></small>`; | |
| row.after(holder); | |
| currentAudio = holder.querySelector("audio"); | |
| }; | |
| row.addEventListener("click", activate); | |
| row.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); activate(); } }); | |
| results.appendChild(row); | |
| } | |
| } | |
| async function run() { | |
| const query = q.value.trim(); | |
| const mode = modeSel.value; | |
| const params = new URLSearchParams({q: query, tape: tapeSel.value, mode}); | |
| count.textContent = "Searching…"; | |
| const data = await fetch(`api/search?${params}`).then(r => r.json()); | |
| render(data, mode === "semantic" ? "" : query); | |
| if (mode === "semantic" && query && data.total > 0) { | |
| count.textContent = `${data.shown} closest utterances by meaning · press ▶ to hear the tape`; | |
| } | |
| } | |
| q.addEventListener("input", () => { | |
| clearTimeout(debounce); | |
| debounce = setTimeout(run, modeSel.value === "semantic" ? 550 : 250); | |
| }); | |
| modeSel.addEventListener("change", () => { q.placeholder = PLACEHOLDERS[modeSel.value]; run(); }); | |
| tapeSel.addEventListener("change", run); | |
| $("#form").addEventListener("submit", (e) => { e.preventDefault(); run(); }); | |
| q.value = "Houston"; | |
| run(); | |
| </script> | |
| </body> | |
| </html> | |