
Translate Video
- 41 installs
- 44 repo stars
- Updated July 8, 2026
- aviz85/claude-skills-library
translate-video is a Claude skill that runs a full transcribe, translate, refine, and embed pipeline to add native-quality translated subtitles to a video.
About
translate-video is an end-to-end video translation pipeline that transcribes, translates, refines subtitles, and embeds RTL-safe captions into the video via ffmpeg. A developer uses it to produce foreign-language or Hebrew subtitles for shorts or regular videos, with per-mode line and character limits. It reuses the transcribe skill and bundles Python scripts for timestamps, post-processing, and RTL fixes.
- End-to-end pipeline: transcribe, translate, refine subtitles, embed RTL-safe captions
- Shorts and regular modes with different subtitle line and character limits
- RTL fix step for Hebrew, Arabic, and Farsi
Translate Video by the numbers
- 41 all-time installs (skills.sh)
- Ranked #923 of 1,337 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
translate-video capabilities & compatibility
Requires ffmpeg and the transcribe skill's ElevenLabs API key; see DEPENDENCIES.md.
- Capabilities
- transcribe · speech generator · youtube downloader
- Use cases
- translation · transcription
- Platforms
- macOS
- Pricing
- Bring your own API key
What translate-video says it does
End-to-end video translation pipeline: transcribe → translate → refine subtitles → embed.
Enforces MAX 2 lines, MAX chars/line (38 for --shorts, 42 for --regular):
Do NOT use Docker ffmpeg for long videos on ARM Mac — x86 emulation is ~100x slower.
npx skills add https://github.com/aviz85/claude-skills-library --skill translate-videoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 41 |
|---|---|
| repo stars | ★ 44 |
| Last updated | July 8, 2026 |
| Repository | aviz85/claude-skills-library ↗ |
What it does
Translate a video's subtitles into another language and embed refined, RTL-safe captions into the final video.
Who is it for?
Producing translated or Hebrew subtitles for a video and embedding them, including RTL languages.
Skip if: Simple one-off transcription without translation or embedding, which the transcribe skill handles.
When should I use this skill?
User asks to translate a video, add foreign or Hebrew subtitles, or produce translated captions.
What you get
A final subtitled video plus original and translated SRT files and a readable transcript.
- Original SRT
- Translated refined SRT
- Readable transcript md
By the numbers
- 6-step pipeline (transcribe to embed)
- Line limits 38 chars for shorts, 42 for regular
Files
Translate Video
End-to-end video translation pipeline: transcribe → translate → refine subtitles → embed.
Usage
/translate-video /path/to/video.mp4 he --regular
/translate-video /path/to/video.mp4 he --shorts$1— video file path (required)$2— target language code (default:he). See references/languages.md$3—--shorts(TikTok/Reels) or--regular(YouTube/tutorials). If omitted, ask the user.
---
Pipeline
Step 1: Transcribe
If audio > 25MB, extract first:
ffmpeg -i "$VIDEO" -vn -acodec libmp3lame -ab 128k "$AUDIO.mp3" -yTranscribe with word-level JSON (always include --json):
cd ~/.claude/skills/transcribe/scripts && [ -d node_modules ] || npm install --silent
npx ts-node transcribe.ts -i "$INPUT" -o "$BASENAME.srt" --jsonProduces: {basename}.srt, {basename}.md, {basename}_transcript.json
Step 2: Translate
Read .md for full context. Translate the .srt — preserve all timestamps and index numbers exactly. See translation rules in references/modes.md.
Step 3: Refine Subtitles
Read references/modes.md for full rules.
--shorts: Fix text only, preserve all timestamps. No merging.
--regular: Merge into full sentences using word-level timestamps. 1. Plan subtitle groups from .md (word counts per group) 2. Fill GROUP_SIZES in scripts/build-timestamps.py and run it 3. Replace English text in output SRT with translated text
Step 4: Post-process (both modes)
Enforces MAX 2 lines, MAX chars/line (38 for --shorts, 42 for --regular):
python3 ~/.claude/skills/translate-video/scripts/postprocess.py "$SRT" 42Step 5: RTL Fix (Hebrew / Arabic / Farsi only)
python3 ~/.claude/skills/translate-video/scripts/rtl-fix.py "$SRT"Step 6: Embed & Open
~/.local/bin/ffmpeg-ass -i "$VIDEO" \
-vf "subtitles=$SRT:force_style='FontSize=24,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,Outline=2,Shadow=1,Alignment=2,MarginV=30'" \
-c:v libx264 -preset fast -crf 23 -c:a copy "$OUTPUT" -y
open "$OUTPUT"⚠️ Do NOT use Docker ffmpeg for long videos on ARM Mac — x86 emulation is ~100x slower.
---
Output Files
| File | Description |
|---|---|
{name}.srt | Original language SRT |
{name}.md | Readable transcript |
{name}_transcript.json | Word-level timestamps |
{name}_{lang}.srt | Translated + refined SRT |
{name}_{lang}_subtitled.mp4 | Final video |
Supporting Files
| File | Purpose |
|---|---|
| references/modes.md | Detailed --shorts and --regular rules |
| references/languages.md | Language codes + RTL flags |
| scripts/build-timestamps.py | Word-index cursor for --regular timestamps |
| scripts/postprocess.py | Enforce line limits on any SRT |
| scripts/rtl-fix.py | Apply RTL Unicode markers |
Dependencies
Skills (Claude Code)
| Skill | Purpose | Path |
|---|---|---|
/transcribe | Generates SRT + readable text from audio/video | ~/.claude/skills/transcribe/ |
/embed-subtitles | Burns SRT onto video with FFmpeg | ~/.claude/skills/embed-subtitles/ |
Both skills must be installed with their node_modules (cd scripts && npm install).
System Tools
| Tool | Purpose | Install |
|---|---|---|
| FFmpeg | Video/audio processing, subtitle embedding | brew install ffmpeg |
| Python 3 | RTL Unicode mark injection script | Pre-installed on macOS |
| Node.js | Runs transcribe + embed-subtitles scripts | brew install node |
APIs (via transcribe skill)
| API | Purpose | Config |
|---|---|---|
| ElevenLabs Scribe v2 | Transcription (default, best quality) | ~/.claude/skills/transcribe/scripts/.env |
| Groq whisper-large-v3 | Transcription (--cheap mode) | Same .env |
No Additional APIs Needed
Translation and refinement are done by Claude directly (no translation API required). RTL fix uses Python's built-in Unicode support (no packages needed).
Language Codes
| Code | Language | RTL? |
|---|---|---|
he | Hebrew | Yes |
ar | Arabic | Yes |
fa | Farsi | Yes |
en | English | No |
es | Spanish | No |
fr | French | No |
de | German | No |
ru | Russian | No |
zh | Chinese | No |
ja | Japanese | No |
pt | Portuguese | No |
it | Italian | No |
tr | Turkish | No |
nl | Dutch | No |
pl | Polish | No |
ko | Korean | No |
RTL Languages
For Hebrew (he), Arabic (ar), and Farsi (fa), run rtl-fix.py after postprocess.py:
python3 ~/.claude/skills/translate-video/scripts/rtl-fix.py "$SRT"Subtitle Refinement Modes
MODE A: --shorts (Preserve Timestamps)
PRIMARY RULE: DO NOT change, merge, or regroup timestamps.
Fix only the text within each entry:
- Fix grammar/naturalness — minimal edits only
- Do NOT merge entries
- Do NOT move words between entries
- Only rephrase within each entry (no added/removed content)
Line limits: MAX 2 lines, MAX 38 chars/line
If you MUST split an entry (line too long): divide timestamp proportionally.
Never:
- ❌ Merge 2+ entries into 1
- ❌ Regroup words across entry boundaries
- ❌ Change timestamps (unless splitting)
---
MODE B: --regular (Merge into Readable Sentences)
Goal: Full sentences per subtitle, comfortable reading pace.
Timestamp rule: Use word-level cursor — see ../scripts/build-timestamps.py
Workflow
Step 1 — Plan groups (read .md only, NOT the full JSON)
Read .md for full context. Plan subtitle groups — each group = one natural sentence or clause. Record how many words each group contains (consecutive, in order).
Step 2 — Build timestamps with cursor script
Fill in GROUP_SIZES in build-timestamps.py and run it. The script uses a sequential index cursor — no text search, no ambiguity about repeated words.
Step 3 — Replace English with translation
Edit the output SRT: replace each English group text with its translation. Timestamps stay as computed.
Line limits: MAX 2 lines, MAX 42 chars/line
Target entry count: ~1 entry per 3–5 seconds of speech
Allowed:
- ✅ Merge multiple raw entries into one
- ✅ Rephrase freely for naturalness (same meaning)
- ✅ Prefer breaking at existing punctuation (period/comma already in text)
Never:
- ❌ Add content not said
- ❌ Remove content that was said
- ❌ Add punctuation that doesn't exist in the original
---
Mode Comparison
--shorts | --regular | |
|---|---|---|
| Merge entries? | ❌ Never | ✅ Yes, into sentences |
| Change timestamps? | ❌ Never | ✅ Via cursor script |
| Max chars/line | 38 | 42 |
| Best for | Reels, TikTok, Stories | YouTube, tutorials, talks |
| Entry count | Same as raw | ~3–5x fewer |
"""
build-timestamps.py — Regular mode timestamp builder
Uses sequential word-index cursor (no text search — avoids duplicate-word ambiguity).
Usage:
1. Set WORDS_JSON and OUTPUT_SRT paths
2. Fill GROUP_SIZES with the word count of each subtitle group (in order)
3. Run — produces an English SRT with correct timestamps
4. Edit the SRT: replace English text with translated text per entry
How GROUP_SIZES works:
- Each number = consecutive words from the JSON that belong to one subtitle
- Cursor advances sequentially: group N starts exactly where group N-1 ended
- sum(GROUP_SIZES) must equal total number of words in the JSON
Example:
GROUP_SIZES = [
16, # "All right... for everyone."
7, # "Honestly... blind spot."
...
]
"""
import json, sys
WORDS_JSON = 'PATH_TO_transcript.json' # e.g. ~/Downloads/video_transcript.json
OUTPUT_SRT = 'PATH_TO_output.srt' # e.g. ~/Downloads/video_he.srt
# FILL THIS IN — one number per subtitle, in speech order
GROUP_SIZES = [
# e.g. 16, 7, 8, ...
]
def to_srt_time(seconds: float) -> str:
ms = round(seconds * 1000)
h = ms // 3600000; ms %= 3600000
m = ms // 60000; ms %= 60000
s = ms // 1000; ms %= 1000
return f'{h:02d}:{m:02d}:{s:02d},{ms:03d}'
with open(WORDS_JSON, encoding='utf-8') as f:
words = json.load(f)['words']
if sum(GROUP_SIZES) != len(words):
print(f'WARNING: GROUP_SIZES sums to {sum(GROUP_SIZES)}, but JSON has {len(words)} words')
cursor = 0
entries = []
for size in GROUP_SIZES:
group = words[cursor:cursor + size]
if not group:
print(f'ERROR: ran out of words at cursor={cursor}')
sys.exit(1)
start_ts = to_srt_time(group[0]['start'])
end_ts = to_srt_time(group[-1]['end'])
text = ' '.join(w['word'] for w in group)
entries.append((start_ts, end_ts, text))
cursor += size
lines = []
for i, (s, e, t) in enumerate(entries, 1):
lines += [str(i), f'{s} --> {e}', t, '']
with open(OUTPUT_SRT, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
print(f'Done: {len(entries)} entries written to {OUTPUT_SRT}')
print(f'Cursor: {cursor}/{len(words)} words consumed')
"""
postprocess.py — Enforce MAX 2 lines and MAX_CHARS per line on a translated SRT.
Splits long lines at word boundaries. If an entry exceeds 2 lines, creates sub-entries
with proportionally divided timestamps.
Usage:
python3 postprocess.py <srt_file> [max_chars]
max_chars defaults:
38 for --shorts mode
42 for --regular mode
"""
import re, sys
INPUT = sys.argv[1] if len(sys.argv) > 1 else 'input.srt'
MAX_CHARS = int(sys.argv[2]) if len(sys.argv) > 2 else 42
def ts_to_ms(ts):
h, m, rest = ts.split(':'); s, ms = rest.split(',')
return int(h)*3600000 + int(m)*60000 + int(s)*1000 + int(ms)
def ms_to_ts(ms):
h = ms//3600000; ms %= 3600000
m = ms//60000; ms %= 60000
s = ms//1000; ms %= 1000
return f'{h:02d}:{m:02d}:{s:02d},{ms:03d}'
def split_line(line, max_chars):
clean = line.strip('\u202b\u202c\u200f\u200e')
if len(clean) <= max_chars:
return [clean]
words = clean.split(' '); chunks = []; current = ''
for word in words:
test = (current + ' ' + word).strip()
if len(test) <= max_chars:
current = test
else:
if current: chunks.append(current)
current = word
if current: chunks.append(current)
return chunks
with open(INPUT, 'r', encoding='utf-8') as f:
content = f.read()
blocks = re.split(r'\n\n+', content.strip())
result = []
for block in blocks:
lines = block.split('\n')
if len(lines) < 3: continue
ts_match = re.match(r'(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})', lines[1])
if not ts_match: continue
start_ts, end_ts = ts_match.group(1), ts_match.group(2)
text_lines = [l.strip('\u202b\u202c\u200f\u200e') for l in lines[2:] if l.strip()]
all_sublines = []
for tl in text_lines:
all_sublines.extend(split_line(tl, MAX_CHARS))
chunks = [all_sublines[i:i+2] for i in range(0, len(all_sublines), 2)]
start_ms = ts_to_ms(start_ts); end_ms = ts_to_ms(end_ts)
chunk_ms = max(1, (end_ms - start_ms) // len(chunks))
for i, chunk in enumerate(chunks):
c_start = ms_to_ts(start_ms + i * chunk_ms)
c_end = ms_to_ts(start_ms + (i+1)*chunk_ms - 1) if i < len(chunks)-1 else end_ts
result.append((c_start, c_end, chunk))
output_lines = []
for idx, (s, e, tls) in enumerate(result, 1):
output_lines += [str(idx), f'{s} --> {e}'] + ['\u202b' + l + '\u202c' for l in tls] + ['']
with open(INPUT, 'w', encoding='utf-8') as f:
f.write('\n'.join(output_lines))
max_len = max(len(l.strip('\u202b\u202c')) for _, _, ls in result for l in ls)
print(f'Done: {len(result)} entries, max {max_len} chars/line')
"""
rtl-fix.py — Apply RTL Unicode markers to Hebrew/Arabic/Farsi SRT files.
MANDATORY for he/ar/fa — run AFTER postprocess.py, BEFORE ffmpeg embed.
U+200F (RLM) + U+202B (RLE) are both required for libass+FriBidi to render
RTL text correctly. Do NOT add U+202C at end — it breaks RTL detection.
Usage:
python3 rtl-fix.py <srt_file>
"""
import re, sys
SRT_FILE = sys.argv[1] if len(sys.argv) > 1 else 'input.srt'
with open(SRT_FILE, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.split('\n')
result = []
for line in lines:
stripped = line.strip()
if (stripped
and not re.match(r'^\d+$', stripped)
and not re.match(r'\d{2}:\d{2}:\d{2}', stripped)):
clean = line.strip('\u202b\u202c\u200f\u200e')
line = '\u200F\u202B' + clean
result.append(line)
with open(SRT_FILE, 'w', encoding='utf-8') as f:
f.write('\n'.join(result))
print(f'RTL fix applied to {SRT_FILE}')
Related skills
FAQ
What is the difference between shorts and regular modes?
--shorts fixes text only and preserves timestamps with a 38 char/line limit, while --regular merges into full sentences with a 42 char/line limit.
How are Hebrew and Arabic handled?
A dedicated RTL fix step applies RTL Unicode markers for Hebrew, Arabic, and Farsi.