Add a scrubbable timed lyric editor to the admin page, LRCLIB regeneration tooling with lyrics skills, and keep the five playback buttons on one row on phones
This commit is contained in:
93
.agents/skills/lyrics-lookup/SKILL.md
Normal file
93
.agents/skills/lyrics-lookup/SKILL.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: lyrics-lookup
|
||||
description: Find and inject lyrics for songs saved on worship.hesed.sbs that have none — LRCLIB first (free, often synced), then an agy web search (untimed), then local faster-whisper transcription. Use when the user asks to add/fill/inject lyrics, says a song has no lyrics, or asks to transcribe saved songs. For replacing lyrics that are already there but wrong, use lyrics-regenerate instead.
|
||||
---
|
||||
|
||||
# Add lyrics to songs that don't have any
|
||||
|
||||
Shared lyrics live on the server (`video_notes`, kind `lyrics`) and every user of
|
||||
worship.hesed.sbs sees the same ones. Every save keeps the previous version as a
|
||||
revision (`video_note_revs`), so nothing is destroyed — but **never overwrite
|
||||
existing lyrics from this skill**; that is `lyrics-regenerate`'s job, and it takes
|
||||
a backup first.
|
||||
|
||||
## Sources, cheapest and best first
|
||||
|
||||
| Order | Source | Timed? | Cost | Where |
|
||||
|---|---|---|---|---|
|
||||
| 1 | **LRCLIB** | usually synced | free, no key | server-side: `POST /api/notes/<id>/lyrics/web` |
|
||||
| 2 | **agy web search** (Genius/AZLyrics/hymnary…) | no — untimed | flat-rate agy | `web_lyrics.py --agy` |
|
||||
| 3 | **faster-whisper** on this machine | synced (word timings) | free, ~0.65× real time | `auto_lyrics.py` |
|
||||
|
||||
Never reach for ElevenLabs Scribe or any credit-billed ASR — the user asked for
|
||||
free transcription only.
|
||||
|
||||
## Credentials
|
||||
|
||||
Writing needs admin. Either env var works for every script:
|
||||
|
||||
```bash
|
||||
export YTP_ADMIN_PASSWORD='…' # the /admin password, see the ytplayer-admin-password memory
|
||||
export YTP_TOKEN='ytp_…' # or an API token minted at /admin
|
||||
```
|
||||
|
||||
The password is also vault secret `YTPLAYER_ADMIN_PASSWORD`
|
||||
(`~/development/.secrets/ytplayer-admin.env`) — use it through
|
||||
`vault__secret_exec` when you must not print it.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
cd ~/development/personal/ytplayer
|
||||
|
||||
# 1) LRCLIB for everything that has no lyrics, then agy for the leftovers
|
||||
YTP_ADMIN_PASSWORD=… python3 scripts/lyrics/web_lyrics.py --missing --agy
|
||||
|
||||
# just look, change nothing
|
||||
YTP_ADMIN_PASSWORD=… python3 scripts/lyrics/web_lyrics.py --missing --dry-run
|
||||
|
||||
# 2) transcribe what the web doesn't have (needs the venv below)
|
||||
YTP_ADMIN_PASSWORD=… ~/.local/share/lyrics-asr/.venv/bin/python \
|
||||
scripts/lyrics/auto_lyrics.py --missing
|
||||
```
|
||||
|
||||
`--missing` lists songs from `GET /api/admin/media` and keeps only the ones with
|
||||
`lyricsLines == 0`. Use `--ids A,B,C` to aim at specific videos. Ids are YouTube
|
||||
ids (11 chars) or uploads (`upl_<12 hex>`).
|
||||
|
||||
Whisper venv, once:
|
||||
|
||||
```bash
|
||||
uv venv ~/.local/share/lyrics-asr/.venv --python 3.12
|
||||
~/.local/share/lyrics-asr/.venv/bin/pip install -r scripts/lyrics/requirements.txt
|
||||
```
|
||||
|
||||
The same transcriber also runs unattended as the `lyrics-worker` container in
|
||||
`docker-compose.yml` (`auto_lyrics.py --watch 300 --state …`), which is what gives
|
||||
newly downloaded songs lyrics "at their own pace". It authenticates with
|
||||
`LYRICS_WORKER_TOKEN`.
|
||||
|
||||
## What lands in the database
|
||||
|
||||
```jsonc
|
||||
{ "lines": [{ "t": 12.4, "text": "Holy You are", "kind": "line" }], // t null = untimed
|
||||
"tags": ["from LRCLIB (synced)"], // provenance — always tag
|
||||
"offset": 0 }
|
||||
```
|
||||
|
||||
Tags in use: `from LRCLIB (synced)` / `(plain text)`, `from the web (untimed) — check and Tap-sync`,
|
||||
`auto-transcribed`, `from the file (synced)` (embedded in an admin upload).
|
||||
The tag is how later runs tell machine lyrics from published ones — keep it accurate.
|
||||
|
||||
## Things that bite
|
||||
|
||||
- **LRCLIB 503/429** on bursts. `lrclib_regen.py:http_json` already retries with
|
||||
backoff; if you write new code against LRCLIB, copy it. Keep ~0.4 s between calls.
|
||||
- **Wrong-artist matches.** A common title ("Still") matches another genre's song.
|
||||
`artist_ok()` in `lrclib_regen.py` is the verification — port it rather than
|
||||
trusting a title+duration hit.
|
||||
- **Karaoke / minus-one tracks have no vocals.** Whisper returns noise; the script
|
||||
calls them instrumental below `--min-words 25` and skips them. That's correct.
|
||||
- **Untimed lyrics are fine.** The app shows them as a plain scrolling list and the
|
||||
user can Tap-sync them in the admin lyric editor.
|
||||
- **Never redistribute.** These are third-party lyrics in a private library.
|
||||
98
.agents/skills/lyrics-regenerate/SKILL.md
Normal file
98
.agents/skills/lyrics-regenerate/SKILL.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: lyrics-regenerate
|
||||
description: Back up and replace lyrics on worship.hesed.sbs that are wrong, mistimed or misheard — typically machine transcripts — with published lyrics from LRCLIB. Use when the user says lyrics are incorrect/off/mistimed, names songs whose lyrics are bad, or asks to re-check songs on LRCLIB. Always backs up first and never writes an unverified artist match.
|
||||
---
|
||||
|
||||
# Replace bad lyrics from LRCLIB
|
||||
|
||||
Machine transcripts (Whisper, Scribe) mishear words and drift out of time.
|
||||
LRCLIB's published lyrics are usually correct and often **synced**. This skill
|
||||
swaps them in — backup first, artist verified, one song at a time.
|
||||
|
||||
Script: `scripts/lyrics/lrclib_regen.py`.
|
||||
|
||||
## The rule: back up before you touch anything
|
||||
|
||||
The server keeps every previous version as a revision, and `/admin` can restore
|
||||
one, but the script **also** writes an offline JSON backup of the current lyrics
|
||||
of every song it will consider — on a dry run too. Do not skip it, do not write
|
||||
your own one-off loop that lacks it.
|
||||
|
||||
```
|
||||
/mnt/c/Users/josh/Documents/ytplayer-lyrics-backup-<YYYYmmdd-HHMMSS>.json
|
||||
```
|
||||
|
||||
(`--backup-dir` or `YTP_BACKUP_DIR` to move it; on the devbox it falls back to `~/`.)
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
cd ~/development/personal/ytplayer
|
||||
export YTP_ADMIN_PASSWORD='…' # or YTP_TOKEN=ytp_…
|
||||
|
||||
# 1) ALWAYS dry-run first and read every line of the output
|
||||
python3 scripts/lyrics/lrclib_regen.py --tagged auto-transcribed
|
||||
|
||||
# 2) apply once the matches look right
|
||||
python3 scripts/lyrics/lrclib_regen.py --tagged auto-transcribed --apply
|
||||
```
|
||||
|
||||
Picking the songs:
|
||||
|
||||
| Flag | Picks |
|
||||
|---|---|
|
||||
| `--ids A,B,C` | exactly those videos |
|
||||
| `--tagged auto-transcribed` | songs whose lyrics carry that tag (machine transcripts) |
|
||||
| `--since '2026-09-19 01:45' --until '2026-09-19 02:00'` | songs whose lyrics were **saved** in that window |
|
||||
| *(none)* | every song the server has lyrics for |
|
||||
|
||||
`--since/--until` is the one to reach for when the user says *"the songs that got
|
||||
lyrics at the same time as X"* — read X's `updatedAt` from
|
||||
`GET /api/notes/<id>` and bracket it by a few minutes.
|
||||
|
||||
Other flags: `--tolerance 6` (max duration difference, seconds), `--loose`
|
||||
(accept matches whose artist doesn't line up — risky, see below).
|
||||
|
||||
## Reading the output
|
||||
|
||||
```
|
||||
eJBlOV6cM7Y LRCLIB synced | Israel Houghton – Holy You Are | 41 lines (was 38)
|
||||
_n6dfB2Z-Ko UNSURE plain | Night Ranger – Still | 52 lines (was 44)
|
||||
↳ artist doesn't match "Hillsong Worship" / the video title — left alone (use --loose to accept)
|
||||
tYM05iaVu3I no match | Jesus At The Centre | … | keeping 60 lines (auto-transcribed)
|
||||
```
|
||||
|
||||
- **LRCLIB** — verified match, will be written on `--apply`.
|
||||
- **UNSURE** — title and duration fit but the artist doesn't appear in the channel
|
||||
name or the video title. **Left alone by default. Do not pass `--loose` to make
|
||||
it go away** — check the song by hand instead; this guard is what stopped a
|
||||
Hillsong song being overwritten with a Night Ranger one.
|
||||
- **no match** — LRCLIB doesn't have it. Existing lyrics are kept. Fall back to
|
||||
`web_lyrics.py --agy --ids <id> --overwrite`, or fix it in the admin lyric editor.
|
||||
|
||||
Replaced songs are tagged `from LRCLIB (synced)` / `(plain text)`, which is also
|
||||
how you tell later what has already been fixed.
|
||||
|
||||
## Restoring
|
||||
|
||||
- Per song, in the UI: `/admin` → recent edits → **Restore** on the older revision.
|
||||
- From the JSON backup: `PUT /api/notes/<id>/lyrics` with
|
||||
`{"data": <songs[id].data>, "baseRev": <current rev from GET /api/notes/<id>>}`.
|
||||
Use the *current* rev, not the backed-up one — `baseRev` is optimistic
|
||||
concurrency, not a version to travel back to.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **LRCLIB rate-limits**: 503/429 on bursts. `http_json()` retries with backoff and
|
||||
the loop sleeps 0.4 s between songs. A song that fails all retries is reported and
|
||||
skipped — re-run it later rather than hammering.
|
||||
- **Duration match is ±6 s** against `/api/streams` metadata. Live or extended cuts
|
||||
legitimately miss; raise `--tolerance` deliberately, per song.
|
||||
- **Title cleaning** strips "(Official Video)", "Lyrics", "[HD]" etc.
|
||||
`clean_title`'s `NOISE` regex is used with `.sub()` and `.search()` — never give
|
||||
it a `/g`-style shared match state; a stateful regex silently skipped every other
|
||||
song once already.
|
||||
- **Karaoke/minus-one** versions match the original recording's lyrics, which is
|
||||
usually what you want, but the timing won't line up. Check before applying.
|
||||
|
||||
Related skills: `lyrics-lookup` (songs with **no** lyrics), `deploy-prod`.
|
||||
Reference in New Issue
Block a user