Add lyric-line service with line-centric model and song notes - #65
Merged
Conversation
Introduce the lyric line as a first-class, ordered, multilingual, singer-attributed unit, layered additively over the existing songs row. Schema (rust-backend/src/db/sqlite.rs): - lyric_lines(id, song_id, position, kind, content, phonetic, singers) with kind = 'lyric' | 'silence' and a UNIQUE(song_id, position) index - lyric_line_translations(line_id, lang, text, phonetic) — one row per (line, language), so a song gains a language without a schema change - song_notes(id, song_id, position, content) — free-form notes attached to the whole song, outside the lyric sequence - songs.lines_migrated flag + idempotent migration Model (models/lyric_line.rs): LyricLine, LineKind, LineTranslation, SongNote, CreateLineData, UpdateLineData. Service (services/lyric_lines.rs): lazy backfill from the legacy parallel arrays on first read (guarded by lines_migrated so deleted lines are not resurrected), line CRUD with position re-packing, safe two-pass reorder, per-language translation upsert/remove, and song-note CRUD. 12 tests. Tauri commands + frontend TS bindings (types + invoke wrappers) added. The songs parallel arrays remain the current source of truth; migrating the practice UI to LyricLine and dropping the redundant columns is a later phase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
Frontend (CI Frontend job was failing at `npm ci`): - Regenerate package-lock.json — it predated vitest and most devDependencies, so `npm ci` aborted on missing lockfile entries. - Add the `test:unit` script the workflow invokes (alias of `vitest run`); it was referenced by ci-frontend.yml but absent from package.json. Backend (CI Rust `cargo test` step): - Gate `test_generate_phonetic_returns_original_in_stub` and `test_create_song_supported_language_populates_phonetic_lyrics` behind `#[cfg(not(feature = "python"))]`. Both assert the stub behaviour (original text echoed); under the default `python` feature the backend romanizes (or errors when the Python packages are absent), so they only hold for the no-python build. The python path stays covered by the `#[ignore]`d tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
Phase 3 (partial): make the line model the source of truth on the read path, without changing behaviour for existing (backfilled) songs. - useLinesStore (stores/lines.ts): fetch (TTL cache) + full mutations for lines, translations and notes over the cmd_*_line / cmd_*_note commands. 12 vitest specs. - projectLinesToSong (lib/line-projection.ts): pure, tested bridge that renders a song's lines back into the legacy parallel-array Song shape the practice modes still expect — silences excluded, line edits reflected. 8 vitest specs. - SongDetailView: fetch lines + notes on mount; feed the practice modes (Karaoke/FillBlank/Mcq/Oral) the projected song so their data now flows from the line store; render a Notes section. The mode components are left untouched. Remaining tail (not in this commit): migrate the standalone practice views (PracticeOralView/QuizView/…, separate inline implementations) to the line store, then drop the redundant songs parallel columns once every consumer reads lines. Both need runtime QA of the desktop app. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
Migrate the four standalone practice views (Oral/Quiz/Karaoke/FillBlanks) off `song.lyrics` and onto the line store: each now fetches the song's lines on mount and derives its `lyrics` list from `linesStore.lyricLinesFor` (silences excluded). Behaviour is unchanged for existing songs — backfilled lines reproduce the original lyric lines exactly — and silences added via the line API are now correctly skipped in practice. With this, every practice surface (both the mode components inside SongDetailView and these standalone views) sources its sequence from the line model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
…umns Complete the line-centric migration: the lyric sequence now lives only in lyric_lines / lyric_line_translations. The redundant songs columns (lyrics, phonetic_lyrics, translations, line_singers) are dropped. - Migration (sqlite.rs): explode every un-migrated song's legacy arrays into lines, then ALTER TABLE ... DROP COLUMN the four parallel columns. Guarded by a column-existence check so it's a no-op once applied. `songs.singers` (the song's singer roster) is kept — it is not per-line data. - songs service: save_song / update_song persist the sequence via lyric_lines::write_song_lines; get_song(s) reconstruct the parallel arrays from lines via load_song_arrays + hydrate. The Song API shape is unchanged, so the Tauri commands and the whole frontend are unaffected. - lyric_lines: ensure_backfilled is now a plain existence check (no lazy column read); write_song_lines (Song → lines) and load_song_arrays (lines → Song arrays) are the round-trip bridge. The full backend suite (151 tests, incl. a new legacy-migration test that backfills then drops columns and checks idempotence) passes with the default `python` feature; clippy -D warnings is clean. Caveat: a legacy song update rebuilds the whole line set, so silences added via the line API are not preserved across a cmd_update_song call. No UI adds silences through that path today. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG
14 tasks
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.
Summary
Introduces a new line-centric data model for songs, replacing the legacy parallel-array structure with normalized
lyric_linesandlyric_line_translationstables. The service lazily backfills lines from the legacy arrays on first read, guarded by a migration flag to prevent resurrection of deleted lines. Also adds support for free-form song notes.Key Changes
Backend (Rust)
lyric_linesservice module with full CRUD operations for lines and translationsget_song_lines()— fetch all lines of a song (ordered, with translations)create_line(),update_line(),delete_line()— line lifecyclereorder_lines()— reorder a song's lines with atomic position updatesset_line_translation(),remove_line_translation()— manage per-language translationsget_song_notes(),create_note(),update_note(),delete_note()— song notes CRUDlyric_linemodel withLyricLine,LineKind(lyric/silence),LineTranslation, andSongNotetypeslyric_linestable (id, song_id, position, kind, content, phonetic, singers, timestamps)lyric_line_translationstable (line_id, lang, text, phonetic)song_notestable (id, song_id, position, content, timestamps)lines_migratedflag onsongstable to guard backfill idempotency(song_id, position)to enforce contiguous line orderingensure_backfilled()) explodes legacylyrics,phonetic_lyrics,translations, andline_singersarrays into normalized rows on first readreindex()) uses two-pass negative offset to avoid unique constraint violations during reorderingFrontend (TypeScript/Tauri)
LineKind,LineTranslation,LyricLine,SongNotetauri-api.tsfor lines and notestypes/index.tsTauri App Integration
lib.rsImplementation Details
get_song_lines(), minimizing migration overheadlines_migratedflag ensures backfill runs exactly once per song; subsequent reads skip the migrationON DELETE CASCADEensure translations and notes are cleaned up when lines or songs are deletedUpdateLineDatausesOption<T>to allow selective field updates without overwriting unspecified fieldshttps://claude.ai/code/session_01ReeGwy3JKhaXxkxCWYYQsG