274_release2.wav (2026-09-17) supersedes 274_release1.wav and is the master
used by the white/lyrics renders and Song274Transect. Same length, 10:19.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Song274Transect draws Route 274 in its true shape (the "Ground" view of
song-274/route/274-transect.html) and plays it along with the song.
- data.ts: reads song-274/route/ at load time (lyrics index, roster, route and
the optional east/west stubs); nothing is copied into the code. Only sung
nodes are shown; districts and 東に続く light nothing.
- progress.ts: a "covered so far" paint over the dimmed road. Its head is pinned
to each sung object's km as that line starts and glides between pins on a
monotone cubic, so it never runs backwards. After 東に続く it runs out along
the east stub; the west stub is always painted.
- camera.ts: fixed close zoom during each narration part (lines under 20 s
apart), following a Gaussian-smoothed head; pull back to frame the covered
road plus some road ahead during breaks, and to the full map after the lyric.
- nodeAnimation.ts: lightUp / switchOff as reusable functions over a NodeLook,
with long lines switching off after MAX_LIT_S.
- palette.ts: every colour as a named token (dark default, light alongside),
including shrine and railway families.
remotion.config.ts sets song-274/ as the public dir so staticFile() reaches the
song's audio/ and route/ in place.
Verified: eslint + tsc clean; stills checked across parts, breaks and the ending.
Render with --muted and add audio with ffmpeg: Remotion's own AAC lands 42.75 ms
late.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
Text-based deliverables for the 274 video, synced to the lyrics source of
truth in the wiki repo (ncmr-songs wip/274/274.lrc at c7e2494, "put the lyric
back into road order").
- 274.lrc: byte-identical copy of the wiki LRC.
- 274.srt: generated from it with scripts/lrc2srt.py (117 cues).
- 274.ass: hand-styled in Aegisub; every event re-timed to the LRC, with each
address shown from its own timestamp until its section's last place ends.
- BOM.md: untracked assets with BLAKE2b hashes — release audio
274_release1.wav, the white background plate, and the Noto Serif JP
Medium/SemiBold fonts the ASS uses.
Verified: all 117 ASS events and SRT cues match the LRC's order, text and
timestamps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PLt25nCVMsoroKALYUBu6u
Work happens on both Windows and Linux, so a committed lockfile would do harm
rather than good here:
- Every dependency in package.json is pinned exactly (no ^ or ~), so a lockfile
has no floating ranges to guard. A plain `npm install` resolves identically
without it.
- The lock records platform-specific optional binaries for whichever OS built it
(@rollup/rollup-linux-x64-gnu vs -win32-x64-msvc, for instance). Committed from
one platform it is wrong on the other, and `npm ci` refuses rather than
adapting, since reproducing the recorded tree exactly is its entire purpose.
That is a strict mechanism pointed at the wrong target: it would break installs
across machines while guaranteeing versions that were already exact. Cloning is
rare here, so the reproducibility upside is small and the cross-platform cost is
immediate.
Both .gitignore and README now state the reasoning, including when to revisit (a
dependency loosened to a range, or transitive drift exact pinning cannot control).
Silently ignoring it would have invited the same question again.
Verified: `git check-ignore -v package-lock.json` matches, and a regenerated
lockfile leaves `git status` clean.
Records why archive/feat-bom-generator exists and what was in it, so a future
reader does not have to reconstruct the reasoning from the branch name. Also
notes that archive tags need an explicit push.
`feat/bom-generator` carried one commit that never landed on master: file
content hashes in the BOM ("v2 with hashes"). The Python rewrite dropped that
capability, so port it forward rather than losing it when the branch goes.
- `--hash` records a short BLAKE2b digest per file. Opt-in, because it reads
every asset in full and that is wasted work for a plain inventory.
- Hashes are streamed in chunks with `hashlib.blake2b`, not `md5` via haveged
imports, and not whole-file reads — these assets run to hundreds of megabytes.
- YAML gains `hash:` after `size:`; Markdown gains a hash column only when
hashes were requested, so the common table stays readable.
Fix a pre-existing bug found while testing: the BOM marker was written as
`; generated by ...`, but `;` is not a YAML comment, so every generated BOM
failed to parse with "mapping values are not allowed here". Now `#`. Verified by
re-parsing output from both the PyYAML path and the built-in fallback writer.
Sizes alone cannot tell you whether an asset changed; a same-length re-render
keeps the byte count identical. That is the gap the hash closes.
Make each directory answerable by path alone: src/ is what the Remotion build
compiles, scripts/ is what you run by hand.
Removed:
- `ffmpeg.sh` — never worked; every parameter was assigned `$1`, so bake/bypass
were broken from the start. Superseded by `scripts/ffmpeg_tools.py bake|still`.
- `src/lrc2srt.ts` — the TypeScript port; `scripts/lrc2srt.py` is the script that
was actually maintained, and it now handles the multi-timestamp cases the TS
version did.
- `scripts/bom-gen.ts` — superseded by `scripts/bom_gen.py`.
- `scripts/ffmpegusage.txt` — legacy command notebook. Every recipe in it is now
reproduced by `ffmpeg_tools.py`, including the `-map 0:v:0 -map 1:a:0 -c:v copy`
mux and the FLAC `-compression_level 12` case, so it has no unique content left.
Also drops the `bom-gen` alias from the dispatcher (dash-case was TypeScript
naming) and documents the boundary in README.md and REMOTION-QUICKSTART.md.
`npm run lint` (eslint src && tsc) passes with src/ reduced to Remotion input.
The repo is still being assembled as a templated workspace, so the branch
conventions are the intended end state rather than a current rule. Recording
that in the README so the 'master is protected and immutable' line is not read
as an active restriction while the template is still taking shape.
Replaces the scattered TypeScript/shell helpers with one Python toolchain that
runs as plain files (`python3 scripts/tools.py <tool> ...`) with no third-party
dependencies.
New tools
- `_common.py` shared plumbing (Report, run, ffmpeg discovery, argparse ergonomics)
- `hwprobe.py` what this machine can actually encode (software + hardware)
- `srt2ass.py` SRT -> ASS, the missing step between lrc2srt and baking
- `ffmpeg_tools.py` mux / bake / still / strip / probe, with hardware encode
- `bom_gen.py` BOM generator, ported from scripts/bom-gen.ts
- `tools.py` dispatcher so any tool is one command away
- `README.md` usage for all of the above
Rewritten
- `lrc2srt.py` now handles multiple timestamps per line, timestamp-less cue lines
and blank/comment lines, and survives malformed input. The legacy version
emitted an empty cue plus a stray cue for every line.
Hardware encoding
This host is AMD (Strix Halo, VCN 4.0.5), so nvenc in the legacy notes is
reference only. `hwprobe.py` probes and reports what genuinely works here:
VA-API via /dev/dri/renderD128 gives h264_vaapi, hevc_vaapi and av1_vaapi.
`ffmpeg_tools.py --video auto` picks the hardware encoder when the render node
is usable and falls back to libx264/libx265 when it is not.
Two bugs found and fixed while wiring this up:
- the VA-API capability probe fed frames straight to the encoder without
`format=nv12,hwupload`, so ffmpeg failed with "Function not implemented" and
the toolchain silently fell back to software on a machine where hardware
works. VA-API encoders only accept GPU-resident frames; the probe now uploads.
- `bake --video copy` only failed when the copied stream happened to be VA-API.
Burning subtitles always changes pixels, so it now refuses `copy` outright
with a clear message.
Also makes -q/-n usable on either side of the mode word, and ignores
__pycache__ in .gitignore.