| name | openclaw-videotranslate-skill |
| description | Translate video subtitles and generate multi-language dubbed audio tracks using OpenClaw's video translation skill with adaptive API scheduling |
| triggers | ["translate video subtitles to another language","add dubbed audio track to video","convert video subtitles from English to Chinese","create multi-language video with TTS dubbing","extract and translate video captions","generate foreign language voiceover for video","translate and dub video content automatically","sync translated audio with video subtitles"] |
OpenClaw Video Translation & Dubbing Skill
Skill by ara.so — Hermes Skills collection.
A professional-grade OpenClaw skill for translating video subtitles and generating dubbed audio across multiple languages. Features a 3D adaptive scheduler that handles API rate limits, automatic audio duration alignment, and lossless multi-track video output.
What It Does
This skill processes videos in two modes:
- Subtitle-only mode: Translates subtitles while keeping original audio, outputs dual-subtitle video
- Subtitle + Dubbing mode: Translates subtitles AND generates TTS audio in the target language, with automatic timing alignment
Key capabilities:
- Extracts embedded or external subtitles (SRT/VTT)
- Translates using LLM or web translation APIs
- Generates natural-sounding TTS audio with voice mapping
- Automatically stretches/compresses audio to match original timing
- Outputs lossless multi-track MKV videos
- Handles API rate limits with adaptive 3D scheduling (batch size, payload size, concurrency)
Installation
Prerequisites
brew install ffmpeg
sudo apt install ffmpeg
choco install ffmpeg
Install the Skill
git clone https://github.com/zbjincheng/openclaw-skill-videotranslate.git
cd openclaw-skill-videotranslate
pip install -e .
pip install -e ".[dev]"
Environment Setup
cp .env.example .env
Edit .env to add your API credentials:
TRANSLATION_ENDPOINT=https://api.openai.com/v1/chat/completions
TRANSLATION_CREDENTIAL=your_api_key_here
TTS_ENDPOINT=https://api.openai.com/v1/audio/speech
TTS_CREDENTIAL=your_api_key_here
Configuration
The skill is configured via manifest.yaml. Here's a complete example:
skill:
name: translation_dubbing_skill
version: "1.0.0"
parameters:
video_path:
type: path
required: true
description: "Path to source video file"
subtitle_path:
type: path
required: false
description: "Path to .srt or .vtt subtitle file"
source_language:
type: string
required: true
default: "en"
description: "Source language code (en, zh-CN, ja, es, fr, de, ko)"
target_language:
type: string
required: true
default: "zh-CN"
description: "Target language code for translation"
processing_mode:
type: enum
required: true
default: "subtitle_and_dubbing"
values:
- subtitle_only
- subtitle_and_dubbing
translation_provider:
type: enum
required: true
values: [llm, web]
translation_endpoint:
type: string
required: true
description: "Translation API endpoint"
translation_credential:
type: secret
required: true
description: "API key for translation service"
tts_provider:
type: enum
required: false
values: [llm, web]
tts_endpoint:
type: string
required: false
description: "TTS API endpoint"
tts_credential:
type: secret
required: false
description: "API key for TTS service"
scheduler:
max_concurrency: 10
initial_batch_size: 20
max_payload_tokens: 4000
backoff_multiplier: 2.0
Usage Patterns
Basic Subtitle Translation (No Dubbing)
from translation_dubbing_skill.entry import TranslationDubbingSkill
from translation_dubbing_skill.models import ProcessingMode
skill = TranslationDubbingSkill(
video_path="input.mp4",
source_language="en",
target_language="zh-CN",
processing_mode=ProcessingMode.SUBTITLE_ONLY,
translation_provider="llm",
translation_endpoint="https://api.openai.com/v1/chat/completions",
translation_credential="${TRANSLATION_CREDENTIAL}",
output_dir="./output"
)
result = skill.execute()
print(f"Output video: {result['output_video']}")
print(f"Subtitle file: {result['output_subtitle']}")
Full Translation + Dubbing
from translation_dubbing_skill.entry import TranslationDubbingSkill
from translation_dubbing_skill.models import ProcessingMode
skill = TranslationDubbingSkill(
video_path="lecture.mp4",
subtitle_path="lecture.srt",
source_language="en",
target_language="ja",
processing_mode=ProcessingMode.SUBTITLE_AND_DUBBING,
translation_provider="llm",
translation_endpoint="https://api.openai.com/v1/chat/completions",
translation_credential="${TRANSLATION_CREDENTIAL}",
tts_provider="llm",
tts_endpoint="https://api.openai.com/v1/audio/speech",
tts_credential="${TTS_CREDENTIAL}",
output_dir="./output"
)
result = skill.execute()
Using External Subtitle Files
skill = TranslationDubbingSkill(
video_path="video.mp4",
subtitle_path="video.srt",
source_language="en",
target_language="es",
processing_mode=ProcessingMode.SUBTITLE_ONLY,
translation_provider="web",
translation_endpoint="https://translation-api.example.com/translate",
translation_credential="${WEB_TRANSLATION_KEY}"
)
Custom Scheduler Configuration
skill = TranslationDubbingSkill(
video_path="long_video.mp4",
source_language="en",
target_language="zh-CN",
processing_mode=ProcessingMode.SUBTITLE_AND_DUBBING,
translation_provider="llm",
translation_endpoint="${TRANSLATION_ENDPOINT}",
translation_credential="${TRANSLATION_CREDENTIAL}",
tts_provider="llm",
tts_endpoint="${TTS_ENDPOINT}",
tts_credential="${TTS_CREDENTIAL}",
scheduler_config={
"max_concurrency": 5,
"initial_batch_size": 10,
"max_payload_tokens": 2000,
"backoff_multiplier": 3.0,
"max_retries": 5
}
)
Working with Subtitles
Parsing Subtitle Files
from translation_dubbing_skill.subtitle import SubtitleParser
parser = SubtitleParser()
entries = parser.parse("video.srt")
for entry in entries:
print(f"[{entry.start_time} -> {entry.end_time}]")
print(f" {entry.text}")
print(f" Speaker: {entry.speaker_id}")
Creating Subtitle Objects
from translation_dubbing_skill.models import SubtitleEntry
entry = SubtitleEntry(
index=1,
start_time="00:00:01,500",
end_time="00:00:04,200",
text="Hello, welcome to the tutorial.",
speaker_id="narrator",
duration_ms=2700
)
Writing Translated Subtitles
from translation_dubbing_skill.subtitle import SubtitleSerializer
translated_entries = [
SubtitleEntry(
index=1,
start_time="00:00:01,500",
end_time="00:00:04,200",
text="你好,欢迎来到教程。",
speaker_id="narrator",
duration_ms=2700
)
]
serializer = SubtitleSerializer()
serializer.write("output_zh.srt", translated_entries, format="srt")
Translation Providers
LLM Provider (OpenAI-compatible)
from translation_dubbing_skill.providers.translation import LLMTranslationProvider
provider = LLMTranslationProvider(
endpoint="https://api.openai.com/v1/chat/completions",
credential="${OPENAI_API_KEY}",
model="gpt-4o",
source_language="en",
target_language="zh-CN"
)
translated = await provider.translate_batch(subtitle_entries)
The LLM provider:
- Automatically strips
<think> reasoning blocks
- Parses JSON responses robustly
- Handles context window overflow with payload slicing
- Supports custom system prompts for translation style
Web Provider (Generic Translation API)
from translation_dubbing_skill.providers.translation import WebTranslationProvider
provider = WebTranslationProvider(
endpoint="https://api.translator.com/v1/translate",
credential="${WEB_API_KEY}",
source_language="en",
target_language="fr"
)
translated = await provider.translate_batch(subtitle_entries)
TTS Providers
LLM TTS Provider (OpenAI-compatible)
from translation_dubbing_skill.providers.tts import LLMTTSProvider
provider = LLMTTSProvider(
endpoint="https://api.openai.com/v1/audio/speech",
credential="${OPENAI_API_KEY}",
model="tts-1-hd",
voice_mapping={
"narrator": "alloy",
"speaker1": "echo",
"speaker2": "nova"
}
)
audio_bytes = await provider.synthesize(
text="你好,欢迎来到教程。",
speaker_id="narrator",
target_duration_ms=2700
)
Web TTS Provider
from translation_dubbing_skill.providers.tts import WebTTSProvider
provider = WebTTSProvider(
endpoint="https://tts-api.example.com/synthesize",
credential="${TTS_API_KEY}",
voice_mapping={"default": "zh-CN-XiaoxiaoNeural"}
)
audio = await provider.synthesize(
text="翻译后的文本",
speaker_id="default",
target_duration_ms=3000
)
Audio Duration Alignment
The skill automatically aligns TTS audio to match original subtitle timing:
from translation_dubbing_skill.align import AudioAligner
aligner = AudioAligner()
aligned_audio = aligner.align_duration(
audio_bytes=tts_output,
target_duration_ms=2700,
original_duration_ms=3200
)
Uses time-stretching (not pitch-shifted) to preserve voice quality.
Video Muxing
Creating Multi-Track Output
from translation_dubbing_skill.mux import VideoMuxer
muxer = VideoMuxer()
output_path = muxer.mux_video(
video_path="original.mp4",
original_audio_path="original_audio.aac",
dubbed_audio_path="dubbed_zh.mp3",
original_subtitle_path="original_en.srt",
translated_subtitle_path="translated_zh.srt",
output_path="output_dual_track.mkv",
source_language="en",
target_language="zh-CN"
)
Output structure:
- Video: Original video stream (lossless copy)
- Audio Track 1 (default): Dubbed target language
- Audio Track 2: Original source language
- Subtitle Track 1 (default): Translated target language
- Subtitle Track 2: Original source language
Progress Tracking
Monitor Processing Status
from translation_dubbing_skill.progress import ProgressListener
class MyProgressListener(ProgressListener):
def on_state_change(self, stage: str, current: int, total: int, message: str):
print(f"[{stage}] {current}/{total} - {message}")
def on_error(self, error: Exception):
print(f"Error: {error}")
def on_complete(self, result: dict):
print(f"Complete: {result}")
skill = TranslationDubbingSkill(
video_path="video.mp4",
progress_listener=MyProgressListener()
)
skill.execute()
Error Handling
Common Error Types
from translation_dubbing_skill.errors import (
SubtitleExtractionError,
TranslationAPIError,
TTSAPIError,
AudioAlignmentError,
VideoMuxingError,
RateLimitError
)
try:
result = skill.execute()
except SubtitleExtractionError as e:
print(f"Could not extract subtitles: {e}")
except TranslationAPIError as e:
print(f"Translation failed: {e}")
print(f"Failed entries: {e.failed_entries}")
except TTSAPIError as e:
print(f"TTS synthesis failed: {e}")
except RateLimitError as e:
print(f"Hit API rate limit: {e}")
print(f"Retry after: {e.retry_after} seconds")
except VideoMuxingError as e:
print(f"FFmpeg muxing failed: {e}")
Automatic Retry Logic
The 3D scheduler automatically handles:
- HTTP 429 (Rate Limit): Exponential backoff with jitter
- Context Window Overflow: Payload slicing and re-batching
- Network Errors: Configurable retry with backoff
skill = TranslationDubbingSkill(
scheduler_config={
"max_retries": 5,
"backoff_multiplier": 2.0,
"initial_backoff_seconds": 1.0
}
)
Testing
Run the test suite:
pytest
pytest -m "not integration"
pytest -m integration
pytest tests/test_subtitle_parser.py
pytest --cov=src/translation_dubbing_skill
Troubleshooting
FFmpeg not found
ffmpeg -version
brew install ffmpeg
sudo apt install ffmpeg
API Rate Limits
If you're hitting rate limits frequently:
skill = TranslationDubbingSkill(
scheduler_config={
"max_concurrency": 3,
"initial_batch_size": 5,
"max_payload_tokens": 1500
}
)
Audio Sync Issues
If dubbed audio doesn't align well:
from translation_dubbing_skill.align import AudioAligner
aligner = AudioAligner(
stretch_algorithm="rubberband",
quality="high"
)
Subtitle Extraction Fails
ffmpeg -i video.mp4
skill = TranslationDubbingSkill(
video_path="video.mp4",
subtitle_path="video.srt",
)
Memory Issues with Large Videos
Process in smaller chunks or reduce concurrency:
skill = TranslationDubbingSkill(
scheduler_config={
"max_concurrency": 2,
"chunk_size": 50
}
)
Advanced: Custom Providers
Implementing a Custom Translation Provider
from translation_dubbing_skill.providers.translation import TranslationProvider
from translation_dubbing_skill.models import SubtitleEntry
from typing import List
class MyCustomTranslationProvider(TranslationProvider):
async def translate_batch(
self,
entries: List[SubtitleEntry]
) -> List[SubtitleEntry]:
translated = []
for entry in entries:
translated_text = await self._translate(entry.text)
translated.append(
SubtitleEntry(
index=entry.index,
start_time=entry.start_time,
end_time=entry.end_time,
text=translated_text,
speaker_id=entry.speaker_id,
duration_ms=entry.duration_ms
)
)
return translated
async def _translate(self, text: str) -> str:
pass
Implementing a Custom TTS Provider
from translation_dubbing_skill.providers.tts import TTSProvider
class MyCustomTTSProvider(TTSProvider):
async def synthesize(
self,
text: str,
speaker_id: str,
target_duration_ms: int
) -> bytes:
audio_bytes = await self._generate_audio(text, speaker_id)
from translation_dubbing_skill.align import AudioAligner
aligner = AudioAligner()
aligned = aligner.align_duration(
audio_bytes,
target_duration_ms
)
return aligned
Command-Line Interface
If the skill provides a CLI wrapper:
openclaw-videotranslate \
--video input.mp4 \
--source en \
--target zh-CN \
--mode subtitle_only \
--translation-provider llm \
--translation-endpoint "${TRANSLATION_ENDPOINT}" \
--translation-credential "${TRANSLATION_CREDENTIAL}" \
--output ./output
openclaw-videotranslate \
--video lecture.mp4 \
--subtitle lecture.srt \
--source en \
--target ja \
--mode subtitle_and_dubbing \
--translation-provider llm \
--translation-endpoint "${TRANSLATION_ENDPOINT}" \
--translation-credential "${TRANSLATION_CREDENTIAL}" \
--tts-provider llm \
--tts-endpoint "${TTS_ENDPOINT}" \
--tts-credential "${TTS_CREDENTIAL}" \
--output ./output
Best Practices
- Use subtitle_only mode first to verify translation quality before dubbing
- Start with small concurrency (3-5) and increase based on API limits
- Monitor token usage - adjust
max_payload_tokens to avoid context overflow
- Use external subtitles when possible for better control
- Test with short videos before processing long content
- Keep original files - the skill doesn't modify source videos
- Use environment variables for credentials, never hardcode
Resources