一个 YouTube 翻译搬运工具思路
最开始想做这个东西,纯粹是因为懒。
有一段时间我在看 YouTube 上的英文技术视频,有些讲得很好,但字幕是机翻的,读起来费劲,又没有配音,一边听英文一边看中文字幕,脑子转得挺累。我就想,要是有个工具能帮我直接把英文视频翻成中文配音版,然后顺手发到 B 站,不是挺好?但我自己没有能跑AI的硬件,所以在选型阶段就尽量使用API成本低的方案。
于是就做了这个MVP,如果你想要一个稳定的项目,可能需要自己修改,该项目只提供思路。
一个 Python 脚本,pipeline.py,大概两千行,跑完之后吐出两个视频:P1 是中文配音加中文字幕,P2 是原声加双语字幕,格式直接对应 B 站的分 P 投稿。成本,一个视频大概两毛钱。
代码放在 GitHub 上,有兴趣的直接去看:FlickerMi/video-translator-mvp
先说整个流程
流程本身不复杂,六个步骤线性走下来:
下载 → 提取字幕 → 翻译 → 配音 → 合成视频 → 上传 B 站用 yt-dlp 下视频,优先用 YouTube 官方字幕,没有就调 Whisper 转录,翻译和元数据生成用 OpenAI 兼容接口,配音用 Edge TTS,合成用 FFmpeg,上传用 biliup。
API 这块我统一走的 NixAPI,一个 key 打通 GPT、Gemini、Claude、DeepSeek,不用到处注册账号、管一堆密钥,而且价格比官方便宜不少。两毛钱里大部分都是 Whisper 转录的钱,翻译+元数据加起来连一分都不到。对于这种自用小工具来说,这个成本结构挺理想的。
工具都是现成的,难的不是把它们串起来,而是串起来之后怎么让最终效果不那么难看。
真正麻烦的地方:字幕翻译
这是整个 pipeline 里我改动最多的地方,也是最值得说的地方。
最早的版本,我的做法很简单——把英文 SRT 里的每一段字幕逐条丢给模型翻译。[1|0-2000] Let's take a look at 翻成一条中文,[2|2000-3500] what this new model can do. 再翻成另一条中文。
效果很烂。
原因很明显:英文字幕是"碎片化"的,一句话经常被切成三四段,逐条翻的话,每段都是半句,模型拿到的上下文不完整,翻出来的中文也是半句,读起来支离破碎。有时候一句完整的英文句子会被翻成两句意思差不多、用词却不一样的中文,时间轴上靠在一起,特别奇怪。
然后我改了方案。
现在的做法是:先把碎片合并,再翻译。把连续的几十段英文字幕打包发给模型,让它自己判断哪些碎片属于同一句话,合并之后翻成完整的中文句。格式要求也很简单,输出时带上合并后的时间戳:
[0-4000]
让我们看看 Cursor 新的 Composer 模型能做什么。
[4000-6000]
我来做一个小小的改动。这样翻出来的中文就顺多了。
但还有一个问题:分批的时候怎么切?如果固定每 20 段切一刀,很容易把一句话切断在批次边界,前半句在这批、后半句在下批,翻出来的中文就对不上。所以我加了一个 _build_translation_batches 函数,按自然断点找切割位置——两段字幕之间间隔超过 700ms,或者这段字幕以句号问号结尾,才考虑在这里切。尽量让每个批次在语义上是完整的。
视频字幕翻译的几条实践
做这个项目之前,我对字幕翻译的理解很浅,就是"把英文换成中文"。做完之后才意识到,这个理解从一开始就错了。
字幕翻译不是翻译,是再创作。
原视频的人在说话,有他的语气、停顿、节奏,那是他的表达方式。你要做的不是把他说的话逐句搬过来,而是在中文的语境里,重新把同一件事说一遍——说得自然,说得能被听进去。字幕里有时间限制、有阅读速度的限制、有口播节奏的限制,这些约束加在一起,逼着你在翻译的同时做出大量的取舍和改写。
忠实原文当然重要,但过度忠实会让字幕变得拗口,让观众看不下去。
这个认知确立之后,后面几条实践就都顺理成章了。
告诉模型这是口播,不是书面文字。
书面翻译追求完整和准确,会保留英文原句里的定语从句、状语从句、各种修饰成分。但念出来就很别扭。口播要的是短句,"这是一个能够让用户在不需要任何编程基础的情况下完成配置的工具"——这种句子念出来,听众早在你念完之前就跟丢了。
所以我在提示词里明确要求模型:译文要适合中文配音,优先短句,避免书面腔,遇到过长的英文句子可以拆成两句短句说。这不是降低翻译质量,是换了一套标准。
专有名词不要硬翻。
技术视频里有大量产品名、品牌名、行业缩写,这些东西翻成中文往往很奇怪,而且没有必要。"游标" 不如 "Cursor","人工智能大模型" 不如 "LLM","应用程序接口" 不如 "API"。我在代码里维护了一个 TERM_MAP,统一这些名词的写法——不只是"翻不翻"的问题,还有大小写、空格格式的统一。中英混排如果写法不一致,看起来就很乱。
控制字幕密度。
字幕不是越多越好,也不是越完整越好。人眼能在一条字幕上停留的时间是有限的,如果一条字幕里塞了二十几个字,大部分人来不及读完就换下一条了。
我的经验值是中文字幕大约 5 字/秒比较舒服。超过这个密度,就要考虑把这句话拆开显示。代码里的 _split_dense_sentence 就是干这件事的——先量一下这句话的文本密度,如果超出阈值,按停顿标点再细分。
切行要有规矩,不能随便断。
字幕需要换行的时候,有两个容易踩的坑:一是英文单词不能从中间断开,Cursor 不能一行显示 Cur、下一行显示 sor;二是标点不能放在行首,逗号、句号必须跟着前一行的末尾走。
听起来是小事,但字幕密度大的时候这种情况很常见,不处理的话显示效果很难看。
YouTube 自动字幕需要单独清洗。
YouTube 自动生成的字幕是"滚动式"的格式——每一个字幕块包含两行,底行是新出现的内容,顶行是上一句的延续。如果直接把这种格式丢给翻译,会出现大量重复文本,翻出来的中文也会重复。
处理方式是只取每个字幕块的"底行",以它首次出现的时刻为开始、下一个新内容出现的时刻为结束,重建一份干净的单行字幕时间轴,再拿去翻译。
配音比想象中难处理
配音这块,我踩了不少坑。
最开始的直觉是:既然字幕有时间戳,TTS 生成的音频就应该严格贴合时间轴,每段音频的时长等于字幕的时长。所以我的做法是,先生成自然语速的音频,然后量一下实际时长,如果比字幕窗口长,就用 atempo 加速压进去。
但这样做出来的效果,语速很不均匀。有些句子翻成中文本来就短,时间窗口却很长,被拉慢到 0.7 倍速,读起来像念经。有些句子中文很长,时间窗口却很短,被压到 1.5 倍速,又听着发紧。
后来我换了策略,以自然语速为主,只在超出时间窗明显的时候才做有限度的加速。具体逻辑是 _edge_tts_adaptive:先以 +0% 的自然语速生成,测量实际时长;如果自然时长超出时间窗的比例没超过 1.15 倍,就不管它,直接用;超出了才加速,而且加速上限是 22%,不让它变得太赶。
音频比时间窗短怎么办?不拉慢,后面自动补静音。
这样整体听感均匀多了,偶尔有句话稍微溢出一点窗口,大部分时候也听不出来。
一个容易忽略的漂移问题
还有一个坑,我是后来才发现的。
所有句子的 TTS 生成完、按时间轴拼接完之后,整段配音音频的总时长,和原视频的时长,往往是不一样的。差一两秒很常见,差三四秒也有过。原因是 TTS 生成的每句音频或多或少都有细小的误差,累积下来就漂移了。
如果不处理这个漂移,音频和视频就会越到后面越不同步,最后几分钟嘴型对不上,字幕也飘走了。
解决方法写在 step5_compose 里:在合成视频之前,先量一下配音音频的总时长和视频时长,算出漂移比例,然后用 atempo 对整段配音做全局等比缩放,同时把字幕时间轴也按同样的比例压缩。这样音频、字幕、视频三者就完全对齐了。
scale_ratio = dubbed_dur_ms / video_dur_ms
# 配音比视频长超过 0.5% 时做全局压缩
if scale_ratio > 1.005:
# atempo 缩放音频 + 字幕时间轴同步压缩
_scale_srt_timestamps(srt_zh_path, scale_ratio)做完这个之后,音画同步的问题基本消失了。
调参这件事
整个 pipeline 里有很多可以调的参数,控制字幕密度、TTS 加速阈值、批次大小之类的。我把常见的几组配置打包成了三个预设:natural、balanced、tight。
natural 是尽量不提速,口播更接近正常说话节奏,但有些句子可能会溢出时间窗;tight 是严格贴合时间轴,音频压得比较紧,听感稍微急一点;balanced 就是中间值,我日常用这个。
说实话这些参数我自己也在不断调,没有什么最优解,只是在不同视频上试出来的经验值。
最后
整个项目写下来,最大的感受是:这种"把几个工具串起来"的项目,表面上看很简单,实际上细节非常多。每一步都有它自己的坑,字幕有字幕的坑,TTS 有 TTS 的坑,合成还有合成的坑。把坑一个个填完,才能出一个勉强满意的效果。
但好在 AI 工具现在已经很成熟了,翻译质量、TTS 音色,放到两年前都是要花大价钱或者效果很差的事情,现在随便接个 API 就能解决,而且便宜得离谱。
两毛钱一个视频,我觉得挺值的。