From fcda9ebd5e3b3e2dbaba376a763cdab0f560d357 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 09:46:18 +0000 Subject: [PATCH] fix(lyrics): keep explicit [bg:] trailing ends as line ends A background layer's trailing marker is exact source timing. Without it on Line.End, normalization treats a terminal bg cue end equal to the next line start as a next-line fallback and drops it. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016L1sBqsygMkdhf583gTEnE --- model/lyrics_lrc.go | 19 +++++++++++++++---- model/lyrics_lrc_test.go | 8 ++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/model/lyrics_lrc.go b/model/lyrics_lrc.go index eaa35d9ee..09dd62627 100644 --- a/model/lyrics_lrc.go +++ b/model/lyrics_lrc.go @@ -53,8 +53,12 @@ func parseLRC(language, text string) (*Lyrics, error) { flushLine := func() { value, cues, end := parseEnhancedLine(priorLine) var merged bool - value, cues, merged = mergeBgLayers(value, cues, pendingBg) + var bgEnd *int64 + value, cues, bgEnd, merged = mergeBgLayers(value, cues, pendingBg) hasBg = hasBg || merged + if bgEnd != nil && (end == nil || *bgEnd > *end) { + end = bgEnd + } for idx := range timestamps { startCopy := timestamps[idx] shift := timestamps[idx] - timestamps[0] @@ -207,13 +211,20 @@ func parseLRC(language, text string) (*Lyrics, error) { // mergeBgLayers appends parsed [bg:] content to a line's value and cues as a // background agent layer, mirroring the TTML background-vocal representation. -func mergeBgLayers(value string, cues []Cue, bgLines []string) (string, []Cue, bool) { +// It also returns the latest explicit trailing end among the merged layers, so +// an exact source end is not mistaken for a next-line fallback during +// normalization. +func mergeBgLayers(value string, cues []Cue, bgLines []string) (string, []Cue, *int64, bool) { merged := false + var latestEnd *int64 for _, bg := range bgLines { - bgValue, bgCues, _ := parseEnhancedLine(bg) + bgValue, bgCues, bgEnd := parseEnhancedLine(bg) if len(bgCues) == 0 { continue } + if bgEnd != nil && (latestEnd == nil || *bgEnd > *latestEnd) { + latestEnd = bgEnd + } offset := 0 if value != "" { offset = len(value) + 1 @@ -228,7 +239,7 @@ func mergeBgLayers(value string, cues []Cue, bgLines []string) (string, []Cue, b } merged = true } - return value, cues, merged + return value, cues, latestEnd, merged } func fillMainAgentID(lines []Line) { diff --git a/model/lyrics_lrc_test.go b/model/lyrics_lrc_test.go index 700e49a6e..ea52d1a4d 100644 --- a/model/lyrics_lrc_test.go +++ b/model/lyrics_lrc_test.go @@ -183,6 +183,14 @@ var _ = Describe("parseLRC", func() { Expect(lyrics.Line[1].Value).To(Equal("Next")) }) + It("keeps an explicit [bg:] trailing end that equals the next line start", func() { + lyrics, err := parseLRC("xxx", "[00:01.00]Main\n[bg: <00:01.50>Ooh<00:03.00>]\n[00:03.00]Next") + Expect(err).ToNot(HaveOccurred()) + Expect(lyrics.Line).To(HaveLen(2)) + Expect(lyrics.Line[0].Value).To(Equal("Main Ooh")) + Expect(lyrics.Line[0].End).To(Equal(new(int64(3000)))) + }) + It("should skip a [bg:] line that has no inline timing", func() { lyrics, err := parseLRC("xxx", "[00:01.00]Hi\n[bg: untimed]\n[00:03.00]Next") Expect(err).ToNot(HaveOccurred())