psyche-subtitle-toolkit
Extract, translate, and mux ASS, SRT, and WebVTT subtitles in MKV files. Built for Psyche but usable as a standalone CLI or Rust library.
No cloud required. No telemetry. Every translation provider is opt-in.
Features
- Extract ASS, SRT, and WebVTT subtitle tracks from MKV files via mkvmerge/mkvextract
- Translate subtitle dialogue through 7 pluggable providers
- Protect ASS override tags (
{\pos(...)},{\an7}, etc.) during translation - Automatic chunking (200 lines per request) for LLM context limits
- Concurrent chunk translation with configurable parallelism
- Retry with exponential backoff on HTTP, provider, and malformed output errors
- Mux translated subtitles back into the MKV, replacing the original track
- Process single files or entire directories
- Translate standalone
.ass,.srt, and.vttfiles without MKV (viatranslate-asssubcommand) - Resume interrupted translations with
--resume
Supported Providers
| Provider | Flag | Auth | --parallel |
Notes |
|---|---|---|---|---|
| Ollama | --provider ollama |
None | 3 | Default. Any Ollama model. |
| Anthropic | --provider anthropic |
--api-key |
2 | Messages API. Custom endpoint via --anthropic-url. |
| OpenAI | --provider openai |
--api-key |
2 | Compatible with any OpenAI-compatible API. |
| OpenRouter | --provider openrouter |
--api-key |
2 | 400+ models, including free models. |
| DeepL | --provider deepl |
--api-key |
5 | Free tier (500K chars/month) or pro tier. |
| Google Translate | --provider google |
--api-key |
10 | v2 API. First 500K chars/month free. |
| Gemini | --provider gemini |
--api-key |
2 | LLM-based. 1,500 req/day free on Flash models. |
The --parallel column shows recommended concurrency for each provider.
Installation
Or build from source:
Requirements
mkvmergeandmkvextractfrom MKVToolNix must be in yourPATH.
CLI Usage
Inspect MKV tracks
Output shows all tracks with a * marking the selected ASS subtitle track:
* track 2: type=subtitles codec=SubStationAlpha language=eng name=HIDIVE_English
track 3: type=subtitles codec=SubStationAlpha language=jpn name=
Translate subtitles
# Ollama (default, local)
# OpenAI
# DeepL (free tier)
# Google Translate
# Gemini
# OpenRouter (free model)
Translate standalone subtitle files
# ASS file
# SRT file (auto-detected by extension or content)
# WebVTT file (auto-detected by extension or WEBVTT header)
Resume interrupted translations
If a batch run is interrupted (crash, network failure), restart with --resume to skip already-translated files:
# First run — interrupted at file 15/20
# Restart — skips files 1-14, continues from 15
Progress is saved to .psyche-subtitle-toolkit-progress.json in the input directory and auto-deleted when all files complete.
Full options
-i, --input <INPUT> MKV file or directory containing MKV files
--to <TO> Target language code (e.g. pt-BR, en, ja)
--provider <PROVIDER> Translation backend [default: ollama]
--track <TRACK> Specific subtitle track ID to translate
--model <MODEL> Model name [default: llama3.1]
--ollama-url <URL> Ollama base URL [default: http://localhost:11434]
--openai-url <URL> OpenAI base URL [default: https://api.openai.com]
--anthropic-url <URL> Anthropic base URL [default: https://api.anthropic.com]
--api-key <KEY> API key (required for openai, openrouter, anthropic, deepl, google, gemini)
--deepl-url <URL> DeepL base URL [default: https://api-free.deepl.com]
--keep-temp Preserve extracted/translated ASS files
--dry-run Show what would be translated without modifying files
--source-lang <LANG> Source language code (e.g. en, ja)
--resume Save progress and skip already-translated files on restart
--parallel <N> Max concurrent chunk translations [default: 1]
Library Usage
Add to your Cargo.toml:
[]
= { = "../psyche-subtitle-toolkit" }
Translate an MKV file
use Arc;
use ;
# async
Translate ASS content directly
use Arc;
use ;
# async
Implement a custom provider
use async_trait;
use ;
How It Works
- Inspect --
mkvmerge -Jidentifies tracks and selects the ASS, SRT, or VTT subtitle - Extract --
mkvextract trackspulls the ASS file to a temp directory - Parse -- The ASS parser reads dialogue lines, preserving headers and styles
- Strip tags -- ASS override tags (
{\pos(...)},{\an7}) are removed and stored - Chunk -- Cues are split into 200-line batches
- Translate -- Each chunk is sent to the provider as
<N> textnumbered lines (concurrent if--parallel > 1) - Retry -- Failed chunks (HTTP errors, malformed output) are retried up to 3 times with exponential backoff
- Apply -- Translated text is mapped back to cues by ID
- Reinject tags -- Original override tags are prepended back
- Mux --
mkvmergereplaces the original subtitle track in-place
Testing
Provider tests use wiremock to mock HTTP endpoints -- no real API calls.
Release Notes
v0.2.0
- SRT support — parse, translate, and render SubRip subtitles
- WebVTT support — parse, translate, and render WebVTT subtitles
- Anthropic provider — Messages API (
/v1/messages) with custom endpoint support - Remove
--source-lang— all providers auto-detect source language, making the flag redundant - Format auto-detection —
translate-assCLI auto-detects ASS/SRT/VTT by extension or content - MKV format priority — ASS > SRT > VTT when multiple subtitle tracks exist
- Refactored pipeline —
translate_document()helper shared by ASS, SRT, and VTT pipelines
v0.1.0
Initial release:
- 7 translation providers (Ollama, OpenAI, OpenRouter, DeepL, Google, Gemini)
--parallel Nfor concurrent chunk translation--resumefor interrupted batch recovery--dry-runto preview without modifying files- Retry with exponential backoff on HTTP and malformed output errors
- DeepL/Google batch mode (per-line array elements)
- 200 lines per chunk
- Progress output to stderr
translate-asssubcommand for standalone subtitle files