视频中文字幕 — 翻译并嵌入
获取磁盘上已有的英文视频,生成其简体中文硬编码版本及对应的SRT字幕文件。
下载功能刻意不在范围内。假设源MP4文件已就位。
处理流程与视频主题无关——该技能基于一段22分钟的《Apex英雄》身法视频构建,但脚本既不知道也不关心视频内容。不同视频之间变化的是术语表和字幕条位置。
优质输出的标准
使用者关心的是特定且已确定的样式。以下不是需要每次重新考虑的默认值——它们是通过对实际输出进行迭代得出的,擅自更改将被视为回归错误:
- 仅简体中文。 绝不混用繁体中文。交付前需核实。
- 每行始终只显示一条字幕。 双行字幕是最常见的投诉之一。如果一行过长,请将其拆分为两个连续的字幕片段,而非换行。
build_srt.py会强制执行此规则。 - 字幕应简短。 “一行”并非允许过宽的行。目标渲染宽度不超过约1100像素(约为1080p画面宽度的57%)。
- 无背景框。 白色文字,硬黑色描边,柔黑色阴影。
- 思源黑体加粗(随附于
assets/NotoSansSC-Bold.ttf)。直立且粗重,拥有强壮的拉丁字形——这类视频中文句子中常夹杂英文术语,显示字体难以应对。 - 社区实际使用英文的术语保持英文。 哪些术语适用因主题而异,这正是术语表要解决的问题。
- 内嵌章节卡片下方添加中文行,而非上方。
工作流程
1. 探针源文件并选择术语表
ffprobe -v error -show_entries format=duration -show_entries stream=codec_type,codec_name,width,height,r_frame_rate -of default=nw=1 source.mp4
准确记录时长和帧率——后续所有步骤都依赖它们。
然后确定视频内容并阅读 references/glossaries/README.md,它会导向正确的文件并说明应阅读其哪些章节。翻译前完整阅读匹配术语表的 字幕使用原则 部分——该部分简短,包含了该游戏的约定。
已收录:Apex Legends、Warframe、osu!、Minecraft 速通/MCSR、StarCraft 星际争霸、Counter-Strike 反恐精英、ARC Raiders。
如果此视频无对应术语表,这属于正常情况。 根据可验证的官方简体中文名称进行翻译,保留您必须决定的术语列表,并在最后提交该列表。如果某术语没有官方中文名称,请使用描述性翻译并说明其非官方——切勿将猜测当作定论。切勿为已有正式名称的事物自创名称。
2. 获取带时间戳的英语转录文本
如果视频来自YouTube,其自动字幕可作为可用基础:yt-dlp --write-auto-subs --sub-langs "en.*" --skip-download。它们是带有单词级时间标签的滚动字幕,因此需去重为句子片段。
自动字幕会严重扭曲术语,您必须在翻译时进行修正。 ASR针对通用英语模型进行音标转录,因此每个非通用词汇的领域术语都会变成看似合理但错误的词语。来自Apex身法视频的实际例子:"forward rez/raft/raz" → RAS,"Neor strafe" → Neo strafe,"pedo strafe" → Pito strafe,"mantel boost" → mantle boost,"alerts" → lurches,"my VO was so high" → my velo (velocity),"I couldn't attach out of that" → couldn't tap-strafe out of that。在任何术语密集的视频中都应预期类似程度的损伤——术语表正是让您能识别它们的关键。
当语音确实无法恢复时——如音乐爆鸣、重叠喊叫——为该片段留空 zh 以将其丢弃,并告知用户您丢弃了哪些时间戳。编造合理的文字比没有文字更糟糕。
3. 翻译成segments JSON
[{"i": 1, "start": 0.24, "end": 4.90,
"en": "Seeing movement players pull off tricks like these is super cool and all, but",
"zh": "看身法玩家秀出这种操作确实很酷,但真正的问题是"}]
保留 en——它并非装饰。build_srt.py 利用它来判断句子延续还是独立语句,从而决定是否合并两个字幕片段,这能防止出现像 算了好 或 是,长官你好啊 这样的粘连问题。
4. 构建SRT
python scripts/build_srt.py segments.json sub.srt --font assets/NotoSansSC-Bold.ttf
它会输出一份报告。将非零的 overlaps、multiline、latin_cjk_runons 或 term_split_across_cues 视为编码前必须修复的错误。少数 under_0.35s 的提示在它们为1-3个字符的插话时是可接受的——请检查它们,而非直接假设。
5. 查找并翻译内嵌章节卡片
python scripts/find_chapters.py source.mp4
创作者通常将章节卡片直接嵌入视频,而非使用YouTube章节功能,因此元数据中不显示。此脚本报告候选窗口;从每个窗口中提取一帧,阅读实际卡片文字后再编写翻译。输出 chapters.json:[{"start":42,"end":46,"zh":"第 1 章:定义 lurch 组合"}]。
6. 检查字幕与屏幕上文字可能碰撞的位置
Outro卡片通常是罪魁祸首——正常位置的字幕会落在创作者的社交媒体信息上。提取一帧,找到间隙并声明:
[{"from": 1334.0, "to": 100000, "bottom": 820}]
默认字幕条位于典型FPS HUD之上。不同游戏或非游戏内容可能将其自身元素放在此处——MOBA小地图、节奏游戏判定线、访谈的下三分之一区域。在接受默认位置前,请提取一帧并查看。
7. 构建覆盖层并编码
python scripts/build_overlay.py sub.srt ov --font assets/NotoSansSC-Bold.ttf \
--duration 1354.351 --chapters chapters.json --avoid avoid.json
python scripts/burn_in.py source.mp4 ov/concat.txt out.mp4
检查覆盖层报告的漂移应为几毫秒,而非数百毫秒。
在提交约20分钟的编码前,通过PIL将一张覆盖层PNG合成到真实帧上并查看。这只需几秒钟,却能捕获位置和字体错误,否则这些错误将在20分钟后才显现。
8. 验证
python scripts/verify.py out.mp4 --expect-frames <frames> \
--contact-sheet sheet.jpg --at 75 300 660 945 1340
查看联系表。确认:单行字幕、在明亮帧上清晰可读、章节行在其卡片下方、无碰撞HUD或outro文字。
9. 后期修复——拼接,而非重新编码
当用户要求更改几行时(这会发生),重建SRT,对比前一个SRT找出哪些提示发生了变化,然后:
python scripts/splice.py out.mp4 source.mp4 sub.srt out_fixed.mp4 \
--font assets/NotoSansSC-Bold.ttf --change-from 310 --change-to 316 \
--duration 1354.351 --chapters chapters.json --avoid avoid.json
python scripts/verify.py out_fixed.mp4 --expect-frames <frames> --seams 310 316
只需15秒而非20分钟。帧数必须完全一致。
交付物
随 out.mp4 一起交付SRT(对话行与章节行合并并重新编号),以便日后无需重做工作即可编辑字幕。提供中文标题和描述——如果视频是转载,请指出原视频的第一人称描述隐含了作者身份,且音乐归属区块应保留其原始英文,因为许可条款要求使用该确切形式。
如果您必须解决未出现在术语表中的术语,请列出它们。一次决定并记录下来的术语,价值远高于下次做出不同的相同决定;references/glossaries/README.md 说明了如何将这样的列表转化为术语表文件。
参考资料
references/glossaries/README.md— 应阅读哪个术语表、其哪些章节,以及在视频没有术语表时该如何处理。从此处开始。references/glossaries/*.md— 术语表本身。内容较多。阅读原则部分及相关章节;通过grep搜索单个术语。references/pipeline.md— 技术说明及本流程中的特定陷阱。在调试任何看似时序、拼接或渲染问题之前,请先阅读;大多数问题已被诊断过一次。