Skip to content

LavX/ai-subtitle-translator

Repository files navigation

AI Subtitle Translator

LLM-powered subtitle translation API using OpenRouter. Translate SRT files and subtitle content through any OpenRouter-supported model (Gemini, Claude, Llama, GPT, Mistral, and more). Built as a standalone microservice for LavX's Bazarr fork, but works with any HTTP client.

Keywords: subtitle translation API, SRT translator, AI subtitle translation, LLM translation service, OpenRouter translation, automated subtitle localization, Bazarr AI translation, multilingual subtitle converter

CI Python 3.11+ License: MIT

Features

  • Translate SRT subtitle files or raw subtitle lines via REST API
  • Routes through OpenRouter to 300+ LLMs (Gemini, Claude, Llama, GPT, Mercury, Mistral, etc.)
  • Automatic batch sizing per model. Adapts to model context windows and retries with smaller batches on failure
  • RTL language support (Arabic, Hebrew, Persian, Urdu, etc.)
  • Async job queue with real-time progress tracking, cost reporting, and per-batch status updates
  • Job persistence via SQLite. Jobs survive restarts, automatically recovered on startup
  • AES-256-GCM API key encryption between any client and this service (optional, enabled by default)
  • Request authentication via HMAC token derived from the encryption key (automatic when encryption is enabled)
  • Dynamic reasoning detection. Fetches model capabilities from OpenRouter API, no hardcoded lists
  • Rate limit handling with exponential backoff, serialized retries, and staggered parallel requests
  • Per-request config overrides: model, temperature, API key, reasoning, parallel batch count

Bazarr setup

Using this with Bazarr? One command:

curl -sSL https://raw.githubusercontent.com/LavX/ai-subtitle-translator/main/install.sh | bash

Auto-detects your Bazarr container, configures networking, and prints the encryption key. See the full Bazarr Setup Guide for details.

Quick start

Docker

git clone https://github.com/LavX/ai-subtitle-translator.git
cd ai-subtitle-translator

# Set your OpenRouter API key
echo "OPENROUTER_API_KEY=sk-or-..." > .env

docker compose up -d

Service runs at http://localhost:8765. Interactive docs at /docs.

Manual

python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
export OPENROUTER_API_KEY=sk-or-...
cd src && uvicorn subtitle_translator.main:app --host 0.0.0.0 --port 8765

API endpoints

Full interactive docs at /docs (Swagger) or /redoc when running.

Method Endpoint Auth Description
GET /health Health check
GET /api/v1/models List available translation models
GET /api/v1/status Service status and queue stats
GET /api/v1/config Current configuration
PUT /api/v1/config token Update config at runtime
POST /api/v1/translate/content token Translate subtitle lines (synchronous)
POST /api/v1/translate/file token Translate SRT file (synchronous)
POST /api/v1/jobs/translate/content token Submit async translation job
POST /api/v1/jobs/translate/file token Submit async SRT translation job
GET /api/v1/jobs token List all jobs (filter by status)
GET /api/v1/jobs/{id} token Job status, progress, metrics, and result
DELETE /api/v1/jobs/{id} token Cancel or delete a job
POST /api/v1/test token Test encryption and API key validity

Endpoints marked token require an X-Auth-Token header when encryption is enabled (see Authentication).

Translate subtitle content

curl -X POST http://localhost:8765/api/v1/translate/content \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: YOUR_AUTH_TOKEN" \
  -d '{
    "sourceLanguage": "en",
    "targetLanguage": "hu",
    "title": "Breaking Bad",
    "lines": [
      {"position": 1, "line": "Say my name."},
      {"position": 2, "line": "You are goddamn right."}
    ]
  }'

Submit async translation job

curl -X POST http://localhost:8765/api/v1/jobs/translate/content \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: YOUR_AUTH_TOKEN" \
  -d '{
    "sourceLanguage": "en",
    "targetLanguage": "hu",
    "title": "Breaking Bad S05E07",
    "mediaType": "Episode",
    "fileName": "breaking.bad.s05e07.srt",
    "jobName": "bb-s05e07-hungarian",
    "lines": [
      {"position": 1, "line": "Say my name."}
    ],
    "config": {
      "model": "anthropic/claude-haiku-4.5",
      "temperature": 0.3,
      "reasoning": {"effort": "high"},
      "parallelBatches": 4
    }
  }'

Job status response

Poll GET /api/v1/jobs/{id} for live progress and metrics:

{
  "jobId": "3f1318c0-...",
  "status": "processing",
  "progress": 66,
  "message": "Translated 400/600 lines (4/6 batches)",
  "jobName": "bb-s05e07-hungarian",
  "fileName": "breaking.bad.s05e07.srt",
  "sourceLanguage": "en",
  "targetLanguage": "hu",
  "title": "Breaking Bad S05E07",
  "mediaType": "Episode",
  "model": "anthropic/claude-haiku-4.5",
  "totalLines": 600,
  "totalBatches": 6,
  "completedBatches": 4,
  "completedLines": 400,
  "tokensUsed": 25000,
  "totalCost": 0.0084,
  "elapsedSeconds": 12.5
}

Job statuses: queued | processing | completed | partial | failed | cancelled

Per-request config override

Every translate endpoint accepts an optional config block to override defaults:

{
  "config": {
    "apiKey": "sk-or-different-key",
    "model": "anthropic/claude-haiku-4.5",
    "temperature": 0.5,
    "parallelBatches": 2,
    "reasoning": {"effort": "high"}
  }
}

Reasoning effort levels: xhigh, high, medium, low, minimal, none (disables reasoning).

Configuration

Set via environment variables or .env file:

Variable Default Description
OPENROUTER_API_KEY (required) Your OpenRouter API key
OPENROUTER_DEFAULT_MODEL amazon/nova-2-lite-v1:free Default translation model
OPENROUTER_TEMPERATURE 0.3 Sampling temperature
BATCH_SIZE 100 Max subtitle lines per batch (auto-adjusted per model)
PARALLEL_BATCHES_PER_JOB 4 Concurrent batches per translation job
MAX_RETRIES 3 Retry attempts on failure (rate limits get 3 extra)
REQUEST_TIMEOUT 120.0 HTTP request timeout in seconds
LOG_LEVEL INFO Log level (DEBUG for full request/response logging)
CORS_ALLOWED_ORIGINS * Comma-separated allowed CORS origins
ADMIN_API_KEY (empty) Required as X-Admin-Key header for PUT /config when set
HOST 0.0.0.0 Server bind address
PORT 8765 Server port
ENCRYPTION_ENABLED true Enable AES-256-GCM API key encryption
ENCRYPTION_STRICT false When true, reject plaintext API keys (require enc: prefix)
ENCRYPTION_KEY (auto-generated) 64-char hex AES-256 key. Overrides key file when set
ENCRYPTION_KEY_FILE /app/data/encryption.key Path to persistent encryption key file
DB_PATH /app/data/jobs.db SQLite database path for job persistence
JOB_RETENTION_HOURS 24 Hours to keep completed/failed jobs before cleanup

Job persistence

Jobs are stored in SQLite and survive container restarts. On startup, any queued or in-progress jobs from the previous session are automatically recovered and re-queued.

Mount a volume to /app/data to persist across container recreations:

docker run -d --name ai-subtitle-translator \
  -v /path/to/data:/app/data \
  -e OPENROUTER_API_KEY=sk-or-... \
  ghcr.io/lavx/ai-subtitle-translator:latest

The data directory contains:

  • jobs.db - SQLite database with all job history
  • encryption.key - auto-generated encryption key (chmod 600)

Authentication

When encryption is enabled (the default), all mutating endpoints require an X-Auth-Token header. The token is derived from the same encryption key that Bazarr already has, so there is no extra secret to manage.

How the token is computed

Both the client and server compute HMAC-SHA256(encryption_key_bytes, "subtitle-translator-auth-v1") and use the hex digest as the token. In Python:

import hmac, hashlib
token = hmac.new(bytes.fromhex(encryption_key_hex), b"subtitle-translator-auth-v1", hashlib.sha256).hexdigest()

Using the token

Pass it as a header on every request to a protected endpoint:

curl -H "X-Auth-Token: <token>" http://localhost:8765/api/v1/jobs

Read-only endpoints (/health, /api/v1/models, /api/v1/status, GET /api/v1/config) do not require authentication.

When encryption is disabled (ENCRYPTION_ENABLED=false), auth is skipped entirely and all endpoints are open.

API key encryption

API keys sent between clients and the translator can be encrypted in transit using AES-256-GCM with a pre-shared key. This prevents API keys from being visible in plaintext on the network, even over HTTP.

How it works

  1. On first startup, the translator generates an encryption key and saves it to /app/data/encryption.key
  2. Read the key: docker exec ai-subtitle-translator cat /app/data/encryption.key
  3. Paste the 64-character hex key into Bazarr's AI Subtitle Translator settings
  4. Bazarr encrypts the OpenRouter API key before sending it in requests
  5. The translator decrypts it on receipt
  6. The same key is used to derive the auth token (see Authentication)

Encrypted API keys use the format enc:base64data. Plaintext keys are accepted by default. Set ENCRYPTION_STRICT=true to require encryption on all requests.

Test encryption

curl -X POST http://localhost:8765/api/v1/test \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: YOUR_AUTH_TOKEN" \
  -d '{"apiKey": "enc:your-encrypted-key-here"}'

Returns encryption status and OpenRouter API key validation in one call.

Regenerate key

# Via CLI (inside container)
docker exec ai-subtitle-translator python -m subtitle_translator.cli regenerate-key

# Or set via environment variable (overrides key file)
docker run -e ENCRYPTION_KEY=your64charhexkey... ...

Disable encryption

docker run -e ENCRYPTION_ENABLED=false ...

Adaptive batch sizing

Different LLMs handle different batch sizes. Small-context models choke on 100 lines, while large-context models handle them fine. The translator adjusts automatically:

  1. Known limits - small-context models get smaller default batches
  2. Context-length heuristic - estimates safe batch size from the model's context window
  3. Adaptive retry - if a batch fails, halves the size and retries. Remembers the safe size for future requests to the same model (in-memory, resets on restart)

You can use any OpenRouter model and the service will find the right batch size.

Rate limit handling

When OpenRouter returns 429 Too Many Requests:

  • Retries up to 6 times with exponential backoff (5s, 10s, 20s, 30s cap)
  • Serializes retries across parallel batches so they don't all hit the API at once
  • Staggers initial parallel requests by 0.5s to spread the load
  • Reports partial status if some batches completed before retries ran out

Reasoning support

Model reasoning capabilities are detected automatically from the OpenRouter /models API. No hardcoded model lists to maintain. When reasoning is requested:

  • Models supporting effort get {"reasoning": {"effort": "high"}}
  • Models supporting max_tokens get {"reasoning": {"max_tokens": N}}
  • effort: "none" disables reasoning entirely
  • response_format: json_object is skipped when reasoning is active (prevents single-object response bugs with certain models)

Tested models for subtitle translation

Models tested through elimination rounds (5/10/20/30/40/50 subtitle lines, 80% accuracy threshold for Hungarian).

Model Speed Accuracy Notes
meta-llama/llama-4-maverick ~3s 92% Fastest
anthropic/claude-haiku-4.5 ~13s 93% Best accuracy
amazon/nova-2-lite-v1:free ~17s 95% Best free model
google/gemini-2.5-flash-preview-09-2025 ~8.5s 92% Good all-rounder
inception/mercury-2 ~3s ~90% ~$0.10/episode, 580+ tokens/sec

Full model list with metadata: GET /api/v1/models

Development

pip install -e ".[dev]"
pytest                    # 548 tests, 98% coverage
ruff check src/ tests/    # lint
ruff format src/ tests/   # format

Project structure

src/subtitle_translator/
  main.py                 # FastAPI application
  config.py               # Settings from environment variables
  crypto.py               # AES-256-GCM encryption and key management
  cli.py                  # CLI commands (key regeneration)
  api/
    routes.py             # REST API endpoints
    models.py             # Pydantic request/response models
  core/
    translator.py         # Translation orchestration
    srt_parser.py         # SRT file parsing and composing
    batch_processor.py    # Parallel batch processing with adaptive retry
    batch_sizing.py       # Per-model batch size resolution
  providers/
    base.py               # Abstract translation provider interface
    openrouter.py         # OpenRouter API implementation
  queue/
    job_manager.py        # Async job queue with progress tracking
    job_store.py          # SQLite persistence layer
    worker.py             # Background job worker

License

MIT. See LICENSE.

Links

About

LLM-powered subtitle translation API using OpenRouter. Translate SRT files through 300+ AI models (Gemini, Claude, Llama, GPT, Mistral). Async job queue, adaptive batch sizing, real-time progress, cost tracking. Built for Bazarr integration.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages