Song details, lyrics and artist info for every track, in every language - #713
Merged
Merged
Conversation
added 6 commits
September 27, 2026 10:51
The top bar reads as one extraction row with the now-playing card after
it, and the card now names the track from its own tags.
- Composer: Split sits after the options it acts on; the now-playing card
moves to the end of the row behind a rule, labelled "Now playing", with
full-height artwork, the favourite on the left and an info button on the
right. Library rows drop the repeated stem count.
- Audio tags: an upload's artist, title, album and lyrics are read with
ffprobe at import, a link's from yt-dlp (with the channel accepted when
the title corroborates it). Older jobs are filled in once through
POST /api/jobs/{id}/audio-tags.
- Band: looked up on Wikidata during extraction, alongside separation, and
kept only on a strict name match. The info button opens its history,
members and studio albums; a band the user saves always wins.
- Lyrics tab: found by itself on LRCLIB from the tags or the saved band,
kept per track, embedded lyrics used directly. Synced lyrics fill word
by word, timed against the vocals stem's loudness
(GET /api/jobs/{id}/vocal-envelope), and a line click seeks there.
- Library Tags section shows only the open track's tags.
- Fixes a stale thumbnail when switching to a track without one.
Identification runs alongside separation and never fails a job: AcoustID fingerprint (optional user key in Settings) or title parsing, then MusicBrainz for the recording, release year and performers, Wikidata for the performer and the musical or film a song comes from, and LRCLIB for synced lyrics, shifted to the vocal stem when the timing is off. Songs with no lyrics anywhere can be transcribed with Whisper after separation (auto on CUDA, off by default on CPU). Now playing shows the song, the show it comes from and the performer. The lyrics tab plays karaoke style from the server's lyrics.
- Lyrics are kept only when LRCLIB's song and artist match the track (the artist, one credited artist, or the show); otherwise the tab says none were found instead of showing another song's words. - Accent-stripped LRCLIB copies (common for Polish) get their letters back from an intact copy, or from a Whisper transcript when none exists, keeping LRCLIB's timing. - Tags written in cp1250/cp1251 or UTF-8 read as Latin-1 are decoded. - The artist box shows official website, Instagram, Spotify and Apple Music links from Wikidata when they exist. - A band lookup that fails no longer hides the musical or film. - Settings shows only the last two characters of the AcoustID key. - Regression tests for failing services, cancel and delete during lookups, and data written before identification existed.
- The error banner's rules were on the "error" class, which every small status line also takes, so a lyrics or artist-box error floated over the top bar as a faint strip nobody could read or close. The banner is now styled by its id: a solid card with an icon, a close button and Escape to dismiss; status lines stay in their panels. - The timeline ruler spaced labels a minute apart at most, piling 59 labels onto an hour-long video. Long tracks now step up to 5, 10, 15 minutes and beyond, at most about 12 labels across; songs are unchanged. - Mending lyrics that lost their accents now runs on CPU and Mac under "auto" too; only "off" stops it. Full transcription stays CUDA-only.
Chinese, Japanese and Korean artists got almost nothing: on a benchmark
of real YouTube uploads, 0 of 6 Chinese and 0 of 5 Korean songs found
an artist, where English found 4 of 4. Now 6 of 6 and 5 of 5.
- Titles: CJK brackets and quotes, _ and full-width separators, MV and
other noise in each language, names written in two scripts
("周杰倫 Jay Chou", "IU(아이유)") kept as alternatives, compilations.
- MusicBrainz: every alternative name is searched, an artist is found by
name or alias in any script, covers and tributes are never taken, and
a busy answer (503/429) is asked again. LRCLIB likewise.
- A band from the title alone only when two names in different scripts
point to one artist, or a compilation names one artist throughout.
- Lyrics match across scripts, traditional and simplified Chinese and
full-width text, through the artist's MusicBrainz and Wikidata
aliases; songs filed under a translated title are found through the
recording's other titles. Another artist's song is still never kept.
The traditional-to-simplified table is derived from OpenCC
(Apache-2.0), with its licence in app/_vendor/opencc.
- Karaoke fills Chinese and Japanese a character at a time and Korean
by syllables; each line carries its language, so Korean wraps between
words and Han characters take the line's own glyph forms.
- The artist box reads the artist's own Wikipedia when the reader's and
English have only a stub, shows the native name beside the translated
one, searches a name in its own script, and gives a zh-Hans reader
simplified text.
A key AcoustID refuses was saved all the same, and every import then fell back to the title search without a word. AcoustID gives each account two keys of the same shape, and the user key on the profile page, the one people paste, cannot look fingerprints up. The key is now tried once on save with a fingerprint that is not one: refused for the key, it is not saved and Settings says to use an application key from acoustid.org/new-application; refused for the fingerprint, it is good. Offline it is kept on trust. The key goes only to AcoustID, in a POST body, and is never logged or echoed. STEMDECK_ACOUSTID_CHECK_KEY=0 turns the check off, as the browser tests' server does.
added 3 commits
September 28, 2026 07:57
Code-quality review on #713: the NaN test on an identity's score is now math.isfinite, which also turns an infinite score to 0, and the empty except around killing a finished fingerprint says why it is empty.
…y check - Lyrics repair matched a stripped word against a regex of optional groups, which backtracks exponentially: two LRCLIB uploads with a word of forty accented letters froze the server for hours. It is now a linear walk over the positions reached. - MusicBrainz retries on 503/429 ignored a cancelled job on most paths, including the wait for its turn, and could run about a minute per request. Every lookup hands its cancel check down, and one request never runs past MUSICBRAINZ_REQUEST_BUDGET_SEC in all. - The AcoustID key check on Save waited on a server that sends one byte at a time; it now waits at most twice the request timeout and keeps the key on trust past that. A refusal with no reason code is not a verdict on the key. - Escape closed the error box and the popup beneath it together; the box now takes that Escape alone. - A dead timeout branch in the artist box is gone. Tests for what the range added without one: MusicBrainz retries and cancel, work titles, title aliases, alias lookups and their caches, mending gates, the key check when AcoustID is down or slow, the ruler at 30 s to 13 h (its tick logic moved to rulerTicks.js), native Wikipedia editions, the error box at phone width, and a gate holding the browser's name matching and Chinese table identical to the server's.
… found tests/acceptance drives the portable StemDeck.exe over WebView2's DevTools port against the real services, one test per manual checklist id, on request only: `npm run test:acceptance`. It never runs in CI (the default config's testDir is tests/e2e, and it refuses to start when CI is set), isolates itself from the installed app with a throwaway LOCALAPPDATA, reads an AcoustID key only from the environment and never writes one, and reports service outages apart from bugs. Its first run found two bugs, both older than this branch: - A dev build (0.18.2.dev0) was offered an update to 0.18.1: the dev check needed a word boundary after "dev" (#714). - Cancel waited out yt-dlp's metadata probe, up to twenty seconds: the probe now runs on its own thread and a cancel stops the wait at once, as it does the pause between network retries (#715).
added 10 commits
September 28, 2026 21:40
… hand - LRCLIB's copy of a track's own length was trusted as the track's timing and never checked. Green Day "Basket Case" (the video) sings 16 s after that copy's first line. Exact-length LRCLIB lyrics are now checked against the vocals too and moved when the gap is clearly a second or more; a transcription and a file's own lyrics are never moved (#716). - The Lyrics tab gets Align: start the lyrics (or a chosen line) at the playhead, nudge earlier or later with the offset shown, auto-detect from the vocals (saying so when it cannot tell), and reset. The offset is kept beside the lyrics (offset_sec in lyrics.json, or the browser's store), never rewritten into them, and survives a re-split. New endpoints POST /api/jobs/{id}/lyrics/offset and .../lyrics/align (#717). - Acceptance check L4 runs auto-detect on the real Basket Case video.
Every change is saved as it is made, so Done only closes the panel, as Escape does. Filled in the accent colour, so it reads as the way out. In all ten languages.
Lyrics kept before a lookup was held to the track's own song and
artist were never looked at again: NIHIL "Barro" kept Renato Vianna's
"Joao de Barro" from a fuzzy search. GET /api/jobs/{id}/lyrics now
checks LRCLIB lyrics by that rule and drops them for good when the
song matches neither the track's name nor its other titles, or the
artist differs in the same script (another script was matched through
aliases and stands). Other versions of another song are no longer
offered. The Lyrics tab drops an entry it kept itself for another
song and looks the track up again; a version the user picked keeps its
artist, since a cover can be their choice.
…settings tab (#718) - Settings gets a Song details tab after General, holding Song identification (the AcoustID key, moved), Discogs (new) and Transcribe lyrics (moved). - A Discogs personal access token, optional, works like the AcoustID key: checked with Discogs on save (refused tokens named, offline kept on trust, the wait bounded), only its last two characters shown, never logged, put in a URL or sent to the browser. - With a token, the artist box fills from Discogs what Wikipedia lacks: a band's profile, members, releases and links, credited "Data provided by Discogs". The band is found through its MusicBrainz or Wikidata link to Discogs, or a release crediting that exact artist with that song; a shared name alone is never enough. Served by GET /api/jobs/{id}/artist-extra; without a token nothing is sent.
…718) - The box names Wikipedia wherever it searched or found nothing. With no Discogs token, a note says only Wikipedia was searched and opens Settings on Song details; it shows when nothing was found or a band has no history, members or albums, never for a band Wikipedia covers. - With a token, a name Wikipedia does not know lists Discogs artists to pick from (a shared name is the user's to settle), and the pick is shown from Discogs with its credit. GET /api/discogs/artist?q= and /api/discogs/artist/{id}; the token stays on the server. - The privacy text says typed names go to Discogs with a token set.
…together (#706) "NataliaKukulska" found nothing on MusicBrainz or LRCLIB, so W biegu lost its lyrics. The channel's spelling is taken only when it adds accents and keeps the words apart.
…ied reader (#709) Wikidata sometimes files a traditional title under zh-hans, which left 11月的蕭邦 beside 周杰伦的床边故事. Any Chinese label is folded now; folding one already simplified changes nothing.
Code quality flagged it. It is raised again on the job's thread; narrower, a SystemExit from yt-dlp would end the probe thread quietly and the import would go on with no answer.
… them by hand (#716, #717) Lyrics timed to another cut of a song drift line by line, which one offset cannot fix. The vocals are transcribed with word timestamps in the lyrics' own language, the words aligned in order to the lyric (per character for Chinese and Japanese, per syllable for Korean), each line start put on the voice's onset, and lines the first pass missed heard again on their own. The result is kept only when enough of the lyric was found; otherwise the lyrics stay as they were. A held first word no longer loses its line. Sync lines is the manual side: tap T or Enter as each line starts, drag a line's marker on the Lyrics lane above the waveform like the beat grid, nudge, undo and redo, re-time from the vocals, or reset. A timing the user made always wins over a detected one and is kept with the lyrics. Also: the name search keeps the artist's own song in another length and times it from the vocals; deleting a track waits for a running re-time to let go of the vocals; the acceptance suite drags with the real Windows cursor when the DevTools mouse does not reach WebView2.
- Opening the Lyrics tab dropped lyrics the lookup had found under another of the artist's names (Cat Stevens for Yusuf Islam), and the user's own timing with them. The lookup now marks those "by_alias", and lyrics the user timed, moved or that were found line by line in the vocals are never dropped (#702). - Escape and the other Sync lines keys were taken from a dialog open over the Lyrics panel: the app's dialogs are role="dialog" elements, not native <dialog>s, so none was ever seen as open (#717). - Lyrics with a byte order mark or a line break other than "\n" split into different lines in the page and on the server, so saving the user's own timing of them was refused. Stored lyrics now carry one kind of break (#717). - Two patterns were quadratic on crafted input: a line of unclosed "<" sent to /lyrics/align held the server for seconds, and blank lines in a file's lyrics tag for most of one (#716). - Automatic alignment could write over an offset the user set while it worked. It now writes under the Lyrics tab's lock, and only when nothing changed meanwhile (#716). - The Discogs token would have been sent on to wherever a redirect pointed; it now goes to Discogs alone (#718). - A damaged audio_tags read back from disk made the lyrics lookup raise; tags are cleaned like the other fields (#706). - Deleting or cancelling a track while its audio was being fingerprinted could fail on Windows with the file still open: the fingerprint is stopped first (#706).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The track that is playing now knows what it is. Each import is identified while it separates, the now-playing card names the song, the show it comes from and the artist, and the Lyrics tab follows the singer karaoke style. It works the same for a Mandarin ballad or a K-pop single as for an English band.
Closes #699
Closes #702
Closes #703
Closes #704
Closes #705
Closes #706
Closes #707
Closes #708
Closes #709
Closes #710
Closes #711
Closes #712
Closes #714
Closes #715
Closes #716
Closes #717
Closes #718
What changes for someone using it
tests/acceptancedrives the real desktop build against the real services, one test per manual check, on request only and never in CI. Its first run found A dev build like 0.18.2.dev0 is offered an update to an older release #714 (a dev build offered an older release) and Cancel is ignored for up to 20 seconds while a YouTube link is being resolved #715 (Cancel deaf while a link resolves), both fixed here.Measured
A benchmark of 45 real YouTube uploads across the ten languages, artist found / lyrics found:
The two Korean and Portuguese lyrics still missing in that run were LRCLIB answering 503; LRCLIB is now asked again when it is busy, and both are found. The remaining misses are a fan upload shorter than any recording and a TV performance.
Checked with real imports: Wicked "Dancing Through Life", The Greatest Showman "This Is Me", The Phantom of the Opera, Natalia Kukulska "W biegu", and a 59 minute Teresa Teng compilation, whose artist is now found with no click.
Line timing, checked on 57 songs across all ten languages (1,466 lines): where the timing from the vocals and LRCLIB's disagreed by more than half a second, that stretch of the vocals was transcribed again on its own to see which was right. Share of lines starting within half a second of the singing:
What is left is mostly repeated filler ("na-na-na") landing on a later repeat, which Sync lines fixes by hand.
Size
About 50k added lines, but most of it is not code:
It is one pull request because it was built and tested as one build, and the acceptance run passed on exactly this combination. One real duplication is known: lyrics are matched to a song both on the server and in the browser. #719 tracks moving that to the server alone.
Worth knowing before merging
uv.lockchanges (openai-whisper, tiktoken, regex, more-itertools), so existing desktop installs get the full download once rather than an in-app update.app/_vendor/openccand is listed in the three notice files. No new dependency comes with it.Tests