--- 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-.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": "", "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 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=`, 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 () (synced\|untimed)` | `--from-file` | Related: `lyrics-lookup` (LRCLIB first, then this), `lyrics-regenerate` (replace wrong lyrics from LRCLIB), `deploy-prod`.