Skip to content

Feat/lyrics romanization - #23

Merged
4 commits merged into
Ryoku-dev:mainfrom
PeekMusic:feat/lyrics-romanization
Nov 5, 2026
Merged

4 commits merged into
Ryoku-dev:mainfrom
PeekMusic:feat/lyrics-romanization

Conversation

@PeekMusic

Copy link
Copy Markdown

What changed

An opt-in setting, Settings ▸ Playback ▸ Romanized lyrics (lyrics_romanize, off by default), shows a Latin reading under each lyrics line, between the line and its translation. It covers Korean, Chinese, Cyrillic (Russian, Ukrainian, Belarusian, Serbian, Macedonian, Bulgarian, Kyrgyz), Hindi and Punjabi.

  • crates/core/src/romanize.rs (new): the script is detected once per song. Korean uses Revised Romanization with final/initial merges. Cyrillic picks the language from letters only that language uses. Devanagari and Gurmukhi use tables, with inherent-vowel/schwa deletion and Gurmukhi gemination. Chinese uses the pinyin crate (plain table only, default-features = false).
  • LyricLine.romanized: Option<String>, #[serde(default)]: old cache entries still parse, and an unset reading is not serialized.
  • Romanization runs only on the way to the client (get_lyrics in ryotunesd for both the core and the Spotify paths, and in the Tauri host). The lyrics cache and downloaded .lrc/embedded lyrics stay untouched.
  • Flipping the setting emits a one-shot lyrics-changed event. Open lyrics views (sidebar, Now Playing, mini player) refetch from the cache and swap the lines in place: no loading state, the timing offset is kept, and the view lands back on the active line. A hidden view refetches when it is shown again.
  • The reading sits tight under its line and takes the line's state colour (active/upcoming/past) a step softer. Songs with readings get a little more space between line pairs.
  • Ported from PeekMusic (Metrolist fork, GPL-3.0), credited in the romanize.rs module header the way crates/spotify credits Sonora. Fixes made while porting: merged Korean pairs are written once (국내 → gungnae, not gungnnae), Hindi keeps and drops the inherent vowel correctly (कमल → kamal, not kml), and Russian -ого/-его → -ovo/-evo only at word end (много stays mnogo).

Why

Lyrics in these scripts can't be sung along by anyone who doesn't read them. The Spotify models already carried romanized fields from Sonora, but nothing filled them, and the main provider chain never had a reading.

Surface area

lyrics (QML LyricsPanel in Now Playing, sidebar and mini-player; Svelte LyricsView), settings (QML Settings ▸ Playback, Svelte Settings ▸ Playback), provider-independent core (ryotunes-core), daemon/Tauri get_lyrics + set_setting

Validation

  • ./scripts/release-check.sh
  • cd ui && pnpm check && pnpm build
  • cargo fmt --all -- --check
  • relevant Rust/frontend tests: 9 new tests in romanize.rs (per language, the Japanese guard, identical-line suppression, cache back-compat)
  • affected Ryoku/Hyprland lifecycle tested when applicable: native QML client against a locally built ryotunesd on Ryoku/Hyprland. The toggle flipped with lyrics open in the sidebar, Now Playing and the mini player

The Svelte UI is updated for parity and passes pnpm check, but I could not run it: on Linux ryotunes only launches the native client.

Regression contracts

  • background WebKit hibernation
  • MPRIS / tray / explicit Quit semantics
  • stable Home DOM / scrolling
  • live Ryoku theme bridge
  • main vs mini-player window rules
  • replacement-package rollback / ryoku-desktop safety
  • steady-state CPU / frontend timer behaviour: no timer and no background work. One romanization pass per get_lyrics reply while the setting is on, and one refetch per open lyrics view when it is flipped.

Evidence

  • Release ryotunesd: 30,064,840 → 30,374,504 bytes (+302 KiB, +1.0 %).

  • pinyin alone in an isolated binary: +256 KiB, +136 KiB RSS. No RustSec advisory. Cargo.lock gains only pinyin 0.11.0, which has no dependencies.

  • Steady state, native client in Now Playing with a synced Korean/Chinese track playing, 60 s per phase after 10 s settling, setting flipped over the daemon socket:

    ryotunesd CPU ryotunesd PSS QML client CPU QML client PSS
    Romanization off 0.88 % 206.9 MiB 12.78 % 413.6 MiB
    Romanization on 0.97 % 207.0 MiB 10.33 % 415.1 MiB

    Within run-to-run noise. The +1.5 MiB in the client is the extra text items. Nothing keeps working after the flip.

2026_11_03_17_34_51_screenshot 2026_11_03_17_35_01_screenshot 2026_11_03_17_35_23_screenshot

Follow-up

Japanese romanization, pending a decision on its cost. kakasi adds ~1.5 MiB binary and ~1.2 MiB RSS, is unmaintained since 2022 and misreads common lyrics (君の名は → "kun no mei ha"). lindera + IPADIC downloads a 12.3 MB archive at build time.

…jabi lines

An opt-in setting (lyrics_romanize, off by default) shows a Latin reading under each
lyrics line, between the line and its translation, in the QML client and the Svelte UI.

- crates/core/src/romanize.rs: script detected once per song; Korean (Revised
  Romanization), Cyrillic (7 languages), Devanagari and Gurmukhi are rule/table based,
  Chinese uses the pinyin crate (plain table only). Japanese songs are left alone so
  kanji-only lines never get pinyin.
- Applied in get_lyrics on the daemon and the Tauri host only: the lyrics cache and
  downloaded .lrc files stay untouched, and old cache entries still parse.
- Ported from PeekMusic (Metrolist fork, GPL-3.0) with fixes: merged Korean
  final/initial pairs, Hindi inherent vowel and schwa deletion, Russian -ого.

Release ryotunesd grows by 302 KiB (+1.0 %).
Flipping lyrics_romanize now emits a one-shot lyrics-changed event from the daemon and
the Tauri host. Open lyrics views (sidebar, Now Playing, mini player) refetch the
current track from the cache and swap the lines in place: no loading state, the offset
is kept, and the view lands back on the active line before the frame is drawn. A
hidden view refetches when it is shown again, and a fetch already in flight is
followed by one more.
The reading takes the line's state colour a step softer and drops the plain line's
trailing leading; songs with readings get more space between line pairs.
@PeekMusic

Copy link
Copy Markdown
Author

A note for the Japanese follow-up, after looking into providers and local dictionaries.

Providers. Boidu's TTML sometimes carries Apple's transliteration: a <transliterations> block (xml:lang="ja-Latn", ko-Latn) keyed per line (<text for="L4"> ↔ itunes:key), with word timing. For 夜に駆ける it covered every line. Ryotunes already fetches this TTML; parse_ttml_aaml just skips the block. It can't be the source of romanization, though. Without an API key, Boidu only serves tracks already in its cache (anything else is a 401, per its docs), and the block isn't part of its documented response. Netease has community romaji (romalrc with &rv=-1) for many Japanese songs, none for Chinese, and it would cost an extra request on an unofficial API. At most, Boidu's transliteration could be used opportunistically when the TTML we already fetch has it.

Local dictionaries, measured in isolated release binaries (same profile as the workspace):

kakasi 0.1 vibrato 0.5 + IPADIC (mecab-ipadic 2.7.0)
Binary +1.5 MiB (dictionary embedded) +594 KiB
Shipped data none system.dic.zst, 7.67 MiB, loaded at runtime
RAM +1.2 MiB +59 MiB while loaded (70 MiB peak); back to ~3 MiB after drop + malloc_trim
Load n/a ~190 ms
君の名は kun no mei ha ✗ kimi no na wa ✓
僕の心の全てを奪った — boku no kokoro no subete o uba ta (Boidu: … ubatta)
Maintenance no release since 2022 active

vibrato's readings were right on every test line (一言 hitokoto, 越し goshi, 未だに imadani). What's left is post-processing: joining っ and auxiliaries onto the previous word (uba ta → ubatta, desho u → deshou), plus a particle rule (は/へ/を → wa/e/o), which I already applied in the test. IPADIC's license allows redistribution with its notice.

59 MiB can't stay resident in the daemon. The workable shape would be to load the dictionary only for a Japanese track, romanize, free it and trim, then keep the result with that track's cached lyrics so each song pays the ~190 ms once.

So the choice is roughly: kakasi (small, noticeably wrong readings) or vibrato + IPADIC (+7.7 MiB package, short-lived memory, good readings). Happy to build whichever you prefer as a follow-up, or to leave Japanese out.

@neur0map neur0map closed this pull request by merging all changes into Ryoku-dev:main in 5844e0c Nov 5, 2026
@neur0map

neur0map commented Nov 5, 2026

Copy link
Copy Markdown
Member

Merged, thank you.

@PeekMusic

Copy link
Copy Markdown
Author

Thank you for the merge! Just wanted to remind, the Japanese romanization isn't added yet due to the libraries needed for that being kind of big (check previous comment). Id probably go for the vibrato library, if you're okay with that I'll add that soon.

@PeekMusic
PeekMusic deleted the feat/lyrics-romanization branch November 5, 2026 19:13
@PeekMusic PeekMusic mentioned this pull request Nov 5, 2026
8 of 13 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants