B视频 是做什么的?
更多触发方式
PR的演练视频、制作演示录制、带旁白的演示、录制两个版本、讲解视频、无drawtext滤镜、Playwright的recordVideo、缺少无头shell
每个声明集对应一个MP4文件:标题卡片、A片段、B片段、证据卡片、裁决卡片,每段均有配音。
专为搭载Apple Silicon的macOS构建(say命令,Playwright缓存位于~/Library/Caches/ms-playwright,chrome-mac-arm64)。在其他平台,请设置CHROMIUM_EXE环境变量并使用--eleven参数指定语音。需要Bun、ffmpeg和Python 3。
执行步骤
S=${SKILL_DIR:-~/.claude/skills/narrated-ab-video}
$S/preflight.sh /tmp/vid # 检查 chromium、playwright、ffmpeg、say 语音
bun $S/record.ts a.json raw/a-run # 每侧一个场景(复制 examples/scenario.json)
bun $S/record.ts b.json raw/b-run
bun $S/cards.ts cards.json cards # 生成标题/证据/裁决 PNG 以及 banner-A/B.png
python3 $S/build.py plan.json # [--voice Samantha] [--rate 182]
open -a "QuickTime Player" demo.mp4 # 以暂停模式打开;切勿自行播放
示例文件位于 examples/ 目录:scenario.json(步骤包括:wait、select、click、clickIfPresent、type、send、waitStable、expand、fetchText、waitText、probeEnabled、reload;详见 record.ts 头部注释),cards.json,plan.json。计划文件中的路径相对于计划文件本身。已通过两个真实片段构成的4段、40秒构建进行了端到端验证。
如此构建的原因(痛点挖掘)
- Playwright 固定的无头 shell 经常未下载。
record.ts和cards.ts启动最新缓存中的完整chromium-*构建(或$CHROMIUM_EXE)而非报错。 - recordVideo 需要 Playwright 自带的 ffmpeg 构建版本。 缺少则无法生成 webm。preflight 从
browsers.json读取修订号并运行bunx playwright install ffmpeg。 - Homebrew 安装的 ffmpeg 可能缺少
drawtext滤镜。 角标由cards.ts生成为透明 PNG,在 build.py 中叠加。无需寻找字体或滤镜。 - 模型回复需要10到60秒的静默时间。
send标记“已发送”,waitStable标记“已回复”。build.py 以speed(默认10倍速)播放该段时间,其余部分以1倍速播放。 - 旁白通常比视频片段长。 build.py 保持最后一帧(
tpad stop_mode=clone)直到语音结束,因此回复画面在解说期间保持可见。 - 动态输入(OTP码、先前轮次的ID): 在
type步骤中使用{{fn:NAME}},并从场景旁的hooks.ts中导出NAME(page, state)。state.lastJson保存匹配captureJsonFrom的最后一个POST JSON。 - 选择器: 按钮通过角色和名称正则表达式匹配(例如
"Tools used"、"^Send$")。CSS 仅用于输入框和下拉框。在编写脚本前可先截图快速确认名称。 - 拼接需要相同的编码参数。 每个片段均使用相同的编解码器、帧率和音频布局重新编码,然后通过
-c copy合并。 - 更好的语音:
ELEVENLABS_API_KEY=... build.py plan.json --eleven <voice_id>(模型 eleven_multilingual_v2;片段按语音+文本哈希缓存,因此重复运行不消耗字符)。请将密钥保存在权限为 0600 的文件中,切勿写入计划文件,使用后删除。 - 单侧演示(无A/B对比): 录制一个场景,为
cards.json提供单个角标,并将同一片段切割为多个计划片段,每个片段对应一个声明,使用spans加速等待时间(例如5倍速)。构建方式相同,无 A 侧。 - 用户可能仅粘贴
<voice_id> <key>。 这意味着使用--eleven <voice_id>重新构建。较慢的语音会延长视频时长(72秒的say版本变为92秒),因为 build.py 会保持最后一帧直到语音结束。 - 拍摄前重新搭建已拆除的演示环境: 短期有效的认证(本地JWT、隧道URL、临时云资源)通常次日即失效。在录制前先通过一次API运行重新验证;如果出现“JWT token expired”的401错误,则拍摄的轮次将永远收不到结果。
- 丢失原始片段: 从之前的输出MP4中裁剪该侧的录像(通过角标颜色采样找到其span),保存为
<dir>/a.webm(使用-f mp4和空的 marks.json),并使用一个明确的 span。由于角标已嵌入,请忽略banner。 - 音频:
say -o仅写入文件。未经用户明确同意,切勿播放视频或任何音频。以暂停模式打开并交给用户。 - 先证明再拍摄。 先运行A/B对比并断言结果(通过API或日志)。视频展示的是已证明的结果,并非测试本身。
- 卡片尺寸必须与片段尺寸一致。 build.py 不会进行缩放。若录制时使用
"viewport": [1440, 900],则cards.json中的"size"必须为[1440, 900],否则拼接时会出现帧尺寸不一致。 - 异步UI需要标记,而非固定等待。
waitText在正则表达式匹配时标记(或通过timeout:<mark>标记),probeEnabled配合waitMs轮询按钮并将probes[]记录在 marks.json 中,tryMs在 type/send/click 上标记skipped:<step>而非中止,因此“保持禁用”的一侧仍能完成片段。 - 探测与你点击相同的元素。 CSS 探测(
button[aria-label=...])命中的按钮可能与角色名称send不同,导致读取禁用状态时发送已通过;请在probeEnabled中使用"role"。 - 开发者模式覆盖层(查询开发者工具、错误提示)会出现在片段中。 在首次等待后添加
{"clickIfPresent": "^Close .* devtools$"}。 - 等待一个从未“稳定”的回复: 实时运行时长标记会每秒改变正文文本。在构建前检查
marks.json中是否有replied标记。 - 确认证据在可见文本中。 折叠的工具卡片可能不会展开(
expand无声地点击无效内容)。读取marks.json中的body以查找声明字符串。如果缺失,请更改提示或改在卡片上证明。 - 在拍摄“基础版本失败”的声明前,先证明两者等价。 曾经有一次本地基础版本因某个标记(本地开启、远程关闭)而失败,而实际环境从未遇到该问题,但旁白却声称失败。请先匹配远程的标记,然后再拍摄。
- 标题卡片:每侧测试的提交和标记。每个片段对应一个声明。以裁决卡片结束。
相关技能
narrated-screenshot:生成带注释的静态图片而非视频。pr-ab-evidence:该技能拍摄的红色/绿色 A/B 对比方法。