apollo-11-search / index.html
davanstrien's picture
davanstrien HF Staff
Add semantic search (EmbeddingGemma embeddings config, mode=semantic + meaning toggle)
3fa46ca verified
Raw
History Blame Contribute Delete
12.1 kB
<!doctype html>
<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 => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[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>