Overview
Melofy watches what you are playing and shows the lyrics in a language you read, one line at a time, against the playhead. Timed lyrics come from LRCLIB. The translation goes through Gemini.
It is three things in one repository: a Next.js web app, an MV3 browser extension that puts a lyrics panel inside YouTube Music, and a separate harness that scores translation quality against reference translations.
Motivation
I listen to a lot of music in languages I do not read. The translations that exist are static blocks of prose sitting under the original, and by the time you have found the right line the song has moved on.
I wanted the line to arrive when the line arrives. That is the whole idea, and nothing I could find actually did it.
The second reason was that I wanted to build an LLM feature and be able to say something concrete about whether the output was good. That turned out to be the harder half of the project.
Decisions
A line-at-a-time translation with no context flattens slang and idiom into nonsense. So the first pass writes a brief for the song, covering theme and a small glossary, and the second pass translates each line against that brief. Lines stream back as NDJSON so they appear as they finish instead of after the whole song.
IndexedDB in the browser, then Redis, then MongoDB, then LRCLIB and the model. The key is a normalized hash of artist, title, and target language. A popular song is translated once for everybody, which is the only reason the free tier is affordable at all.
The web app and the extension both go through a single translation service on the server. Cache lookups, the free limit, key handling, and error shapes live there. Two clients reimplementing the same rules would have drifted within a month.
It talks to a running instance over HTTP. Because it shares no code, it measures what is actually deployed rather than a convenient internal function. A second model scores fidelity, fluency, and idiom handling against references, and Langfuse traces let me walk a bad score back to the exact call.
Friction
There is no API for it, so the extension reads the player out of the page. That is a scraper against a UI I do not control and cannot pin, and it will break when Google reshuffles a class name.
Every translation costs money. There is a daily limit per address, stored hashed, and past that you supply your own key. Keys are encrypted in transit and never written to the database, because I did not want to run a service that accumulates other people API keys.
There is no clean metric for "kept the meaning". Scoring with a second model works well enough to catch regressions between model versions, but it agrees with me less often than I would like, and I do not treat the number as ground truth.
The state of it
The limitations, in plain terms. Every project here is something I built and use, not something I am selling.
Stack
This is a side project with a free tier I pay for. Ten translations, then bring your own key.
The extension supports YouTube Music only. Spotify and Apple Music connect on the web, and the extension has no equivalent for them.
Translation quality is uneven across languages. It is good for the ones I can check and unverified for the ones I cannot.
The eval harness measures fidelity, fluency, and idiom. It does not measure whether the timing feels right, which is the thing people actually notice.
There is no mobile app, and the web app on a phone is usable rather than good.
Next project