Skip to content
Back to skills

Julien Media Subtitle Translation

ASecurity

Translates SRT subtitle files using LLM APIs (OpenRouter/Llama). Use when user wants to translate subtitles, SRT files, or needs batch subtitle translation for movies/series. Handles HTML tags, batch processing, retries, and cost estimation.

  • 666 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added February 9, 2026
ai-agentspythongobashapi

Works with

  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill julien-media-subtitle-translation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Julien Media Subtitle Translation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Julien Media Subtitle Translation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-julien-media-subtitle-translation/badge)](https://www.skillsdirectory.com/skills/majiayu000-julien-media-subtitle-translation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: julien-media-subtitle-translation
description: Translates SRT subtitle files using LLM APIs (OpenRouter/Llama). Use when user wants to translate subtitles, SRT files, or needs batch subtitle translation for movies/series. Handles HTML tags, batch processing, retries, and cost estimation.
triggers:
  - translate subtitles
  - translate srt
  - subtitle translation
  - traduire sous-titres
  - batch translate subtitles
  - srt file translation
  - movie subtitles
  - series subtitles
  - translate episodes
---

# Subtitle Translation Skill

Translate SRT subtitle files efficiently using LLM APIs with proper cost management.

## When to Use

- User wants to translate `.srt` subtitle files
- Batch translation of multiple episodes/movies
- Translation between any language pair (default: EN→FR)

## Step 0: Ask User for API Provider

**Always ask the user which provider to use:**

```
Which API provider do you want to use?

1. **OpenRouter** (Recommended) - Best price/quality, many models
2. **OpenAI** - GPT-4o-mini, reliable but needs credit card
3. **Ollama Local** - Free, runs on your machine (needs GPU)
4. **Ollama Hostinger** - Free, runs on your VPS

For large batches (100+ episodes): OpenRouter or Ollama Hostinger
For small batches (<20 episodes): Any option works
```

### Provider Configuration

| Provider | API URL | Auth |
|----------|---------|------|
| OpenRouter | `https://openrouter.ai/api/v1/chat/completions` | Bearer token |
| OpenAI | `https://api.openai.com/v1/chat/completions` | Bearer token |
| Ollama Local | `http://localhost:11434/api/chat` | None |
| Ollama Hostinger | `http://YOUR_VPS_IP:11434/api/chat` | None |

## Step 1: Choose Model Based on Complexity

**Ask user about subtitle complexity:**

```
What type of content are you translating?

A) Simple dialogue (sitcoms, slice-of-life) → Fast/cheap model
B) Standard content (action, drama) → Balanced model
C) Complex content (technical, poetry, wordplay) → Quality model
```

### Model Recommendations (December 2024)

| Complexity | OpenRouter | OpenAI | Ollama |
|------------|------------|--------|--------|
| **Simple** | `mistral-7b-instruct` ($0.03/M) | - | `mistral:7b` |
| **Standard** | `llama-3.3-70b-instruct` ($0.11/M in, $0.34/M out) | `gpt-4o-mini` ($0.15/M in, $0.60/M out) | `llama3.1:70b` |
| **Complex** | `claude-3.5-sonnet` ($3/M in, $15/M out) | `gpt-4o` ($2.50/M in, $10/M out) | `llama3.1:70b` |

### Low-Resource Servers (VPS < 16GB RAM)

For servers with limited RAM, use lightweight multilingual models:

| Model | RAM Required | Quality | Speed | Best For |
|-------|--------------|---------|-------|----------|
| **`aya:8b`** | ~6GB | Good | Fast | Multilingual - 100+ languages native |
| `mistral:7b` | ~5GB | OK | Fast | European languages |
| `gemma2:9b` | ~7GB | Good | Medium | General purpose |

**Aya 8B is recommended** for subtitle translation on VPS because:
- Trained specifically for multilingual tasks (100+ languages)
- Low memory footprint (~6GB VRAM/RAM)
- Good quality for dialogue translation
- Free with Ollama

```bash
# Install on VPS
ollama pull aya:8b

# Test
ollama run aya:8b "Translate to French: Hello, how are you?"
```

### Live Model Search (OpenRouter)

```python
def search_models(min_context=8000):
    """Search OpenRouter for available models with pricing"""
    r = requests.get("https://openrouter.ai/api/v1/models")
    models = r.json().get("data", [])

    # Filter and sort by price
    suitable = []
    for m in models:
        ctx = m.get("context_length", 0)
        if ctx >= min_context:
            price_in = m.get("pricing", {}).get("prompt", 0)
            price_out = m.get("pricing", {}).get("completion", 0)
            suitable.append({
                "id": m["id"],
                "name": m.get("name", m["id"]),
                "context": ctx,
                "price_in": float(price_in) * 1_000_000,  # Per M tokens
                "price_out": float(price_out) * 1_000_000
            })

    return sorted(suitable, key=lambda x: x["price_in"])[:10]
```

## Key Learnings (Production-Tested)

### Cost Reality Check

**Always multiply naive estimates by 1.5x** due to:
- System prompts repeated per batch
- JSON formatting overhead
- Instruction tokens

| Model | Input | Output | Real Cost/Episode |
|-------|-------|--------|-------------------|
| Llama 3.3 70B | $0.11/M | $0.34/M | ~$0.007 |
| GPT-4o-mini | $0.15/M | $0.60/M | ~$0.012 |

### Optimal Configuration

```python
BATCH_SIZE = 25          # Subtitles per API request (sweet spot)
DELAY_BETWEEN_REQUESTS = 0.5  # Avoid rate limits
MAX_RETRIES = 3          # Per batch
REQUEST_TIMEOUT = 90     # Seconds
```

## Implementation Steps

### 1. Parse SRT Format

```python
def parse_srt(content):
    """Parse SRT into list of (index, timing, text) tuples"""
    blocks = []
    current = []
    for line in content.strip().split('\n'):
        if line.strip() == '':
            if current:
                idx = current[0]
                timing = current[1]
                text = '\n'.join(current[2:])
                blocks.append((idx, timing, text))
                current = []
        else:
            current.append(line)
    if current:
        idx = current[0]
        timing = current[1]
        text = '\n'.join(current[2:])
        blocks.append((idx, timing, text))
    return blocks
```

### 2. HTML Tag Preservation (Critical)

**Never send HTML tags to the LLM** - they get corrupted. Strip before, reapply after.

```python
import re

def extract_formatting(text):
    """Extract HTML tags with positions for later restoration"""
    tags = []
    for match in re.finditer(r'<[^>]+>', text):
        tags.append((match.start(), match.end(), match.group()))
    return tags

def strip_html_tags(text):
    """Remove HTML tags for clean translation"""
    return re.sub(r'<[^>]+>', '', text)

def apply_formatting(translated, original_tags):
    """Reapply original HTML structure to translation"""
    if not original_tags:
        return translated
    # Preserve italic tags at start/end
    result = translated
    for start, end, tag in original_tags:
        if tag == '<i>' and not result.startswith('<i>'):
            result = '<i>' + result
        elif tag == '</i>' and not result.endswith('</i>'):
            result = result + '</i>'
    return result
```

### 3. Batch Translation with Retry Queue

```python
def translate_batch(texts, source='English', target='French'):
    """Translate batch of texts, return list or None on failure"""
    prompt = f"""Translate these {source} subtitles to {target}.
Return ONLY a JSON array of translated strings, same order.
Keep it natural and conversational.

{json.dumps(texts, ensure_ascii=False)}"""

    # API call with retries
    for attempt in range(MAX_RETRIES):
        try:
            response = call_api(prompt)
            return json.loads(response)
        except:
            time.sleep(2 ** attempt)  # Exponential backoff
    return None
```

### 4. Failed Batch Queue

Store failed batches for later retry:

```python
def add_failed_batch(file_path, batch_index, blocks_data):
    """Queue failed batch for retry"""
    failed_data = load_failed_batches()
    failed_data["batches"].append({
        "fr_srt": str(file_path),
        "batch_index": batch_index,
        "blocks": blocks_data,
        "retry_count": 0,
        "timestamp": datetime.now().isoformat()
    })
    save_failed_batches(failed_data)
```

### 5. Resilient Execution

**Always wrap main() in auto-restart**:

```python
def run_resilient():
    """Auto-restart on errors (up to 50 times)"""
    max_restarts = 50
    restart_count = 0

    while restart_count < max_restarts:
        try:
            main()
            break
        except KeyboardInterrupt:
            log("Manual stop (Ctrl+C)")
            break
        except Exception as e:
            restart_count += 1
            log(f"ERROR: {e}")
            log(f"Auto-restart {restart_count}/{max_restarts} in 30s...")
            time.sleep(30)
```

### 6. Progress Tracking

```python
PROGRESS_FILE = "translation_progress.json"

def load_progress():
    try:
        with open(PROGRESS_FILE) as f:
            return json.load(f)
    except:
        return {"completed": [], "failed": [], "total_cost": 0.0}

def save_progress(state):
    state["last_update"] = datetime.now().isoformat()
    with open(PROGRESS_FILE, 'w') as f:
        json.dump(state, f, indent=2)
```

## API Configuration

### OpenRouter (Recommended)

```python
OPENROUTER_API_KEY = "sk-or-v1-..."
MODEL = "meta-llama/llama-3.3-70b-instruct"
API_URL = "https://openrouter.ai/api/v1/chat/completions"

headers = {
    "Authorization": f"Bearer {OPENROUTER_API_KEY}",
    "Content-Type": "application/json"
}
```

### Check Credits

```python
def get_credits():
    r = requests.get("https://openrouter.ai/api/v1/credits",
                     headers={"Authorization": f"Bearer {API_KEY}"})
    data = r.json().get("data", {})
    return data.get("total_credits", 0) - data.get("total_usage", 0)
```

## File Naming Convention

| Input | Output |
|-------|--------|
| `movie.eng.srt` | `movie.fr.srt` |
| `episode.en.srt` | `episode.fr.srt` |

## Cost Estimation Formula

```
episodes × 250 subtitles × 50 tokens × 2 (in+out) × price/token × 1.5 (overhead)
```

Example: 600 episodes EN→FR with Llama 3.3 70B:
- Naive: 600 × 250 × 50 × 2 × $0.20/M = $3.00
- **Real: $3.00 × 1.5 = ~$4.50**

## Common Issues & Solutions

| Issue | Solution |
|-------|----------|
| HTML tags corrupted | Strip before translation, reapply after |
| Rate limit errors | Add 0.5s delay between requests |
| Parsing failures | Retry with exponential backoff |
| Script stops randomly | Use `run_resilient()` wrapper |
| Cost higher than expected | Multiply estimates by 1.5x |

## Skill Chaining

### Skills Required Before
- None (can work standalone)

### Input Expected
- Directory path containing `.srt` files
- Source/target languages (default: EN→FR)
- API provider choice (OpenRouter/OpenAI/Ollama)
- API key (if not Ollama)

### Output Produced
- Translated `.srt` files with language suffix
- `translation_progress.json` - tracks completed files
- `failed_batches.json` - retry queue
- `translation.log` - detailed logs

### Compatible Skills After
- **video-transcoding**: Burn subtitles into video
- **media-organization**: Organize translated files

### Tools Used
- `Write` (create translation script)
- `Bash` (run translation)
- `Read` (check progress/logs)
- `AskUserQuestion` (provider/model selection)
- `WebFetch` (live model pricing)

### Visual Workflow

```
User: "Translate my subtitles to French"
    ↓
[Ask] Which API provider?
    ├─► OpenRouter (recommended)
    ├─► OpenAI
    ├─► Ollama Local
    └─► Ollama Hostinger
    ↓
[Ask] Content complexity?
    ├─► Simple → mistral-7b ($0.03/M)
    ├─► Standard → llama-3.3-70b ($0.20/M)
    └─► Complex → claude-3.5-sonnet ($9/M)
    ↓
[Calculate] Cost estimate × 1.5
    ↓
[Confirm] "~$X for Y episodes. Proceed?"
    ↓
[Execute] translate_srt.py
    ├─► Parse SRT → Strip HTML → Batch (25/req)
    ├─► Translate → Reapply formatting
    └─► Track progress → Queue failures
    ↓
[Done] X.fr.srt files created
```

### Usage Example

**Scenario**: Translate 600 One Piece episodes EN→FR

**Interaction**:
1. Claude asks provider → User: "OpenRouter"
2. Claude asks complexity → User: "Standard (anime)"
3. Claude calculates: 600 × $0.007 × 1.5 = **~$6.30**
4. User confirms → Script runs
5. Result: 600 `.fr.srt` files, actual cost ~$4-7

**Output structure**:
```
/anime/onepiece/
├── Episode.001.eng.srt  (original)
├── Episode.001.fr.srt   (NEW)
├── translation_progress.json
├── failed_batches.json
└── translation.log
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.