# Architecture of Folia's Lyrics Rendering System: A Three-Stage Pipeline

> Explore the architecture of Folia's lyrics rendering system. Discover the three-stage pipeline that synchronizes animated text on a per-word basis.

- Repository: [冬霧/folia-major](https://github.com/chthollyphile/folia-major)
- Tags: architecture
- Published: 2026-07-06

---

**Folia's lyrics rendering system processes raw lyric files through a three-stage pipeline—parsing, layout preparation, and visualization—to deliver animated, per-word synchronized text using framer-motion.**

The `chthollyphile/folia-major` repository implements a sophisticated architecture for rendering synchronized lyrics in music applications. This system transforms raw LRC, YRC, QRC, and other format files into fluid, animated visual elements through a modular pipeline that separates parsing logic from layout calculations and UI rendering. Understanding this architecture reveals how the application maintains 60fps performance while handling complex CJK text segmentation and word-level animations.

## Parsing Stage: Off-Main-Thread Processing

Raw lyric files are parsed off the main thread to prevent UI jank during file loading. The architecture supports multiple formats including `lrc`, `enhanced-lrc`, `yrc`, `qrc`, `krc`, `vtt`, and `ttml`.

### Web Worker Implementation

The Web Worker in [`src/workers/lyricsParser.worker.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/workers/lyricsParser.worker.ts) handles format detection and delegates to the core parser, keeping the main thread responsive.

```ts
// src/workers/lyricsParser.worker.ts
self.onmessage = (e) => {
  const { type, format, content, translation } = e.data;
  if (type !== 'parse') return;

  const result = parseLyricsByFormat(normalizeWorkerFormat(format), content, translation || '');
  self.postMessage({ type: 'result', data: result });
};

```

The **core parser** lives in [`src/utils/lyrics/parserCore.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/utils/lyrics/parserCore.ts) and exposes the `parseLyricsByFormat` function. This function returns a structured `LyricData` object containing `Line` arrays, where each line contains `Word` objects with timing metadata.

### API Proxy Layer

A thin Express-style proxy in [`src/api/lyric-proxy.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/api/lyric-proxy.ts) provides an HTTP interface to the parsing engine for server-side rendering scenarios.

```ts
// src/api/lyric-proxy.ts
router.post('/parse', async (req, res) => {
  const { format, content, translation } = req.body;
  const parsed = await parseLyricsByFormat(format, content, translation);
  res.json(parsed);
});

```

## Layout Preparation: Semantic Grouping and Normalization

Parsed words often fragment into visually noisy timing units (e.g., "It", "'", "s" as separate entities). The layout stage in [`src/utils/lyrics/cjkSemanticLayout.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/utils/lyrics/cjkSemanticLayout.ts) normalizes these into clean **layout units** and **display words**.

### Building Layout Units

The `buildPostLyricLayoutUnits` function constructs layout units that handle **CJK semantic grouping** and **sticky punctuation** attachment. When `semantic` is enabled, it uses `Intl.Segmenter` to merge consecutive CJK characters while preserving per-character timing data.

```ts
// src/utils/lyrics/cjkSemanticLayout.ts
export const buildPostLyricLayoutUnits = (
  line: Pick<Line, 'fullText' | 'words'>,
  options: BuildPostLyricLayoutUnitsOptions = {}
): LyricLayoutUnit[] => {
  const rawUnits = options.semantic
    ? buildCjkSemanticLayoutUnits(line)          // CJK grouping via Intl.Segmenter
    : createSingleWordLayoutUnits(line.words);   // fallback – one unit per word

  return options.sticky
    ? applyStickyPunctuationLayoutUnits(rawUnits) // attach punctuation / contractions
    : rawUnits;
};

```

**Sticky punctuation** logic attaches apostrophes, trailing punctuation, and contraction suffixes to preceding words via `applyStickyPunctuationLayoutUnits`, ensuring tokens like "It's" render as single visual units.

### Converting to Display Words

The `buildDisplayWordsFromLayoutUnits` function flattens layout units into the final **display words** consumed by visualizer components. Sticky non-semantic units collapse into single words, while semantic CJK units preserve their original word arrays for per-character timing.

```ts
// src/utils/lyrics/cjkSemanticLayout.ts
export const buildDisplayWordsFromLayoutUnits = (units: LyricLayoutUnit[]): Word[] =>
  units.flatMap(unit => unit.isSticky && !unit.isSemantic
    ? [{ text: unit.text, startTime: unit.startTime, endTime: unit.endTime }]
    : unit.words);

```

## Visualization Stage: Component Architecture

Visualizer components receive `Line` objects and **render profiles**, then animate individual words using `framer-motion` variants. The architecture includes multiple visualizer implementations: Classic, Partita, Monet, and Cappella.

### Classic Visualizer Implementation

The Classic visualizer in [`src/components/visualizer/classic/Visualizer.tsx`](https://github.com/chthollyphile/folia-major/blob/main/src/components/visualizer/classic/Visualizer.tsx) demonstrates the standard rendering pattern, building layout units and mapping display words to motion components.

```tsx
// components/visualizer/classic/Visualizer.tsx
const layoutUnits = buildPostLyricLayoutUnits(line, { semantic: true, sticky: true });
const displayWords = buildDisplayWordsFromLayoutUnits(layoutUnits);
// … map displayWords → <Word> components with motion variants

```

### Partita and Advanced Visualizers

The Partita visualizer in [`src/components/visualizer/partita/VisualizerPartita.tsx`](https://github.com/chthollyphile/folia-major/blob/main/src/components/visualizer/partita/VisualizerPartita.tsx) implements a column-chunk layout with sophisticated caching via `layoutCacheRef`. It groups layout units into chunks using `buildSequentialColumns` before rendering.

```tsx
// components/visualizer/partita/VisualizerPartita.tsx
const layoutUnits = buildPostLyricLayoutUnits(line, {
  semantic: tuning.useSemanticLayout,
  sticky: true,
});
const columns = buildSequentialColumns(line, theme, windowHeight, tuning);
// columns → <PartitaChunk> → <PartitaWord> (framer-motion)

```

Alternative implementations include the **Monet** visualizer ([`src/components/visualizer/monet/VisualizerMonet.tsx`](https://github.com/chthollyphile/folia-major/blob/main/src/components/visualizer/monet/VisualizerMonet.tsx)) for alternate rendering styles and the **Cappella** visualizer ([`src/components/visualizer/cappella/VisualizerCappella.tsx`](https://github.com/chthollyphile/folia-major/blob/main/src/components/visualizer/cappella/VisualizerCappella.tsx)) for karaoke-style displays.

### Runtime State Management

All visualizers rely on the `useVisualizerRuntime` hook in [`src/components/visualizer/runtime.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/components/visualizer/runtime.ts) to fetch the active line, pre-heat upcoming lines, and compute render hints based on the current playback position.

```ts
// components/visualizer/runtime.ts
export const useVisualizerRuntime = ({ currentTime, currentLineIndex, lines, getLineEndTime }) => {
  // returns activeLine, upcomingLine, recentCompletedLine, nextLines
};

```

Animation timing is driven by a shared **MotionValue** `currentTime` that synchronizes word-level animations across all visualizer components.

## End-to-End Data Flow

A typical lyric rendering flow follows this sequence:

```ts
// 1️⃣ Fetch raw lyric file (LRC, YRC, etc.) – e.g. via /api/lyric-proxy
// 2️⃣ Parse in a worker → LyricData { lines: Line[] }
// 3️⃣ UI receives the data, picks the active Line (runtime)
// 4️⃣ Layout stage:
//    const units = buildPostLyricLayoutUnits(activeLine, {
//          semantic: visualizerTuning.useSemanticLayout,
//          sticky: true,
//    });
//    const displayWords = buildDisplayWordsFromLayoutUnits(units);
// 5️⃣ Visualizer renders each displayWord with motion variants.
// 6️⃣ Animation timing is driven by the shared MotionValue `currentTime`.

```

## Summary

- **Three-stage pipeline**: The architecture separates parsing (worker), layout (CJK/sticky normalization), and visualization (framer-motion) into distinct concerns.
- **Web Worker parsing**: [`src/workers/lyricsParser.worker.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/workers/lyricsParser.worker.ts) prevents UI blocking by parsing `lrc`, `yrc`, `qrc`, and other formats off the main thread.
- **Semantic layout engine**: [`src/utils/lyrics/cjkSemanticLayout.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/utils/lyrics/cjkSemanticLayout.ts) handles CJK character grouping via `Intl.Segmenter` and sticky punctuation attachment via `applyStickyPunctuationLayoutUnits`.
- **Visualizer modularity**: Multiple visualizers (Classic, Partita, Monet, Cappella) consume normalized display words from `buildDisplayWordsFromLayoutUnits`.
- **Runtime synchronization**: `useVisualizerRuntime` manages active line state and pre-heating, while a shared `currentTime` MotionValue drives frame-accurate word animations.

## Frequently Asked Questions

### What lyric formats does Folia support?

Folia supports `lrc`, `enhanced-lrc`, `yrc`, `qrc`, `krc`, `vtt`, and `ttml` formats through the `parseLyricsByFormat` function in [`src/utils/lyrics/parserCore.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/utils/lyrics/parserCore.ts). The parser normalizes all formats into a standard `LyricData` structure containing `Line` and `Word` objects.

### How does Folia handle CJK character segmentation?

CJK segmentation occurs in `buildCjkSemanticLayoutUnits` within [`src/utils/lyrics/cjkSemanticLayout.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/utils/lyrics/cjkSemanticLayout.ts). When the `semantic` option is enabled, the function uses `Intl.Segmenter` to identify consecutive CJK characters and merges them into single layout units while preserving per-character timing data for animation purposes.

### Why does Folia use Web Workers for lyric parsing?

Parsing large lyric files or complex formats like QRC can block the main thread, causing frame drops in the audio visualization. The [`src/workers/lyricsParser.worker.ts`](https://github.com/chthollyphile/folia-major/blob/main/src/workers/lyricsParser.worker.ts) implementation moves this computation to a background thread, ensuring the UI remains responsive at 60fps while lyrics load.

### What is the difference between layout units and display words?

**Layout units** (`LyricLayoutUnit`) are intermediate objects created by `buildPostLyricLayoutUnits` that handle CJK semantic grouping and sticky punctuation logic. **Display words** are the final flattened `Word` array produced by `buildDisplayWordsFromLayoutUnits`, optimized for direct consumption by visualizer components. Sticky units collapse into single display words, while semantic CJK units retain their original word arrays for granular timing control.