feat(mockups): 17 voyage with real content (FLAC, synced lyrics, Odyssey passage)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-30 04:45:08 +01:00
1 parent 6db056fba2
commit 93e9165a88
14 files changed
+4076 -1

No files matched your search

+7
View File
@@ -341,6 +341,13 @@ and brief one agent from a copy of 16, using `/content/library.json`, `parseLrc`
- Resumed from §18. 17 (real content) is started now, as agreed; the user's reaction to 16 is asked
in parallel.
- **17 is finished and committed**; report in `docs/design/002-sea-chart-spikes/17-voyage-real-report.md`.
Real FLAC playback with seeking, line-synced lyrics (click a line to seek), the Butler Odyssey passage
in a reading view, fallback to 16 without the folder. Agent's choices: "Play voyage" stays silent (no
excerpts, avoids near-autoplay); part buttons dropped in real mode (sample part times don't match the
recordings). Open: passage start points are keyword guesses; parts/motifs aren't timed to the
recordings yet (report proposes a small mapping file in the content folder).
## Open questions (in priority order)
1. ~~Which sea-chart approach (08–11) reads best, or which mix?~~ 11's navigation + 10's style (§9).
@@ -0,0 +1,133 @@
# 17 Voyage with real content — report
Status: **done** (not committed). Built from `17-voyage-real-brief.md`, starting from a copy of 16.
## What was built
`src/mockups/17-voyage-real/` is 16 unchanged on the stage (look 2, Inter labels, frieze accents, five
beats, stylised 3D, combined paths, fan works placeholders). Only the content side changed. URL params
are as in 16, plus **`?read=1`**, which opens About's Odyssey reading view for the first song (used for
screenshots).
- **`content.ts`** (new) reads the mounted folder at runtime:
- `loadLibrary()` fetches `/content/library.json` before the panel is built (top-level await, 4 s
timeout). A 404 or a failure means "not mounted".
- `realFor(song)` matches a sample song to the library by normalised title. Case, accents,
apostrophes and punctuation are ignored. A miss is logged once and does not crash.
- `loadLyrics()` parses `.lrc` files with `parseLrc` (`src/shared/lrc.ts`) and splits `.txt` files
into plain lines. `loadBook()` fetches `/content/homer/odyssey/<n>.json`. Both are cached.
- It also holds the keywords that mark each song's passage, `passageStart()`, and a footnote-marker
stripper for Gutenberg's inline note numbers.
- Only types are imported from `src/content/`.
1. **Real player** (`player.ts`, `panel.ts`).
- The `<audio>` element is real and sits hidden in the panel's DOM. Play/pause, elapsed time and
total time all come from the element, and the scrub bar seeks it (click, drag, ←/→ ±5 s).
- While the song plays, the clock is read every animation frame, so the highlight moves smoothly.
`timeupdate` alone fires only about 4 times a second.
- A seek made before the metadata has loaded (`?t_play`) sets the start position and is applied
again once the metadata arrives.
- The saga cover is a 56 px square in the player card. The card is sticky at the top of the tab, so
the controls stay put while the lyrics scroll under it.
- With real audio, the "Preview player" note, the part segment buttons and the part ticks are gone,
because the sample's part times are invented and would be wrong against the recording.
- The header play button and its eyebrow ("Playing · 1:00 / 3:36") use the real clock.
- If the file fails to load, the player drops back to the simulated clock and the tab re-renders
as 16.
2. **Line-synced lyrics.**
- The `.lrc` lines are shown in serif italic, with empty LRC lines drawn as a faint `· · ·` break.
- The current line has a terracotta wash, an ink outline and bold type, and lines already sung turn
iron-soft. The match leads the timestamp by 0.15 s.
- **Keeping the line in view.** The current line is smoothly scrolled to about a third of the way
down the space below the sticky player, once per line change.
- **The reader's own scrolling wins.** A wheel, touch, scrollbar drag or keyboard scroll in the view
pauses following for 4 s. After that the lyrics catch up on the next line. Seeking or clicking a
line resumes following at once.
- **Click-to-seek.** Clicking a line seeks to its timestamp and plays. The timestamp appears in the
gutter on hover or focus.
- **Plain `.txt` lyrics** are shown unsynced, with the note "These lyrics are not timed, so they do
not follow the song".
- **Missing or failed lyrics** each get a one-line note.
- **Parts and motifs.** Part headings are gone from the lyrics. A compact summary above them reads
"Parts: Verse 1 · Verse 2", followed by the motif chips (the motif pick works as before) and "From
the sample data, not yet timed to the recording".
3. **The Odyssey passage.**
- About's Odyssey row now reads "Book IX". The line ranges are dropped because the prose has no line
numbers.
- The **Read the passage →** button replaces About with a reading view, which has "← Back to About",
the book heading, Butler's summary line, the Roman-names note and the paragraphs.
- **Where the passage starts.** The view opens at the first paragraph that mentions the song's
keyword and puts a meander rule before it: "The passage for Polyphemus begins here · first mention
of 'Cyclops'". Keywords are highlighted in the text.
- **Earlier paragraphs.** "Show the 13 paragraphs before this passage" expands the whole book and
scrolls to the rule. If no keyword is found, the view starts at the beginning of the book.
- The text ends with "End of Book IX · Project Gutenberg #1727".
- **Keywords (Butler's spelling).**
- Horse and the Infant: horse (VIII, Demodocus).
- Full Speed Ahead: Cicons / Ismarus.
- Warrior of the Mind: Minerva (XIII).
- Polyphemus: Polyphemus / Cyclops.
- Survive: olive (the stake).
- Remember Them: rams.
- My Goodbye: "put your eye out" (the taunt).
- Storm: Aeolus.
- These are content guesses, the same as the sample's `TODO(verify)` references.
4. **Fallback.** Without `CONTENT_DIR` the page behaves like 16: the simulated player, placeholder
lyrics by part, and the Gutenberg link. The player note starts with **"Content folder not mounted."**,
and the end of About has a small "Content folder not mounted" note. `body[data-content]` is
`mounted` or `missing`.
5. **Voyage play.** It stays **silent: no excerpts**. Beats hold for 3.4 s, so an excerpt would be a
jarring audio stab, and starting sound without a click is close to the autoplay the brief rules out.
As in 16, moving to a new song loads that song stopped at 0:00. The old `<audio>` element is removed.
A song that is playing keeps playing while the beat changes but the song stays the same (Landfall →
Curtain).
## Screenshots (1440×900, `screenshots/`)
- `17-voyage-real-listen.png`: beat 3, Listen & lyrics, `?t_play=60`. It is playing at 1:00 / 3:36,
with the cover, the current line lit and kept in view under the sticky player.
- `17-voyage-real-odyssey.png`: Polyphemus, Book IX reading view, opened at the first "Cyclops"
paragraph.
- `17-voyage-real-nocontent.png`: served without `CONTENT_DIR`. It shows the simulated player, the
"Content folder not mounted" note and the placeholder lyrics.
- `17-voyage-real-nocontent-about.png`: About without content (Gutenberg link and the note).
- `17-voyage-real-beat1.png`: the Troy frieze, with About's "Book VIII · Read the passage →".
- `-txt.png` was skipped. No sample song (tracks 1–10) maps to a `.txt` lyric; in the library only
Scylla and two others outside the sample do.
The credits footer is fully visible in every one.
## Verified
- `bun test` (64 pass), `bun run typecheck` (clean) and `bun run build`. The scratchpad build was
served on port 4117 with and without `CONTENT_DIR`.
- A CDP script drove headless Chrome against the real server, with no virtual time:
- The real FLAC loads (`readyState` 4, duration 216 s).
- After the header play click, `currentTime` advances (2.94 → 4.95 s over 2 s).
- A scrub click at 50 % seeks to 109.4 s.
- Clicking a lyric line stamped 1:08 seeks there, and that line becomes current.
- A range request on the audio URL returns `206 bytes 1000000-1000099/42425888`.
- Following keeps the current line 249 px down a 520 px view.
- After a user scroll to the top, the view stays at 0 for 2.5 s, then follows again by 7.5 s.
- Moving to beat 5 swaps the element to Survive, stopped.
- The reading view (Survive → "olive", 20 earlier paragraphs) expands to all 44 paragraphs, and Back
returns to About.
- The console has no errors; the only message is a three.js warning inherited from 16.
## What doesn't, or is rough
- **Parts and motifs are not time-aligned.** Later, each part could get a `startSec` (the type already
has it), or better, an LRC line index, by marking the first line of each part once per song. That
could be a small JSON sidecar in the content folder, keyed by song, mapping part → first line index;
the line's timestamp then gives the time. Motifs could be stamped the same way, with a line range
per motif. With that, the player could show "Verse 2" again, put the part ticks on the scrub bar, and
light motif chips as they sound.
- **Keyword starts are approximate.** The Polyphemus passage opens where the Cyclops comes home, not
at the landing on the island (the first "Cyclopes", 8 paragraphs earlier). My Goodbye's Homer
counterpart is debatable. The keywords should be confirmed with the content work.
- Butler's summary line is in capitals from the source, and contains a doubled dash ("—-").
- Headless virtual time does not run the audio clock, so `?t_play` screenshots show the seek point,
not real progress. Real progress was checked over CDP instead.
- Library loading delays the panel by one request (fast locally). A slow server would hold the panel
for up to 4 s before it falls back.
- Desktop 1440×900 only. The `.txt` path is implemented but has not been seen on screen.