Reel 字幕 + 叠层效果
为一个已经剪辑好的 9:16 Reel(通常为 2160x3840,30fps,例如 talking-head-edit 的输出)添加字幕和叠层效果。一切由 edit.json(逐帧的字幕页和叠层)驱动,两个渲染器都读取该文件,确保 HyperFrames 和 Remotion 版本逐帧匹配。
SK=~/.claude/skills/reel-captions-overlays
PY=$($SK/scripts/find_python.sh) # faster-whisper large-v3 + opencv ($REELKIT_PYTHON, 仓库 .venv, 或 python3)
AUDIO_PY=~/.cache/reel-audio/venv/bin/python # 音乐阶段 (scripts/setup_audio.sh 创建)
ANA_PY=~/.cache/reel-audio/analysis-venv/bin/python # 参考风格分析 (scripts/setup_audio.sh --analysis)
默认工具:HyperFrames (Apache-2.0, 固定版本 [email protected])。Remotion (4.0.539) 作为同位构建保留。Remotion 对个人和三人以下公司免费;较大公司需要购买商业使用许可 (remotion.pro/license)。HyperFrames 没有此类许可条件。
工作流程
- 设置 (首次运行)。
$SK/scripts/new_reel.sh <reel.mp4> <workdir>该脚本使用 large-v3 模型进行带时间戳的词级转录,每 0.5 秒检测人脸框 (生成faces_src.json) 和镜头剪切点 (生成cuts.txt),将overlays.example.json复制到<workdir>/overlays.json,然后停止。 - 填写
overlays.json。首先阅读words_raw.json。corrections:通过令牌序列修正品牌名称,例如["Ackme"]→"Acme",["re","-motion,"]→"Remotion,",["Acme","Voice."]→"Acme Voice."。将品牌词对粘合为一个令牌可确保它们在同一字幕页上。 保持外语原文不变(例如 "Même voix, autre langue.")。sansWindows:B-roll 切换镜头、覆盖和 promptDemo 跨度的[开始,结束]秒数。这些区域的字幕切换到粗无衬线字体(相同的 75% 高度车道)。overlays:参见下面的样式规范和图形计划。时间以秒为单位,取自词级时间戳。sponsored: true:用于付费或品牌 Reel。它启用演示节拍规则(见下方的赞助 Reel)和验证警告。
- 构建两个项目。
$SK/scripts/new_reel.sh <reel.mp4> <workdir> <workdir>/overlays.json - 渲染前预览。
cd <workdir>/hyperframes && npx [email protected] snapshot --at 2.5,20,30 --no-end(或在remotion/目录中执行npx remotion still Reel x.png --frame=N --scale=0.5)。 - 渲染。这些需要 Chrome,因此在 Bash 沙箱外运行 (
dangerouslyDisableSandbox)。在沙箱内,Chrome 在 Mach 端口查找时会失败。<workdir>/hyperframes/render.sh <out>_captions_hyperframes.mp4渲染一个 1080x1920 布局,超采样至 2160x3840,CRF 16,带有 faststart。<workdir>/remotion/render.sh <out>_captions_remotion.mp4以--scale=2、CRF 16、AAC 192k 和 faststart 执行相同操作,然后运行scripts/repair_remotion.py(参见注意事项)。
- 验证。
$PY -I $SK/scripts/verify.py <reel.mp4> <out.mp4> <workdir>/edit.json <workdir>/faces_src.json <workdir>/qc <label>(如果帧数、损坏帧、可读性、卡片边缘或 CTA 优惠不通过,则退出代码为 1 并打印 FAIL;在 60 秒 4K Reel 上约需 9 分钟)- 检查帧计数和时长是否与源匹配。
- 扫描每一帧是否有损坏(错误视频帧或黑色方块)。
corrupt_frames必须为 0。 - 抽查 5 个字幕页:页面开始前一帧的色带必须为空,页面开始后两帧的色带必须填满。
- 测量每个叠层下的人脸框像素变化。这应该接近 0,除了全屏覆盖和 promptDemo 卡片(它们故意遮挡脸部,因此被排除在人脸、损坏帧和可读性检查之外)。
- 列出
demo_beats(promptDemo 叠层,或任何带"demo": true的叠层)。当edit.json中有sponsored: true且演示节拍少于 2 个时,打印 WARN 并将其添加到warnings中(非硬性失败;修复 Reel 或在报告中说明原因)。 - 之前/之后分隔符 (v7, 警告)。对于每个 promptDemo 的前后行对,报告
before_after:两个剪辑共有的词、剪辑间隙、渲染中的静音检测,以及剪辑是否包含切换色带和切换音效 (build_edit ba)。如果一对行重复了词但缺少所有元素(间隙 >= 400 毫秒、色带、音效),则打印 WARN:重复的行没有分隔符会被视为剪辑故障(参见 promptDemo)。 - CTA 优惠 (v7, 硬性检查)。带有
offer的cta必须完全可见 >= 2.5 秒,大小 >= 40 像素(在 360 像素宽手机上为 13+ 像素),并且实际绘制(其色带内的像素变化)。<label>_offer_mobile.jpg以手机尺寸显示该优惠;请查看。如果赞助 Reel 的 CTA 没有offer,则生成 WARN。 - 手机尺寸可读性:将 8 个字幕页缩小至 360 像素宽,测量笔划粗细、文本高度和轮廓光晕(字形亮度减去其周围环)。最低要求:笔划 >= 4.0 像素,高度 >= 28 像素,光晕 >= 130。
legibility_ok必须为 true。<label>_mobile.jpg显示裁剪区域;请查看。 - 生成一个接触表,其中包含每个叠层中间的一帧静止图像,以及一个同步裁剪表。在报告前打开这两个文件。
- 音乐 + 音效 (默认阶段;使用
--no-music或MUSIC=0跳过)。填写<workdir>/music_plan.json(首次运行时从music_plan.example.json复制而来;使用默认风格预设steady-funk-bed;参见下方的音乐和声音设计),然后执行$SK/scripts/music_stage.sh <out_hyperframes.mp4> <workdir> <out>_music.mp4此脚本将背景音乐和音效混合到语音下方,使用视频流复制(不重新编码)和音频裁剪至视频时长进行复用,生成<out>_music_mobile.mp4手机副本 (MOBILE_OUT=可将其放置于其他位置),并运行music_verify.py,包括对照计划预设的风格检查。必须输出 PASS。它拒绝覆盖已存在的输出(使用FORCE=1允许覆盖)。如果任何句子的语音低于其下方音乐超过 6 dB,则也会失败(在报告中为voice_over_music,--min-voice-over-music):重要的是每一行都能被清晰听到。背景音乐是一个稳定的、约为 -13 dB(相对于语音)的未压缩律动,因此即使段落平均值通过,一句安静的话也可能沉入其中。在上游使用talking-head-edit的语音电平调整或分段增益来修复问题,而不是降低整个背景音乐。一个典型的通过检查的访谈 Reel 测得最小 11.2 dB,中位数 14.1 dB。打开<workdir>/qc/music_sections.png。首次运行会创建~/.cache/reel-audio/venv(scripts/setup_audio.sh)。 - 手机副本(用于发送,当音乐阶段被跳过时):
ffmpeg -i <out.mp4> -vf scale=720:1280:flags=lanczos -c:v libx264 -preset slow -crf 22 -maxrate 2800k -bufsize 5600k -pix_fmt yuv420p -c:a copy -movflags +faststart <out>_mobile.mp4(对于 60 秒视频约 14-19 MB,低于 25 MB 限制)。复制已处理的音频,切勿重新编码:二次 128k AAC 编码曾将一个 Reel 的真实峰值从 -1.3 提升至 -0.7 dBTP。
样式规范 (来自参考 Reel)
以下所有尺寸均基于 1080x1920 设计空间(以 2 倍渲染)。共享 CSS 位于 templates/style.css。
- 字幕 (v2:v1 字幕为细白线,在手机上难以阅读。更粗、更大、带轮廓是默认设置。)
- 每页一个词,偶尔两个:短功能词与其邻居合并,品牌词对被粘合。
- 文本大小写和标点按实际发音书写 ("content.", "Then")。切勿使用全大写。无逐词高亮:每页 1-2 个词时只会增加噪音。
- 字体:DM Serif Display(随附于版本
@fontsource/[email protected],OFL 许可),136px,白色。切勿再使用如 Instrument Serif Regular 之类的细衬线字体作为字幕字体。Instrument Serif 仅保留用于纸色或红色卡片标题。 - 轮廓: 一个带描边的词副本(使用
::before和data-t,-webkit-text-stroke: 6px,因此字形外 3px),颜色为 86% 的近黑色,绘制在填充文字后方,以确保不削弱字母。阴影: 0 0 3px,0 5px 12px 和 0 14px 34px,均为深色。 - 适配:
build_edit.py根据templates/fonts/caption_metrics.json为每页设置fs字体大小,确保页面宽度不超过 820px(例如 "automatically." 使用 127px)。如果字体更改,请使用scripts/caption_metrics.py重新生成度量数据。 - 位置: 一条居中车道,位于高度的 75% 处 (y=1440),低于说话者的手部和麦克风,高于底部 20% 的 UI。仅在特写镜头时降至下巴 + 6%(上限为 76%)。在典型的桌面设置中,手部位于 55-72%,因此“下巴下方”车道 (60%) 会与手部重叠。
- 动画:一个 4 帧弹入效果 (缩放 0.86→1,y 偏移 14→0,淡入),提前一帧开始,确保词在其第一帧就可见。
- 页面精确地在词的起始帧开始,并保持到下一页,或者在停顿时保持到词结束 + 0.2 秒。
- B-roll 和覆盖字幕: Inter 900,120px,字距微调 -0.03em,相同的轮廓和 75% 车道。
- 标题卡片 (
titleCard,用于钩子短语)- 砖红色
#B23A2E盒子,810px 宽 (75%),顶部位于 y=190。 - 顶行:Anton 全大写,118px。其下方:白色 Instrument Serif 副标题,62px。
- 其底部大约位于画面高度的 23%。此位置故意比参考值高 (顶部边缘 22-28%),因为脸部大约从 26% 开始。
- 砖红色
- CTA (
cta)- 白色斜体衬线 "Comment" (82px) 位于一个红色盒子上方,盒子内为 Anton 关键字 (196px,例如 "GUIDE")。与标题卡片使用相同的上方色带区域。
- 盒子在踢脚线后 3 帧弹入。
- 优惠行 (
offer, 赞助 Reel, v7):品牌优惠位于下巴下方的一张纸色卡片上,例如 "Use my link for a\nFREE 14-day trial."。使用品牌自己的措辞。\n设置换行,全大写词 ("FREE") 获得白底红字框。"Comment / WORD" 仍是主要元素;优惠行是辅助行。build_edit.py推导出offerF(来自offerAt,默认 CTA 开始后 10 帧),offerY(下巴 + 3%,来自 CTA 窗口上的--faces,保持在画面高度的 55-60%,约在 1385px 处,高于字幕墨水) 和offerFs(Inter 适配至 720px,最大 46px;低于 40px 时发出警告)。卡片宽 780px (x 150-930,避开右侧 UI) 并倾斜 -1.5°。 - 优惠必须完全在屏幕上 >= 2.5 秒。 如果说话者说完 CTA 后 Reel 结束得太快,则保持最后一帧:
$SK/scripts/hold_end.sh <reel.mp4> <frames> <reel_hold.mp4>对剪辑进行流复制,追加 N 个已编码的最后一帧副本并附带数字静音,检查语音是否解码后样本一致。选择 N 使得 Reel 正好在音乐节拍前结束 (节拍 k = k * 60 / bpm,cue_at为 0),将 CTA 的end和音乐计划部分的end设置为新长度,并使用新的--frames运行build_edit。背景音乐在保持期间继续播放,并在最后一帧处切断(例如:32 帧,在 68.2 秒结束,正好在 110 BPM 背景音乐的第 125 拍之前,位于 68.207 秒;优惠完全显示 3.2 秒)。
- 全屏覆盖 (
takeover,用于工具或产品提及,持续 1.5-3 秒)- 全屏深红色
#8E1F17,带微妙的 8x14 浅红色网格。 - 一个白色圆角搜索药丸(左侧为放大镜;右侧为麦克风和镜头),其粗斜体查询内容在 14 帧内逐字显示。
- 其下方,一个白色圆角卡片,包含工具名称(衬线 128px)、一行描述、红色波形和 2-3 个标签。
- 切勿伪造真实产品 UI。 使用仅包含文本的 neutral 卡片。语音在其下方继续。
- 全屏深红色
- 浮动卡片 (
floatCard,用于统计数据、提示或标签;可选kicker,width)- 纸色
#FBF7F1卡片,470px 宽,顶部位于 y=200,在左侧或右侧。旋转 ±3-4° 并带有深阴影。 - 内容可以是:可选踢脚线 + 大号 Anton 数字 + 衬线标签 + 小号大写行,或旗帜变体:
flag: "es"(踢脚线 + CSS 旗帜 + 词) 或flag: "us-es"(踢脚线 "Same voice, different language" + 美国国旗 → 西班牙国旗 + 词)。 - Emoji 旗帜使用 CSS 绘制,因此从不依赖 emoji 字体。
- 纸色
- 步骤列表 (
steps):一张纸色卡片位于顶部 (y 150),带红色踢脚线和编号行 (DM Serif 54px)。每个项目有自己的at时间,并在说话者说出时弹入;当前数字为红色,较早的行变暗。当说话者说“三个步骤”、“首先...然后”时使用。 - 新旧对比分割 (
split):两个倾斜的窗格并排位于顶部,深色 "Old way" (带删除线) 和红色 "New way"。新窗格在newAt时弹出。使用\n控制old/new中的换行。当说话者对比过去和现在的方式时使用。 - 芯片卡片 (
chipCard):一张纸色卡片,带踢脚线、作为轮廓文本芯片(无 Logo)的工具名称和一行小字。用于“我使用的工具”。 - 提示演示 (
promptDemo,产品演示节拍,3-7 秒;仅 HyperFrames,Remotion 跳过)。在赞助商要求听到(而不仅仅是听说)产品后添加。- 全屏纸色卡片,带有覆盖在演示音频上的微弱网格。从顶部开始:
- 红色标签药丸 (y 168),说明剪辑的真实制作方式,例如 "Acme Voice · v2 model";无 Logo,无 "MCP",除非 MCP 确实制作了它;
- DM Serif 标题,命名任务 (y 252,例如 "Fix one word", "Sad to excited, one take");
- 一张 "Prompt" 卡片 (y 400, 940px),其提示在
typeAt和typeEnd之间逐字显示,随后是小型模型脚本,其中[direction]标签为红色(脚本超过 150 字符后为 24px); - 每个剪辑对应一行波形,在提示卡片下方流动 (
.stack网格:短提示时为 y 770,长提示 + 脚本时会下推,使得脚本的最后一行永不被隐藏;v6 错误)。键入的提示覆盖在一个不可见的完整副本之上,因此卡片从第一帧起就具有最终高度,并且当提示换行时各行不会跳动(v6 捕获了第三行处的 53px 跳动)。
- 每行包含:
- 一个标签 (
kind:beforegre...
- 一个标签 (
- 全屏纸色卡片,带有覆盖在演示音频上的微弱网格。从顶部开始:
[内容截断,待翻译]