# Parsers

`@braccato/parsers` turns lyrics text into an array of `Lyric` objects. It reads TTML, LRC, SRT, QRC and plain text, so one package covers every provider this API serves. The output is what [`@braccato/core`](https://docs.betterlyrics.org/braccato/renderer) renders, but it is plain data and works without the renderer, in the browser or in Node.

## Install

```bash
npm i @braccato/parsers
```

## Detect the format

`detectParser` looks at the text and returns the parser that can read it. It tries TTML, LRC, SRT, QRC and plain text, in that order. Plain text accepts anything, so you always get a parser back.

```javascript
import { detectParser } from "@braccato/parsers";

const lyrics = detectParser(text).parse(text, durationMs);
```

Every parser has the same two methods: `detect(input)` and `parse(input, duration)`. The duration is the song length in **milliseconds**. The API takes `d` in seconds, so multiply by 1000.

You can also import a parser directly:

```javascript
import { TTMLParser, LRCParser, SRTParser, QRCParser, PlainParser } from "@braccato/parsers";
```

## The Lyric shape

Each `Lyric` is one line. Only `startTimeMs`, `durationMs` and `words` are always present. The other fields depend on what the source provides.

```typescript
interface Lyric {
  startTimeMs: number;
  durationMs: number;
  words: string;
  key?: string;
  parts?: LyricPart[];
  agent?: string;
  translations?: { [lang: string]: string };
  romanization?: string;
  timedRomanization?: LyricPart[];
  isInstrumental?: boolean;
}

interface LyricPart {
  startTimeMs: number;
  durationMs: number;
  words: string;
  isBackground?: boolean;
  explicit?: boolean;
}
```

| Field | Description |
| --- | --- |
| `words` | The full line text, background vocals included |
| `parts` | Syllable or word timing. A line without `parts` is line-synced |
| `agent` | The vocalist: `v1`, `v2`, and so on, with `v1000` for a group |
| `key` | The source's own line id, used to attach translations and romanizations |
| `isInstrumental` | A synthetic line with empty `words` that fills a gap nobody sings over |

The TTML and QRC parsers insert instrumental lines for gaps longer than 5 seconds: an intro before the first line, and a break between two lines. They add an outro only when you pass the song duration.

To use the types without the runtime code, import them from `@braccato/parsers` (`import type`) or install `@braccato/types`.

## TTML

The default `/getLyrics` endpoint returns the document in the `ttml` field. Pass the string as received. Do not reformat it (see [whitespace handling](https://docs.betterlyrics.org/docs/response-format#important-whitespace-handling)).

```javascript
import { TTMLParser } from "@braccato/parsers";

const { ttml } = await fetch(url).then((response) => response.json());
const lyrics = TTMLParser.parse(ttml);
```

The TTML parser reads syllable timing, `ttm:role="x-bg"` background vocals, `ttm:agent` vocalists, explicit flags, `itunes:key`, translations and transliterations. It maps agent ids to stable slots (`v1`, `v2`, `v1000` for a group), so you do not need to know the ids a document declares.

### parseTTMLContent

`TTMLParser.parse` ignores its duration argument, because a TTML document states its own duration. Use `parseTTMLContent` when you also want the document language or sync level, or when you want to give a fallback duration:

```javascript
import { parseTTMLContent } from "@braccato/parsers";

const { lyrics, isWordSynced, language } = parseTTMLContent(ttml, {
  songDurationMs: 170000,
  instrumentalGapMs: 5000,
});
```

| Option | Description |
| --- | --- |
| `songDurationMs` | Used only when `<body>` has no `dur`. Needed for the outro instrumental line |
| `instrumentalGapMs` | Silence longer than this becomes an instrumental line. Defaults to 5000 |

`language` comes from the document's `xml:lang`. You can pass it to the renderer as `lyricsOptions.language`.

## Kugou

`/kugou/getLyrics` returns LRC-style text in the `lyrics` field. It is line-synced, so the parsed lines have no `parts`.

```javascript
import { LRCParser } from "@braccato/parsers";

const { lyrics: lrc } = await fetch(url).then((response) => response.json());
const lyrics = LRCParser.parse(lrc, durationSeconds * 1000);
```

`LRCParser.parse` also runs a set of timing fixers made for word-timed LRC. To get the document exactly as written, use `parseLRC`, and run `lrcFixers` yourself if you want them:

```javascript
import { parseLRC, lrcFixers } from "@braccato/parsers";

const lyrics = parseLRC(lrc, durationSeconds * 1000);
lrcFixers(lyrics);
```

`lrcFixers` changes the array in place.

## QQ

`/qq/getLyrics` returns a QRC document inside a `<QrcInfos>` XML envelope in the `lyrics` field. The parser accepts the envelope or a bare QRC body. See [QRC format](https://docs.betterlyrics.org/docs/qrc-format) for the structure.

```javascript
import { parseQRC } from "@braccato/parsers";

const { lyrics: qrc } = await fetch(url).then((response) => response.json());
const lyrics = parseQRC(qrc, durationSeconds * 1000, {
  title: "Wanderlust",
  artist: "The Weeknd",
});
```

The third argument is optional song metadata. The QRC parser:

-   Skips header tags such as `[ti:]` and `[ar:]`
-   Drops credit lines such as `Lyrics by：` and `Composed by：`
-   With metadata, drops lines near the start that only repeat the title or artist
-   Turns a `Name:` prefix at the start of a line into an `agent`, and removes it from the text. The singer carries over to the next lines until a new prefix appears

`QRCParser.parse(input, duration)` does the same without metadata, and `detectParser` returns `QRCParser` for a QQ response.

## Parse once

Parsing is not free. Parse when the lyrics arrive, keep the result, and give the same array to the renderer. Do not parse on every playback tick.

Source: https://docs.betterlyrics.org/braccato/parsers
