Contents

Languages: a show in another language, and an episode dubbed

The October 2026 product audit (docs/audits/product-audit-2026-10.md, finding 27) found Reelwright English only from end to end:

  • the voices were Kokoro's American and British English ones;
  • Rhubarb listened for English words;
  • the fonts drew Latin text only;
  • no show could be made in another language, and no episode dubbed.

This is what was built, how it works, and what it does not do yet. Studio's own words stay English: its menus, buttons and messages are not translated. What people write in it, and everything a show makes, can be in another language.

A show's language

show.json names the language a show is made in (language, a BCP 47 tag: es, ja, pt-BR, zh…), ShowManifest 1.2. English when it names none, so every show made before keeps its file and its behavior byte for byte.

  • Made in it. New show offers the languages Reelwright voices itself (init --blank --language es on the command line), and Show settings changes it. The kit and the demo are English shows.
  • Written in it. The showrunner's context says the show's language when it is not English: screenplays, dialogue, captions, signs and titles are written in it; ids, slugs and Fountain's own markup are not.
  • Cast in it. A character made for a show in another language gets one of that language's Kokoro voices, nearest in presentation and pitch (core/src/adapters/voices.ts, nearestLanguageVoice), whether drawn from a pattern or made from words.

Two tags are the same language when their language subtags are (es and es-MX).

Voices

Kokoro-82M v1.0 speaks nine languages, each named by the first letter of its voices: a and b English (American, British), e Spanish, f French, h Hindi, i Italian, j Japanese, p Brazilian Portuguese, z Mandarin. tools/tts-kokoro/models.json pins all 54 voice packs by sha256 and lists the languages with the G2P each is read through, as Kokoro's own pipeline does for v1.0:

Language G2P
English misaki's English G2P, espeak-ng for words it does not know (as before)
Spanish, French, Hindi, Italian, Brazilian Portuguese misaki's EspeakG2P on espeak-ng's es, fr-fr, hi, it, pt-br
Japanese misaki's cutlet: MeCab (fugashi) with UniDic (unidic-lite), romanized to Kokoro's phonemes
Mandarin misaki's legacy ZHG2P: jieba's words, pypinyin's tones, cn2an's numbers

tools/tts-kokoro/synth.py reads a line in the language it is told (--lang, the request's lang), else in its voice's own. Other languages go in sentence chunks of up to 400 characters; a chunk past Kokoro's 510 phonemes is split at its clauses, never truncated. English is read exactly as before: the same lines give byte-identical WAVs and word timings through the old and the new synth.py.

Word timings. Kokoro's predicted durations give every phoneme's span. In a language other than English, the phonemes' words (space-separated) are laid on the text's words when they line up one for one, or all of them on a text that is one word (a Japanese or Mandarin sentence). Otherwise none, and the compiler times the words from the mouth cues, as it does for a recording.

Behind the provider interface (core/src/voices/provider.ts, ADR-0009):

  • a request carries the line's language when it is not English;
  • a provider says the languages it reads, and a line in another one is never sent to it;
  • a take's cache key holds the language when it is not English (an English take keys as it always has).

A voice per language. A character's voice binding may name its language (CharacterInstance 1.7, up to 16 bindings). A line is voiced with the first binding of an available provider that names the line's language, else one that names none: the character's own voice, which speaks the show's language. In a dub, that voice is lent: it reads the dub's words in their own language's phonemes with its own accent, and the voicing says so once a character (voice.lent).

Changing tools/tts-kokoro changes the provider's version, which is in every generated take's key: a show's generated English takes are made again once, the same audio as before. Recorded and pinned takes (take in the episode) are untouched.

Lip sync

Rhubarb's default recognizer, PocketSphinx, knows English words only. A line in another language goes to its phonetic recognizer (-r phonetic), which listens for sounds in any language and reads no dialog text (core/src/adapters/rhubarb.ts). The recognizer is in the cache key, the record and the tool's name (rhubarb 1.14.0 (phonetic)), so the same take synced either way is two entries; an English line keys as before. In a dub, each line is synced in its own language: the dub's lines as the dub's, the lines it leaves out as the show's.

Dubbing

An episode carries its dubs (EpisodeIR.dubs, 1.52): for each language, the lines it says there by scene and line id, each with its own caption (when not as the compiler cuts it) and its own take (once recorded or pinned).

  • Written by the showrunner or by hand: edit_episode's set_dub writes lines into a dub and keeps its other lines, or takes the dub away. Validation checks each dubbed line is a line of the episode, once, and that no dub is in the show's own language.
  • Put in place (core/src/compile/dub.ts): each line the dub has says its words, with the dub's caption and take in place of the episode's; delivery that names words (emphasis, pauses) is dropped, and word anchors into a dubbed line (a cue with it, a marker, a cut on a word) move to the same share of the way through the new words. The performance, the shots, the sounds and the music stay the episode's. A line the dub leaves out is heard as written, in the show's language with the character's own voice (its take is the episode's), and the compile says so (dub.untranslated).
  • Rendered in a chosen language: reelwright render --language ja, voice --language, compile --language; renders.start's language in the session protocol, on the desktop, the web service and a person's own runners; Episodes' "Render a dub…" on a dubbed episode's card. The dub is voiced, lip-synced and captioned in its language, its Timeline says so (Timeline.language, 1.29) and is written beside the episode's (.reelwright/timelines/tl.<slug>.<tag>.json). reelwright render --language writes it beside the episode's (renders/<slug>.<tag>/<slug>.<tag>.mp4, its captions with it); a render Studio starts, on the desktop, the service or a runner, is the episode's next version like any other (renders/<slug>/v<N>/), its render.json naming the language. Episodes lists the newest of each dub by its language ("v3 · Preview in Japanese").

The version numbers (EpisodeIR 1.52, Timeline 1.29, CharacterInstance 1.7, ShowManifest 1.2) were the next free ones while other features held 1.47 to 1.49 and 1.24; the lead renumbers at merge.

Captions

Captions are cut and wrapped by the core (core/src/compile/captions.ts). Chinese and Japanese have no spaces between words:

  • a row is 32 columns, 16 of their characters (a character of theirs is two columns wide);
  • a row breaks between any two of their characters, never before a stop, a closing bracket, a small kana or the long vowel mark, nor after an opening bracket;
  • a long line breaks into cards after its sentence stops (。!?) and then its commas (、,), with no spaces added.

English and other spaced languages cut and wrap exactly as before. Thai and its neighbours also leave spaces out, but break only between words, which takes a dictionary: they wrap at spaces.

Type

Every bundled font (RwFontFallbacks, engine/addons/reelwright/runtime/graphic/) has, behind its own letters, Noto fonts for the other writing systems:

  • Noto Sans or Noto Serif (behind a serif family) for Cyrillic, Greek and Vietnamese;
  • Noto Sans Devanagari, Noto Sans Arabic;
  • Noto Sans CJK for Chinese, Japanese and Korean: one weight, emboldened for bold text.

Titles, signs and screens (RwTitleCard, RwGraphicDrawer), and Studio's own text (the theme's Inter), draw them; a family keeps its own Latin letters. A typeface a style brought in (StyleSpec 1.1 typography.fonts) has the sans chain behind it too, its bold the bold one; a face shorter than Inter (ascent under 0.968 em) gets lines that tall, as the fallbacks are. Letters that join or reorder (Arabic, Devanagari, Hebrew, Thai…) are drawn shaped, a whole string at a time and never letter-spaced; Chinese and Japanese wrap between characters as captions do.

The fallbacks are subset by tools/fonts/subset.py from the Noto fonts Debian and Ubuntu ship (the same packages make the same bytes), and their vertical metrics are set to Inter's: Godot measures a font's height over its fallbacks (Font.get_height), so a taller fallback would have made every label and title card taller, Latin text included. Measured: every family's ascent, descent and height at 12 to 100 px are its own with the chain behind it.

Size. The Cyrillic and Greek, Devanagari and Arabic fallbacks are 590 KB in all. The CJK font is 4.2 MB (13,176 characters: kana, the CJK marks, every Han character of GB 2312 and JIS X 0208, the 2,350 Hangul syllables of KS X 1001). The desktop Studio carries it. The web Studio leaves it out of its download (the Web preset's exclude_filter) and serves it beside the page (fonts/): it is fetched when a show made in, or dubbed into, Chinese, Japanese or Korean is opened, and reaches every font already drawn (RwFontFallbacks.fetch_cjk).

Licences

Everything new is free to redistribute. The fonts are bundled; the voices, the model and the Python packages are fetched on first use, as before (Kokoro from Hugging Face by pinned sha256, the packages by uv from PyPI as tools/tts-kokoro/uv.lock pins them).

What Licence Notes
Kokoro-82M v1.0, its ONNX export and all 54 voice packs Apache-2.0 hexgrad/Kokoro-82M, onnx-community/Kokoro-82M-v1.0-ONNX-timestamped model cards
jf_gongitsune, jf_nezumi, jf_tebukuro, jm_kumo Apache-2.0 trained on TNC announcers' readings (koniwa's tnc, CC BY 3.0): credit TNC (テレビ西日本) and koniwa
ff_siwis Apache-2.0 trained on the SIWIS French Speech Synthesis Database (CC BY 4.0): credit SIWIS
misaki 0.9.4 Apache-2.0 G2P for every language
espeak-ng (in espeakng-loader 0.2.4) and phonemizer-fork 3.3.2 GPL-3.0-or-later run as a library by the TTS tool, as for English before
fugashi 1.5.2 (with MeCab) MIT, MeCab BSD-3-Clause Japanese
unidic-lite 1.0.8 (UniDic 2.1.2) MIT; UniDic BSD-3-Clause (or GPL, LGPL) Japanese dictionary, in the wheel
jaconv 0.4.0 MIT Japanese
mojimoji 0.0.13 Apache-2.0 Japanese
jieba 0.42.1 (with its dictionary) MIT Mandarin
pypinyin 0.55.0 MIT Mandarin
cn2an 0.5.23, proces 0.1.7 MIT Mandarin numbers
ordered-set 4.1.0 MIT Mandarin
Noto Sans, Noto Serif, Noto Sans Devanagari, Noto Sans Arabic (subsets) OFL-1.1 engine/addons/reelwright/ui/theme/fonts/Noto-LICENSE.txt
Noto Sans CJK JP 2.004 (subset) OFL-1.1 Adobe's copyright; NotoSansCJK-LICENSE.txt

What it does not do yet

  • Titles and signs are not dubbed. A dub replaces lines (and their captions); an episode's title cards, words over the picture and the set's screens say what the show wrote, in the show's language.
  • Korean has no voice. Kokoro does not speak it; its letters draw, its lines are not voiced.
  • The screenplay and storyboard PDFs are Latin only. They are set in Courier, a PDF's own font; other letters print as a question mark.
  • Fountain has no capitals in Chinese and Japanese. Fountain reads a line in capitals with a blank line before it as a character's name, and a caseless line is "in capitals": write a cue as @名前 (Fountain's forced character), or keep a blank line between an action line and the line after it.
  • voice --keep pins the episode's own takes only; a dub's takes stay in the voice store until a dub line's take is set.
  • Kokoro's other languages are graded lower than its English (VOICES.md: C to D for Japanese and Mandarin, none for Spanish and Portuguese); its pitch, timbre and age measurements are for English voices, and the other languages' carry their F0 only.