From 1ce3cb8949eacf734988f742b51a5cbcb981cb09 Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Sun, 20 Sep 2026 18:07:08 +0800 Subject: [PATCH] Document the admin lyric editor, the lyrics skills and the LRCLIB matching guards --- CLAUDE.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 47aaad1..242adc5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -403,6 +403,35 @@ Local DB file: `server/data/ytplayer.db` (gitignored). `BUILD_TAG` is computed f Skills live in `.agents/skills/` (symlinked into `.claude/skills/`): - **deploy-prod** — the Dokploy deploy + buildTag verification flow (manual-only; use for any "deploy"/"release" request). - **mobile-app-ui-design** — UI/UX design work on the PWA screens. +- **lyrics-lookup** — fill in songs that have NO lyrics (LRCLIB → agy web search → local faster-whisper). +- **lyrics-regenerate** — back up, then replace wrong/mistimed lyrics from LRCLIB (`scripts/lyrics/lrclib_regen.py`). + +### Matching a song on LRCLIB (learned the hard way) +`/api/streams` gives the YouTube **channel**, not the artist, so a title+artist +query finds nothing for a lyric-video channel ("Integrity Worship", "Christian +Lyrics") — `lrclib_lookup` falls back to a title-only search and then ranks +candidates whose artist *verifies* 50 points above those that don't. Two guards +sit on the result, and both exist because they caught a real wrong song: +- `artist_ok()` ignores `GENERIC` words. Channel "**Christian** Lyrics" once + vouched for a track featuring **Christian** Burns (Nicky Romero, "Still the + Same Man"). Never verify an artist on a word that says nothing about who + recorded the song. +- `title_run()` compares **whole words, never substrings** — "Still" is a + substring of "(You Can Still) Rock in America", and a one-word title is + rejected outright. It gates the `LENGTH` rule (same title, within 3 s of the + same length ⇒ accept even when the artist can't be checked). +An unverified match is printed as `UNSURE` and skipped; `--loose` overrides that +and is almost always the wrong answer. + +### Admin lyric editor (`frontend/admin.html`) +`/admin?v=` (or the Edit/Lyrics buttons) opens a song with its audio from +`/api/streams`, the waveform from `/api/media/:id/peaks`, and one row per line: +**Set** stamps the playhead, ±0.2 s nudges, **Tap mode** stamps the highlighted +line on Space and walks down, plus shift-all, line/section/cue kinds, a ✎ Text +LRC view and a ⤓ LRCLIB pull. Saves are optimistic (`baseRev`, 409 → keep +mine / load theirs). **Rows are built once per load and then only their classes +and values change** — re-rendering on every tick would steal focus from the +field being typed in and reset the scroll position. ## Commit rules One changeset = one commit, single-line imperative message, **no AI attribution