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 eson 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
languagewhen it is not English; - a provider says the
languagesit 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'sset_dubwrites 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 cuewithit, 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'slanguagein 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 --languagewrites 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 --keeppins the episode's own takes only; a dub's takes stay in the voice store until a dub line'stakeis 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.