video-dubbing · diff
git:20260421.63dcdf9 to git:20260508.ef8a852
159 added, 268 removed. Audit A to A.
---
name: video-dubbing
- description: Use when translating video audio to another language while preserving timing and rhythm, creating dubbed videos with AI-generated voice using translate-polisher and mlx-tts
+ description: >
+ Translate video/audio content into another language with dubbed voice and synchronized subtitles.
+ Use this skill whenever the user asks to: translate a video, dub a video into another language,
+ generate foreign-language voiceover for a video, add translated subtitles with dubbed audio,
+ or any workflow involving ASR → translate → TTS → video composition.
+ Also trigger when the user mentions replacing audio track with translated narration,
+ or syncing subtitles with generated dubbing.
---
- # Video Dubbing
-
- ## Overview
-
- Complete video dubbing pipeline integrating **translate-polisher** (高质量四步精翻) and **mlx-tts** (Apple Silicon 本地 TTS).
-
- **Workflow:**
- ```
- Input Video → Demucs (分离人声) → Whisper (转录) →
- translate-polisher (精翻) → mlx-tts (合成) → FFmpeg (合并) → Output Video
- ```
-
- ## Integration Skills
-
- - **translate-polisher**: 四步精翻工作流(分析→初译→审校→终稿),产出出版级翻译
- - **mlx-tts**: Qwen3-TTS 本地语音合成,支持 prompt-based 声音设计,无需云端
-
- ## Requirements
-
- ```bash
- # Core tools
- pip install openai-whisper demucs
- brew install ffmpeg uv
-
- # MLX TTS (macOS Apple Silicon)
- uv tool install --force "mlx-audio" --prerelease=allow
-
- # For translation
- pip install anthropic # translate-polisher 使用 Claude API 实现精翻
-
- # 可选:安装带字幕支持的 FFmpeg
- brew install ffmpeg --with-libass # 如果需要硬字幕烧录
- ```
-
- ## Quick Reference
-
- | Step | Tool | Output |
- |------|------|--------|
- | 1. Extract | `ffmpeg` | `audio.wav` |
- | 2. Separate | `demucs` | `vocals.wav`, `no_vocals.wav` |
- | 3. Transcribe | `whisper` | `segments.json` |
- | 4. Translate | `translate-polisher` | `segments_translated.json` |
- | 5. TTS | `mlx-tts` | `dubbed_audio.wav` |
- | 6. Merge | `ffmpeg` | `output.mp4` |
-
- ## Usage
-
- ### 分步执行(推荐)
+ # Video Dubbing & Subtitle Sync
- ```bash
- # Step 1: 提取音频
- python video_dubbing.py input.mp4 --step extract
+ Complete workflow for translating video/audio content into another language with dubbed voice and precisely synchronized subtitles.
- # Step 2: 分离人声
- python video_dubbing.py input.mp4 --step separate
+ ## Overview
- # Step 3: 转录字幕
- python video_dubbing.py input.mp4 --step transcribe
+ This skill produces a final video where:
+ - The original audio is replaced by AI-generated dubbing in the target language
+ - Hardcoded subtitles appear exactly when the dubbed words are spoken
+ - The video duration matches the original (or is explicitly controlled)
- # Step 4: 准备翻译文件
- python video_dubbing.py input.mp4 --step translate
- # 输出提示:请使用 translate-polisher 翻译 to_translate.md
+ ## Critical Insight
- # Step 4b: 使用 translate-polisher 翻译(在 Claude Code 中执行)
- /translate --from en --to zh --audience technical --style conversational input_dubbed/to_translate.md
- # 将翻译结果保存为 input_dubbed/translated.md
+ **Never reuse the original subtitle timestamps for translated text.**
+ Translated text has different length, word count, and speaking rhythm. If you burn subtitles using the original English timestamps while playing Chinese dubbing, the subtitles will appear 3–5 seconds too early (or too late).
- # Step 5: 合成语音
- python video_dubbing.py input.mp4 --step synthesize
+ The correct approach:
+ 1. Generate the dubbed audio first
+ 2. Run ASR on the **generated dubbing** to obtain its real timestamps
+ 3. Use those real timestamps for the translated subtitles
+ 4. Then burn them into the video
- # Step 6: 合并视频
- python video_dubbing.py input.mp4 --step merge
- ```
+ ## Workflow
- ### 快捷执行
+ ### 1. Extract Original Subtitles
- 如果已经完成了翻译步骤(translated.md 已存在):
+ Use `whisper` on the original audio to get the source transcript and baseline timing:
```bash
- # 一键执行(会自动跳过已完成的步骤)
- python video_dubbing.py input.mp4 --target-lang zh
-
- # 自定义声音风格
- python video_dubbing.py input.mp4 --voice-prompt "a confident teenage girl, warm and energetic"
-
- # 遮盖原英文字幕区域
- python video_dubbing.py input.mp4 --mask-subtitles 100,500,520,80
+ whisper original_audio.m4a \
+ --model turbo \
+ --language <source_lang> \
+ --output_format all \
+ --output_dir .
```
- ## 关键功能
-
- ### 1. 音色一致(解决声音突变问题)
+ Outputs: `.srt`, `.txt`, `.json`, `.vtt`, `.tsv`
- **问题**:mlx-tts 每次调用会重新生成声音特征,导致音色不一致。
+ ### 2. Translate the Text
- **解决方案**:
- - 首先生成第一个片段作为**参考音色**
- - 后续片段使用 `--ref_audio` 和 `--ref_text` 参数引用参考音色
- - 确保整个视频使用同一个人的声音
+ Translate the full transcript into the target language. Preserve paragraph/sentence boundaries so the text flows naturally when spoken.
- ```python
- # 流程
- 片段1 → 生成参考音频 → 片段2(使用ref) → 片段3(使用ref) → ...
- ```
+ Save as `transcript_<lang>.txt`.
- ### 2. 翻译步骤(使用 translate-polisher)
+ ### 3. Generate Dubbed Audio
- **不再硬编码 API 调用**,而是生成翻译文件让用户使用 translate-polisher skill 处理:
+ Generate the full narration in one pass using TTS (e.g., mlx-tts, edge-tts, etc.). Use natural speed (do not slow down or speed up at generation time).
```bash
- # Step 1-3 完成后,脚本会生成 to_translate.md 文件
- python video_dubbing.py input.mp4 --step translate
-
- # 输出:
- # 待翻译文件已保存: input_dubbed/to_translate.md
- # 请执行以下命令进行翻译:
- # /translate --from en --to zh --audience technical --style conversational input_dubbed/to_translate.md
-
- # Step 4: 使用 translate-polisher 翻译
- /translate --from en --to zh --audience technical --style conversational input_dubbed/to_translate.md
-
- # 翻译完成后,保存译文到 input_dubbed/translated.md
- # 然后继续执行后续步骤
-
- # Step 5-6: 合成语音并合并
- python video_dubbing.py input.mp4 --step synthesize
- python video_dubbing.py input.mp4 --step merge
+ mlx_audio.tts.generate \
+ --model mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit \
+ --text "$(cat transcript_zh.txt)" \
+ --instruct "a professional narrator, clear and authoritative" \
+ --speed 1.0 \
+ --output_path dubbing_raw.wav
```
- **translate-polisher 参数说明**:
- - `--from en`: 源语言(英文)
- - `--to zh`: 目标语言(中文)
- - `--audience technical`: 目标读者为技术人员
- - `--style conversational`: 翻译风格为口语化
-
- 更多参数参考 @translate-polisher skill。
-
- ### 3. 剥离原字幕(遮盖原英文字幕)
+ > **Why one pass?** Generating the entire text at once preserves natural prosody, pauses, and intonation across sentence boundaries. Splitting into chunks creates audible seams.
- **问题**:原视频可能包含硬字幕(烧录在视频中的字幕),与新中文字幕重叠。
+ ### 4. Adjust Dubbing Duration to Match Video
- **解决方案**:
- 使用 `--mask-subtitles` 参数指定原字幕区域,用黑色块遮盖:
+ Measure the raw dubbing duration and the original video duration:
```bash
- python video_dubbing.py input.mp4 --mask-subtitles 100,500,520,80
- # 格式: x,y,w,h (x坐标,y坐标,宽度,高度)
+ ffprobe -v error -show_entries format=duration -of csv=p=0 dubbing_raw.wav/audio_000.wav
+ ffprobe -v error -show_entries format=duration -of csv=p=0 original_video.mp4
```
- **如何确定字幕区域**:
+ Use `ffmpeg atempo` to stretch or compress the entire dubbing track:
+
```bash
- # 使用 ffplay 查看视频,确定字幕位置
- ffplay -vf "drawgrid=w=100:h=100" input.mp4
+ # Example: raw=87.4s, target=77.5s → atempo = 87.4 / 77.5 = 1.128
+ ffmpeg -y -i dubbing_raw.wav/audio_000.wav \
+ -af "atempo=<ratio>" \
+ -ar 24000 \
+ dubbing_adjusted.wav
```
- ### 4. 字幕与语音同步
+ > **Why atempo?** It changes speed without altering pitch, so the voice still sounds natural.
- **问题**:TTS 生成的语音时长可能与原字幕时长不一致,导致不同步。
+ ### 5. Obtain Real Subtitle Timestamps (The Key Step)
- **解决方案**:
- - 记录每个片段的**实际 TTS 时长**
- - 根据实际时长**重新计算字幕时间**
- - 输出同步后的字幕文件 `subtitles_zh_synced.srt`
+ Run ASR on the **adjusted dubbing audio** to get timestamps that match what the listener actually hears:
- ```python
- # 同步逻辑
- if TTS时长 > 原时长:
- 使用 TTS时长 # 避免字幕提前消失
- else:
- 使用原时长 × 0.8 # 保留一点静音缓冲
+ ```bash
+ whisper dubbing_adjusted.wav \
+ --model turbo \
+ --language <target_lang> \
+ --output_format srt \
+ --output_dir .
```
- ## Voice Prompts (mlx-tts)
-
- | 风格 | Prompt |
- |------|--------|
- | 专业女声 | `a warm, professional female voice, clear and natural` |
- | 自信少女 | `a confident teenage girl, energetic and expressive` |
- | 温柔女声 | `a warm, gentle female voice, slightly soft` |
- | 磁性男声 | `a deep, masculine voice with authority` |
- | 新闻播报 | `a professional news anchor, clear and authoritative` |
- | 教学讲解 | `a patient educator, clear and encouraging` |
- | 激动演讲 | `an energetic public speaker, passionate and enthusiastic` |
-
- 更多声音设计参考 mlx-tts skill。
- |------|--------|
- | 专业女声 | `a warm, professional female voice, clear and natural` |
- | 自信少女 | `a confident teenage girl, energetic and expressive` |
- | 温柔女声 | `a warm, gentle female voice, slightly soft` |
- | 磁性男声 | `a deep, masculine voice with authority` |
- | 新闻播报 | `a professional news anchor, clear and authoritative` |
- | 教学讲解 | `a patient educator, clear and encouraging` |
- | 激动演讲 | `an energetic public speaker, passionate and enthusiastic` |
-
- 更多声音设计参考 mlx-tts skill。
-
- ## Translation Styles (translate-polisher)
-
- 在 `/translate` 命令中使用 `--style` 参数指定:
-
- | 风格 | 说明 |
- |------|------|
- | `auto` | 根据原文自动匹配风格 |
- | `formal` | 专业严谨 |
- | `technical` | 精确简洁,术语密集 |
- | `conversational` | 口语化,亲切随和 |
- | `storytelling` | 叙事流畅,生动 |
+ This produces `dubbing_adjusted.srt` with real timestamps.
- ## Translation Audiences (translate-polisher)
+ ### 6. Replace ASR Text with Correct Translation
- 在 `/translate` 命令中使用 `--audience` 参数指定:
+ The ASR output may contain recognition errors (e.g., "试用范围" instead of "适用范围"). Replace each subtitle segment's text with your carefully translated text, **keeping the ASR timestamps intact**.
- | 读者 | 说明 |
- |------|------|
- | `general` | 普通读者 |
- | `technical` | 开发者/工程师 |
- | `academic` | 研究者/学者 |
- | `business` | 商务人士 |
+ Save the result as `subtitle_<lang>_synced.srt`.
- ## Implementation
+ > **Rule:** Timestamps come from the dubbing ASR. Text comes from your human-reviewed translation. Never the other way around.
- See @video_dubbing.py for full implementation.
+ ### 7. Merge Audio and Video
- **Key parameters:**
- - `--target-lang`: 目标语言 (zh, en, ja)
- - `--voice-prompt`: mlx-tts 声音风格描述
- - `--translate-style`: translate-polisher 翻译风格
- - `--translate-audience`: 目标读者类型
- - `--keep-temp`: 保留中间文件
+ ```bash
+ ffmpeg -y -i original_video.mp4 -i dubbing_adjusted.wav \
+ -c:v copy -c:a aac -shortest output_temp.mp4
+ ```
- ## Subtitles
+ ### 8. Burn Subtitles into Video
- 脚本会自动生成中文字幕文件 (`subtitles_zh.srt`),并提供两种输出模式:
+ Use `moviepy` (or ffmpeg with libass if available) to hardcode the synced subtitles:
- ### 硬字幕(推荐)
- 如果 FFmpeg 支持 libass,字幕会直接烧录到视频中:
```bash
- brew install ffmpeg --with-libass # 安装带字幕支持的 FFmpeg
+ python3 -m venv venv
+ source venv/bin/activate
+ pip install moviepy
+ python3 burn_subtitles.py
```
- ### 软字幕(备用)
- 如果 FFmpeg 不支持字幕烧录,脚本会:
- 1. 生成配音视频(无字幕)
- 2. 单独输出 SRT 字幕文件
- 3. 字幕文件与视频同名,播放器(VLC、IINA)会自动加载
-
- ### 手动添加字幕
- 如果需要硬字幕但 FFmpeg 不支持,可用以下工具:
- - **HandBrake**: 导入视频 + SRT 字幕,导出带字幕视频
- - **Final Cut Pro / Premiere**: 导入 SRT 作为字幕轨道
- - **ffmpeg + libass**: 重新编译 FFmpeg 带字幕支持
-
- 保持原视频节奏的关键:
+ Helper script (`scripts/burn_subtitles.py`):
```python
- # Whisper 提取的时间信息
- segment = {
- "start": 1.52,
- "end": 4.85,
- "text": "Hello world"
- }
-
- # translate-polisher: 只翻译文本,保持时间
- # mlx-tts: 生成后使用 ffmpeg atempo 调整语速匹配原时长
+ #!/usr/bin/env python3
+ import re
+ from moviepy import VideoFileClip, TextClip, CompositeVideoClip
- # 语速调整示例
- if tts_duration < original_duration:
- # TTS 太快,放慢语速
- atempo = tts_duration / original_duration # 如 0.8
- else:
- # TTS 太慢,加快语速
- atempo = tts_duration / original_duration # 如 1.2
+ def parse_srt(filepath):
+ with open(filepath, 'r', encoding='utf-8') as f:
+ content = f.read()
+ blocks = re.split(r'\n\s*\n', content.strip())
+ subtitles = []
+ for block in blocks:
+ lines = block.strip().split('\n')
+ if len(lines) >= 3:
+ time_line = lines[1]
+ text = '\n'.join(lines[2:])
+ match = re.match(r'(\d+):(\d+):(\d+),(\d+) --> (\d+):(\d+):(\d+),(\d+)', time_line)
+ if match:
+ h1, m1, s1, ms1, h2, m2, s2, ms2 = map(int, match.groups())
+ start = h1 * 3600 + m1 * 60 + s1 + ms1 / 1000.0
+ end = h2 * 3600 + m2 * 60 + s2 + ms2 / 1000.0
+ subtitles.append((start, end, text.strip()))
+ return subtitles
- # ffmpeg 应用
- ffmpeg -i input.wav -filter:a "atempo={atempo}" output.wav
- ```
+ if __name__ == "__main__":
+ video = VideoFileClip("output_temp.mp4")
+ subs = parse_srt("subtitle_zh_synced.srt")
+ clips = [video]
+ W, H = video.size
+ duration = video.duration
- ## Common Mistakes
+ for start, end, text in subs:
+ if start >= duration:
+ break
+ end = min(end, duration)
+ txt_clip = TextClip(
+ text=text,
+ font="/System/Library/Fonts/PingFang.ttc",
+ font_size=48,
+ color="white",
+ stroke_color="black",
+ stroke_width=2,
+ method="caption",
+ size=(W - 120, None),
+ text_align="center",
+ horizontal_align="center",
+ vertical_align="bottom"
+ )
+ txt_clip = txt_clip.with_position(("center", H - txt_clip.h - 120))
+ txt_clip = txt_clip.with_start(start).with_end(end)
+ clips.append(txt_clip)
- | Issue | Cause | Fix |
- |-------|-------|-----|
- | 翻译质量差 | 使用简单翻译 API | 使用 translate-polisher 四步精翻 |
- | 机器音太重 | TTS 参数不当 | 优化 mlx-tts voice prompt |
- | 音频不同步 | 时长不匹配 | ffmpeg atempo 自动调整 |
- | 中文太长 | 字数比英文多 | 适当加快语速或精简翻译 |
- | mlx-tts 失败 | 文本太长 | 自动分块生成 |
- | 翻译失败 | API 限制 | 自动降级到 Google Translate |
+ final = CompositeVideoClip(clips, size=video.size)
+ final = final.with_audio(video.audio)
+ final.write_videofile("output_final.mp4", codec="libx264", audio_codec="aac", fps=video.fps, preset="fast")
+ ```
- ## Pro Tips
+ ### 9. Verify
- **优化翻译质量:**
```bash
- # 针对技术视频使用专业风格
- python video_dubbing.py input.mp4 --translate-style technical --translate-audience technical
-
- # 针对故事类视频使用叙事风格
- python video_dubbing.py input.mp4 --translate-style storytelling
+ ffprobe -v error -show_entries format=duration -of csv=p=0 output_final.mp4
```
- **定制声音:**
- ```bash
- # 教学视频用耐心声音
- --voice-prompt "a patient educator, clear and encouraging, moderate pace"
+ Duration should match the original video (or the adjusted dubbing).
- # 新闻视频用专业声音
- --voice-prompt "a professional news anchor, clear and authoritative"
+ ## Common Mistakes to Avoid
- # 青少年内容用活力声音
- --voice-prompt "an energetic young adult, enthusiastic and friendly"
- ```
+ | Mistake | Why It Fails | Correct Approach |
+ |---------|-------------|------------------|
+ | Reuse original timestamps for translated subtitles | Text length and rhythm differ across languages | Re-ASR the dubbed audio for real timestamps |
+ | Generate TTS sentence-by-sentence and concatenate | Audible seams, unnatural pauses | Generate the entire script in one pass |
+ | Adjust TTS speed parameter to match duration | Speeds above ~1.3x or below ~0.7x sound robotic | Generate at natural speed, then use `ffmpeg atempo` |
+ | Burn subtitles with ffmpeg `subtitles=` filter | Requires libass; often missing in default ffmpeg builds | Use moviepy or install ffmpeg with `--enable-libass` |
+ | Forget to clip subtitles that exceed video duration | moviepy crashes or renders empty frames | Cap `end = min(end, video.duration)` |
- **背景音乐处理:**
- ```bash
- # 原始背景音降低音量后与新配音混合
- ffmpeg -i no_vocals.wav -i dubbed_audio.wav -filter_complex \
- "[0:a]volume=0.3[bg];[bg][1:a]amix=inputs=2:duration=longest" \
- final_audio.wav
- ```
+ ## Dependencies
- **批量处理:**
- ```bash
- for video in *.mp4; do
- python video_dubbing.py "$video" --target-lang zh \
- --voice-prompt "a warm, professional female voice"
- done
- ```
+ - `ffmpeg` (with standard filters)
+ - `whisper` (OpenAI Whisper or compatible)
+ - `mlx-audio` / `edge-tts` / any TTS engine
+ - `moviepy` (for subtitle burning if ffmpeg lacks libass)
+ - Python 3 with ` Pillow`
- ## See Also
+ ## Output Files
- - @mlx-tts - 详细的声音设计和 TTS 使用指南
- - @translate-polisher - 四步精翻工作流详解
- - https://github.com/Blaizzy/mlx-audio - MLX TTS 项目
+ | File | Description |
+ |------|-------------|
+ | `output_final.mp4` | Final video with dubbed audio + hardcoded subtitles |
+ | `subtitle_<lang>_synced.srt` | Synced subtitles matching the dubbed audio |
+ | `dubbing_adjusted.wav` | Time-stretched/compressed dubbing track |
+ ## Example Trigger Phrases
+
+ - "把这段视频翻译成中文并配音"
+ - "Extract English subtitles, translate to Chinese, generate new dubbing"
+ - "给这个视频加上日语配音和字幕"
+ - "Replace the audio with Spanish narration and burn captions"