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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016L1sBqsygMkdhf583gTEnE
This commit is contained in:
Claude 2026-10-15 09:46:18 +00:00
parent e6cd424157
commit fcda9ebd5e
No known key found for this signature in database
2 changed files with 23 additions and 4 deletions

View File

@ -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) {

View File

@ -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())