🏡 index : quine-global/quintodrome.git

package model

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("parseLyricsfile", func() {
	DescribeTable("returns nil,nil for YAML without the Lyricsfile version marker",
		func(input string) {
			lyrics, err := parseLyricsfile("", []byte(input))
			Expect(err).ToNot(HaveOccurred())
			Expect(lyrics).To(BeNil())
		},
		Entry("arbitrary YAML", "hello: world\n"),
		Entry("Lyricsfile-shaped but unversioned", `metadata:
  title: 'Looks close'
lines:
  - text: "But should not be claimed"
    start_ms: 1000
`),
	)

	It("returns an error for invalid YAML", func() {
		_, err := parseLyricsfile("", []byte("not: valid: yaml: ["))
		Expect(err).To(HaveOccurred())
	})

	It("parses line-level metadata without cues", func() {
		input := `version: '1.0'
metadata:
  title: 'Sample Track'
  artist: 'Test Artist'
  language: 'eng'
  offset_ms: -100
lines:
  - text: "We're no strangers to love"
    start_ms: 18800
  - text: "You know the rules and so do I"
    start_ms: 22801
`
		lyrics, err := parseLyricsfile("", []byte(input))
		Expect(err).ToNot(HaveOccurred())
		Expect(lyrics).To(HaveLen(1))

		l := lyrics[0]
		Expect(l.Kind).To(Equal("main"))
		Expect(l.Lang).To(Equal("eng"))
		Expect(l.DisplayArtist).To(Equal("Test Artist"))
		Expect(l.DisplayTitle).To(Equal("Sample Track"))
		Expect(l.Synced).To(BeTrue())
		Expect(l.Offset).ToNot(BeNil())
		Expect(*l.Offset).To(Equal(int64(-100)))
		Expect(l.Agents).To(BeNil())

		Expect(l.Line).To(HaveLen(2))
		Expect(*l.Line[0].Start).To(Equal(int64(18800)))
		Expect(l.Line[0].End).ToNot(BeNil())
		Expect(*l.Line[0].End).To(Equal(int64(22801)))
		Expect(l.Line[0].Value).To(Equal("We're no strangers to love"))
		Expect(l.Line[0].Cue).To(BeNil())

		Expect(*l.Line[1].Start).To(Equal(int64(22801)))
		Expect(l.Line[1].End).To(BeNil())
		Expect(l.Line[1].Value).To(Equal("You know the rules and so do I"))
		Expect(l.Line[1].Cue).To(BeNil())
	})

	DescribeTable("resolves the lyric language",
		func(metaLanguage, callerLang, want string) {
			input := "version: '1.0'\nmetadata:\n  title: 'T'\n"
			if metaLanguage != "" {
				input += "  language: '" + metaLanguage + "'\n"
			}
			input += "lines:\n  - text: \"line\"\n    start_ms: 0\n"

			lyrics, err := parseLyricsfile(callerLang, []byte(input))
			Expect(err).ToNot(HaveOccurred())
			Expect(lyrics).To(HaveLen(1))
			Expect(lyrics[0].Lang).To(Equal(want))
		},
		Entry("prefers the document's own language", "eng", "deu", "eng"),
		Entry("falls back to the caller language when metadata omits it", "", "deu", "deu"),
		Entry("uses xxx when neither is provided", "", "", "xxx"),
	)

	It("parses plain-only Lyricsfile lyrics as unsynced lines", func() {
		input := `version: '1.0'
metadata:
  title: 'Plain Track'
  artist: 'Plain Artist'
  language: 'en'
lines: []
plain: |
  [Verse 1]
  First line

  Second line
`
		lyrics, err := parseLyricsfile("", []byte(input))
		Expect(err).ToNot(HaveOccurred())
		Expect(lyrics).To(HaveLen(1))

		l := lyrics[0]
		Expect(l.Kind).To(Equal("main"))
		Expect(l.Lang).To(Equal("en"))
		Expect(l.DisplayArtist).To(Equal("Plain Artist"))
		Expect(l.DisplayTitle).To(Equal("Plain Track"))
		Expect(l.Synced).To(BeFalse())
		Expect(l.Agents).To(BeNil())
		Expect(l.Line).To(Equal([]Line{
			{Value: "[Verse 1]"},
			{Value: "First line"},
			{Value: "Second line"},
		}))
	})

	It("produces word cues with inclusive UTF-8 byte offsets for monophonic word data", func() {
		input := `version: '1.0'
metadata:
  title: 'Karaoke'
  artist: 'Singer'
  language: 'eng'
lines:
  - text: "Hello world"
    start_ms: 1000
    end_ms: 3000
    words:
      - text: "Hello "
        start_ms: 1000
        end_ms: 1500
      - text: "world"
        start_ms: 1500
        end_ms: 3000
`
		lyrics, err := parseLyricsfile("", []byte(input))
		Expect(err).ToNot(HaveOccurred())
		Expect(lyrics).To(HaveLen(1))

		l := lyrics[0]
		Expect(l.Synced).To(BeTrue())
		Expect(l.Agents).To(BeNil())
		Expect(l.Line).To(HaveLen(1))

		line := l.Line[0]
		Expect(*line.Start).To(Equal(int64(1000)))
		Expect(*line.End).To(Equal(int64(3000)))
		Expect(line.Value).To(Equal("Hello world"))
		Expect(line.Cue).To(HaveLen(2))

		Expect(*line.Cue[0].Start).To(Equal(int64(1000)))
		Expect(*line.Cue[0].End).To(Equal(int64(1500)))
		Expect(line.Cue[0].Value).To(Equal("Hello "))
		Expect(line.Cue[0].ByteStart).To(Equal(0))
		Expect(line.Cue[0].ByteEnd).To(Equal(5))
		Expect(line.Cue[0].AgentID).To(Equal(""))

		Expect(*line.Cue[1].Start).To(Equal(int64(1500)))
		Expect(*line.Cue[1].End).To(Equal(int64(3000)))
		Expect(line.Cue[1].Value).To(Equal("world"))
		Expect(line.Cue[1].ByteStart).To(Equal(6))
		Expect(line.Cue[1].ByteEnd).To(Equal(10))
		Expect(line.Cue[1].AgentID).To(Equal(""))
	})

	It("prefers final word end_ms over next line start when inferring line end", func() {
		input := `version: '1.0'
metadata:
  title: 'Overlap From Words'
lines:
  - text: "Long vocal"
    start_ms: 1000
    words:
      - text: "Long "
        start_ms: 1000
        end_ms: 2000
      - text: "vocal"
        start_ms: 2000
        end_ms: 4000
  - text: "echo"
    start_ms: 3000
    end_ms: 3500
    words:
      - text: "echo"
        start_ms: 3000
        end_ms: 3500
`
		lyrics, err := parseLyricsfile("", []byte(input))
		Expect(err).ToNot(HaveOccurred())
		Expect(lyrics).To(HaveLen(1))

		l := lyrics[0]
		Expect(l.Agents).To(Equal([]Agent{
			{ID: "voice-0", Role: "main"},
			{ID: "voice-1", Role: "voice"},
		}))
		Expect(l.Line).To(HaveLen(2))
		Expect(l.Line[0].End).ToNot(BeNil())
		Expect(*l.Line[0].End).To(Equal(int64(4000)))
		Expect(l.Line[0].Cue[1].End).To(Equal(l.Line[0].End))
		Expect(l.Line[1].Cue[0].AgentID).To(Equal("voice-1"))
	})

	It("synthesises voice agents for overlapping lines and attributes per-cue", func() {
		input := `version: '1.0'
metadata:
  title: 'Duet'
lines:
  - text: "Lead vocal"
    start_ms: 1000
    end_ms: 4000
    words:
      - text: "Lead "
        start_ms: 1000
        end_ms: 2000
      - text: "vocal"
        start_ms: 2000
        end_ms: 4000
  - text: "echo"
    start_ms: 2000
    end_ms: 3000
    words:
      - text: "echo"
        start_ms: 2000
        end_ms: 3000
`
		lyrics, err := parseLyricsfile("", []byte(input))
		Expect(err).ToNot(HaveOccurred())
		Expect(lyrics).To(HaveLen(1))

		l := lyrics[0]
		Expect(l.Agents).To(Equal([]Agent{
			{ID: "voice-0", Role: "main"},
			{ID: "voice-1", Role: "voice"},
		}))
		Expect(l.Line).To(HaveLen(2))

		Expect(l.Line[0].Value).To(Equal("Lead vocal"))
		Expect(*l.Line[0].Start).To(Equal(int64(1000)))
		Expect(*l.Line[0].End).To(Equal(int64(4000)))
		Expect(l.Line[0].Cue).To(HaveLen(2))
		Expect(l.Line[0].Cue[0].AgentID).To(Equal("voice-0"))
		Expect(l.Line[0].Cue[1].AgentID).To(Equal("voice-0"))
		Expect(l.Line[0].Cue[0].ByteStart).To(Equal(0))
		Expect(l.Line[0].Cue[0].ByteEnd).To(Equal(4))
		Expect(l.Line[0].Cue[1].ByteStart).To(Equal(5))
		Expect(l.Line[0].Cue[1].ByteEnd).To(Equal(9))

		Expect(l.Line[1].Value).To(Equal("echo"))
		Expect(*l.Line[1].Start).To(Equal(int64(2000)))
		Expect(*l.Line[1].End).To(Equal(int64(3000)))
		Expect(l.Line[1].Cue).To(HaveLen(1))
		Expect(l.Line[1].Cue[0].AgentID).To(Equal("voice-1"))
		Expect(l.Line[1].Cue[0].ByteStart).To(Equal(0))
		Expect(l.Line[1].Cue[0].ByteEnd).To(Equal(3))
	})

	It("emits empty lines with Synced=false for instrumental tracks", func() {
		input := `version: '1.0'
metadata:
  title: 'Solo Piano'
  artist: 'Composer'
  language: 'eng'
  instrumental: true
`
		lyrics, err := parseLyricsfile("", []byte(input))
		Expect(err).ToNot(HaveOccurred())
		Expect(lyrics).To(HaveLen(1))

		l := lyrics[0]
		Expect(l.Kind).To(Equal("main"))
		Expect(l.Lang).To(Equal("eng"))
		Expect(l.DisplayArtist).To(Equal("Composer"))
		Expect(l.DisplayTitle).To(Equal("Solo Piano"))
		Expect(l.Synced).To(BeFalse())
		Expect(l.Line).To(BeEmpty())
		Expect(l.Agents).To(BeNil())
	})

	It("strips agent attribution when overlapping lines carry no cues", func() {
		input := `version: '1.0'
lines:
  - text: "Lead"
    start_ms: 1000
    end_ms: 4000
  - text: "echo"
    start_ms: 2000
    end_ms: 3000
`
		lyrics, err := parseLyricsfile("", []byte(input))
		Expect(err).ToNot(HaveOccurred())
		Expect(lyrics).To(HaveLen(1))

		l := lyrics[0]
		Expect(l.Line).To(HaveLen(2))
		Expect(l.Agents).To(BeNil())
		Expect(l.Line[0].Cue).To(BeNil())
		Expect(l.Line[1].Cue).To(BeNil())
	})
})