Communitygithub.com

字幕烧录工具

使用真实的 ASS 字幕文件和 ffmpeg 的 libass 渲染器,将样式化、可读的字幕烧录到视频中。当用户需要添加或烧录字幕、要求同步语音的文字叠加、或希望更改现有字幕视频的字体/大小/位置/换行时使用此技能。

字幕烧录工具 是什么?

该技能提供了一套完整的字幕烧录工作流程,从检查 ffmpeg 支持 libass 过滤器开始,到编写 ASS 文件、处理换行、验证边距、渲染测试帧,最后完成最终视频输出。它核心解决了实际项目中反复出现的五个痛点:系统默认 ffmpeg 缺少字幕过滤器、换行需按自然停顿而非字符数、边距需考虑平台二次裁剪、垂直位置需逐项目确认、以及字幕移动问题——通过将锚点改为顶部(Alignment 8, MarginV 1215)并让事件间留 0.12 秒空白来避免视觉闪烁。技能支持 Cursor 等具备终端执行的 AI 编码环境(需要 /opt/homebrew/opt/ffmpeg-full/bin/ffmpeg),使用自定义 ASS 模板(Inter Bold, 70px, 白色文本, 黑色背景框, 下部居中, 侧边距 120px),换行规则严格遵循口语停顿,标点约定为仅保留句末问号/感叹号而删除句号/逗号。测试帧循环(从关键帧提取单帧)可快速验证溢出和定位,避免全量渲染浪费。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Istok-Media-Tool/video-edit-skills/tree/HEAD/video-edit/skills/subtitle

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

字幕烧录

为什么需要这个技能

让字幕显示正确需要在真实项目上经过多次修正,每一次都值得记录,避免重蹈覆辙:

  1. 系统自带的 Homebrew ffmpeg 完全没有 drawtext/subtitles 过滤器(未编译 libass/freetype)——烧录真正的样式字幕需要不同的 ffmpeg 构建,而不是临时解决方法。
  2. 按字符数平衡字幕行会导致无意义的换行——换行必须落在人自然说话停顿的位置,绝不能断在短语中间。
  3. 靠近左右边缘的字幕行在 Instagram 上传后的自有裁剪下会被截断——需要真正的边距,而不仅仅是“适合画面”。
  4. 字体大小和垂直位置因项目而异,值得确认——但在全量渲染前务必通过单帧检查进行可视化验证:几秒钟就能捕捉溢出/裁剪问题,而全量重新渲染则不会。
  5. 字幕在各画面之间不得移动。第一个版本将锚点设在底部(Alignment 2, MarginV 500),导致 3 行字幕比 2 行或 1 行的起始位置更高,并且因 libass 的碰撞避免机制,与前一字幕重叠了几百分之一秒的字幕会被持续向上推——用户看到字幕“上下跳动”。固定规则:锚点设在顶部(Alignment 8, MarginV = 第一行的 y 坐标;在 1080×1920 上设为 1215 可将 3 行字幕的最低行保持在原底部锚点的位置),这样每个字幕的第一行都固定在相同位置,额外行向下延伸,并且绝不允许多个 Dialogue 事件在时间上重叠。此外,在连续字幕之间留出短暂的空白:每个事件在下一个事件开始前 0.12 秒(25fps 下 3 帧)结束。这是用户的明确要求——短暂的空白告诉大脑字幕发生了变化;连续的字幕会被视为同一文本闪烁。应缩短前一个字幕的 End,绝不能延迟下一个 Start(后者与语音同步)。
  6. 字幕文本不必是逐字转写——可以修正语法/措辞问题(听错的词、别扭的句子结构),并根据用户要求增删词语,就像编辑任何人都能阅读的文本一样。

第一步:确保 ffmpeg 能够烧录字幕

ffmpeg -hide_banner -filters 2>&1 | grep -E "subtitles|^\s*\S*\s+ass\b"

如果没有匹配项,则常规 ffmpeg 无法完成此操作。安装完整构建:

brew install ffmpeg-full

ffmpeg-full 是 keg-only(不符号链接到 PATH)——在字幕烧录步骤中显式使用它:/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg。

实际遇到的陷阱: 安装 ffmpeg-full 拉入了更新的共享 x265,破坏了 PATH 上的常规 ffmpeg/ffprobe(dyld 报错 "Library not loaded: libx265.*.dylib"),因为后者链接的是旧版本。安装 ffmpeg-full 后立即运行:

ffmpeg -version && ffprobe -version

如果任一命令出错,使用 brew reinstall ffmpeg(常规公式,不是 -full)修复,并在继续之前重新验证两个命令正常工作——不要假设常规 ffmpeg 在安装后未受影响。

第二步:获取精确的字幕文本和时间

如果视频来自基础技能的视频剪辑工作流,你已经知道每个片段的确切保留文本及其原始时间戳——但一旦片段被剪切并拼接,时间会重置为新视频自己的时间线。通过按顺序累加实际渲染的片段时长来计算每个片段在最终视频中的位置:

ffprobe -v error -show_entries format=duration -of csv=p=0 clip_01.mp4

……并累加。在每个片段的窗口内,略微向内偏移以匹配语音实际开始/停止的位置(如果片段包含来自剪切步骤的前/后填充,则减去它们),这样字幕不会在无声处出现/消失。

第三步:编写 ASS 文件

使用此起始模板——这是在真实项目上确认的样式(Inter Bold, 白色文本, 不透明黑色框, 下部居中, 宽侧边距):

[Script Info]
Title: <视频名称> - 字幕
ScriptType: v4.00+
WrapStyle: 2
ScaledBorderAndShadow: yes
PlayResX: 1080
PlayResY: 1920

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Inter,70,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,3,16,0,8,120,120,1215,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.15,0:00:05.93,Default,,0,0,0,,Zamisli univerzum koji postoji\Ntoliko dugo da su sve zvezde\Nodavno ugašene

在烧录之前,确保每个事件在下一个事件开始前 0.12 秒结束(将前一个事件的 End 设置为下一个 Start - 0.12;绝不能移动 Start)——参见教训 5。

调整 PlayResX/PlayResY 以匹配实际视频分辨率(本示例为 1080x1920 竖屏)。WrapStyle: 2 意味着 libass 不会自动换行文本——每个换行必须是 Text 字段中的显式 \N。这是故意的:它迫使你仔细选择每个断点(参见步骤 4),而不是让渲染器猜测,并且当行溢出时它会表现为裁剪的边而不是无声地多出一行。

字体/大小/颜色/位置都是起始默认值,而不是固定规则——如果是新项目,请与用户确认,或者如果已知,则复用用户上次确认的值。

在真实项目中确认的标点惯例

字幕屏幕最后一行末尾的句点 AND 逗号被删除(字幕不需要像散文那样有句号——在这个大小下它看起来是视觉杂乱)。即使句子延续到下一个字幕屏幕,此规则也适用:屏幕切换本身就表示停顿,因此 "svoje znanje kroz praksu," 显示为 "svoje znanje kroz praksu"。用户明确要求这样做(2026-11-05,“与句点相同”)。问号和感叹号保留。屏幕内部的逗号(行中或非最后一行的行末)保留。默认对每个视频应用,包括双语字幕中的第二语言行,以及用户在计划编辑器中编辑的字幕文本。

第四步:每行都在人实际说话停顿处断开

这是最值得做对的部分,也最容易偷懒出错。朗读句子,就像在口头表达一样,并在自然的呼吸/短语边界处断开——从句边界、完整思路之后、表示真实口语停顿的逗号处。如果自然的断点在其他位置,绝不要因为某行“需要”更短而断开——而是寻找更短的自然断点(较长的句子通常有不止一个有效的停顿点),不要为了达到宽度目标而强迫使用不自然的断点。

来自真实修正的具体例子——错误(按字符数平衡,断在短语中间):

Zamisli univerzum koji postoji toliko
dugo da su sve zvezde odavno ugasene

正确(在自然口语停顿处断开):

Zamisli univerzum koji postoji
toliko dugo da su sve zvezde
odavno ugašene

1、2 或 3 行都可以——没有固定的最大行数。重要的是每个断点都是说话者实际停顿的位置,并且每行都适合边距(下一步)。

第五步:根据实际的侧边距(而非原始画面宽度)验证宽度

样式中的 MarginL/MarginR(在 1080 宽的画面中各 120px,大约每侧 11% )存在是因为视频目标平台(Instagram 等)在上传后会进行自己的轻微裁剪,太靠近原始边缘的文本可能会被裁剪,而不仅仅是被画面边界裁剪。在判断某行是否太长时,将 PlayResX - MarginL - MarginR 视为真正的可用宽度,而不是整个画面宽度。

由于 WrapStyle: 2 禁用了自动换行,实际过宽的行不会自动换行——它会溢出边距,表现为裁剪/截断的文本。在步骤 6 中捕获此问题,避免它进入全量渲染。

第六步:在烧录完整视频前渲染测试帧

在每个最长/风险最高的 2-3 个字幕中选择一个时间戳,只渲染该帧:

/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -nostdin -y -i video.mp4 -ss <t> -update 1 \
  -vf "subtitles=captions.ass" -frames:v 1 check.png -loglevel error

检查每个测试帧是否有:文本超出边距、比预期更多的行(表明某行比预期宽,需要上游重新措辞/换行)、以及垂直位置是否真正读作“中下部”而不是太靠近底部边缘或高到遮住面部。修复并重新检查,然后继续——这个循环很快(每帧几秒钟),能捕捉到全量渲染可能需要几分钟才能复现的错误。

第七步:烧录最终视频

/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -nostdin -y -i video.mp4 \
  -vf "subtitles=captions.ass" -c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p \
  -c:a copy output_titlovi.mp4 -loglevel error

音频保持不变(-c:a copy),因为字幕仅涉及视频。如果输出需要通过具有文件大小限制的工具,则制作一个次要的压缩副本,而不是压缩主文件——与基础和缩放技能相同的模式:

ffmpeg -nostdin -y -i output_titlovi.mp4 -c:v libx264 -preset slow -crf 24 -pix_fmt yuv420p -c:a aac -b:a 128k output_titlovi_share.mp4 -loglevel error

根据反馈迭代

字幕工作往往需要多轮小范围的具体修正(字体大小、位置、错误的词、标点)。每轮只应修改变化的部分——直接编辑 .ass 文件(纯文本),在受影响的部分重新运行步骤 6 的快速帧检查,然后用步骤 7 重新烧录。除非底层视频本身发生变化,否则无需重复步骤 1-2。

通过交互式工具修正错误词

用户更倾向于在同一个计划编辑器工具(plan-editor 技能——用户的标准编辑器)中自行修正字幕文本:打开“编辑文本”开关,每个句子都可以就地编辑。只要存在字幕,就用 --ass captions.ass --durations ... 显示该工具(参见 plan-editor 技能),而不是要求用文字描述修正——即使仅需检查字幕且没有缩放计划(此时每个片段都从宽景开始,缩放计划保持不变)。

回复中包含 CAPTIONS_JSON: {"<对话索引>": "第1行\n第2行"},仅含已更改的屏幕。应用它:

python3 scripts/apply_caption_edits.py captions.ass caption_edits.json captions_fixed.ass

它只更改文本(时间和样式保持不变),将每行转换为显式的 \N 换行。然后对编辑后的屏幕重新运行步骤 6 的帧检查(更长的词可能溢出边距),并用步骤 7 重新烧录。

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

相关技能