Multilingual Subtitle Translator
Translate SRT/VTT subtitle files to any language while preserving timestamps and formatting.Problem Statement
Who: Video localization teams, content creators, streaming platformsWhy: Manual subtitle translation is slow and expensive. Automated translation with timestamp preservation speeds up localization.
What You’ll Build
A recipe that reads subtitle files, translates the text while preserving timing, and outputs properly formatted subtitle files.Input/Output Contract
| Input | Type | Required | Description |
|---|---|---|---|
subtitles_file | string | Yes | Path to SRT or VTT file |
target_language | string | Yes | Target language code (e.g., es, fr, de) |
preserve_timestamps | boolean | No | Keep original timing (default: true) |
| Output | Type | Description |
|---|---|---|
translated_subtitles_file | string | Path to translated subtitle file |
ok | boolean | Success indicator |
Prerequisites
Copy
export OPENAI_API_KEY=your_key_here
pip install praisonaiagents
Step-by-Step Build
1
Create Recipe Directory
Copy
mkdir -p ~/.praison/templates/multilingual-subtitle-translator
cd ~/.praison/templates/multilingual-subtitle-translator
2
Create TEMPLATE.yaml
Copy
name: multilingual-subtitle-translator
version: "1.0.0"
description: "Translate subtitle files while preserving timestamps"
author: "PraisonAI"
license: "MIT"
tags:
- translation
- subtitles
- localization
- video
requires:
env:
- OPENAI_API_KEY
packages:
- praisonaiagents
inputs:
subtitles_file:
type: string
description: "Path to the SRT or VTT subtitle file"
required: true
target_language:
type: string
description: "Target language code (e.g., es, fr, de, ja, zh)"
required: true
preserve_timestamps:
type: boolean
description: "Keep original timing"
required: false
default: true
outputs:
translated_subtitles_file:
type: string
description: "Path to the translated subtitle file"
ok:
type: boolean
description: "Success indicator"
cli:
command: "praison recipes run multilingual-subtitle-translator"
examples:
- 'praison recipes run multilingual-subtitle-translator --input ''{"subtitles_file": "video.srt", "target_language": "es"}'''
safety:
dry_run_default: false
requires_consent: false
overwrites_files: true
network_access: true
pii_handling: false
3
Create recipe.py
Copy
# recipe.py
import os
import re
from pathlib import Path
from praisonaiagents import Agent, Task, PraisonAIAgents
def run(input_data: dict, config: dict = None) -> dict:
"""
Translate subtitle files while preserving timestamps.
"""
subtitles_file = input_data.get("subtitles_file")
target_language = input_data.get("target_language")
preserve_timestamps = input_data.get("preserve_timestamps", True)
if not subtitles_file:
return {"ok": False, "error": {"code": "MISSING_INPUT", "message": "subtitles_file is required"}}
if not target_language:
return {"ok": False, "error": {"code": "MISSING_INPUT", "message": "target_language is required"}}
if not os.path.exists(subtitles_file):
return {"ok": False, "error": {"code": "FILE_NOT_FOUND", "message": f"File not found: {subtitles_file}"}}
try:
# Read subtitle file
with open(subtitles_file, "r", encoding="utf-8") as f:
content = f.read()
# Detect format
is_vtt = subtitles_file.lower().endswith(".vtt") or content.startswith("WEBVTT")
# Parse subtitles
subtitles = parse_subtitles(content, is_vtt)
# Create translation agent
translator = Agent(
name="Subtitle Translator",
role="Professional Translator",
goal=f"Translate subtitles to {target_language} naturally",
instructions=f"""
You are a professional subtitle translator.
- Translate to {target_language} naturally
- Keep translations concise (subtitles have character limits)
- Preserve meaning and tone
- Handle idioms appropriately
- Keep proper nouns unchanged unless they have standard translations
""",
)
# Translate in batches
batch_size = 20
translated_subtitles = []
for i in range(0, len(subtitles), batch_size):
batch = subtitles[i:i + batch_size]
texts = [s["text"] for s in batch]
task = Task(
name=f"translate_batch_{i}",
description=f"""
Translate these subtitle lines to {target_language}:
{chr(10).join(f'{j+1}. {t}' for j, t in enumerate(texts))}
Return translations in the same numbered format.
""",
expected_output="Numbered translations matching input order",
agent=translator,
)
agents = PraisonAIAgents(agents=[translator], tasks=[task])
result = agents.start()
# Parse translations
translations = parse_translations(result.get(f"translate_batch_{i}", ""), len(texts))
for j, sub in enumerate(batch):
translated_subtitles.append({
"index": sub["index"],
"timestamp": sub["timestamp"],
"text": translations[j] if j < len(translations) else sub["text"]
})
# Format output
output_content = format_subtitles(translated_subtitles, is_vtt)
# Save translated file
file_path = Path(subtitles_file)
ext = file_path.suffix
output_file = f"{file_path.stem}_{target_language}{ext}"
with open(output_file, "w", encoding="utf-8") as f:
f.write(output_content)
return {
"ok": True,
"translated_subtitles_file": output_file,
"artifacts": [{"path": output_file, "type": "text", "size_bytes": os.path.getsize(output_file)}],
"warnings": [],
}
except Exception as e:
return {"ok": False, "error": {"code": "PROCESSING_ERROR", "message": str(e)}}
def parse_subtitles(content: str, is_vtt: bool) -> list:
"""Parse SRT or VTT content into structured data."""
subtitles = []
if is_vtt:
# Remove WEBVTT header
content = re.sub(r'^WEBVTT.*?\n\n', '', content, flags=re.DOTALL)
# Split into blocks
blocks = re.split(r'\n\n+', content.strip())
for block in blocks:
lines = block.strip().split('\n')
if len(lines) >= 2:
# Find timestamp line
for i, line in enumerate(lines):
if '-->' in line:
index = lines[i-1] if i > 0 and lines[i-1].isdigit() else str(len(subtitles) + 1)
timestamp = line
text = '\n'.join(lines[i+1:])
subtitles.append({"index": index, "timestamp": timestamp, "text": text})
break
return subtitles
def parse_translations(result: str, expected_count: int) -> list:
"""Parse numbered translations from agent output."""
translations = []
lines = result.strip().split('\n')
current_text = []
for line in lines:
match = re.match(r'^\d+\.\s*(.+)$', line)
if match:
if current_text:
translations.append(' '.join(current_text))
current_text = [match.group(1)]
elif current_text:
current_text.append(line)
if current_text:
translations.append(' '.join(current_text))
# Pad if needed
while len(translations) < expected_count:
translations.append("")
return translations
def format_subtitles(subtitles: list, is_vtt: bool) -> str:
"""Format subtitles back to SRT or VTT format."""
lines = []
if is_vtt:
lines.append("WEBVTT\n")
for sub in subtitles:
lines.append(sub["index"])
lines.append(sub["timestamp"])
lines.append(sub["text"])
lines.append("")
return '\n'.join(lines)
4
Create test_recipe.py
Copy
# test_recipe.py
import pytest
import tempfile
import os
from recipe import run, parse_subtitles, format_subtitles
def test_missing_subtitles_file():
result = run({"target_language": "es"})
assert result["ok"] is False
assert result["error"]["code"] == "MISSING_INPUT"
def test_missing_target_language():
result = run({"subtitles_file": "test.srt"})
assert result["ok"] is False
assert result["error"]["code"] == "MISSING_INPUT"
def test_file_not_found():
result = run({"subtitles_file": "/nonexistent.srt", "target_language": "es"})
assert result["ok"] is False
assert result["error"]["code"] == "FILE_NOT_FOUND"
def test_parse_srt():
srt_content = """1
00:00:01,000 --> 00:00:04,000
Hello world
2
00:00:05,000 --> 00:00:08,000
How are you?
"""
subtitles = parse_subtitles(srt_content, is_vtt=False)
assert len(subtitles) == 2
assert subtitles[0]["text"] == "Hello world"
def test_parse_vtt():
vtt_content = """WEBVTT
00:00:01.000 --> 00:00:04.000
Hello world
00:00:05.000 --> 00:00:08.000
How are you?
"""
subtitles = parse_subtitles(vtt_content, is_vtt=True)
assert len(subtitles) == 2
@pytest.mark.integration
def test_end_to_end():
# Create temp SRT file
with tempfile.NamedTemporaryFile(mode='w', suffix='.srt', delete=False) as f:
f.write("1\n00:00:01,000 --> 00:00:04,000\nHello world\n")
temp_file = f.name
try:
result = run({"subtitles_file": temp_file, "target_language": "es"})
assert result["ok"] is True
finally:
os.unlink(temp_file)
Run Locally
Copy
# Translate to Spanish
praison recipes run multilingual-subtitle-translator \
--input '{"subtitles_file": "movie.srt", "target_language": "es"}'
# Translate to Japanese
praison recipes run multilingual-subtitle-translator \
--input '{"subtitles_file": "video.vtt", "target_language": "ja"}'
Deploy & Integrate: 6 Integration Models
- Model 1: Embedded SDK
- Model 2: CLI Invocation
- Model 3: Plugin Mode
- Model 4: Local HTTP Sidecar
- Model 5: Remote Managed Runner
- Model 6: Event-Driven
Copy
from praisonai import recipe
result = recipe.run(
"multilingual-subtitle-translator",
input={
"subtitles_file": "video.srt",
"target_language": "es"
}
)
if result.ok:
print(f"Translated: {result.output['translated_subtitles_file']}")
Copy
# Batch translate to multiple languages
for lang in es fr de ja; do
praison recipes run multilingual-subtitle-translator \
--input "{\"subtitles_file\": \"video.srt\", \"target_language\": \"$lang\"}"
done
Copy
class SubtitleTranslatorPlugin:
def translate(self, file_path, target_lang):
from praisonai import recipe
return recipe.run(
"multilingual-subtitle-translator",
input={"subtitles_file": file_path, "target_language": target_lang}
)
Copy
praison recipes serve --port 8765
Copy
const response = await fetch('http://localhost:8765/recipes/multilingual-subtitle-translator/run', {
method: 'POST',
body: JSON.stringify({
subtitles_file: '/path/to/video.srt',
target_language: 'es'
})
});
Copy
response = requests.post(
"https://api.localization-service.com/translate",
headers={"Authorization": f"Bearer {api_key}"},
json={
"subtitles_url": "https://cdn.example.com/video.srt",
"target_language": "es"
}
)
Copy
# Trigger on video upload
def handle_video_upload(event):
video_id = event['video_id']
languages = ['es', 'fr', 'de', 'ja']
for lang in languages:
queue.send({
"recipe": "multilingual-subtitle-translator",
"input": {
"subtitles_file": f"s3://bucket/{video_id}.srt",
"target_language": lang
}
})
Troubleshooting
Timestamps are misaligned
Timestamps are misaligned
Ensure
preserve_timestamps: true (default). If timing needs adjustment for translated text length, consider post-processing.Special characters corrupted
Special characters corrupted
The recipe uses UTF-8 encoding. Ensure your source file is UTF-8 encoded.
Next Steps
- Video Caption Generator - Generate captions first
- Voice-to-Voice Translator Lite - Translate audio directly

