- voice: first-person throughout (we/our -> I/my), matching the post guideline
- privacy: internal LAN IPs replaced with placeholder hostnames
(n8n.internal, gpu-node.internal); one compose volume path generalised
- ZH twins: Traditional -> Simplified characters (site serves zh-Hans),
whole-file renormalisation where the twin was still Traditional
- dynamicCta: true on 12 posts so the contextual dual-track CTA renders
- long configs wrapped in <details class="tech-deep-dive"> with a summary
naming the artefact and line count
- self-hosted-tts: token refresh now writes .tmp + atomic mv, with a note on
the 401 race the old truncating redirect created
The YuE2 and subtitles posts shipped text-only: render_from_art.mjs only READS
ogImage/banner, and after the 2026-10-19 bulk migration no script had to write
them, so both pages advertised /og-default.jpg and rendered no hero banner.
- new metaphors (in build_captions.py, frozen here via sync_prompts_catalog.py),
captions, art-meta evidence, banner plan and the five derived files per slug
- frontmatter: ogImage + banner in EN and ZH for both posts
- scripts/check-assets.mjs is now step 1 of npm run build, so CI fails when a
published post lacks a language-correct og card, banner, banner .webp or
400/800 list thumb. It reported 16 problems on the unfixed tree, all here.
offsetTop is measured from the nearest positioned ancestor, so inside an
unpositioned .yue2-box it returned the line's distance from the page top
(thousands of px). scrollTop clamped that to maxScroll, so the box jumped to the
bottom the moment singing started and the text was unreadable.
Now: box.scrollTop += (pr.top - br.top) - (clientHeight - lineHeight)/2, plus
position:relative on the box. Verified in headless Chrome with a numeric probe:
the active line sits ~124px into a 274px box at t=48/100, nothing is highlighted
and scrollTop=0 at t=0, and the box settles at maxScroll only for the final lines.
findIndex(t > now) - 1 returned -1 before the first line started, which the
`if (i < 0) i = lines.length - 1` guard turned into the LAST line — so playback
began scrolled to the bottom. Use a last-passed-index scan instead (-1 = before
the first line), centre the active line inside the box (so upcoming lyrics are
visible), and scroll the box itself rather than scrollIntoView(), which dragged
the whole page. Also bumps the box to 250px. Fixes yue2 post + both Suno posts.
Twelve Mandarin songs came back exactly 240.000 s with the outros cut.
Documents the ffprobe tell, the BPM myth, the seed-cache trap, the
2.0-2.8 hanzi/s spread, the 300 s VRAM ceiling, and the archive-the-clipped
policy. Ships 11 finished tracks with ASR-derived synced lyrics (EN + ZH).