Gondola lyrics toolkit: parses and generates lyric.
npm install @gondola-lyrics/kit| Format | Line level | Word level | Annotation | Meta |
|---|---|---|---|---|
| LRC | ✅ | ✅ | ✅ | ✅ |
| TTML | ✅ | ✅ | ✅ | ✅ |
Important
This package is ESM-only.
In your TS config, use module: node20 / module: nodenext / moduleResolution: bundler.
Parsing returns a canonical runtime.Lyric together with the diagnostics it produced, so the result is ready to hand to a player without a second conversion step.
import { parse, parseLrc } from '@gondola-lyrics/kit'
const content = `[00:01.00]Like the first time tangled by the fishing line`
const parsed = parse(content)
parsed?.lyric.format // => 'lrc'
parsed?.lyric.lines[0]?.time?.start // => 1000
parsed?.diagnostics // => []
// `parse` recognizes the format and returns `null` when it cannot.
// `parseLrc` takes a known format and never returns `null`, so an unreadable source only carries `status: INVALID`.
parseLrc('no timestamps here').lyric.status // => LyricStatus.INVALID
// LRC keeps translations and romanizations in separate files, attached here by timestamp.
const annotated = parseLrc({
original: `[00:01.00]Like the first time
[00:03.00]tangled by the line`,
translations: `[00:01.00]就像第一次
[00:03.00]被渔线缠住那样`,
romans: `[00:01.00]raiku za fasuto taimu
[00:03.00]tanguru do bai za rain`,
})
annotated.lyric.lines[0]?.annotation?.translations[0].text // => '就像第一次'
annotated.lyric.lines[0]?.annotation?.romans[0].text // => 'raiku za fasuto taimu'
// An exact timestamp match is assigned before any fuzzy one, and rows matching no line are dropped.
// A drifting track attaches within a tolerance, where the nearest unclaimed line wins.
const drifted = {
original: `[00:01.00]Like the first time
[00:03.00]tangled by the line`,
translations: `[00:01.30]就像第一次
[00:03.20]被渔线缠住那样`,
}
parseLrc(drifted).lyric.lines[0]?.annotation // => undefined
parseLrc(drifted, { annotationTolerance: 500 }).lyric.lines[0]?.annotation?.translations[0].text // => '就像第一次'Every parser canonicalizes its lyric and validates the result before returning, so lyric is already in canonical order and diagnostics is exactly what @gondola-lyrics/model reports for it.
generate takes the target format explicitly, so a lyric parsed from one format can be written out as another.
import { generate, parseLrc } from '@gondola-lyrics/kit'
const { lyric } = parseLrc('[00:01.00]Like the first time')
// Separate tracks come back as separate fields, so the result parses again unchanged.
const { original, translations, romans } = generate(lyric, 'lrc')
parseLrc(generate(lyric, 'lrc')) // round-tripsgenerateLrc is the same thing without the dispatch step.
import { detectFormat } from '@gondola-lyrics/kit'
detectFormat('[00:01.00]Like the first time') // => 'lrc'
detectFormat('plain text without timestamps') // => undefinedDetection reads only the original text. It answers whether the source is recognizable, not whether it is valid: a recognized source can still parse into an INVALID lyric.
The kit does not invent diagnostics. It canonicalizes the lyric and hands back exactly what @gondola-lyrics/model reports for it.
const { lyric, diagnostics } = parseLrc(source)
// The same list, computed by the kit on the way out.
import { validateLyric } from '@gondola-lyrics/model/runtime'
validateLyric(lyric) // deep-equals `diagnostics`| Import | Contains |
|---|---|
@gondola-lyrics/kit |
Everything |
@gondola-lyrics/kit/parser |
Parsing |
@gondola-lyrics/kit/generator |
Generation |
Build
npm run buildCheck
npm run check
npm run format