Skip to content

feat(lyrics): word-level karaoke (enhanced LRC + TTML)#25

Merged
InstaZDLL merged 7 commits into
mainfrom
feat/word-level-lyrics
May 15, 2026
Merged

feat(lyrics): word-level karaoke (enhanced LRC + TTML)#25
InstaZDLL merged 7 commits into
mainfrom
feat/word-level-lyrics

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 15, 2026

Summary

  • Adds word-level synchronized lyrics on top of the existing line-level LRC pipeline. Two new source formats: Enhanced LRC ([mm:ss.xx]La <mm:ss.xx>nuit <mm:ss.xx>tombe) and TTML (Apple Music XML).
  • Karaoke fullscreen + side panel animate the active word (opacity + scale, 150 ms transitions); plain LRC keeps its existing line-level highlight.
  • Lyrics editor gains a Line ↔ Word granularity toggle. In word mode: Space stamps the next word, Enter advances the line, Backspace undoes the last word. Output is serialized back to Enhanced LRC.
  • Detection is content-based (detect_format) so embedded USLT frames carrying Enhanced LRC are auto-recognised — no per-track config.
  • TTML on MP3 is skipped at file-write time (lofty has no clean ID3v2 mapping for arbitrary XML); the DB cache is still updated and the editor surfaces a lyrics.toast.tagWriteSkipped warning so the user knows the file itself wasn't touched.
  • Lyrics format 'ttml' added to the app.lyrics CHECK constraint via a new migration (table rebuild — SQLite has no ALTER CONSTRAINT).
  • i18n: 9 new keys (lyrics.format.*, lyrics.toast.tagWriteSkipped, lyricsEditor.granularity.*, lyricsEditor.captureHintWord, lyricsEditor.notCaptured) propagated to all 17 locales.

Test plan

  • bun run typecheck
  • bun run lint
  • cargo check --manifest-path src-tauri/Cargo.toml --all-targets
  • cargo test commands::lyrics — 8 tests green (Plain / LRC / EnhancedLrc / TTML with + without XML decl / LRC headers non-regression / word_stamp_present)
  • Manual: word-level karaoke renders correctly on Enhanced LRC import (panel + fullscreen).
  • Manual: TTML import on a Vorbis / FLAC file → tag written via ItemKey::Lyrics, re-fetch returns from cache.
  • Manual: TTML on MP3 → tag_write_skipped warning surfaces, DB cache updated.
  • Manual: editor word-mode capture cycle (Space → Enter → Backspace), save with write_to_file=true, verify USLT contains <mm:ss.xx> stamps via Mp3tag.
  • Non-regression: plain LRC continues to highlight line-by-line.

Summary by CodeRabbit

  • New Features

    • Word-level karaoke: capture/edit word-by-word timestamps, granularity toggle (line vs word), per-word highlighting/animations and fullscreen support
    • Support for Enhanced LRC and TTML timed-word formats; import/detection, format badge, and serialization as Enhanced LRC on save
    • UI shows a warning when TTML tag writes are skipped (TTML kept in cache)
  • Documentation

    • Updated docs covering format detection, editor workflows, serialization and tag-write behavior
  • Chores

    • Database migration to persist TTML format
  • Localization

    • Added translations for new UI strings and toast messages across languages

Review Change Stack

add parsers, render, edit, and persist word-level synchronized lyrics
on top of the existing line-level LRC pipeline. enhanced LRC arrives
via .elrc imports + embedded USLT frames; TTML via .ttml / .xml imports.
the karaoke fullscreen and side panel animate the active word, and the
editor gains a "word by word" granularity toggle (Space / Enter /
Backspace shortcuts) that serializes back to enhanced LRC. TTML writes
target ItemKey::Lyrics and skip cleanly on MP3 (no clean ID3v2 mapping),
surfacing a warning so the user knows the file itself wasn't touched.
i18n keys propagated to all 17 locales.
@github-actions github-actions Bot added scope: frontend React/Vite frontend (src/) scope: backend Rust/Tauri backend (src-tauri/) scope: i18n Translations (src/i18n/) scope: docs Docs, README, assets type: feat New feature size: xl > 500 lines labels May 15, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0cb2440e-488b-425c-8a87-0906082b6d2d

📥 Commits

Reviewing files that changed from the base of the PR and between 4240f9d and ecd87c1.

📒 Files selected for processing (1)
  • src/lib/tauri/lyrics.ts

📝 Walkthrough

Walkthrough

This PR introduces word-level karaoke lyrics support: backend TTML/Enhanced LRC detection and tag-write handling (including MP3 TTML skip), unified parsing into word-timed LyricsLine structures, per-word playback rendering, a word-capture editor with granularity toggle, and i18n updates across locales.

Changes

Word-Level Lyrics Support

Layer / File(s) Summary
Feature documentation and scope
CLAUDE.md, docs/features/integrations.md
Feature docs describe word-level karaoke with Enhanced LRC inline word stamps, TTML parsing, editor word-mode workflows, detection, persistence, and MP3 TTML skipping.
Database migration
src-tauri/migrations/app/20260516120000_lyrics_ttml_format.sql
SQL migration extends lyrics.format CHECK to accept ttml.
Backend format types and detection
src-tauri/src/commands/lyrics.rs
Adds LyricsFormat::Ttml, LyricsPayload.tag_write_skipped; adds prioritized detect_format (TTML → enhanced LRC → LRC → plain).
Payload and save flow
src-tauri/src/commands/lyrics.rs
Fetch/import/cache paths populate tag_write_skipped: None; LyricsSaveFormat accepts EnhancedLrc/Ttml; save_lyrics resolves format and surfaces tag_write_skipped when writes are skipped.
Format-aware tag writing
src-tauri/src/commands/lyrics.rs
write_lyrics_to_file becomes format-aware and returns whether tags were rewritten; TTML writes are skipped on MP3 (returns false); correct ItemKey selection and lyrics-key clearing implemented.
Frontend types and parsers
src/lib/tauri/lyrics.ts
Introduces LyricsLine/LyricsWord types, unified timestamp helpers, parseLrc/parseEnhancedLrc/parseTtml, parseLyrics dispatcher, and serializeEnhancedLrc/findActiveWordIndex.
Word-level rendering & import UI
src/components/layout/LyricsPanel.tsx, src/components/player/FullscreenLyrics.tsx
Components use LyricsLine[], compute activeWordIndex, render per-word spans when words[] exist, expand import extensions, and show format badges for enhanced_lrc/ttml.
Word-capture editor in LyricsEditorModal
src/components/common/LyricsEditorModal.tsx
Adds granularity toggle (line/word), per-word SyncedWord and words[] state, tokenizer preserving whitespace, capture/undo/advance flows, per-word chips, Enhanced LRC serialization on save, and tag_write_skipped warning handling.
Localization across locales
src/i18n/locales/*.json
Adds consistent i18n keys: lyrics.format.* (lrc,enhanced_lrc,ttml,plain), lyrics.toast.tagWriteSkipped, lyricsEditor.granularity.*, captureHintWord, and notCaptured.
Unit tests
src-tauri/src/commands/lyrics.rs (tests)
Tests added/extended for detect_format (TTML, Enhanced LRC, LRC, plain) and word_stamp_present.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hop to stamp each little word, a chip beneath my paw,

Timestamps fall like tiny beats and sync up to the law.
TTML tucked in cache for safekeep, LRC keeps chorus bright,
Word-by-word the music glows beneath the stage lights' sight.
A nibble, a nibble — saved with care — we sing the bytes by night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding word-level karaoke support via Enhanced LRC and TTML formats. It follows Conventional Commits format with kebab-case scope and directly reflects the feature addition across the codebase.
Description check ✅ Passed The PR description follows the template structure with Summary, Test plan, and explains the implementation details comprehensively. However, the author did not fill out 'How I tested' and 'Screenshots / clips' sections explicitly, and the 'Linked issues' section shows only 'Closes #' without an actual issue number.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/word-level-lyrics

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/lib/tauri/lyrics.ts Fixed
Comment thread src/lib/tauri/lyrics.ts Fixed
CodeQL flagged the previous `formatLrcTimestamp(...).replace("[", "<")`
chain as incomplete string escaping — it only handles the first match,
which is fine for the known-good output but is a fragile pattern.
Extract a private `formatTimestamp(ms, open, close)` helper and use it
to emit `<mm:ss.xx>` directly when serializing enhanced LRC word stamps.
@InstaZDLL InstaZDLL self-assigned this May 15, 2026
the previous badge used the full localized label ("Synchronisé
mot-à-mot", "TTML mot-à-mot", …) which overflowed the footer slot and
got clipped by the `truncate` on the wrapper span. switch to a short
brand-token style ("WORD" / "TTML") with the full label exposed via
`title` for hover, add `shrink-0` so the badge survives narrow widths,
and move `truncate` onto the source-label child so it can collapse
gracefully instead.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/player/FullscreenLyrics.tsx (1)

61-67: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the shared modal a11y contract for the fullscreen overlay.

This behaves like a modal, but it only handles Escape manually and exposes no dialog semantics, so keyboard users can still tab into the page behind it. Please wire it through useModalA11y(...) and add role="dialog" with aria-modal/aria-labelledby on the foreground container.

As per coding guidelines, src/components/**/*.tsx: Every modal must call useModalA11y(isOpen, onClose) and Modal containers must have role="dialog", aria-modal="true", aria-labelledby (stable heading id) or aria-label (conditional heading).

Also applies to: 79-133

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/player/FullscreenLyrics.tsx` around lines 61 - 67, The
component currently implements its own Escape key handler in the useEffect and
lacks dialog semantics; replace the manual key handling by calling
useModalA11y(isOpen, onClose) at the top of FullscreenLyrics (pass the
component's isOpen prop and onClose callback), remove the custom handleKey
useEffect, and add proper dialog attributes to the foreground container element:
role="dialog", aria-modal="true", and either aria-labelledby pointing to a
stable heading id inside the modal (create a stable id for the title) or an
appropriate aria-label if the heading is conditional; ensure useModalA11y is
imported and used and the heading id is stable so keyboard users cannot tab into
the page behind the overlay.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/features/integrations.md`:
- Line 143: The docs state a 120 ms opacity transition for active-word
highlighting but the implemented spec and PR describe 150 ms; update the
sentence referencing LyricsPanel and FullscreenLyrics to use "150 ms" (or
explicitly note an intentional difference) so the Rendering section matches the
implemented active-word animation timing and QA expectations.
- Line 145: The docs use mixed British/American spelling here: change the word
"synchronised" in the description for the LyricsEditorModal synchronised tab to
match the project's existing variant ("synchronized" or "synced") used
elsewhere; update the text in docs/features/integrations.md (the line describing
**Editor — word mode.** and the reference to LyricsEditorModal) to use the
chosen variant consistently and run a quick search for other occurrences of
"synchronised" to replace them as well.

In `@src-tauri/src/commands/lyrics.rs`:
- Around line 1045-1055: Currently only the target key (ItemKey::Lyrics for TTML
or ItemKey::UnsyncLyrics for others) is inserted via tag.insert_text, leaving
the opposite key intact which can cause stale reads by read_embedded_lyrics; fix
this by removing both keys before inserting: call tag.remove(ItemKey::Lyrics)
and tag.remove(ItemKey::UnsyncLyrics) (or the equivalent removal API on tag) in
the branch that saves non-empty lyrics, then insert the new content with
tag.insert_text as you already do, ensuring no old lyric format shadows the new
one.

In `@src/components/common/LyricsEditorModal.tsx`:
- Around line 419-435: The mapping before calling serializeEnhancedLrc is
converting uncaptured times to -1 which serializeEnhancedLrc turns into
<00:00.00> and is also filtering out non-empty rows with no captured stamps;
update the rowsForSave construction so you only call shift(...) when timeMs >= 0
and otherwise omit the timeMs/endMs properties (or set them to undefined/null)
for both row.level (use syncedRows -> map) and word.level (words?.map) so
unfinished rows/words are preserved as untimed rather than forced to 00:00; also
adjust the final sort to handle undefined times (treat undefined as +Infinity so
untimed lines sort after timed ones) — reference syncedRows, rowsForSave, shift,
serializeEnhancedLrc, and the words mapping when making this change.

In `@src/components/layout/LyricsPanel.tsx`:
- Around line 254-269: The side-panel word rendering in LyricsPanel.tsx (the
{hasWords ? ... line.words!.map(...) block) only animates color/opacity with
120ms; update the word span rendering to use a 150ms transition and include
transform scale so the active word gets the same scale+opacity animation as
fullscreen. Concretely, adjust the style/transition to "opacity 150ms ease,
transform 150ms ease" and apply a transform (e.g., scale(1) for active via wi
=== activeWordIndex and a slightly reduced scale for non-active) and keep the
existing color class logic (activeWordIndex) to match the fullscreen behavior.

In `@src/lib/tauri/lyrics.ts`:
- Around line 244-290: The parser currently drops any text before the first
inline word stamp and reuses the same words array for all repeated line stamps;
fix by (1) including a leading segment from body.slice(0, wordStamps[0].at) when
that prefix is non-empty and treat it as the first word whose timeMs is
wordStamps[0].timeMs (so adjust segments/words creation to account for a prefix
segment), and (2) when pushing per-line entries in the final loop, deep‑clone
the words array (e.g. words.map(w => ({ ...w }))) so each lines entry gets its
own words copy that fillEndTimestamps() can mutate independently; use the
existing symbols wordStamps, segments, words, lineStamps, matchedStampLength,
and lines to locate where to change.

---

Outside diff comments:
In `@src/components/player/FullscreenLyrics.tsx`:
- Around line 61-67: The component currently implements its own Escape key
handler in the useEffect and lacks dialog semantics; replace the manual key
handling by calling useModalA11y(isOpen, onClose) at the top of FullscreenLyrics
(pass the component's isOpen prop and onClose callback), remove the custom
handleKey useEffect, and add proper dialog attributes to the foreground
container element: role="dialog", aria-modal="true", and either aria-labelledby
pointing to a stable heading id inside the modal (create a stable id for the
title) or an appropriate aria-label if the heading is conditional; ensure
useModalA11y is imported and used and the heading id is stable so keyboard users
cannot tab into the page behind the overlay.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6a93d6ec-c593-4217-ac14-1a61032734b9

📥 Commits

Reviewing files that changed from the base of the PR and between 981144c and 3b80c8e.

📒 Files selected for processing (25)
  • CLAUDE.md
  • docs/features/integrations.md
  • src-tauri/migrations/app/20260516120000_lyrics_ttml_format.sql
  • src-tauri/src/commands/lyrics.rs
  • src/components/common/LyricsEditorModal.tsx
  • src/components/layout/LyricsPanel.tsx
  • src/components/player/FullscreenLyrics.tsx
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/id.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/kr.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/zh-CN.json
  • src/i18n/locales/zh-TW.json
  • src/lib/tauri/lyrics.ts

Comment thread docs/features/integrations.md Outdated
Comment thread docs/features/integrations.md Outdated
Comment thread src-tauri/src/commands/lyrics.rs
Comment thread src/components/common/LyricsEditorModal.tsx
Comment thread src/components/layout/LyricsPanel.tsx
Comment thread src/lib/tauri/lyrics.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/components/layout/LyricsPanel.tsx (1)

254-269: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align side-panel word animation with the 150ms opacity+scale karaoke spec.

Line 268 still uses 120ms and no scale transform, so the side panel diverges from the stated behavior and fullscreen implementation.

Proposed fix
-                          <span
+                          <span
                             key={wi}
-                            className={
-                              wi === activeWordIndex
-                                ? "text-pink-500 dark:text-pink-400"
-                                : wi < activeWordIndex
-                                  ? ""
-                                  : "opacity-60"
-                            }
+                            className={`inline-block ${
+                              wi === activeWordIndex
+                                ? "text-pink-500 dark:text-pink-400"
+                                : wi < activeWordIndex
+                                  ? ""
+                                  : "opacity-60"
+                            }`}
                             style={{
-                              transition:
-                                "color 120ms ease, opacity 120ms ease",
+                              transition:
+                                "color 150ms ease, opacity 150ms ease, transform 150ms ease",
+                              transform:
+                                wi === activeWordIndex ? "scale(1)" : "scale(0.97)",
                             }}
                           >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/layout/LyricsPanel.tsx` around lines 254 - 269, The side-panel
word span uses a 120ms color/opacity transition and lacks scale, so update the
transition and add a scale transform to match the 150ms karaoke spec: in
LyricsPanel (the span inside hasWords mapping over line.words in the JSX where
activeWordIndex is compared) change the inline style transition from "color
120ms ease, opacity 120ms ease" to use 150ms (e.g., "color 150ms ease, opacity
150ms ease, transform 150ms ease") and ensure the class/style logic applies a
scale transform (e.g., scale(1.06) when wi === activeWordIndex, scale(1) when wi
< activeWordIndex or otherwise a slightly smaller scale/opacity) so the active
word animates scale+opacity over 150ms consistent with fullscreen behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/components/layout/LyricsPanel.tsx`:
- Around line 254-269: The side-panel word span uses a 120ms color/opacity
transition and lacks scale, so update the transition and add a scale transform
to match the 150ms karaoke spec: in LyricsPanel (the span inside hasWords
mapping over line.words in the JSX where activeWordIndex is compared) change the
inline style transition from "color 120ms ease, opacity 120ms ease" to use 150ms
(e.g., "color 150ms ease, opacity 150ms ease, transform 150ms ease") and ensure
the class/style logic applies a scale transform (e.g., scale(1.06) when wi ===
activeWordIndex, scale(1) when wi < activeWordIndex or otherwise a slightly
smaller scale/opacity) so the active word animates scale+opacity over 150ms
consistent with fullscreen behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d275864-c93b-48f1-b341-d8f6b32ff166

📥 Commits

Reviewing files that changed from the base of the PR and between 3b80c8e and fb9c7bf.

📒 Files selected for processing (1)
  • src/components/layout/LyricsPanel.tsx

InstaZDLL added 3 commits May 16, 2026 00:58
- `write_lyrics_to_file` now purges both UnsyncLyrics and Lyrics keys
  before inserting the new content. Switching format (e.g. plain LRC →
  TTML) used to leave the old USLT frame intact, which `read_embedded_lyrics`
  reads first — the user would see the stale format on next fetch.
- `parseEnhancedLrc` captures any text before the first inline word
  stamp as a virtual leading word at the line's own timestamp (matches
  what other Enhanced LRC tooling does), and deep-clones the `words`
  array per repeated line stamp so `fillEndTimestamps` mutations don't
  bleed across siblings.
- `serializeEnhancedLrc` no longer emits `<00:00.00>word` for words
  with `timeMs < 0` — uncaptured words fold their text into the
  previous segment instead. Prevents a half-finished save from
  introducing phantom zero-second stamps on the next load.
- `FullscreenLyrics` now uses `useModalA11y` (focus trap, Escape,
  focus restore) instead of a bespoke Escape listener, and the
  container carries `role="dialog"` + `aria-modal="true"` +
  `aria-labelledby`. Matches the modal convention applied across the
  rest of the codebase.
- Harmonize the panel's active-word transition to 150 ms (was 120 ms)
  to match the fullscreen view and the doc. No scale on the panel —
  kept intentional given the 14 px line height inside the 320 px slot
  would jitter under scale.
- Drop a stray British spelling in the docs.
apply the same `scale(1.04)` + transform transition to the panel's
active word so the karaoke effect stays consistent across both views.
the panel keeps its accent-color tint that the fullscreen leaves out,
but durations and the scale ramp are now identical (150 ms,
opacity / color / transform). also update the docs accordingly.
the word-mode filter only kept rows with at least one captured stamp
(line or word level), so a user who typed a verse but hadn't stamped
it yet would lose the text on save. line mode kept those rows via
`text.trim().length > 0` — the asymmetry was incidental, not by
design. extend the filter so word mode behaves the same, and bake an
explicit untimed-rows-last sort so the serialised body stays
monotonically ordered (matching line mode's tail behaviour).
`serializeEnhancedLrc` already handles `timeMs < 0` rows with the
`[--:--.--]` placeholder and folds uncaptured words into the previous
segment, so the round-trip stays clean.
@InstaZDLL InstaZDLL enabled auto-merge May 15, 2026 23:08
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/tauri/lyrics.ts`:
- Around line 270-310: The prefix virtual word is currently built once with
timeMs: lineStamps[0] and then cloned for every duplicate line, causing
duplicated lines to inherit the wrong first-word time; update the code that
creates per-line entries (the loop over lineStamps that pushes into lines) to,
for each timeMs, clone the words and if a prefix exists (the first built word
corresponds to prefix), set the cloned first word's timeMs to that timeMs before
pushing — reference symbols: lineStamps, built/words, lines, and the per-line
push where words.map(...) is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a02cefe9-179d-4a9e-b0dd-427395d5545d

📥 Commits

Reviewing files that changed from the base of the PR and between fb9c7bf and 4240f9d.

📒 Files selected for processing (6)
  • docs/features/integrations.md
  • src-tauri/src/commands/lyrics.rs
  • src/components/common/LyricsEditorModal.tsx
  • src/components/layout/LyricsPanel.tsx
  • src/components/player/FullscreenLyrics.tsx
  • src/lib/tauri/lyrics.ts

Comment thread src/lib/tauri/lyrics.ts
a rare LRC convention is multiple line stamps on the same body
(`[00:01][00:30]Hello <00:31>world`). the previous code built the
virtual prefix word once with `lineStamps[0]` and cloned it for every
duplicate, leaving later clones with a first-word time that didn't
match their own line stamp. drop the placeholder timeMs from the
built entry and rewrite it per-duplicate inside the per-line cloning
loop instead.
@InstaZDLL InstaZDLL merged commit 62701c3 into main May 15, 2026
13 checks passed
@InstaZDLL InstaZDLL deleted the feat/word-level-lyrics branch May 15, 2026 23:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants