Find lyrics with agy when LRCLIB has nothing, keeping the best of several non-deterministic answers

This commit is contained in:
Jonathan Sykes
2026-09-21 11:44:43 +08:00
parent 808306ffce
commit e6d14b9eb4
4 changed files with 344 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
---
name: lyrics-agy
description: Find lyrics (with timings when they exist) for worship.hesed.sbs songs using agy, the flat-rate Antigravity CLI, via the agy-bridge MCP server. Use when LRCLIB has no match, when a song still has no lyrics or bad machine-transcribed ones, or when the user says "ask agy for the lyrics". Also how to paste lyrics you already have into a song.
---
# Lyrics from agy
agy searches the open web and, when the song has a caption track or a published
sync, returns **timed** lyrics in LRC form. It is flat-rate, so a run costs
nothing per song — but it is the **last** resort, after LRCLIB (`lyrics-lookup`,
`lyrics-regenerate`), because LRCLIB's synced lyrics are published data while
agy's are derived.
Script: `scripts/lyrics/agy_lyrics.py`.
## Run it
```bash
cd ~/development/personal/ytplayer
export YTP_ADMIN_PASSWORD='…' # or YTP_TOKEN=ytp_…
# every song that still has no lyrics — dry run first, ALWAYS
python3 scripts/lyrics/agy_lyrics.py --missing
python3 scripts/lyrics/agy_lyrics.py --missing --apply
# one song, replacing lyrics that are wrong
python3 scripts/lyrics/agy_lyrics.py --ids ZHl6EwSwjv0 --overwrite --apply
# lyrics you already have (LRC or plain text), no agy call at all
python3 scripts/lyrics/agy_lyrics.py --ids ZHl6EwSwjv0 --from-file words.txt --overwrite --apply
```
Every run backs the current lyrics up to
`Documents/ytplayer-lyrics-backup-<stamp>.json` before writing, and the server
keeps each previous version as a restorable revision.
**One song takes ~2 minutes per attempt**, and `--tries` defaults to 3, so a
batch is slow. Run it detached rather than in a foreground command that will hit
a timeout:
```bash
setsid nohup python3 scripts/lyrics/agy_lyrics.py --missing --apply \
> /tmp/agy-lyrics.log 2>&1 < /dev/null &
```
## How it calls agy
Through MCPJungle, so no agy CLI contract is hard-coded here:
```bash
mcpjungle invoke agy-bridge__agy_ask --input '{"dir": "<repo>", "prompt": "…"}'
mcpjungle invoke agy-bridge__fetch_output --input '{"keep_id": "ask-…"}'
```
Four things about that interface cost real debugging time — do not rediscover them:
1. **`dir` is required.** Without it the call fails schema validation.
2. **The answer arrives on STDERR**, not stdout. Read both streams or you get
an empty string and conclude, wrongly, that agy found nothing.
3. **Long answers are truncated** with `fetch_output(keep_id='…') for more`.
A full set of lyrics is almost always longer than the cap, so always follow
the `keep_id` — otherwise you silently save half a song.
4. **agy can fail and still look like a success.** A quota-exhausted instance
returns prose like `WHY: agy-ask failed (rc=1)` inside a `STATUS: ok`
envelope. One such line even carried a `[05:37.76]` stamp and parsed as a
perfectly good synced lyric. `FAILED` in the script rejects those; keep it.
## It is not deterministic — that is the main gotcha
The same question can come back synced, plain, or empty on consecutive calls.
Observed in one sitting on "Jesus At The Centre": 39 timed lines, then 43
untimed, then nothing. So the script asks up to `--tries` times and keeps the
**best** answer (`score()`: any timed lines beats none, then more lines beats
fewer), stopping early once a timed answer arrives.
If a song saves untimed and you believe a timed version exists, just run it
again with `--overwrite`.
## What it filters out, and why
agy streams its own progress into the answer. Everything below is dropped
before parsing, and every pattern is there because it once ended up saved as a
lyric line:
- `STATUS:` / `SUMMARY:` / `MODEL:` / `INSTANCE:` / `AGY-META:` / `WHY:` envelopes
- `Waiting for task execution…`, `Background task <uuid> completed with…`
- section labels that are not sung (`Verse 1`, `[Chorus]`, `x2`)
- anything over 200 characters (a paragraph of commentary, not a sung line)
After filtering, an answer shorter than `--min-lines` (6) is rejected outright.
## Reviewing before you trust it
agy derives timings, so check them once per song before relying on them in a
service:
- The last timestamp should land near the song's length (a 384 s song ending at
367 s is right; one ending at 120 s means it only got a verse).
- Timestamps must increase monotonically.
- Open `/admin?v=<id>`, press play and watch the highlighted line track the
vocal. Fix drift with **Shift all**, or retime individual lines with **Set** /
**Tap mode**.
Untimed results are fine — the app shows them as a plain scrolling list, and
Tap mode turns them into synced lyrics in one pass of the song.
## Provenance tags
Written into `data.tags` so a later run can tell where lyrics came from:
| Tag | Meaning |
|---|---|
| `from the web via agy (synced)` | agy, with timings |
| `from the web via agy (untimed) — check and Tap-sync` | agy, words only |
| `from a file (<name>) (synced\|untimed)` | `--from-file` |
Related: `lyrics-lookup` (LRCLIB first, then this), `lyrics-regenerate`
(replace wrong lyrics from LRCLIB), `deploy-prod`.