mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
address feedback
This commit is contained in:
parent
3096a09a1b
commit
6db89b790d
@ -228,7 +228,10 @@ func ToLyrics(language, text string) (*Lyrics, error) {
|
||||
|
||||
// flushPriorLine emits one CueLine per accumulated timestamp, copying the
|
||||
// shared text/cue data into each. The last "Repeated" use-case is preserved
|
||||
// (a single text repeats at multiple timestamps).
|
||||
// (a single text repeats at multiple timestamps). For repeated lines with
|
||||
// ELRC word timings, cues carry absolute timestamps anchored to the first
|
||||
// occurrence (timestamps[0]); each subsequent occurrence is shifted by
|
||||
// timestamps[idx]-timestamps[0] so its cues point at the correct moment,
|
||||
func flushPriorLine(cueLines *[]CueLine, timestamps []int64, text string, cues []Cue) {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
for idx := range timestamps {
|
||||
@ -238,11 +241,20 @@ func flushPriorLine(cueLines *[]CueLine, timestamps []int64, text string, cues [
|
||||
Start: &startCopy,
|
||||
Value: trimmed,
|
||||
}
|
||||
// Cues are only meaningful for the first occurrence in a repeat; copy
|
||||
// them onto each cueLine so each independent index has its own cue list.
|
||||
if len(cues) > 0 {
|
||||
offset := timestamps[idx] - timestamps[0]
|
||||
cl.Cue = make([]Cue, len(cues))
|
||||
copy(cl.Cue, cues)
|
||||
for i, c := range cues {
|
||||
cl.Cue[i] = Cue{Value: c.Value}
|
||||
if c.Start != nil {
|
||||
s := *c.Start + offset
|
||||
cl.Cue[i].Start = &s
|
||||
}
|
||||
if c.End != nil {
|
||||
e := *c.End + offset
|
||||
cl.Cue[i].End = &e
|
||||
}
|
||||
}
|
||||
}
|
||||
*cueLines = append(*cueLines, cl)
|
||||
}
|
||||
|
||||
@ -141,5 +141,31 @@ var _ = Describe("ToLyrics", func() {
|
||||
// Last cue has no inferred end.
|
||||
Expect(cl.Cue[1].End).To(BeNil())
|
||||
})
|
||||
|
||||
It("shifts cue timestamps for each repeated line occurrence and allocates fresh pointers", func() {
|
||||
lyrics, err := ToLyrics("eng", "[00:10.00][00:30.00]<00:10.10>Hello <00:10.50>world")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.CueLine).To(HaveLen(2))
|
||||
|
||||
first := lyrics.CueLine[0]
|
||||
Expect(*first.Start).To(Equal(int64(10000)))
|
||||
Expect(first.Cue).To(HaveLen(2))
|
||||
Expect(*first.Cue[0].Start).To(Equal(int64(10100)))
|
||||
Expect(*first.Cue[1].Start).To(Equal(int64(10500)))
|
||||
Expect(*first.Cue[0].End).To(Equal(int64(10500)))
|
||||
Expect(first.Cue[1].End).To(BeNil())
|
||||
|
||||
second := lyrics.CueLine[1]
|
||||
Expect(*second.Start).To(Equal(int64(30000)))
|
||||
Expect(second.Cue).To(HaveLen(2))
|
||||
Expect(*second.Cue[0].Start).To(Equal(int64(30100)))
|
||||
Expect(*second.Cue[1].Start).To(Equal(int64(30500)))
|
||||
Expect(*second.Cue[0].End).To(Equal(int64(30500)))
|
||||
Expect(second.Cue[1].End).To(BeNil())
|
||||
|
||||
Expect(second.Cue[0].Start).ToNot(BeIdenticalTo(first.Cue[0].Start))
|
||||
Expect(second.Cue[1].Start).ToNot(BeIdenticalTo(first.Cue[1].Start))
|
||||
Expect(second.Cue[0].End).ToNot(BeIdenticalTo(first.Cue[0].End))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -60,11 +60,11 @@ func ParseLyricsfile(text string) (*Lyrics, error) {
|
||||
}
|
||||
|
||||
type lyricsfileDocument struct {
|
||||
Version string `yaml:"version"`
|
||||
Metadata lyricsfileMetadata `yaml:"metadata"`
|
||||
Lines []lyricsfileLineEntry `yaml:"lines"`
|
||||
Plain string `yaml:"plain"`
|
||||
Extra map[string]yaml.Node `yaml:",inline"`
|
||||
Version string `yaml:"version"`
|
||||
Metadata lyricsfileMetadata `yaml:"metadata"`
|
||||
Lines []lyricsfileLineEntry `yaml:"lines"`
|
||||
Plain string `yaml:"plain"`
|
||||
Extra map[string]yaml.Node `yaml:",inline"`
|
||||
}
|
||||
|
||||
type lyricsfileMetadata struct {
|
||||
@ -150,13 +150,26 @@ func buildLyricsfileCueLines(entries []lyricsfileLineEntry) ([]CueLine, []Agent)
|
||||
}
|
||||
|
||||
startCopy := entry.StartMs
|
||||
cues := wordsToCues(entry.Words)
|
||||
// Cue.byteStart/byteEnd are positions in cueLine.value per the
|
||||
// OpenSubsonic v2 spec, so when cues are present we reconstruct value
|
||||
// from them rather than trust entry.Text (the Lyricsfile spec only
|
||||
// requires word.text to "approximate" line.text).
|
||||
value := entry.Text
|
||||
if len(cues) > 0 {
|
||||
var sb strings.Builder
|
||||
for _, c := range cues {
|
||||
sb.WriteString(c.Value)
|
||||
}
|
||||
value = sb.String()
|
||||
}
|
||||
cl := CueLine{
|
||||
Index: currentIndex,
|
||||
Start: &startCopy,
|
||||
End: ends[i],
|
||||
Value: entry.Text,
|
||||
Value: value,
|
||||
AgentID: fmt.Sprintf("voice-%d", voiceID),
|
||||
Cue: wordsToCues(entry.Words),
|
||||
Cue: cues,
|
||||
}
|
||||
cueLines = append(cueLines, cl)
|
||||
|
||||
@ -217,4 +230,3 @@ func wordsToCues(words []lyricsfileWordEntry) []Cue {
|
||||
}
|
||||
return cues
|
||||
}
|
||||
|
||||
|
||||
56
model/lyricsfile_test.go
Normal file
56
model/lyricsfile_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package model_test
|
||||
|
||||
import (
|
||||
. "github.com/navidrome/navidrome/model"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("ParseLyricsfile", func() {
|
||||
It("rebuilds CueLine.Value from word cues so byteStart/byteEnd remain valid offsets", func() {
|
||||
yaml := `version: '1.0'
|
||||
metadata:
|
||||
title: 'Punctuated'
|
||||
artist: 'Test Artist'
|
||||
lines:
|
||||
- text: 'Hello, world!'
|
||||
start_ms: 1000
|
||||
end_ms: 3000
|
||||
words:
|
||||
- text: 'Hello '
|
||||
start_ms: 1000
|
||||
end_ms: 2000
|
||||
- text: 'world'
|
||||
start_ms: 2000
|
||||
end_ms: 3000
|
||||
`
|
||||
lyrics, err := ParseLyricsfile(yaml)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.CueLine).To(HaveLen(1))
|
||||
cl := lyrics.CueLine[0]
|
||||
|
||||
Expect(cl.Cue).To(HaveLen(2))
|
||||
Expect(cl.Cue[0].Value).To(Equal("Hello "))
|
||||
Expect(cl.Cue[1].Value).To(Equal("world"))
|
||||
Expect(cl.Value).To(Equal("Hello world"))
|
||||
})
|
||||
|
||||
It("keeps entry.Text on CueLine.Value when the line has no word cues", func() {
|
||||
yaml := `version: '1.0'
|
||||
metadata:
|
||||
title: 'Line only'
|
||||
artist: 'Test Artist'
|
||||
lines:
|
||||
- text: 'Hello, world!'
|
||||
start_ms: 1000
|
||||
end_ms: 3000
|
||||
`
|
||||
lyrics, err := ParseLyricsfile(yaml)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(lyrics.CueLine).To(HaveLen(1))
|
||||
cl := lyrics.CueLine[0]
|
||||
|
||||
Expect(cl.Cue).To(BeEmpty())
|
||||
Expect(cl.Value).To(Equal("Hello, world!"))
|
||||
})
|
||||
})
|
||||
@ -578,7 +578,7 @@ func buildV2CueLines(cueLines []model.CueLine) []responses.CueLine {
|
||||
}
|
||||
|
||||
// buildCue maps each model.Cue to one responses.Cue with inclusive UTF-8
|
||||
// ByteStart/ByteEnd offsets into cl.Value.
|
||||
// ByteStart/ByteEnd offsets into cl.Value.
|
||||
func buildCue(cl model.CueLine) []responses.Cue {
|
||||
if len(cl.Cue) == 0 {
|
||||
return nil
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user