localizing-video 是做什么的?
将外语视频转化为英语版本——同步配音+英语字幕+翻译后的屏幕文字——100%本地/免费,基于Apple Silicon。一个足够智能的代理可以自行推导出底层工具链(yt-dlp, whisper, ffmpeg, scene-detect, mux);该技能的价值在于那些重新推导会踩中的工具选择和隐蔽地雷,以及一个经过验证的参考实现。
参考实现(从这里开始)
本仓库提供了一个可工作的流水线:engine/ 是通用的可运行引擎(build_dub.py——基于分块锚定,无漂移,请参阅配音地雷;make_ass.py;dense_ocr.py;dedup_spans.py;render_overlay.py;run.sh);example/ 是一个完整的示例数据集。请阅读 engine/SETUP.md 和 example/README.md,然后运行引擎——不要从头编写。每个视频唯一的新工作就是生成 en.srt 和 translations.json(翻译步骤)。引擎假定源视频分辨率为1920×1080。
流水线及各阶段决策点
- 下载
yt-dlp(如果遇到403错误,可尝试--extractor-args "youtube:player_client=web_safari,ios,tv";当DASH被DRM保护时,HLS格式通常可用)。 - 语音识别——但请先运行
yt-dlp --list-subs(参见地雷):手动/创作者音轨可以免费超越Whisper;-orig标签会揭示真正的源语言(避免你白花16分钟对已为目标语言的视频运行Whisper);而-orig自动字幕则是免费的辅助语音识别,可用于交叉验证。然后使用 openai-whisper,运行whisper source.mp4 --model large-v3-turbo --language <zh/ja> --word_timestamps True --output_format all(保留JSON)。务必指定语言。(PyTorch没有py3.14的wheel——请从py3.12的虚拟环境中运行whisper。) - 自行翻译字幕(由代理在循环内完成)——免费,保持语气一致,术语统一。保持1:1的片段↔时间戳映射。根据知识修正专有名词(语音识别可能误听)。保留笑点,而不仅仅是文字——双关语几乎无法直译;应翻译创作一个具有相同机制的英语双关语,并在字幕中呈现(笑点的主要载体——配音会覆盖原始音频并降低其音量)。谐音双关语在语音识别中不可见(参见地雷),因此需要通过OCR识别字幕来找到它们。
- 配音——Kokoro-onnx TTS(参见地雷)。按句子分块合成,将每个分块锚定到其真实时间戳(而不是逐段追逐漂移——这会导致延迟累积),进行时间适配,然后混音。调整视觉画面的时间以匹配配音,而不仅仅是让配音去迎合时钟——请参见时间密度。
- 字幕——在黑边区域中烧录英文字幕,而不是覆盖在画面上(参见地雷)。
- 屏幕文字识别——
ocrmac(Apple Vision),密集识别(每秒1帧),去重后得到时间范围。 - 自行翻译屏幕上的文字;将英文覆盖在仅有中文的元素上。
- 混流——
ffmpeg-full(libass),叠加覆盖层 → 黑边 → 字幕烧录 → 配音音频。
地雷——该技能存在的真正原因
| 陷阱 | 现实/修复方法 |
|---|---|
ffmpeg 没有 subtitles 滤镜 | 默认的 brew ffmpeg 缺少 libass。请使用 /opt/homebrew/opt/ffmpeg-full/bin/ffmpeg。(相关技能:hardsubbing-video) |
TTS 即 say 命令,效果平淡/机械 | 默认使用 Kokoro-onnx(免费,神经网络,py3.14 可通过 onnxruntime 支持)。男声 am_michael 等。 |
Kokoro 静默失败:phontab ... No such file | 捆绑的 eSpeak 数据路径损坏。运行 brew install espeak-ng,然后在脚本中覆盖 espeakng_loader.get_data_path/get_library_path → /opt/homebrew/share/espeak-ng-data 和 /opt/homebrew/lib/libespeak-ng.dylib。 |
| 英语比中文/日语更加冗长,导致配音延迟 | 不要使用 atempo(>1.25倍速会产生声码器伪影)——应使用 Kokoro 原生的 speed 参数重新合成。不要使用游标追赶(cursor-start catch-up)来追逐漂移——经验表明,在密集的连续语音中,这会累积约+5秒的延迟。正确做法:(a) 将每个话语锚定到其真实开始时间(即字幕/视频时间线),这样漂移无法累积;(b) 将相邻的 ASR 片段合并为大约句子的长度,这样长英文行可以从短邻居那里借用时间——逐片段适配导致 59% 的行在1.9倍的速度上限下被截断;而按句子分块则将此比例降至约0。锚定可以消除漂移,但不能消除超时——如果一个块的音频时长超过了它的时隙,它仍会与下一个锚点冲突。超时时回退顺序: (1) 更简洁的翻译,(2) 在约1.9倍速上限内以更快的速度重新合成(“压缩” = 原生 speed 重新合成或更短的文本——不是 atempo),(3) 冻结当前画面以延长显示时间(参见时间密度),(4) 借用相邻低密度区域的空闲时间;最后的手段:硬性限制时长并对尾部进行30毫秒的淡出(字幕仍然显示完整行)。 |
| 英文字幕覆盖在视频上,且字体巨大 | SRT→libass 默认使用 PlayResY=288,导致字号和边距放大约4.4倍。应使用显式指定 PlayResY(等于填充后的高度)的 ASS 格式。另外,使用黑边(在底部添加黑边,Alignment=2),这样英文就不会与源视频中已经烧录的字幕冲突(你无法移除那些字幕)。 |
| Vision OCR “需要自定义脚本” | 不需要——运行 pip install ocrmac;ocrmac.OCR(f, language_preference=["zh-Hans","en-US"]).recognize() 返回 (text, conf, (x,y,w,h)),坐标已归一化,原点为左下角。 |
| ASR 结果与屏幕字幕不一致 | 底部字幕是旁白的权威真实来源——很好的 ASR 交叉验证。但是创作者会植入搞笑字幕(无意义文字 ≠ 实际台词);永远不要盲目地用 OCR 替换 ASR。标记分歧点,逐一判断。 |
| ASR 无声地抹平了谐音双关语 | 笑点可能就是这个谐音:例如“人工智能”被说成“人工痔能”,或者“搁置”被说成“割痔疮”。Whisper 会将罕见双关语“纠正”为常见词汇,因此这个梗永远不会出现在转录文本中——而且专有名词修正也无法恢复它。创作者通常会把双关语拼写出来显示在已烧录的字幕中,因此在这里 OCR 不是交叉验证——它通常是唯一可靠的恢复途径(对于拼写出来的谐音,字幕更可靠——这与上面搞笑字幕的情况相反)。判断方法:如果某行文字读起来特别直白,或者画面反应/视觉效果与文字不匹配,则应在翻译前对该字幕进行 OCR。 |
| 在检查 YouTube 自带字幕之前就运行 Whisper | 运行 yt-dlp --list-subs 只花费5秒,但有三重回报。(1) <lang>-orig 标签 = 视频的真实源语言;如果它已经是目标语言(例如 en-orig),则整个流水线是空操作——在下载和运行 Whisper 之前就应停止。(2) 手动/创作者音轨(源语言)通常优于 Whisper:具有真正的标点、正确的专有名词,没有插入的 [music] 标记。(3) 一个 -orig 的自动字幕是免费的辅助 ASR——将其与 Whisper 结果进行差异对比,可以精确找出需要人工修正的行。在对一个16分钟的英语演讲的测量中,单词级别一致率高达98.2%,然而两个引擎在 Arthur Andersen 上产生了分歧(YouTube 正确,Whisper 输出 Anderson),并且两者都搞错了一个品牌名(Saily → “Saylite”/“Saley”)——交叉验证可以标记出这些问题;单独任何一个引擎都无法捕捉到。陷阱: --list-subs 中列出的其他100多种语言是 YouTube 的自动翻译,而不是创作者音轨——不要将那里的 zh-Hant 行误认为是真正的中文源。 |
| 覆盖每一个中文文字框 | 在评论视频中,许多屏幕上的“内容”(文章、嵌入的演讲、推文)本身已经是英文,只是叠加了中文——再覆盖一次既冗余又杂乱。应将范围限定在仅中文元素上。 |
| 静态覆盖在滚动文字上 | 基于每个时间片(平均位置)的覆盖层无法跟踪连续滚动——中文内容会未被覆盖。这需要逐帧 OCR 加框追踪;通常不值得(这些内容通常是已经英文的)。 |
| 保留原始英文片段可听 | 当源视频播放真实的英文片段(如采访、演示)时,应在间隙中以全音量传递原始音频;仅在配音期间将其音量降低(约0.1)。 |
| 在新视频上重用脚本 | 可以适配,但要检查是否有硬编码的长度/路径。render_overlay.py 曾从硬编码的帧数中推导覆盖层持续时间——在更长的视频上,覆盖层在视频中途就静默消失了。应从 len(glob('f1/f_*.png')) 推导持续时间,而不是使用常量。 每个视频都需要重新翻译 en.srt 和 translations.json;索引配对断言加并排抽查可以捕捉到翻译/键对齐错误。 |
时间密度:调整时间线,而不仅仅是配音
英语每秒钟携带的信息密度约为中文或日语的1/1.5到1/2,因此忠实的配音在密集的源视频上总是在争抢时间。压缩配音(原生 speed 参数)是一个手段,但在听起来急促之前,上限约为1.9倍速。更大的杠杆是:视觉时间线是可伸缩的——调整它来适应配音。 三个手段,按成本从低到高排列:
- 首先采用更简洁的翻译(密集→更短)。 将最密集的行改写得更加简短。免费,不会丢失观众看到的任何内容,并且烧录的字幕仍然承载完整的含义,即使配音略去了少许。对于说话人画面的镜头(你无法冻结动态的面部而不产生抖动;重新剪辑B-roll通常超出了就地本地化的范围),这是主要手段。
- 冻结静态幻灯片以争取时间(在压力大的地方争取时间)。 配音最吃力的时刻通常恰好在旁白讲解截图/幻灯片时——而那个画面是静态的,所以在一个自然的停顿处将其保持稍长时间来争取播出时间。这几乎不可察觉,前提是满足三个条件:(i) 这是时间线编辑——之后的所有内容都会因为保持而偏移,因此要在保持后的时间线基础上插入冻结并重新生成字幕/配音/覆盖层的计时(或者在混流时同时保持所有轨道),而不是仅锚定到原始时间;(ii) 仅在音频背景同样暂停时进行冻结——如果在连续的音乐、环境音或原始音频上进行保持,会导致背景音不同步(可以延长或交叉淡入淡出,或者仅在音频背景被压低或静音的配音段下方进行冻结);(iii) “静态”必须是真正的静态——光标、闪烁的插入符、激光笔或画面中的画中画摄像头都会破坏错觉。应使用许多次短暂的保持,而不是一次长时间的拖拽(超过几秒的保持会让人觉得是卡顿/缓冲)。
- 从低密度片段中回收空闲时间(反之亦然——低密度→高密度)。 开场、结束语、缓慢的平移镜头有富余空间;让配音在那里放松,或者将那里的空闲时间向前借给密集片段。
为什么这很有效:密度和可冻结性相关。 旁白最密集正是因为他在讲解幻灯片——而幻灯片恰恰是可冻结的。因此,大部分时间压力往往落在你可以吸收它们的地方。请测量它,而不是假设它: 通过逐帧运动差异(对缩小后的灰度图计算 mean|frame − prev|)来标记每一秒是否为静态——但阈值需要针对每个视频进行校准(编码噪声、码率、帧率、颗粒感都会影响它;对于某个视频,阈值约为4/255),并且整帧的平均值会错过局部运动(光标/插入符/激光笔)→ 应遮罩掉字幕区域,检查一个静态的区域,而不是整个画面。然后将静态遮罩与配音快的片段相交,以查看有多少压力是可冻结的。在一个10分钟的讲解视频测量中,一个平缓的1.4倍速上限需要回收约14秒,其中74%位于可冻结的静态幻灯片上(26%位于不可冻结的说话人画面上,需要通过更简洁的措辞解决)——但这个比例是因视频而异的。超出源视频时长对于独立文件来说是可以接受的;没有外部因素与之同步。
需要与操作者讨论的决策点
- 视觉范围: 针对性(仅中文,推荐)vs 全面(也包括文章段落——在滚动时会退化)。
- 配音声音: 选择哪个免费的 Kokoro 声音;仅在对方放宽无付费限制时才提供付费选项(ElevenLabs)。
- 音频: 完全替换 vs 保留原始音频并在配音时降低其音量。
- 节奏控制努力程度: 接受较快的配音(字幕承载完整含义)vs 调整时间线以获得平缓的速度上限(更简洁的措辞 + 冻结幻灯片——需要一次测量和重新混流)。提供测量数据,以便他们做出决定。
验证(不要相信——要检查)
在最终mp4中抽取画面帧(读取JPG)进行验证:在旁白时刻、幻灯片/覆盖层时刻、英文片段间隙以及最后一行——确认字幕同步、覆盖层位置、黑边以及音视频时长匹配。像素是永久性烧录的。特别要抽取一个密集的幻灯片时刻(配音最快的地方)——那是同步/漂移首先被打破的地方;该时间戳上烧录的中文字幕就是你的同步参照。(你无法自己试听音频——同步通过锚定的时间线是结构性的;请告知操作者这一点,并请他们进行最后的收听。)
可组合的技能
hardsubbing-video(处理 ffmpeg/libass 烧录字幕的地雷——本技能的第8阶段)。