Our library has films and series in Japanese that never came with subtitles, and a household that watches in English and in Thai. This is the tool we built on the AI appliance to fix that: it listens to a video, works out what language is being spoken and where, transcribes it with two speech engines that check each other, translates the result into whichever of 45 languages you tick, and drops a subtitle file next to the video — overnight, unattended, and without a single byte leaving the building. As of October 2026 it is on GitHub for anyone with an NVIDIA card to run.
--target en,th,de. The translator is chosen per
language pair, works in windows of twenty lines with the surrounding dialogue as context, and is
told the genre — a cheeky five-year-old in a comedy stays cheeky in Thai, with the particles a
child would use, not the ones a bank clerk would. Chinese comes out Simplified, Cantonese
Traditional; Japanese and Chinese keep their own punctuation and get a slower reading speed; Thai,
Lao, Khmer and Burmese never break a line between a consonant and its vowel.ffmpeg, Docker with the NVIDIA Container
Toolkit, and Ollama on the same machine for the translator. The only thing to
fill in is where your videos are.git clone https://github.com/dodgypast/mlsubgen.git && cd mlsubgen
cp .env.example .env # MEDIA_DIR = your videos; your uid/gid; default languages
mkdir -p data models # state and the model cache, created by you so the containers can write them
docker compose up -d --build # ~14 GB image (CUDA torch and the speech stacks), 10-20 minutes the first time
docker compose run --rm worker pull # the speech models (~8 GB) and the translators your card can hold
docker compose run --rm worker pull speakers # optional: the two speaker models (33 MB) for --speakersThen open http://<host>:8790, queue a folder, and watch
the log. There is a host install too (setup.sh, user units for the worker and the
web UI), a command line for everything the browser does, and mlsubgen --help that
explains itself. The card decides the rest:
| GPU | profile | what changes |
|---|---|---|
| 24 GB | full | both speech engines resident; the 27–31B translators we use here |
| 12–20 GB | 12gb | whisper in int8; a 12B translator (7 GB) for every language pair |
| 8 GB | 8gb | one speech engine on the card at a time; a 4B translator for every pair |
MLSUBGEN_PROFILE. The smaller translators are noticeably weaker, most of all for
Japanese to English; mlsubgen bench runs the speech stage once and every translator on
the same lines, side by side, so you can see by how much on your own material before a long run.The first versions transcribed beautifully and still missed whole conversations. It took a measuring tool rather than a better model to find out why: a voice-activity detector was deciding which parts of the audio the speech engines were allowed to hear, and on animation with a music bed it simply does not register a lot of dialogue. Thirty seconds of an argument, gone, because the detector called it music. Every downstream improvement — better engines, better merging — could only work on what it was given.
So the detector was demoted. The engines now hear the whole timeline, cut into thirty-second pieces at the quietest moments, skipping only stretches that are silent by energy. The detector keeps two advisory jobs — choosing speech-dense windows for language identification, and suggesting cut points — and never again decides what gets transcribed. Anything the engines invent over music is dealt with where it should be: by two engines disagreeing, by a referee that knows what a looping artefact looks like, and by a gate that checks a doubtful line has sound under it before keeping it.
On the episode that exposed the problem, the change recovered eight minutes of dialogue the engines had never been allowed to hear.
Point it at a folder and it works in rounds of ten files, so subtitles appear every round rather than at the end of the night. For each file:
film.en.srt,
film.th.srt, written atomically, beside the video, where every player
finds them.Each of these was a change made after a tool showed the size of the problem, and each one is checkable again on any file by re-running the tool.
The GPU box in the rack does the listening; its sibling, the N5 AI MAX, runs the house’s voice assistant on its own hardware, so an overnight subtitling job never competes with someone asking for the lights. Subtitling is simply the best use of a 24 GB card while everyone is asleep, and the same rules apply as to everything else we run — measure before changing, fix at the layer that can hold the fix, and never let a component that cannot be trusted make a decision for one that can. If your library has the same problem, the code is on GitHub; if your house does, write to us.