fix(subsonic): emit agent-specific cueLine values (#5679)
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions

* fix(subsonic): emit agent-specific cue line values

* fix(subsonic): preserve cue line edge text
This commit is contained in:
Yuuta 2026-06-29 01:14:18 +03:00 committed by GitHub
parent 5a3ac80a8a
commit 9b862e8833
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 304 additions and 6 deletions

View file

@ -114,12 +114,17 @@ func buildCueLines(line model.Line, index int32, agents lyricAgents) []responses
cueLines := make([]responses.CueLine, 0, len(agentOrder))
for _, agentID := range agentOrder {
value := line.Value
cues := cuesByAgent[agentID]
if len(agentOrder) > 1 {
value, cues = buildAgentCueLineValue(line.Value, cues, line.Cue, agentID)
}
cueLine := responses.CueLine{
Index: index,
Start: line.Start,
End: line.End,
Value: line.Value,
Cue: buildLyricCues(cuesByAgent[agentID], line.End),
Value: value,
Cue: buildLyricCues(cues, line.End),
}
if agentID != "" {
cueLine.AgentID = agentID
@ -149,6 +154,85 @@ func (a lyricAgents) less(left, right string, origI, origJ int) bool {
return origI < origJ
}
func buildAgentCueLineValue(lineValue string, cues, allCues []model.Cue, agentID string) (string, []model.Cue) {
if len(cues) == 0 {
return "", nil
}
remapped := slices.Clone(cues)
var value strings.Builder
leadingGap := cueLineGap(lineValue, 0, remapped[0].ByteStart, allCues, agentID)
if strings.TrimSpace(leadingGap) != "" {
value.WriteString(leadingGap)
}
previousEnd := -1
for i := range remapped {
originalStart := remapped[i].ByteStart
originalEnd := remapped[i].ByteEnd
if i > 0 {
value.WriteString(cueLineGap(lineValue, previousEnd+1, originalStart, allCues, agentID))
}
remapped[i].ByteStart = value.Len()
value.WriteString(remapped[i].Value)
remapped[i].ByteEnd = value.Len() - 1
previousEnd = originalEnd
}
trailingGap := cueLineGap(lineValue, previousEnd+1, len(lineValue), allCues, agentID)
if strings.TrimSpace(trailingGap) != "" {
value.WriteString(trailingGap)
}
return value.String(), remapped
}
type byteRange struct {
start int
end int
}
func cueLineGap(source string, start, end int, allCues []model.Cue, agentID string) string {
start = max(start, 0)
end = min(end, len(source))
if start >= end {
return ""
}
excluded := make([]byteRange, 0, 1)
for _, cue := range allCues {
if strings.TrimSpace(cue.AgentID) == agentID {
continue
}
cueStart := max(cue.ByteStart, start)
cueEnd := min(cue.ByteEnd+1, end)
if cueStart < cueEnd {
excluded = append(excluded, byteRange{start: cueStart, end: cueEnd})
}
}
if len(excluded) == 0 {
return source[start:end]
}
sort.SliceStable(excluded, func(i, j int) bool {
return excluded[i].start < excluded[j].start
})
var gap strings.Builder
cursor := start
for _, r := range excluded {
if r.start > cursor {
gap.WriteString(source[cursor:r.start])
}
cursor = max(cursor, r.end)
}
if cursor < end {
gap.WriteString(source[cursor:end])
}
return gap.String()
}
func buildLyricCues(cues []model.Cue, lineEnd *int64) []responses.LyricCue {
if len(cues) == 0 {
return nil

View file

@ -431,7 +431,7 @@ var _ = Describe("GetLyricsBySongId", func() {
Index: 0,
Start: &lineStart,
End: &lineEnd,
Value: "Hello echo",
Value: "Hello",
AgentID: "lead",
Cue: []responses.LyricCue{
{
@ -447,14 +447,14 @@ var _ = Describe("GetLyricsBySongId", func() {
Index: 0,
Start: &lineStart,
End: &lineEnd,
Value: "Hello echo",
Value: "echo",
AgentID: "__nd_bg__|lead",
Cue: []responses.LyricCue{
{
Start: tokenStartB,
End: &tokenEndB,
ByteStart: 6,
ByteEnd: 9,
ByteStart: 0,
ByteEnd: 3,
Value: "echo",
},
},
@ -465,6 +465,220 @@ var _ = Describe("GetLyricsBySongId", func() {
})
})
It("should preserve shared edge text when remapping agent cue lines", func() {
lineStart := int64(1000)
lineEnd := int64(2000)
cueStart := int64(1200)
cueEnd := int64(1800)
cueLines := buildCueLines(model.Line{
Start: &lineStart,
End: &lineEnd,
Value: "(Hello)",
Cue: []model.Cue{
{
Start: &cueStart,
End: &cueEnd,
Value: "Hello",
ByteStart: 1,
ByteEnd: 5,
AgentID: "lead",
},
{
Start: &cueStart,
End: &cueEnd,
Value: "Hello",
ByteStart: 1,
ByteEnd: 5,
AgentID: "__nd_bg__|lead",
},
},
}, 0, newLyricAgents([]model.Agent{
{ID: "lead", Role: "main"},
{ID: "__nd_bg__|lead", Role: "bg"},
}))
Expect(cueLines).To(Equal([]responses.CueLine{
{
Index: 0,
Start: &lineStart,
End: &lineEnd,
Value: "(Hello)",
AgentID: "lead",
Cue: []responses.LyricCue{
{
Start: cueStart,
End: &cueEnd,
Value: "Hello",
ByteStart: 1,
ByteEnd: 5,
},
},
},
{
Index: 0,
Start: &lineStart,
End: &lineEnd,
Value: "(Hello)",
AgentID: "__nd_bg__|lead",
Cue: []responses.LyricCue{
{
Start: cueStart,
End: &cueEnd,
Value: "Hello",
ByteStart: 1,
ByteEnd: 5,
},
},
},
}))
})
It("should remap cue offsets for interleaved agent cue lines", func() {
r := newGetRequest("id=1&enhanced=true")
lineStart := int64(82889)
lineEnd := int64(86859)
realStart := int64(85593)
realEnd := int64(85934)
slowStart := int64(85934)
slowEnd := int64(86751)
bgStartA := int64(83881)
bgEndA := int64(84243)
bgStartB := int64(86232)
bgEndB := int64(86859)
lyricsJSON, err := json.Marshal(model.LyricList{
{
Lang: "eng",
Agents: []model.Agent{{ID: "v2", Role: "main"}, {ID: "__nd_bg__|v2", Role: "bg"}},
Synced: true,
Line: []model.Line{
{
Start: &lineStart,
End: &lineEnd,
Value: "real slow (When you slide)",
Cue: []model.Cue{
{
Start: &realStart,
End: &realEnd,
Value: "real",
ByteStart: 0,
ByteEnd: 3,
AgentID: "v2",
},
{
Start: &slowStart,
End: &slowEnd,
Value: "slow",
ByteStart: 5,
ByteEnd: 8,
AgentID: "v2",
},
{
Start: &bgStartA,
End: &bgEndA,
Value: "(When you",
ByteStart: 10,
ByteEnd: 18,
AgentID: "__nd_bg__|v2",
},
{
Start: &bgStartB,
End: &bgEndB,
Value: "slide)",
ByteStart: 20,
ByteEnd: 25,
AgentID: "__nd_bg__|v2",
},
},
},
},
},
})
Expect(err).ToNot(HaveOccurred())
mockRepo.SetData(model.MediaFiles{
{
ID: "1",
Artist: "Rick Astley",
Title: "Never Gonna Give You Up",
Lyrics: string(lyricsJSON),
},
})
response, err := router.GetLyricsBySongId(r)
Expect(err).ToNot(HaveOccurred())
compareResponses(response.LyricsList, responses.LyricsList{
StructuredLyrics: responses.StructuredLyrics{
{
DisplayArtist: "Rick Astley",
DisplayTitle: "Never Gonna Give You Up",
Kind: "main",
Lang: "eng",
Synced: true,
Agents: []responses.Agent{
{ID: "v2", Role: "main"},
{ID: "__nd_bg__|v2", Role: "bg"},
},
Line: []responses.Line{
{
Start: &lineStart,
Value: "real slow (When you slide)",
},
},
CueLine: []responses.CueLine{
{
Index: 0,
Start: &lineStart,
End: &lineEnd,
Value: "real slow",
AgentID: "v2",
Cue: []responses.LyricCue{
{
Start: realStart,
End: &realEnd,
ByteStart: 0,
ByteEnd: 3,
Value: "real",
},
{
Start: slowStart,
End: &slowEnd,
ByteStart: 5,
ByteEnd: 8,
Value: "slow",
},
},
},
{
Index: 0,
Start: &lineStart,
End: &lineEnd,
Value: "(When you slide)",
AgentID: "__nd_bg__|v2",
Cue: []responses.LyricCue{
{
Start: bgStartA,
End: &bgEndA,
ByteStart: 0,
ByteEnd: 8,
Value: "(When you",
},
{
Start: bgStartB,
End: &bgEndB,
ByteStart: 10,
ByteEnd: 15,
Value: "slide)",
},
},
},
},
},
},
})
})
It("should keep enhanced line-level lyrics when no cue data is available", func() {
r := newGetRequest("id=1&enhanced=true")