# CJK subtitle burn-in: verify text, font mapping and actual pixels separately

This downloadable recipe uses an original 3.6-second geometric test slate and four original subtitle cues. It performs real local FFmpeg/libass rendering. It does **not** test MiniMax H3 or any generation provider. The silent 1280×720 sample is a controlled preflight exercise, not a full-length media delivery script.

## What was executed

- Node.js v24.14.0; FFmpeg 6.0 arm64; libass 0.17.0; HarfBuzz 6.0.0; FriBidi 1.0.12. Full command arguments and renderer logs are downloadable.
- Unmodified Noto Sans CJK SC Regular 2.004, official `Sans2.004` commit `523d033d6cb47f4a80c58a35753646f5c3608a78`. The font is 16,437,364 bytes. Font SHA-256: `2c76254f6fc379fddfce0a7e84fb5385bb135d3e399294f6eeb6680d0365b74b`.
- The sample’s 43 unique non-whitespace codepoints map to valid nonzero glyph IDs in the selected Unicode cmap format 12 table. Twenty-two preflight scenarios passed; seven media assertions passed. Four frames were decoded and separately inspected visually.
- This FFmpeg build reports CoreText as its font provider. The log records loading the bundled font and selecting `NotoSansCJKsc-Regular`. A supplied fontconfig file does not establish general system-font isolation on this build.

## 1. Keep the font license with the font

Download `NotoSansCJKsc-Regular.otf`, the unmodified `OFL.txt`, and `FONT-COPYRIGHT.txt` together. The official [Noto license](https://github.com/notofonts/noto-cjk/blob/523d033d6cb47f4a80c58a35753646f5c3608a78/LICENSE) permits bundled redistribution subject to its conditions, including keeping the license/copyright and not selling the font by itself. This recipe does not subset or rename the font. Original sample media has separate reuse terms in `LICENSE-original-assets.txt`; FFmpeg itself is not bundled.

The official [Noto download guide](https://github.com/notofonts/noto-cjk/blob/523d033d6cb47f4a80c58a35753646f5c3608a78/Sans/README.md) explains language-specific resources. An SC font’s character coverage does not prove the preferred Japanese, Korean or Traditional Chinese regional shape. Use the appropriate language font and separately revise/check its family name and pinned hash when adapting this recipe.

## 2. Run text and font preflight

Put all downloadable scripts, `sample.srt` and the font in one folder:

```sh
node --version
node run-preflight-tests.mjs
```

No negative test starts FFmpeg. The checks include missing codepoints, missing fonts, invalid UTF-8/UTF-16 input, cue overlap, ASS/HTML markup, variation sequences, changed font hashes, traversal, symlinks and CLI argument boundaries. The parser intentionally accepts a small plaintext SRT subset: at most 32 cues, two lines per cue, 64 codepoints per line, no overlap, an end time within one minute and boundaries aligned to 10 ms. ASS uses centiseconds; this script refuses finer SRT boundaries instead of rounding them into a shorter or zero-length cue.

The [OpenType cmap specification](https://learn.microsoft.com/en-us/typography/opentype/spec/cmap) distinguishes character mappings from missing glyph index zero. This sample checker supports Unicode cmap formats 4 and 12; it does not establish variation-sequence support, complex shaping or regional glyph correctness. It refuses variation selectors/ZWJ instead of silently declaring them covered.

## 3. Perform a real burn-in in a fresh output folder

Use your own FFmpeg 6.0 binary built with libass and libx264. The binary is not included or installed by these scripts. Pass its absolute path:

```sh
node run-recipe.mjs --ffmpeg /absolute/path/to/ffmpeg --out ./fresh-output
```

The runner first checks the SRT and pinned font, generates the original slate, converts the plaintext cues to fixed-style ASS, renders the subtitles and decodes four PNGs. It uses child-process argument arrays with `shell:false`, fixed filenames in a private staging directory, one encoder/filter thread, a one-minute child timeout and bounded logs. User filenames never enter a filter expression. Existing outputs are refused, and the temporary stage is removed in `finally`.

For the supplied 1280×720, 3.6-second source copy you can use:

```sh
node run-recipe.mjs --ffmpeg /absolute/path/to/ffmpeg --out ./another-fresh-output --input ./sample-source.mp4 --srt ./sample.srt --font ./NotoSansCJKsc-Regular.otf
```

This exercise outputs only 3.6 seconds and removes audio. Adapt and review duration, frame size, audio mapping and caption placement before processing a real delivery file; do not use it unchanged as a full-video exporter.

## 4. Read evidence without overclaiming

`coverage-report.json` shows every checked codepoint and glyph ID. `command-log.json` and `ffmpeg-log.txt` show actual rendering commands, tool versions, selected font and diagnostics. `execution-receipt.json` contains output byte counts, SHA-256 values, decoded-frame checks and the preflight receipt binding.

Compare `keyframe-01.png` through `keyframe-04.png` to the intended cue text in `sample.srt`: the first is bilingual, the next two use Simplified/Traditional Chinese, and the fourth combines Japanese and Korean. `visual-review.json` records the limited sample readback. It does not replace a native-language typography review.

The runner measures a changed-pixel region at x=0, y=600, width=1280, height=120 against the source slate. This establishes that visible pixels changed in the subtitle band. A nonempty PNG, changed pixels or nonzero cmap IDs alone **does not prove the characters are correct**. Review replacement boxes, punctuation, regional forms, clipping and timing separately.

## Failures and useful changes

| Failure | Change before retrying |
|---|---|
| `invalid_utf8` / `UTF16-BOM-rejected` test | Explicitly export the subtitle as UTF-8; do not relabel unknown bytes as UTF-8. |
| `subtitle_precision_not_supported` | Use 10 ms-aligned cue boundaries; this sample refuses 201–209 ms rather than collapsing it to 0.20–0.20 seconds in ASS. |
| `missing_glyph` | Identify the exact missing U+ codepoint in the coverage report; choose a legally available font covering it and redo the family/hash/visual checks. |
| `missing_input_file`, `font_hash_mismatch` | Restore the bundled font and its license; do not rely on an unrecorded system fallback. |
| `subtitle_markup_refused` / `unsupported_variation_sequence` | Use the supported plaintext sample, or build and validate an explicit ASS/variation-sequence workflow separately. |
| `no_visible_subtitle_difference` / `font_selection_unconfirmed` | Inspect the actual renderer log, cue times and decoded frames; a successful process exit alone is insufficient. |
| `output_exists` / path boundary failures | Use a fresh controlled directory and ordinary local file paths. The runner does not overwrite or fetch network media. |

Different FFmpeg builds/platforms may produce different encoded bytes even with matching version numbers. The supplied hashes identify this actual local run; they are not a cross-platform bit-identical-output promise. No provider generation, billing, production task or search-traffic result is claimed.
