User story
As a video editor producing line-caption shorts from non-English source video, I want captions:create to transcribe and force-align Mandarin Chinese, Hindi, or Bahasa Indonesia audio correctly, so that lines.json contains accurate text and per-line timestamps in the source language instead of garbled English-model output.
Background
scripts/line-captions/create-line-captions.js drives transcription and alignment for the line-caption shorts pipeline (see docs/implementation-guides/LINE_CAPTION_SHORTS.md), but every stage is hardcoded to English:
Transcriber (scripts/transcribe/Transcriber.js:10) defaults this.model to medium.en — an English-only whisper.cpp model. create-line-captions.js never overrides options.model, so any audio is transcribed through an English-only model regardless of its actual language.
Transcriber.runWhisper() (scripts/transcribe/Transcriber.js:110-126) never passes a --language arg to whisper.cpp's transcribe() call at all.
align-transcript.js defaults language: cli.language || 'en' (scripts/align/align-transcript.js:248) for the WhisperX forced-alignment step, and accepts --language on its CLI (align-transcript.js:18) — but create-line-captions.js spawns it with only --audio/--raw, never forwarding a language (create-line-captions.js:124-126).
run_whisperx_align.py calls whisperx.load_align_model(language_code=args.language, ...) (scripts/align/run_whisperx_align.py:187-188). WhisperX's built-in alignment-model registry (whisperx/alignment.py:32-76, installed version 3.8.5) has entries for zh and hi, but no entry for id (Bahasa Indonesia). Aligning Indonesian audio will fail model resolution unless an explicit override model is supplied.
scripts/line-captions/chunkLines.js:16-30 (buildWordGroups) reconstructs whisper's BPE sub-word tokens into whole "words" using a leading-space heuristic (!t.text.startsWith(' ')) that assumes whitespace-delimited words. Mandarin Chinese text has no spaces between words at all, so this heuristic is meaningless for zh — depending on whisper's tokenizer behavior it will either merge whole phrases into one "word" or split every character into its own "word," making wordsPerLine = 3 chunking incoherent. Hindi and Bahasa Indonesia are space-delimited, so this heuristic likely works unmodified for them, but needs verification against real transcripts.
None of this fails loudly — a non-English video run through the current pipeline today silently produces English (mis)transcriptions with no error.
Acceptance criteria
Happy path
Given a Mandarin Chinese source video
When I run npm run captions:create -- --video <path> --language zh
Then Transcriber uses a multilingual whisper.cpp model (not a .en variant), whisper.cpp is invoked with --language zh, WhisperX aligns using language_code="zh", and the resulting lines.json contains correctly segmented Chinese text with sane per-line timestamps (verified by manual read-through of lines.doc.txt).
Given a Hindi source video
When I run npm run captions:create -- --video <path> --language hi
Then transcription, alignment, and chunking all complete successfully and lines.doc.txt reads as coherent Hindi (Devanagari) text grouped by speaker.
Given a Bahasa Indonesia source video
When I run npm run captions:create -- --video <path> --language id
Then the pipeline either successfully aligns using an explicit override alignment model, or — if no adequate model exists — the alignment step degrades gracefully to the existing CUT_START_BIAS timestamp heuristic (per token.t_end being unpopulated, as already documented in CLAUDE.md's Data Schemas section) rather than crashing.
Error path / edge case
Given an unsupported/unrecognized --language code
When I run captions:create with it
Then the script fails fast with a clear error before spending time on transcription, rather than passing an invalid code through to whisper.cpp/WhisperX and failing deep in the pipeline.
Given --language is omitted
When I run captions:create
Then behavior is unchanged from today (defaults to English, medium.en model) — this issue must not change default behavior for existing English workflows.
Out of scope
- Burned-in caption rendering / font glyph support for non-Latin scripts (tracked separately — see dependency below).
- Bilingual (dual-language) subtitle rendering (tracked separately — see dependency below).
- Automatic language detection — this issue is explicit-language-flag only.
- Any change to
scripts/diarize/* — diarization is acoustic/language-agnostic and needs no changes.
Technical context
scripts/transcribe/Transcriber.js — DEFAULT_MODEL = 'medium.en' (line 10), constructor accepts options.model (line 38) but not options.language; runWhisper() (lines 110-126) builds the additionalArgs array passed to @remotion/install-whisper-cpp's transcribe().
scripts/align/align-transcript.js — resolveArgs() (~line 240-250) resolves --language; main() spawns run_whisperx_align.py with '--language', language (line 274).
scripts/align/run_whisperx_align.py:110,187-188 — argparse default 'en'; whisperx.load_align_model(language_code=..., model_name=...) accepts an explicit model_name override for languages missing from the default registry.
scripts/line-captions/create-line-captions.js:20-29 (parseArgs) — needs a new --language flag; :124-126 needs to forward it to align-transcript.js; the new Transcriber({...}) call at :115 needs a model/language derived from it.
scripts/line-captions/chunkLines.js — buildWordGroups() word-boundary heuristic; needs a per-script chunking strategy (see acceptance criteria).
- whisper.cpp multilingual models (no
.en suffix, e.g. medium) support a --language arg; confirm via @remotion/install-whisper-cpp's downloadWhisperModel/transcribe API which language codes it accepts.
Implementation details
- Add a
--language <code> CLI flag to create-line-captions.js, defaulting to en (preserves current behavior when omitted).
- Map the language code to a whisper.cpp model name (e.g.
en → medium.en, anything else → medium) and pass it as Transcriber's options.model.
- Add a
language option to Transcriber and thread it through runWhisper() as a whisper.cpp --language arg.
- Forward
--language from create-line-captions.js into the align-transcript.js spawn call.
- For
id specifically: research whether a usable community wav2vec2 alignment model exists to pass as run_whisperx_align.py's model_name override; if not, make the WhisperX alignment step optional/skippable per-language so the pipeline falls back to whisper's own token timestamps instead of hard-failing.
- Add a language-aware branch to
chunkLines.js's word-boundary detection: keep the existing space-prefix heuristic for space-delimited scripts, add a character-count-based chunking mode for zh (since "words" don't exist as a concept), gated on the language passed through from create-line-captions.js.
- Validate the
--language value against a fixed allow-list (en, zh, hi, id) and fail fast with a clear message otherwise.
Additional test scenarios
- Unit test:
chunkLines.js's new character-count chunking path for a synthetic Chinese-token transcript, verifying lines never exceed the character cap and never span a speaker boundary.
- Unit test: language-code validation rejects an unsupported code before any subprocess is spawned.
- Integration test (
tests/integration/): create-line-captions.js --language zh against a short fixture audio clip produces a lines.json with non-empty, non-Latin text fields.
- Manual test: run all three target languages end-to-end and read
lines.doc.txt for coherence (no automated correctness check for transcription quality itself).
Hard constraints
- Must not change default (
en) behavior when --language is omitted.
- Must not silently mistranscribe — an unsupported language code must fail before transcription starts, not after.
- The Bahasa Indonesia alignment gap must be handled explicitly (documented fallback or explicit override model), not left to crash mid-pipeline.
- Any new field added to
lines.json/transcript.json (e.g. a language meta field) must be declared in remotion/types/lineCaptions.ts and documented in CLAUDE.md's Data Schemas section, per the repo's Agent Implementation Convention.
Dependency issues
- Independent of the bilingual-subtitles issue, but that issue depends on this one (needs a working non-English transcript to translate/render against).
- Independent of the CJK/Devanagari font-rendering issue — this issue only produces correct
lines.json data; it does not render captions.
User story
As a video editor producing line-caption shorts from non-English source video, I want
captions:createto transcribe and force-align Mandarin Chinese, Hindi, or Bahasa Indonesia audio correctly, so thatlines.jsoncontains accurate text and per-line timestamps in the source language instead of garbled English-model output.Background
scripts/line-captions/create-line-captions.jsdrives transcription and alignment for the line-caption shorts pipeline (seedocs/implementation-guides/LINE_CAPTION_SHORTS.md), but every stage is hardcoded to English:Transcriber(scripts/transcribe/Transcriber.js:10) defaultsthis.modeltomedium.en— an English-only whisper.cpp model.create-line-captions.jsnever overridesoptions.model, so any audio is transcribed through an English-only model regardless of its actual language.Transcriber.runWhisper()(scripts/transcribe/Transcriber.js:110-126) never passes a--languagearg to whisper.cpp'stranscribe()call at all.align-transcript.jsdefaultslanguage: cli.language || 'en'(scripts/align/align-transcript.js:248) for the WhisperX forced-alignment step, and accepts--languageon its CLI (align-transcript.js:18) — butcreate-line-captions.jsspawns it with only--audio/--raw, never forwarding a language (create-line-captions.js:124-126).run_whisperx_align.pycallswhisperx.load_align_model(language_code=args.language, ...)(scripts/align/run_whisperx_align.py:187-188). WhisperX's built-in alignment-model registry (whisperx/alignment.py:32-76, installed version 3.8.5) has entries forzhandhi, but no entry forid(Bahasa Indonesia). Aligning Indonesian audio will fail model resolution unless an explicit override model is supplied.scripts/line-captions/chunkLines.js:16-30(buildWordGroups) reconstructs whisper's BPE sub-word tokens into whole "words" using a leading-space heuristic (!t.text.startsWith(' ')) that assumes whitespace-delimited words. Mandarin Chinese text has no spaces between words at all, so this heuristic is meaningless forzh— depending on whisper's tokenizer behavior it will either merge whole phrases into one "word" or split every character into its own "word," makingwordsPerLine = 3chunking incoherent. Hindi and Bahasa Indonesia are space-delimited, so this heuristic likely works unmodified for them, but needs verification against real transcripts.None of this fails loudly — a non-English video run through the current pipeline today silently produces English (mis)transcriptions with no error.
Acceptance criteria
Happy path
Given a Mandarin Chinese source video
When I run
npm run captions:create -- --video <path> --language zhThen
Transcriberuses a multilingual whisper.cpp model (not a.envariant), whisper.cpp is invoked with--language zh, WhisperX aligns usinglanguage_code="zh", and the resultinglines.jsoncontains correctly segmented Chinese text with sane per-line timestamps (verified by manual read-through oflines.doc.txt).Given a Hindi source video
When I run
npm run captions:create -- --video <path> --language hiThen transcription, alignment, and chunking all complete successfully and
lines.doc.txtreads as coherent Hindi (Devanagari) text grouped by speaker.Given a Bahasa Indonesia source video
When I run
npm run captions:create -- --video <path> --language idThen the pipeline either successfully aligns using an explicit override alignment model, or — if no adequate model exists — the alignment step degrades gracefully to the existing
CUT_START_BIAStimestamp heuristic (pertoken.t_endbeing unpopulated, as already documented inCLAUDE.md's Data Schemas section) rather than crashing.Error path / edge case
Given an unsupported/unrecognized
--languagecodeWhen I run
captions:createwith itThen the script fails fast with a clear error before spending time on transcription, rather than passing an invalid code through to whisper.cpp/WhisperX and failing deep in the pipeline.
Given
--languageis omittedWhen I run
captions:createThen behavior is unchanged from today (defaults to English,
medium.enmodel) — this issue must not change default behavior for existing English workflows.Out of scope
scripts/diarize/*— diarization is acoustic/language-agnostic and needs no changes.Technical context
scripts/transcribe/Transcriber.js—DEFAULT_MODEL = 'medium.en'(line 10),constructoracceptsoptions.model(line 38) but notoptions.language;runWhisper()(lines 110-126) builds theadditionalArgsarray passed to@remotion/install-whisper-cpp'stranscribe().scripts/align/align-transcript.js—resolveArgs()(~line 240-250) resolves--language;main()spawnsrun_whisperx_align.pywith'--language', language(line 274).scripts/align/run_whisperx_align.py:110,187-188— argparse default'en';whisperx.load_align_model(language_code=..., model_name=...)accepts an explicitmodel_nameoverride for languages missing from the default registry.scripts/line-captions/create-line-captions.js:20-29(parseArgs) — needs a new--languageflag;:124-126needs to forward it toalign-transcript.js; thenew Transcriber({...})call at:115needs amodel/languagederived from it.scripts/line-captions/chunkLines.js—buildWordGroups()word-boundary heuristic; needs a per-script chunking strategy (see acceptance criteria)..ensuffix, e.g.medium) support a--languagearg; confirm via@remotion/install-whisper-cpp'sdownloadWhisperModel/transcribeAPI which language codes it accepts.Implementation details
--language <code>CLI flag tocreate-line-captions.js, defaulting toen(preserves current behavior when omitted).en→medium.en, anything else →medium) and pass it asTranscriber'soptions.model.languageoption toTranscriberand thread it throughrunWhisper()as a whisper.cpp--languagearg.--languagefromcreate-line-captions.jsinto thealign-transcript.jsspawn call.idspecifically: research whether a usable community wav2vec2 alignment model exists to pass asrun_whisperx_align.py'smodel_nameoverride; if not, make the WhisperX alignment step optional/skippable per-language so the pipeline falls back to whisper's own token timestamps instead of hard-failing.chunkLines.js's word-boundary detection: keep the existing space-prefix heuristic for space-delimited scripts, add a character-count-based chunking mode forzh(since "words" don't exist as a concept), gated on the language passed through fromcreate-line-captions.js.--languagevalue against a fixed allow-list (en,zh,hi,id) and fail fast with a clear message otherwise.Additional test scenarios
chunkLines.js's new character-count chunking path for a synthetic Chinese-token transcript, verifying lines never exceed the character cap and never span a speaker boundary.tests/integration/):create-line-captions.js --language zhagainst a short fixture audio clip produces alines.jsonwith non-empty, non-Latintextfields.lines.doc.txtfor coherence (no automated correctness check for transcription quality itself).Hard constraints
en) behavior when--languageis omitted.lines.json/transcript.json(e.g. alanguagemeta field) must be declared inremotion/types/lineCaptions.tsand documented inCLAUDE.md's Data Schemas section, per the repo's Agent Implementation Convention.Dependency issues
lines.jsondata; it does not render captions.