From a1c9c5c7603be66f51c692b2a1dbd9799a0239bf Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 25 Jun 2026 08:19:17 -0700 Subject: [PATCH 1/7] =?UTF-8?q?feat(sim):=20Dev=20Mode=20=E2=80=94=20pool?= =?UTF-8?q?=20clip=20picker=20+=20live=20analysis=20under=20one=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a single "Dev Mode" switch at the bottom of the control panel (off by default, persisted in localStorage). When on, a panel appears below it with all dev controls and data; when off the default view is just the experience + the three control fieldsets. Dev panel: - Pool picker: a ` of the current altitude's pool members + (`clip_id · title`), its value reflecting the clip currently playing. Choosing a + member sets `activeClipId` to it and reloads the base media, overriding the random + landing pick. A `🎲 Re-roll random` button re-runs the server-canonical random + pick for the current scale. The dropdown is rebuilt on every scale landing, so it + always lists the current pool; single-clip scales (cosmos/orbit) show one entry. +2. **Active clip** — `id`, `title`, `source`, `license`, `base_file`. +3. **Annotations & affect** — each annotation key with salience (or legacy + `min_level`), time window (`appear`–`disappear`), and tier count; each affect key + with `min_level` and tier count. Read-only. +4. **RenderPlan** — the relocated `#readout` (the `/api/alteration` JSON). +5. **Cache & playback** — preload status (`N/total cached`), whether the active + clip is served **from memory** (a preloaded blob) vs the network, and live video + resolution / duration / loop position. + +## Behavior + +- **Pool override is per-landing.** Changing altitude re-rolls randomly as today; + the operator then picks again from the new scale's pool. The pick is not persisted + across landings (YAGNI — "select any video while on an altitude"). +- **Refresh points.** Pool picker + clip metadata + annotations rebuild whenever the + scale lands (hooked into `renderScaleReadout()`) and after a manual pick/re-roll. + The RenderPlan readout updates on each `update()` (knob change) as today. The + cache/playback stats refresh on a light interval (~500 ms) while Dev Mode is on, + and the cache count also ticks up as the preloader fills. +- **`pickRandomMember()`** is factored out of `landScale()` so the initial landing, + ring advances, and the Re-roll button all share the one server-canonical pick. + +## Testing + +No JS unit harness in the repo and no API change, so verification is: `node --check` +on `app.js`, the Python suite stays green (unaffected), and by-eye review by the +operator (no headless browser yet — §9 E2E policy noted, machinery pending). diff --git a/simulator/static/app.js b/simulator/static/app.js index 51a6082..376d3d5 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -52,22 +52,28 @@ async function loadData() { ringIndex = 0; } -// Land on the current scale: pick a random pool member (content-pipeline §11.1). -// A real server ring picks via a delta=0 advance (Python-canonical pick); the -// synthesized fallback ring has a pool of one, so it just takes that member. -async function landScale() { +// Server-canonical random pick of a pool member for the current scale: a delta=0 +// advance (content-pipeline §11.1, Python owns the randomness). The synthesized +// fallback ring has a pool of one, so it just returns that member. Returns a +// clip_id or null. Shared by the initial landing AND the Dev Mode re-roll button. +async function pickRandomMember() { const scale = ring && ring.scales[ringIndex]; - if (!scale) { activeClipId = null; return; } + if (!scale) return null; if (serverRing) { try { const resp = await fetch("/api/ring/advance", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ from_index: ringIndex, delta: 0 }), }); - if (resp.ok) { activeClipId = (await resp.json()).target_clip_id; currentClipId = null; return; } + if (resp.ok) return (await resp.json()).target_clip_id; } catch (_) { /* fall through to the primary member */ } } - activeClipId = scale.clip_id; + return scale.clip_id; +} + +// Land on the current scale: pick a random pool member and force its media to load. +async function landScale() { + activeClipId = await pickRandomMember(); currentClipId = null; } @@ -462,6 +468,7 @@ function renderScaleReadout() { const poolTag = poolN > 1 ? ` · pool ${poolN}` : ""; $("scale-name").textContent = `${s.id} · ${member} (${ringIndex + 1}/${ring.scales.length}${poolTag})`; renderDial(); + refreshDevClip(); // keep the Dev Mode pool picker + clip data in sync with the landing } function controls() { @@ -685,6 +692,136 @@ function jumpToScale(idx) { if (d) advance(d); } +// --- Dev Mode: pool picker + live analysis, all under one toggle --- +// Off by default, state persisted in localStorage. Everything here is read from +// data the renderer already has (clips, ring, the alteration response, the preload +// cache) — no server endpoints. Editing labels stays in /author.html. +const DEV_KEY = "hef.devMode"; +let devMode = false; +let devStatsTimer = null; + +const escapeHtml = (s) => String(s).replace(/[&<>"]/g, + (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); + +function applyDevVisibility() { + const panel = $("dev-panel"); + if (panel) panel.classList.toggle("hidden", !devMode); + if (devMode) { refreshDevClip(); renderDevStats(); } +} + +function initDev() { + const toggle = $("dev-mode"); + if (!toggle) return; + try { devMode = localStorage.getItem(DEV_KEY) === "1"; } catch (_) { devMode = false; } + toggle.checked = devMode; + toggle.addEventListener("change", () => { + devMode = toggle.checked; + try { localStorage.setItem(DEV_KEY, devMode ? "1" : "0"); } catch (_) {} + applyDevVisibility(); + }); + $("pool-select").addEventListener("change", onPoolSelect); + $("pool-reroll").addEventListener("click", reroll); + // Cache + playback stats are live-ish; cheap to repaint twice a second when shown. + devStatsTimer = setInterval(() => { if (devMode) renderDevStats(); }, 500); + applyDevVisibility(); +} + +// Rebuild the pool picker for the current scale and repaint the clip data. Cheap +// (pools are <= a handful), so just rebuild on every landing/pick. +function refreshDevClip() { + if (!devMode) return; + const sel = $("pool-select"); + const scale = ring && ring.scales[ringIndex]; + if (sel && scale) { + const pool = (scale.pool && scale.pool.length) ? scale.pool : [{ clip_id: scale.clip_id, title: scale.title }]; + sel.innerHTML = ""; + for (const m of pool) { + const o = document.createElement("option"); + o.value = m.clip_id; + o.textContent = `${m.clip_id} · ${m.title || (clipsById[m.clip_id] || {}).title || ""}`; + sel.appendChild(o); + } + if (activeClipId) sel.value = activeClipId; + sel.disabled = pool.length < 2; + } + renderDevMeta(); + renderDevAnno(); +} + +function renderDevMeta() { + const dl = $("clip-meta"); + const c = activeClip(); + if (!dl) return; + if (!c) { dl.innerHTML = "
no clip
"; return; } + const rows = [["id", c.id], ["title", c.title], ["source", c.source], + ["license", c.license], ["base_file", c.base_file]]; + dl.innerHTML = rows.map(([k, v]) => + `
${escapeHtml(k)}
${escapeHtml(v != null ? v : "—")}
`).join(""); +} + +function renderDevAnno() { + const box = $("clip-anno"); + const c = activeClip(); + if (!box) return; + const strings = (c && c.strings && c.strings.en) || {}; + const win = (a) => (typeof a.appear === "number" || typeof a.disappear === "number") + ? ` win ${(a.appear ?? 0).toFixed(2)}–${(a.disappear ?? 1).toFixed(2)}` : ""; + const rows = []; + for (const a of (c && c.annotations) || []) { + const measure = a.key.startsWith("measure."); + const sal = typeof a.salience === "number" ? `sal ${a.salience}` + : typeof a.min_level === "number" ? `min L${a.min_level}` : "sal 1"; + const tiers = tierCount(strings[a.key]); + rows.push(`
${escapeHtml(a.key)}` + + ` · ${sal} · ${tiers} tier${tiers > 1 ? "s" : ""}${win(a)}
`); + } + for (const f of (c && c.affect) || []) { + const tiers = tierCount(strings[f.key]); + rows.push(`
${escapeHtml(f.key)}` + + ` · min L${f.min_level ?? 1} · ${tiers} tier${tiers > 1 ? "s" : ""}
`); + } + box.innerHTML = rows.length ? rows.join("") : `
no annotations or affect
`; +} + +function renderDevStats() { + const dl = $("dev-stats"); + if (!dl) return; + const c = activeClip(); + const total = preloadList().length; + const cached = Object.keys(mediaBlobs).length; + const fromMem = !!(c && mediaBlobs[c.base_file]); + const res = vid.videoWidth ? `${vid.videoWidth}×${vid.videoHeight}` : "—"; + const dur = vid.duration ? `${vid.duration.toFixed(1)}s` : "—"; + const loop = vid.duration ? `${Math.round(loopT() * 100)}%` : "—"; + dl.innerHTML = + `
cache
${cached}/${total} cached
` + + `
active source
${fromMem ? "memory (blob)" : "network"}
` + + `
resolution
${res}
` + + `
duration
${dur}
` + + `
loop pos
${loop}
`; +} + +// Pick a specific pool member (Dev Mode override of the random landing pick). +function onPoolSelect() { + const id = $("pool-select").value; + if (!id || id === activeClipId) return; + activeClipId = id; + currentClipId = null; // force the base media to reload + renderScaleReadout(); // refresh scale name + dev clip data + update(); // reload media + plan +} + +// Re-roll: a fresh server-canonical random pick for the current scale. +async function reroll() { + if (busy) return; + const id = await pickRandomMember(); + if (!id) return; + activeClipId = id; + currentClipId = null; + renderScaleReadout(); + update(); +} + // Dev live-reload: poll the asset version and reload when it changes, so an open // tab never keeps running a stale renderer while we iterate (the readout updates // from the live API and masks it otherwise). Reloads on my edits AND on a server @@ -707,6 +844,7 @@ async function main() { await landScale(); // pick the initial scale's pool member before first render preloadAllMedia(); // background: cache every clip in memory for instant altitude swaps buildDial(); // draw the altitude knob from the ring's scales + initDev(); // wire the Dev Mode toggle + pool picker (reads persisted state) renderScaleReadout(); for (const id of ["content", "left", "right", "mood"]) { $(id).addEventListener("input", debounced); diff --git a/simulator/static/index.html b/simulator/static/index.html index 68e91ab..1417144 100644 --- a/simulator/static/index.html +++ b/simulator/static/index.html @@ -50,11 +50,40 @@ + + -
- RenderPlan readout -
-
+ diff --git a/simulator/static/style.css b/simulator/static/style.css index 4e41e39..69cfe18 100644 --- a/simulator/static/style.css +++ b/simulator/static/style.css @@ -66,3 +66,34 @@ input[type=range], select { width: 100%; } .dial-hub { fill: #2c3c5c; stroke: #9cf; stroke-width: 0.6; } .scale-name { display: block; text-align: center; font-size: 12px; color: #cde; } .hint { margin: 0.3rem 0 0; font-size: 11px; color: #789; } + +/* --- Dev Mode --- a single switch; all dev controls/data live in #dev-panel below it. */ +.dev-switch { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; + padding: 0.5rem 0.2rem 0.2rem; border-top: 1px solid #222; user-select: none; } +.dev-switch input { position: absolute; opacity: 0; width: 0; height: 0; } +.dev-switch-track { position: relative; width: 34px; height: 18px; border-radius: 9px; + background: #2a2a2a; border: 1px solid #444; transition: background 0.15s; } +.dev-switch-thumb { position: absolute; top: 1px; left: 1px; width: 14px; height: 14px; + border-radius: 50%; background: #888; transition: transform 0.15s, background 0.15s; } +.dev-switch input:checked + .dev-switch-track { background: #1d3a2a; border-color: #2e6; } +.dev-switch input:checked + .dev-switch-track .dev-switch-thumb { transform: translateX(16px); background: #4e9; } +.dev-switch input:focus-visible + .dev-switch-track { outline: 2px solid #9af; outline-offset: 1px; } +.dev-switch-label { color: #9af; font-weight: 600; letter-spacing: 0.3px; } +.dev-panel { display: flex; flex-direction: column; gap: 0.8rem; } +.dev-panel legend { color: #4e9; } +.dev-btn { margin-top: 0.5rem; width: 100%; padding: 0.4rem; cursor: pointer; + background: #182028; color: #cde; border: 1px solid #2c3c5c; border-radius: 4px; } +.dev-btn:hover { background: #20303f; } +.dev-dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.6rem; font-size: 12px; } +.dev-dl dt { color: #789; } +.dev-dl dd { margin: 0; color: #dde; word-break: break-word; } +.dev-dl dd.mem { color: #4e9; } +.dev-dl dd.net { color: #fc6; } +.dev-anno { font-size: 11px; max-height: 220px; overflow: auto; + font-family: ui-monospace, monospace; } +.dev-anno .anno-row { padding: 0.2rem 0; border-bottom: 1px solid #1d1d1d; } +.dev-anno .anno-key { color: #aee6ff; } +.dev-anno .anno-key.affect { color: #e3cfff; } +.dev-anno .anno-key.measure { color: #ffd79a; } +.dev-anno .anno-meta { color: #678; } +.dev-anno .anno-empty { color: #567; font-style: italic; } -- 2.39.5 From 0928988abc705c2feb6e6eec7e52b121ab2b8281 Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 25 Jun 2026 08:26:10 -0700 Subject: [PATCH 2/7] fix(sim): collapse Dev Mode panel when disabled The generic .hidden { display:none } is defined earlier in the stylesheet than .dev-panel { display:flex }; equal specificity meant source order won and the panel stayed visible with the toggle off. A two-class .dev-panel.hidden rule wins, so all dev controls/data truly hide when Dev Mode is disabled. Co-Authored-By: Claude Opus 4.8 (1M context) --- simulator/static/style.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simulator/static/style.css b/simulator/static/style.css index 69cfe18..f64d528 100644 --- a/simulator/static/style.css +++ b/simulator/static/style.css @@ -80,6 +80,9 @@ input[type=range], select { width: 100%; } .dev-switch input:focus-visible + .dev-switch-track { outline: 2px solid #9af; outline-offset: 1px; } .dev-switch-label { color: #9af; font-weight: 600; letter-spacing: 0.3px; } .dev-panel { display: flex; flex-direction: column; gap: 0.8rem; } +/* Beat the generic `.hidden` (defined earlier, equal specificity): two classes + win, so the panel truly collapses when Dev Mode is off. */ +.dev-panel.hidden { display: none; } .dev-panel legend { color: #4e9; } .dev-btn { margin-top: 0.5rem; width: 100%; padding: 0.4rem; cursor: pointer; background: #182028; color: #cde; border: 1px solid #2c3c5c; border-radius: 4px; } -- 2.39.5 From 0201690ce0c7f6fac7201a298d474eb0caa0f67a Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 25 Jun 2026 08:34:50 -0700 Subject: [PATCH 3/7] feat(sim): real zoom transitions on every ring edge, both directions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cosmos→orbit, reef→abyss and abyss→cosmos transition clips were stale leftovers from an earlier 5-scale ring, built against old/placeholder footage — so those edges flashed placeholder frames mid-zoom. Only orbit-coast and coast-reef had been rebuilt from the current real bases. - build_pool_manifest.generate_media(): now rebuilds EVERY ring edge from the current real primary pool bases (the orbit-coast recipe applied uniformly), overwriting the stale clips, and bakes a reversed companion (.rev.mp4) per edge via an ffmpeg `reverse` pass. - app.js: a `reversed` step (an ascending / zoom-OUT ring move) now plays the baked .rev clip instead of the forward zoom-in, so going up recedes from the current scale. Both directions are added to the in-memory preload set. Media is gitignored (generated locally / on deploy); regenerated all 10 clips locally. 269 passed, 2 skipped; node --check clean; clips serve 200. Visual result to be verified by-eye by the operator. Co-Authored-By: Claude Opus 4.8 (1M context) --- simulator/build_pool_manifest.py | 34 ++++++++++++++++++++++++-------- simulator/static/app.js | 22 +++++++++++++++------ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/simulator/build_pool_manifest.py b/simulator/build_pool_manifest.py index d234f8c..19bd710 100644 --- a/simulator/build_pool_manifest.py +++ b/simulator/build_pool_manifest.py @@ -355,7 +355,7 @@ def build_manifest() -> dict: return {"clips": clips, "ring": {"scales": scales, "transitions": transitions}} -# --- Optional: generate the NEW transition placeholders (orbit-coast, coast-reef) --- +# --- Generate the per-edge transition clips (zoom morph + reversed companion) --- def _ffmpeg() -> str: if shutil.which("ffmpeg"): @@ -365,8 +365,10 @@ def _ffmpeg() -> str: def _make_transition(ff: str, scale_a: str, scale_b: str) -> Path: - """A placeholder zoom/warp morph between two scales, from their PRIMARY pool - members' bases (mirrors setup_scales_media.py but keyed by scale->primary).""" + """A zoom/warp morph between two scales, from their PRIMARY pool members' + bases (mirrors setup_scales_media.py but keyed by scale->primary). This is the + recipe behind the well-liked orbit-coast edge; generate_media() now applies it + uniformly to every edge so none carry stale footage from an earlier ring.""" a = MEDIA / POOLS[scale_a][0] / "base.mp4" b = MEDIA / POOLS[scale_b][0] / "base.mp4" out = MEDIA / "transitions" / f"{scale_a}-{scale_b}.mp4" @@ -381,13 +383,29 @@ def _make_transition(ff: str, scale_a: str, scale_b: str) -> Path: return out +def _make_reverse(ff: str, forward: Path) -> Path: + """The zoom-OUT companion of a forward (zoom-in) edge: the same clip played + backward, written alongside it as `.rev.mp4`. An ascending ring move + crosses its edge `reversed`; the renderer then plays this file, so zooming out + recedes from the current scale back to the higher one instead of zooming in.""" + out = forward.with_suffix(".rev.mp4") + subprocess.run([ + ff, "-y", "-i", str(forward), "-vf", "reverse", "-an", str(out), + ], check=True, capture_output=True) + return out + + def generate_media() -> None: - """Generate the new ring edges introduced by the coast scale; the cosmos-orbit, - reef-abyss and abyss-cosmos edges already exist from the prior 5-scale ring.""" + """(Re)build EVERY ring-edge transition from the current real primary bases — + the orbit-coast recipe applied uniformly, overwriting any stale clips left from + an earlier ring — plus a reversed companion per edge for zoom-out moves.""" ff = _ffmpeg() - for a, b in [("orbit", "coast"), ("coast", "reef")]: - _make_transition(ff, a, b) - print(f"generated transitions/{a}-{b}.mp4 (placeholder zoom)") + n = len(RING_ORDER) + for i in range(n): + a, b = RING_ORDER[i], RING_ORDER[(i + 1) % n] + fwd = _make_transition(ff, a, b) + _make_reverse(ff, fwd) + print(f"generated transitions/{a}-{b}.mp4 (+ .rev) from real bases") def main(argv: list[str]) -> None: diff --git a/simulator/static/app.js b/simulator/static/app.js index 376d3d5..86b0aff 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -93,7 +93,11 @@ function mediaUrl(file) { return mediaBlobs[file] || ("/media/" + file); } function preloadList() { const files = new Set(); for (const c of Object.values(clipsById)) if (c && c.base_file) files.add(c.base_file); - if (ring && ring.transitions) for (const t of ring.transitions) if (t && t.file) files.add(t.file); + // Each ring edge ships both directions: the forward (zoom-in) clip and its baked + // reverse (zoom-out) companion — cache both so up and down moves are instant. + if (ring && ring.transitions) for (const t of ring.transitions) { + if (t && t.file) { files.add(t.file); files.add(reverseFile(t.file)); } + } return [...files]; } @@ -110,7 +114,7 @@ function preloadOrder() { else if (s) add((clipsById[s.clip_id] || {}).base_file); } } - if (ring.transitions) for (const t of ring.transitions) add(t.file); + if (ring.transitions) for (const t of ring.transitions) { add(t.file); add(reverseFile(t.file)); } for (const f of preloadList()) add(f); // sweep up anything not on the ring return ordered; } @@ -515,11 +519,17 @@ function debounced() { clearTimeout(timer); timer = setTimeout(update, 80); } const FAST_BLEND_RATE = 2.5; // playback speed for a collapsed fast-spin pass -function playTransition(file, blended) { +// The zoom-OUT companion of an edge clip: the baked `.rev.mp4` reverse. A +// `reversed` step (an ascending / zoom-out ring move) plays this instead of the +// forward zoom-in clip, so going up recedes from the current scale. +function reverseFile(file) { return file.replace(/\.mp4$/, ".rev.mp4"); } + +function playTransition(file, blended, reversed) { // Play one zoom/warp transition clip start-to-finish, with the alteration // layers hidden. A `blended` (fast-spin) pass runs at FAST_BLEND_RATE so a // quick encoder spin lands fast instead of grinding through every morph. - // Resolves on 'ended' (or a safety timeout that scales with playback rate). + // Zoom-out moves (`reversed`) play the baked reverse clip. Resolves on 'ended' + // (or a safety timeout that scales with playback rate). return new Promise((resolve) => { overlay.style.opacity = "0"; affectLayer.style.opacity = "0"; @@ -527,7 +537,7 @@ function playTransition(file, blended) { vid.style.filter = "none"; vid.loop = false; vid.style.opacity = "1"; - vid.src = mediaUrl(file); + vid.src = mediaUrl(reversed ? reverseFile(file) : file); vid.playbackRate = blended ? FAST_BLEND_RATE : 1; let done = false; const finish = () => { @@ -556,7 +566,7 @@ async function advance(delta) { // A fast spin comes back collapsed to a single blended step (move.fast); // a slow spin chains one full transition per scale crossed. for (const step of move.steps) { - await playTransition(step.file, step.blended); + await playTransition(step.file, step.blended, step.reversed); } ringIndex = move.to_index; activeClipId = move.target_clip_id; // the randomly-picked pool member to load -- 2.39.5 From 53cb89b4c6614e6114ae3e4532e74b7de852603e Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 25 Jun 2026 09:16:57 -0700 Subject: [PATCH 4/7] fix(content): crop NASA burned-in captions off the cosmos Orion clip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cosmos primary is NASA/JPL's "Orion: Dust and Death" — an annotated visualization with lower-third captions ("DUST FILAMENT", "BLUE BUBBLE", …) burned in throughout, so trimming can't avoid them (only ~4s of our window is caption-free). The candidate-pool doc wrongly called it text-free. - build_pool_manifest.decaption_cosmos(): crops the cosmos base/master to the top 66% of frame height (16:9 preserved, center-x, ~1.5x tighter framing), re-fit to 1920x1080 — the recorded, reproducible form of the fix (media is gitignored). Applied in place; re-ran generate_media() so the cosmos-orbit / abyss-cosmos transitions use the cropped base. - docs/content-candidate-pool.md: correct the "text-free" note; document the crop. 269 passed, 2 skipped. De-caption verified by-eye on both caption moments; final look to be confirmed by the operator. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/content-candidate-pool.md | 13 +++++++++++-- simulator/build_pool_manifest.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/content-candidate-pool.md b/docs/content-candidate-pool.md index bd4b57d..8086092 100644 --- a/docs/content-candidate-pool.md +++ b/docs/content-candidate-pool.md @@ -28,15 +28,24 @@ stays non‑commercial**. | id | clip | source | window (s) | license / credit | |----|------|--------|-----------|------------------| | `cosmos` | Orion Nebula flythrough | NASA/JPL‑Caltech — https://images.nasa.gov/details/JPL-20221122-SOLSYSf-0001-Orion%20Dust%20and%20Death | ~30–54 | PD (17 U.S.C. §105) | + +> ⚠️ **Orion is NOT text-free.** The source is the *annotated* "Orion: Dust and +> Death" visualization — NASA burns lower-third captions in throughout +> ("DUST FILAMENT", "BLUE BUBBLE", …), so trimming can't avoid them. We crop past +> them: `simulator/build_pool_manifest.py:decaption_cosmos()` keeps the top 66% of +> the frame (16:9 preserved, ~1.5× tighter framing) and re-fits to 1920×1080. Run +> it ONCE after (re)sourcing the Orion clip, then re-run `generate_media()` so the +> cosmos-orbit / abyss-cosmos transitions use the cropped base. | `cosmos_galaxies` | Flying Through Galaxies | NASA SVS — https://svs.gsfc.nasa.gov/vis/a010000/a014900/a014950/14950_Galaxies_FlyThrough_4k.mp4 | 8–32 | PD | | `cosmos_hudf` | Hubble Ultra Deep Field zoom | NASA SVS — https://svs.gsfc.nasa.gov/vis/a030000/a030600/a030687/hudf-b-1920x1080p30.mov | 20–44 | CC‑BY‑class — credit **NASA, ESA, STScI** | | `cosmos_xdf` | eXtreme Deep Field flythrough | NASA SVS — https://svs.gsfc.nasa.gov/vis/a030000/a030600/a030681/hxdf_fly-b-1920x1080p30.mov | 2–26 | CC‑BY‑class — credit **NASA, ESA, STScI** | > ⚠️ **DISCREPANCY to fix next session.** Operator selected cosmos **#1, #3 > (Orion), #4, #5**. During processing, **#6 Galaxy Traverse was run by mistake -> instead of #3 Orion**. The Orion clip (#3) is text‑free and already lives at +> instead of #3 Orion**. The Orion clip (#3) lives at > `simulator/sample_media/cosmos/base.mp4` (the prior real base), so the pool above -> is correct. **`cosmos_traverse` is an UNSELECTED extra on disk — drop it** (or +> is correct — but it carries burned-in captions (see the Orion note above; now +> cropped out). **`cosmos_traverse` is an UNSELECTED extra on disk — drop it** (or > confirm with operator). The current ring scale id is `cosmos`; when the pool is > wired, `cosmos` becomes one pool member. diff --git a/simulator/build_pool_manifest.py b/simulator/build_pool_manifest.py index 19bd710..3f0ef6c 100644 --- a/simulator/build_pool_manifest.py +++ b/simulator/build_pool_manifest.py @@ -408,6 +408,35 @@ def generate_media() -> None: print(f"generated transitions/{a}-{b}.mp4 (+ .rev) from real bases") +# Fraction of frame HEIGHT kept (from the top) to drop the NASA burned-in +# lower-third captions on the cosmos Orion clip ("DUST FILAMENT", "BLUE BUBBLE", +# …). The "Orion: Dust and Death" visualization is captioned throughout, so +# trimming can't avoid them — we crop past them instead. ~1.5x tighter framing. +COSMOS_CAPTION_KEEP = 0.66 + + +def decaption_cosmos() -> None: + """Re-crop the cosmos Orion base/master IN PLACE to remove NASA's burned-in + lower-third caption band, keeping 16:9 (crop width to match, center-x, top- + anchored) then re-fitting to 1920x1080. Run ONCE after (re)sourcing the Orion + clip — re-running would crop again. Media is gitignored, so this is the + recorded, reproducible form of the operation.""" + ff = _ffmpeg() + f = COSMOS_CAPTION_KEEP + vf = f"crop=iw*{f}:ih*{f}:iw*{(1 - f) / 2}:0,scale=1920:1080" + for name, crf in (("base", "20"), ("master", "18")): + p = MEDIA / "cosmos" / f"{name}.mp4" + if not p.exists(): + continue + tmp = p.with_suffix(".decap.mp4") + subprocess.run([ + ff, "-y", "-i", str(p), "-vf", vf, + "-c:v", "libx264", "-crf", crf, "-pix_fmt", "yuv420p", "-an", str(tmp), + ], check=True, capture_output=True) + tmp.replace(p) + print(f"de-captioned cosmos/{name}.mp4 (kept top {int(f * 100)}%)") + + def main(argv: list[str]) -> None: manifest = build_manifest() MANIFEST.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n") -- 2.39.5 From a2ef792f0f2eb7ef2548e1b907c05a53afdb966f Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 25 Jun 2026 09:20:14 -0700 Subject: [PATCH 5/7] fix(sim): revalidate /media instead of pinning it immutable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `immutable, max-age=1y` media header pinned stale footage in the browser for a year, so a re-sourced or re-cropped clip (e.g. the de-captioned cosmos) never showed without a manual cache clear. Switch /media to `no-cache` (store, but revalidate every load): instant altitude swaps already come from the in-memory blob preload, so the HTTP cache only matters on (re)load — where a conditional request is a cheap 304 when unchanged but picks up edited clips immediately. Co-Authored-By: Claude Opus 4.8 (1M context) --- simulator/app.py | 12 +++++++----- tests/test_simulator_api.py | 5 ++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/simulator/app.py b/simulator/app.py index 0266abb..7ebe72d 100644 --- a/simulator/app.py +++ b/simulator/app.py @@ -237,13 +237,15 @@ def create_app(manifest_path: Optional[Path] = None) -> FastAPI: async def _cache_policy(request, call_next): # Dev preview server: never let a browser serve a stale app.js/style.css — # by-eye iteration needs a plain refresh to always get the latest assets. - # Media is the exception: the clips are static and large, so they must be - # browser-cacheable. The renderer also preloads them into in-memory blob - # URLs (static/app.js), but a cacheable header keeps the preload fetch and - # any fallback off the network on repeat loads — instant altitude swaps. + # Media uses `no-cache` (store, but REVALIDATE every load): instant altitude + # swaps already come from the in-memory blob preload (static/app.js), so the + # HTTP cache only matters on (re)load — where a conditional request returns a + # cheap 304 when unchanged, yet picks up a re-sourced/re-cropped clip + # immediately. `immutable` was wrong here: it pinned stale footage for a year + # so edited clips never showed without a manual cache clear. response = await call_next(request) if request.url.path.startswith("/media/"): - response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + response.headers["Cache-Control"] = "no-cache" else: response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" return response diff --git a/tests/test_simulator_api.py b/tests/test_simulator_api.py index 0fcc4d8..b3c5669 100644 --- a/tests/test_simulator_api.py +++ b/tests/test_simulator_api.py @@ -113,8 +113,11 @@ def test_media_is_cacheable(tmp_path, monkeypatch): resp = client.get("/media/cosmos/base.mp4") assert resp.status_code == 200 cc = resp.headers.get("cache-control", "") + # Stored but revalidated: not no-store (so the blob preload can cache it), and + # not pinned immutable (so a re-sourced/re-cropped clip shows on a plain reload). assert "no-store" not in cc - assert "max-age" in cc + assert "immutable" not in cc + assert "no-cache" in cc @pytest.fixture -- 2.39.5 From 129bb23cb99dd63e1dc098c90fcb99185820cc77 Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 25 Jun 2026 09:28:10 -0700 Subject: [PATCH 6/7] feat(content): swap cosmos Orion to a text-free SVS flythrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the JPL "Orion: Dust and Death" primary (captioned throughout — we'd cropped it, losing framing) with STScI's "Flight Through the Orion Nebula" (visible light, NASA SVS 30957) — a full-frame, text-free flythrough of the same nebula. Sourced via the content pipeline (trim 10–34s + crossfade-loop → 1080p master + base); cosmos-orbit / abyss-cosmos transitions regenerated from it. - build_pool_manifest META: cosmos -> SVS 30957, license CC-BY-class (NASA/ESA/ STScI), new trim window. - Remove the now-obsolete decaption_cosmos() crop helper (the new source needs no caption removal). - docs/content-candidate-pool.md: record the source swap; drop the crop note. 269 passed, 2 skipped. New clip verified text-free + full-frame by-eye; final look to be confirmed by the operator. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/content-candidate-pool.md | 23 +++++++++++---------- simulator/build_pool_manifest.py | 34 +++----------------------------- 2 files changed, 14 insertions(+), 43 deletions(-) diff --git a/docs/content-candidate-pool.md b/docs/content-candidate-pool.md index 8086092..7c12794 100644 --- a/docs/content-candidate-pool.md +++ b/docs/content-candidate-pool.md @@ -27,25 +27,24 @@ stays non‑commercial**. | id | clip | source | window (s) | license / credit | |----|------|--------|-----------|------------------| -| `cosmos` | Orion Nebula flythrough | NASA/JPL‑Caltech — https://images.nasa.gov/details/JPL-20221122-SOLSYSf-0001-Orion%20Dust%20and%20Death | ~30–54 | PD (17 U.S.C. §105) | +| `cosmos` | Orion Nebula flythrough | NASA SVS 30957 — https://svs.gsfc.nasa.gov/30957/ (`orion_vis-1920x1080.mp4`, *Flight Through the Orion Nebula*, visible light) | 10–34 | CC‑BY‑class — credit **NASA, ESA, STScI** | -> ⚠️ **Orion is NOT text-free.** The source is the *annotated* "Orion: Dust and -> Death" visualization — NASA burns lower-third captions in throughout -> ("DUST FILAMENT", "BLUE BUBBLE", …), so trimming can't avoid them. We crop past -> them: `simulator/build_pool_manifest.py:decaption_cosmos()` keeps the top 66% of -> the frame (16:9 preserved, ~1.5× tighter framing) and re-fits to 1920×1080. Run -> it ONCE after (re)sourcing the Orion clip, then re-run `generate_media()` so the -> cosmos-orbit / abyss-cosmos transitions use the cropped base. +> ℹ️ **Orion source swapped to a text-free clip (2026-06-25).** The original +> primary was NASA/JPL's *"Orion: Dust and Death"* — an annotated visualization +> with lower-third captions ("DUST FILAMENT", "BLUE BUBBLE", …) burned in +> throughout, so trimming couldn't avoid them and a crop lost framing. Replaced +> with STScI's **Flight Through the Orion Nebula** (visible light, SVS 30957) — +> a full-frame, text-free flythrough of the same nebula. The earlier crop helper +> (`decaption_cosmos`) is gone; no caption removal is needed for this source. | `cosmos_galaxies` | Flying Through Galaxies | NASA SVS — https://svs.gsfc.nasa.gov/vis/a010000/a014900/a014950/14950_Galaxies_FlyThrough_4k.mp4 | 8–32 | PD | | `cosmos_hudf` | Hubble Ultra Deep Field zoom | NASA SVS — https://svs.gsfc.nasa.gov/vis/a030000/a030600/a030687/hudf-b-1920x1080p30.mov | 20–44 | CC‑BY‑class — credit **NASA, ESA, STScI** | | `cosmos_xdf` | eXtreme Deep Field flythrough | NASA SVS — https://svs.gsfc.nasa.gov/vis/a030000/a030600/a030681/hxdf_fly-b-1920x1080p30.mov | 2–26 | CC‑BY‑class — credit **NASA, ESA, STScI** | > ⚠️ **DISCREPANCY to fix next session.** Operator selected cosmos **#1, #3 > (Orion), #4, #5**. During processing, **#6 Galaxy Traverse was run by mistake -> instead of #3 Orion**. The Orion clip (#3) lives at -> `simulator/sample_media/cosmos/base.mp4` (the prior real base), so the pool above -> is correct — but it carries burned-in captions (see the Orion note above; now -> cropped out). **`cosmos_traverse` is an UNSELECTED extra on disk — drop it** (or +> instead of #3 Orion**. The cosmos primary now lives at +> `simulator/sample_media/cosmos/base.mp4` as the text-free SVS 30957 flythrough +> (see the Orion note above). **`cosmos_traverse` is an UNSELECTED extra on disk — drop it** (or > confirm with operator). The current ring scale id is `cosmos`; when the pool is > wired, `cosmos` becomes one pool member. diff --git a/simulator/build_pool_manifest.py b/simulator/build_pool_manifest.py index 3f0ef6c..e8eb186 100644 --- a/simulator/build_pool_manifest.py +++ b/simulator/build_pool_manifest.py @@ -55,8 +55,9 @@ CCBY_STSCI = "CC-BY-class — credit NASA, ESA, STScI" # --- Per-clip provenance: id -> (title, license, source) --- META: dict[str, tuple[str, str, str]] = { # cosmos - "cosmos": ("Orion Nebula flythrough (NASA/JPL-Caltech)", PD, - "NASA/JPL-Caltech — images.nasa.gov JPL-20221122-SOLSYSf-0001 (Orion); trim ~30–54s, crossfade-loop"), + "cosmos": ("Orion Nebula flythrough (NASA/ESA/STScI)", CCBY_STSCI, + "NASA SVS 30957 orion_vis-1920x1080 (Flight Through the Orion Nebula, visible light); " + "trim 10–34s, crossfade-loop. Text-free — replaces the captioned JPL 'Dust and Death' clip."), "cosmos_galaxies": ("Flying Through Galaxies (NASA SVS)", PD, "NASA SVS a014950 14950_Galaxies_FlyThrough_4k; trim 8–32s, crossfade-loop"), "cosmos_hudf": ("Hubble Ultra Deep Field zoom (NASA/ESA/STScI)", CCBY_STSCI, @@ -408,35 +409,6 @@ def generate_media() -> None: print(f"generated transitions/{a}-{b}.mp4 (+ .rev) from real bases") -# Fraction of frame HEIGHT kept (from the top) to drop the NASA burned-in -# lower-third captions on the cosmos Orion clip ("DUST FILAMENT", "BLUE BUBBLE", -# …). The "Orion: Dust and Death" visualization is captioned throughout, so -# trimming can't avoid them — we crop past them instead. ~1.5x tighter framing. -COSMOS_CAPTION_KEEP = 0.66 - - -def decaption_cosmos() -> None: - """Re-crop the cosmos Orion base/master IN PLACE to remove NASA's burned-in - lower-third caption band, keeping 16:9 (crop width to match, center-x, top- - anchored) then re-fitting to 1920x1080. Run ONCE after (re)sourcing the Orion - clip — re-running would crop again. Media is gitignored, so this is the - recorded, reproducible form of the operation.""" - ff = _ffmpeg() - f = COSMOS_CAPTION_KEEP - vf = f"crop=iw*{f}:ih*{f}:iw*{(1 - f) / 2}:0,scale=1920:1080" - for name, crf in (("base", "20"), ("master", "18")): - p = MEDIA / "cosmos" / f"{name}.mp4" - if not p.exists(): - continue - tmp = p.with_suffix(".decap.mp4") - subprocess.run([ - ff, "-y", "-i", str(p), "-vf", vf, - "-c:v", "libx264", "-crf", crf, "-pix_fmt", "yuv420p", "-an", str(tmp), - ], check=True, capture_output=True) - tmp.replace(p) - print(f"de-captioned cosmos/{name}.mp4 (kept top {int(f * 100)}%)") - - def main(argv: list[str]) -> None: manifest = build_manifest() MANIFEST.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n") -- 2.39.5 From d24f8292760b221ff0c8ac94f1d01410d651930f Mon Sep 17 00:00:00 2001 From: BenStullsBets Date: Thu, 25 Jun 2026 10:48:44 -0700 Subject: [PATCH 7/7] feat(content): swap cosmos primary to the Webb "Cosmic Cliffs" flythrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator found the Orion SVS 30957 flythrough merely good and asked for a more dramatic nebula. Replaces the cosmos-scale primary with STScI's "Exploring the Cosmic Cliffs in 3D" (Carina Nebula, Webb NIRCam — NASA SVS 31348), sourced from its 4K master (Clifs-3d-STScI.mp4, 3840x2160) via the content pipeline: trim 10-34s (clears the title card ~8s and end-credit card ~102s) + crossfade-loop -> 4K master + supersampled 1080p base (5.5 Mbps, crisper than the old 2.9 Mbps 1080p-native Orion encode). - build_pool_manifest META: cosmos -> SVS 31348 Cosmic Cliffs, new CCBY_WEBB credit (NASA, ESA, CSA, STScI). Labels retuned for the Carina framing (emission nebula / protostar; redshift -> distance ~7,500 ly). - Regenerated manifest.json (also corrects stale JPL-Orion provenance left when 129bb23 updated META but not the manifest) + all ring-edge transitions from the new base (cosmos-orbit / abyss-cosmos now zoom from the Cosmic Cliffs). - docs/content-candidate-pool.md: record the swap + history. 269 passed, 2 skipped. New base verified text-free + full-frame by-eye across the loop; final look to be confirmed by the operator (no Chrome on the box). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/content-candidate-pool.md | 25 ++++++++++-------- simulator/build_pool_manifest.py | 13 +++++----- simulator/sample_media/manifest.json | 38 ++++++++++++++-------------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/docs/content-candidate-pool.md b/docs/content-candidate-pool.md index 7c12794..bb74a07 100644 --- a/docs/content-candidate-pool.md +++ b/docs/content-candidate-pool.md @@ -27,15 +27,18 @@ stays non‑commercial**. | id | clip | source | window (s) | license / credit | |----|------|--------|-----------|------------------| -| `cosmos` | Orion Nebula flythrough | NASA SVS 30957 — https://svs.gsfc.nasa.gov/30957/ (`orion_vis-1920x1080.mp4`, *Flight Through the Orion Nebula*, visible light) | 10–34 | CC‑BY‑class — credit **NASA, ESA, STScI** | +| `cosmos` | Cosmic Cliffs — Carina Nebula flythrough | NASA SVS 31348 — https://svs.gsfc.nasa.gov/31348/ (`Clifs-3d-STScI.mp4`, *Exploring the Cosmic Cliffs in 3D*, Webb NIRCam, **4K**) | 10–34 | CC‑BY‑class — credit **NASA, ESA, CSA, STScI** | -> ℹ️ **Orion source swapped to a text-free clip (2026-06-25).** The original -> primary was NASA/JPL's *"Orion: Dust and Death"* — an annotated visualization -> with lower-third captions ("DUST FILAMENT", "BLUE BUBBLE", …) burned in -> throughout, so trimming couldn't avoid them and a crop lost framing. Replaced -> with STScI's **Flight Through the Orion Nebula** (visible light, SVS 30957) — -> a full-frame, text-free flythrough of the same nebula. The earlier crop helper -> (`decaption_cosmos`) is gone; no caption removal is needed for this source. +> ℹ️ **Cosmos primary swapped to the Webb "Cosmic Cliffs" flythrough (2026-06-25, session 0018).** +> History: the original primary was NASA/JPL's *"Orion: Dust and Death"* (captions +> burned in → cropping lost framing); it was then swapped to STScI's text-free +> *Flight Through the Orion Nebula* (SVS 30957, visible light). The operator found +> the Orion clip merely good and asked for a more dramatic nebula, so the primary is +> now STScI's **Exploring the Cosmic Cliffs in 3D** (Carina Nebula, Webb NIRCam, +> **SVS 31348**) — a full-frame, text-free 3D flythrough sourced from its **4K** +> master (`Clifs-3d-STScI.mp4`, 3840×2160) → supersampled 1080p base + 4K master. +> Trim 10–34s clears the opening title card (gone by ~8s) and the end-credit card +> (~102s). The earlier `decaption_cosmos` crop helper remains removed. | `cosmos_galaxies` | Flying Through Galaxies | NASA SVS — https://svs.gsfc.nasa.gov/vis/a010000/a014900/a014950/14950_Galaxies_FlyThrough_4k.mp4 | 8–32 | PD | | `cosmos_hudf` | Hubble Ultra Deep Field zoom | NASA SVS — https://svs.gsfc.nasa.gov/vis/a030000/a030600/a030687/hudf-b-1920x1080p30.mov | 20–44 | CC‑BY‑class — credit **NASA, ESA, STScI** | | `cosmos_xdf` | eXtreme Deep Field flythrough | NASA SVS — https://svs.gsfc.nasa.gov/vis/a030000/a030600/a030681/hxdf_fly-b-1920x1080p30.mov | 2–26 | CC‑BY‑class — credit **NASA, ESA, STScI** | @@ -43,8 +46,8 @@ stays non‑commercial**. > ⚠️ **DISCREPANCY to fix next session.** Operator selected cosmos **#1, #3 > (Orion), #4, #5**. During processing, **#6 Galaxy Traverse was run by mistake > instead of #3 Orion**. The cosmos primary now lives at -> `simulator/sample_media/cosmos/base.mp4` as the text-free SVS 30957 flythrough -> (see the Orion note above). **`cosmos_traverse` is an UNSELECTED extra on disk — drop it** (or +> `simulator/sample_media/cosmos/base.mp4` as the Webb Cosmic Cliffs flythrough +> (SVS 31348 — see the cosmos-primary note above). **`cosmos_traverse` is an UNSELECTED extra on disk — drop it** (or > confirm with operator). The current ring scale id is `cosmos`; when the pool is > wired, `cosmos` becomes one pool member. @@ -116,5 +119,5 @@ Removed from local media (all were gitignored, so repo-invisible): - `forest` — the old Yosemite base, superseded by the `coast` pool. **dropped.** - `reef` — the original placeholder/early reef base, superseded by `reef_*`. **dropped.** - `orbit` / `abyss` — the Increment‑1 single real bases (NASA ISS Exp65 / ctenophore), - not in the new pools. **dropped** (the cosmos Orion base stays as the cosmos pool primary). + not in the new pools. **dropped** (the cosmos base — now the Webb Cosmic Cliffs flythrough, SVS 31348 — stays as the cosmos pool primary). - `review.html` gallery — now `.gitignore`d (local‑only, re‑buildable). diff --git a/simulator/build_pool_manifest.py b/simulator/build_pool_manifest.py index e8eb186..f499dd0 100644 --- a/simulator/build_pool_manifest.py +++ b/simulator/build_pool_manifest.py @@ -51,13 +51,14 @@ RING_ORDER = ["cosmos", "orbit", "coast", "reef", "abyss"] PD = "public-domain (US Gov, 17 U.S.C. §105)" PD_NPS = "public-domain (NPS, no © — 17 U.S.C. §105)" CCBY_STSCI = "CC-BY-class — credit NASA, ESA, STScI" +CCBY_WEBB = "CC-BY-class — credit NASA, ESA, CSA, STScI" # --- Per-clip provenance: id -> (title, license, source) --- META: dict[str, tuple[str, str, str]] = { # cosmos - "cosmos": ("Orion Nebula flythrough (NASA/ESA/STScI)", CCBY_STSCI, - "NASA SVS 30957 orion_vis-1920x1080 (Flight Through the Orion Nebula, visible light); " - "trim 10–34s, crossfade-loop. Text-free — replaces the captioned JPL 'Dust and Death' clip."), + "cosmos": ("Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI)", CCBY_WEBB, + "NASA SVS 31348 Clifs-3d-STScI (Exploring the Cosmic Cliffs in 3D, Webb NIRCam); " + "4K source, trim 10–34s, crossfade-loop. Text-free — replaces the Orion SVS 30957 flythrough."), "cosmos_galaxies": ("Flying Through Galaxies (NASA SVS)", PD, "NASA SVS a014950 14950_Galaxies_FlyThrough_4k; trim 8–32s, crossfade-loop"), "cosmos_hudf": ("Hubble Ultra Deep Field zoom (NASA/ESA/STScI)", CCBY_STSCI, @@ -162,9 +163,9 @@ def measure(key, value, box, min_level): LABELS: dict[str, list[dict]] = { # ---------- cosmos ---------- "cosmos": [ - static_label("detected.nebula", 4, ["cloud", "nebula", "emission nebula", "emission nebula · ionized H II region"], [0.32, 0.28, 0.34, 0.36]), - static_label("detected.star", 2, ["star", "young star", "protostar", "protostar · <1 Myr old"], [0.60, 0.30, 0.10, 0.12]), - measure("measure.redshift", "z ≈ 0.0", [0.36, 0.70, 0.18, 0.08], 4), + static_label("detected.nebula", 4, ["cloud", "nebula", "emission nebula", "emission nebula · the Carina star-forming region"], [0.20, 0.42, 0.5, 0.42]), + static_label("detected.star", 2, ["star", "young star", "protostar", "protostar · a newborn star, <1 Myr old"], [0.54, 0.12, 0.12, 0.14]), + measure("measure.distance", "≈7,500 ly", [0.06, 0.06, 0.2, 0.08], 3), ], "cosmos_galaxies": [ static_label("detected.galaxy", 4, ["galaxy", "spiral galaxy", "barred spiral", "barred spiral · ~10¹¹ stars"], [0.34, 0.30, 0.30, 0.34]), diff --git a/simulator/sample_media/manifest.json b/simulator/sample_media/manifest.json index f660d0a..ca44349 100644 --- a/simulator/sample_media/manifest.json +++ b/simulator/sample_media/manifest.json @@ -2,41 +2,41 @@ "clips": [ { "id": "cosmos", - "title": "Orion Nebula flythrough (NASA/JPL-Caltech)", + "title": "Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI)", "base_file": "cosmos/base.mp4", - "license": "public-domain (US Gov, 17 U.S.C. §105)", - "source": "NASA/JPL-Caltech — images.nasa.gov JPL-20221122-SOLSYSf-0001 (Orion); trim ~30–54s, crossfade-loop", + "license": "CC-BY-class — credit NASA, ESA, CSA, STScI", + "source": "NASA SVS 31348 Clifs-3d-STScI (Exploring the Cosmic Cliffs in 3D, Webb NIRCam); 4K source, trim 10–34s, crossfade-loop. Text-free — replaces the Orion SVS 30957 flythrough.", "right_variants": {}, "annotations": [ { "key": "detected.nebula", "salience": 4, "box": [ - 0.32, - 0.28, - 0.34, - 0.36 + 0.2, + 0.42, + 0.5, + 0.42 ] }, { "key": "detected.star", "salience": 2, "box": [ - 0.6, - 0.3, - 0.1, - 0.12 + 0.54, + 0.12, + 0.12, + 0.14 ] }, { - "key": "measure.redshift", + "key": "measure.distance", "box": [ - 0.36, - 0.7, - 0.18, + 0.06, + 0.06, + 0.2, 0.08 ], - "min_level": 4 + "min_level": 3 } ], "affect": [ @@ -79,15 +79,15 @@ "cloud", "nebula", "emission nebula", - "emission nebula · ionized H II region" + "emission nebula · the Carina star-forming region" ], "detected.star": [ "star", "young star", "protostar", - "protostar · <1 Myr old" + "protostar · a newborn star, <1 Myr old" ], - "measure.redshift": "z ≈ 0.0", + "measure.distance": "≈7,500 ly", "feel.wonder": [ "wow", "wonder", -- 2.39.5