If you've ever pasted an SRT file into an LLM and asked for a translation, you've probably seen this: you send 400 subtitle cues and get back 397. Two short lines got merged into one sentence, one line disappeared, and from that point on every translation sits on the wrong timestamp. The output looks fine at a glance, and you only notice when the dialogue on screen no longer matches what people are saying.
We hit this problem constantly while building GeekLink's subtitle translator, so we pulled the core engine out and open-sourced it under GPL-3.0: jev-subtitle-translator.
Why LLMs mangle subtitles
LLMs are trained to write fluent text, and subtitles aren't fluent text. A single sentence often spans three cues, a cue can be just "Yeah." or "—no!", and a model that's trying to be helpful will happily join fragments into natural sentences. Ask it to "keep the format" and it mostly will, until a batch of 40 cues comes back with 38 items and nothing tells you which two are gone.
Prompt engineering alone doesn't fix this. You need structure the model can't drift away from, plus validation that doesn't trust the model's output.
How the engine handles it
The pipeline looks like this:
Parse SRT and preserve every cue ↓ Translate ID-tagged batches ↓ Validate returned IDs and non-empty translations ↓ Retry only missing or empty IDs ↓ Run deterministic checks and Jev review ↓ Rebuild the SRT from the original cue list A few design decisions do most of the work:
Timestamps never reach the model. The SRT is parsed locally. Subtitle numbers, order, and timestamps stay on your machine. Only the dialogue text and a stable ID per cue are sent to the model.
Structured output, not prose. Each batch is translated with JSON Schema structured output through OpenRouter, so you can use GPT, Claude, Gemini, DeepSeek, Grok, or any other model that supports strict schemas. Local OpenAI-compatible servers (Ollama, LM Studio, vLLM, llama.cpp) work too.
Retry by ID, not by batch. After every response, the engine accepts only expected IDs with unique, non-empty translations. If a batch of 40 returns 35 valid results, the next request contains only the 5 missing cues. You don't pay to re-translate the whole batch, and the good results aren't thrown away.
Rebuild from the original cue list. The final SRT is assembled from the parsed source, not from whatever the model returned. One failed line can't shift or delete the cues after it. If a line still can't be translated after retries, it stays in place as an empty subtitle and gets flagged in the report, so the failure is visible instead of hidden.
Jev: automatic QC that flags, not rewrites
A complete, well-aligned file can still contain bad translations, so the engine adds a QC pass. Local deterministic checks run first and catch empty lines and structural problems without calling any model. Then Jev reviews every source–translation pair and answers one question: does this line need human review?
It's tuned to flag things like dropped meaning, reversed negation, changed names, numbers, dates, or units, and truncated or repeated text. It looks at neighbouring cues in the batch, so a sentence that legitimately spans several cues isn't flagged. The rules favour precision: style differences pass, so the report stays short enough that you'll actually read it.
Jev doesn't rewrite anything. A flag means "look at this line", not "this line is wrong". The review rules live in qc.py if you want to see exactly what it checks or tune it.
Quick start
You'll need Python 3.10+ and an OpenRouter API key. On macOS or Linux:
git clone https://github.com/GeekLinkDev/jev-subtitle-translator.git cd jev-subtitle-translator bash run_web.sh Open http://127.0.0.1:8000, upload an SRT, pick your languages, and click Translate + QC. You can also QC an existing translation without re-translating it. You get the translated SRT plus a JSON report with line-level translation and review status.
Prefer the terminal? There's a CLI for pipelines:
export OPENROUTER_API_KEY="your-api-key" .venv/bin/jev-subtitle-translator translate input.srt \ --source-language en --target-language de \ --model openai/gpt-4o-mini --output translated.srt That writes translated.srt and translated.srt.qc.json. Point --base-url and --qc-base-url at a local server to run everything locally.
Where this fits
This repo covers one job: translating and reviewing existing SRT files. If you're starting from a video, the GeekLink desktop app for Mac and Windows is built on the same engine and adds OCR extraction of hardcoded (burned-in) subtitles, local Whisper transcription, and a bilingual editor for fixing lines side by side.
Contributions welcome
The most useful contributions are real subtitle files that break things: odd SRT formatting, language pairs where Jev misfires, or models that keep dropping IDs. Open an issue or PR on GitHub. And if you've been translating subtitles with a one-off script and your fingers crossed, give the repo a try and a star.

