演示视频
根据正在运行的真实应用录制的脚本化导览。观看者可以看到产品在执行实际工作;慢速部分(模型回复、任务)会被快进处理,每个节拍都有一个简短的字幕,提示需要关注的内容。每次录制都使用相同的格式,以便各个片段可以合并为一个视频。
内部格式
保持以下细节精确无误,以便新片段能与旧片段匹配:
- 视频: 1440×900,30fps,H.264(
libx264 -preset slow -crf 18),yuv420p,+faststart,无音频。 - 标题卡和结束卡: 全白屏幕,居中显示:一个可选的小写灰色品牌行(在
kit.start中设置brand;默认为无),46px粗体标题,20px灰色副标题。保持约2.8秒,然后淡出。片段以标题卡开始;最后一个片段以一行结论卡结束(例如“想法 → 评估 → 在线测试 → 上线”/“无需应用部署”)。 - 字幕: 深色药丸(
rgba(17,17,17,.88)),白色17px半粗体Lato字体,底部居中。每个字幕表达一个想法,约60个字符或更少,句子首字母大写,无句号,使用现在时,从观看者的角度撰写(“结果在聊天旁作为构件打开”)。当节拍变化时改变字幕;每个字幕保持约2.5–4秒。 - 点击: 每次点击前,目标位置会出现一个紫色涟漪效果(
rgba(122,71,255,…))。 - 打字: 可见,逐字输入(每键约25ms;长文本为14ms)。
- 等待: 任何慢速操作都在
ff()内部运行,该函数将回放在约2秒内完成。其他所有内容均按真实时间播放。 - 架构场景: 一个固定的1440×900白色场景页面(标题 → 之前 → 问题突出 → 之后 → 指标计数 → 功能列表),Lato字体,墨水色
#111,紫色强调色#6f43d6;“之前”标签为灰色,“之后”标签为紫色。assets/architecture-example.html是参考:重用其CSS和结构,更改内容。
视频存放位置:演示仓库
每个视频在团队的演示仓库(默认~/src/demos,或$DEMOS_REPO)中都有一个文件夹,包含其录制脚本和一个demo.json清单。产品仓库中不包含演示代码。录制内容不被提交;清单中的output指定MP4文件的输出位置。
~/src/demos/
package.json playwright-core + ffmpeg-static(只需运行一次npm install)
<video>/
demo.json 清单
architecture.html “场景”片段(可选)
record-<part>.js “导览”片段,基于demo-kit编写
.build/ 每个片段的MP4和帧(已gitignored)
清单(demo.json):
{
"name": "<video-name>",
"title": "一行:视频展示的内容",
"description": "目标受众及其要说明的观点",
"owner": "email",
"repos": ["录制功能所在的仓库及分支"],
"needs": [{ "what": "service", "check": "服务启动时返回2xx/3xx的URL", "start": "启动方式" }],
"reset": "如何获得干净的录制,以及录制后遗留的内容",
"segments": [
{ "name": "architecture", "kind": "scenes", "page": "architecture.html", "timeline": "0:3000,1:4500" },
{ "name": "walkthrough", "kind": "walkthrough", "script": "record-walkthrough.js" },
{ "name": "intro", "kind": "video", "file": "~/path/to/existing.mp4" }
],
"output": "~/Downloads/<name>.mp4"
}
此技能的examples/sift/是一个参考文件夹:包含一个两片段视频(一个场景页面和一个真实的导览),以及一个在同一文件夹中的小型示例应用。
如果演示仓库尚未克隆,请创建它(git init,添加上述package.json和.gitignore,其中包含node_modules/、.build/、*.mp4、*.jpg、*.png、*-frames/和*-timeline.json),并告知用户它还没有远程仓库。
工具(位于scripts/,相对于此技能)
| 脚本 | 功能 |
|---|---|
find-demos.js | node find-demos.js [root…] [--json]:列出演示仓库中的每个视频,包含片段、仓库、输出以及上次录制和提交的时间。 |
record.js | node record.js <video-dir> [--only <segment>]:检查needs,录制并编码每个片段到.build/,然后合并到output(旧视频会被保留为*.previous.mp4)。在演示仓库根目录运行。 |
demo-kit.js | 导览脚本的库:kit.start() 提供page、title/untitle、caption、tap、type、ff、scroll/scrollTop、ripple、navigate、startCast、finish。帧会被流式写入磁盘。 |
encode.js | node encode.js <frames-dir> <timeline.json> <out.mp4>:构建MP4文件,压缩ff()窗口。 |
record-scenes.js | node record-scenes.js <page.html> <out.mp4> "0:3000,1:4500,…":录制定义了window.step(n)的场景页面。 |
concat.js | node concat.js <out.mp4> <a.mp4> <b.mp4> …:合并片段,重新编码为内部格式。 |
review.js | node review.js <video.mp4> <out-dir> [stills] [at=s,s,…]:生成2×2联系表,用于检查静帧。 |
assets/example-walkthrough.js是一个基于该工具包的完整示例(而examples/sift/record-walkthrough.js是一个真实示例)。在编写新的导览脚本之前,请先阅读它;它展示了结构,包括在视频中切换应用、快进任务以及结束卡。
工作流程
0. 查找现有内容
首先运行find-demos.js。如果用户想要重新录制或扩展一个已有文件夹的视频,就在该文件夹中工作:使用record.js <folder>重新录制(如果只更改了UI,则使用--only <segment>),或在其manifest中添加一个片段。不要为同一个视频创建第二个文件夹。只有在录制真正新的视频时才创建新文件夹,并以视频名称命名(kebab-case)。
1. 先画故事板
在编写任何代码之前,将故事板编写为编号列表,每个部分包含其节拍:屏幕上发生的事情和对应的字幕。提前决定:
- 哪些必须真实,哪些可以模拟。 优先使用真实的操作(真实模型调用、真实搜索)。如果要模拟数据(例如,数周的实时流量),请在屏幕上通过字幕和在应用内部(如果可以标记的话)进行说明。观看者会按字面意思理解演示,因此将模拟结果呈现为真实会产生误导。
- 哪些不可点击。 任何写入共享环境的操作(保存面板、发送邮件、购买)。显示按钮;但不要点击它。
- 时长: 每个片段约60–90秒。3分钟的合并视频也可以;每个片段应说明一个观点。
如果用户的目标在对话中已明确,请在更新时直接分享故事板。只有在视频目的不明确时才提问。
2. 设置视频文件夹
创建<demos repo>/<video>/目录,包含一个demo.json文件(为视频涉及的所有服务填写needs中的check URL,并写下reset)。如果node_modules缺失,在演示仓库根目录运行npm install。工具包和脚本会从工作目录解析playwright-core和ffmpeg-static,因此请从仓库根目录运行所有命令。必须安装Chrome浏览器(使用channel: 'chrome';无需下载Playwright浏览器)。
3. 预检应用
- 确认视频涉及的每个服务都已启动(使用curl检查每个URL),并且本地HTTPS工作正常(工具包会忽略自签名证书)。
- 重置状态,确保录制从干净状态开始(新的数据库、清除localStorage),并使脚本在状态脏时立即抛出错误。一个3分钟后失败的录制会浪费实际的模型调用和时间。
- 为视频中轮询的慢速端点计时。一个4秒的结果查询会在镜头中显示为加载骨架屏;修复查询或等待它。
- 使用一个简短的Playwright脚本截取每个关键状态的屏幕截图并检查:布局、换行、损坏的图片、在1440×900分辨率下可见区域上方的内容。这比录制成本低。
4. 编写录制脚本
复制assets/example-walkthrough.js的结构:
const kit = require(process.env.DEMO_KIT || `${process.env.HOME}/.claude/skills/demo-video/scripts/demo-kit`);
const demo = await kit.start(); // record.js提供帧和时间线路径
await demo.page.goto(URL, { waitUntil: 'networkidle' });
await demo.title('功能名称', '它让用户可以做什么');
await demo.startCast(); // 在标题覆盖页面后开始
await demo.sleep(2800); await demo.untitle(); await demo.sleep(900);
await demo.caption('…'); await demo.tap(locator); await demo.type(locator, 'text');
await demo.ff(() => page.waitForFunction(() => /* 回复完成 */ true, null, { timeout: 180000 }));
await demo.finish();
- 使用稳定的选择器(
aria-label、ID、data-*),并对照真实DOM检查它们。 - 在
ff()内部等待真实的完成条件,而不是固定的休眠时间,并设置宽裕的超时时间。 - 当字幕提到某内容时,先将其滚动到屏幕上(对于内容在元素内滚动的应用,使用
demo.scroll(px, 'main'))。 demo.navigate(url)切换页面或应用;它会在新页面上重新启动屏幕录制。- 对于架构片段,根据示例构建场景页面,并使用
record-scenes.js录制。同时为其添加上下箭头和空格键导航(示例中已有),以便同一页面在实时演示时也可使用。
5. 录制
在演示仓库根目录,在前台运行整个视频(或单个片段),设置较长的超时时间,并记录输出:
set -o pipefail; node ~/.claude/skills/demo-video/scripts/record.js <video> [--only <segment>] 2>&1 | tee <video>/.build/record.log
不要通过管道传递给tail:它会隐藏录制器的退出码,导致失败的录制看起来像成功。如果必须将其放入后台,请等待实际PID(while kill -0 $PID; do sleep 5; done),而不是相信“已完成”通知——该通知可能在进程结束之前就到达。
6. 检查
record.js已经编码并合并了所有内容;它会打印检查命令:
node ~/.claude/skills/demo-video/scripts/review.js <output.mp4> <video>/.build/review 16
要查看单个片段,请检查<video>/.build/<segment>.mp4。
在宣布视频完成之前,仔细阅读每张联系表。 检查:
- 每个字幕是否与屏幕上当时显示的内容匹配;
- 字幕所指的内容是否可见,而不是在折页下方;
- 没有损坏的图片或替代文本、卡住的骨架屏、错误通知、换行或截断的标题、调试UI;
- 快进的等待看起来像进度条快速闪动,而不是冻结的画面;
- 标题卡和结束卡是否存在。
修复脚本或应用(例如,为无法加载的图片设置占位符、更快的查询),重置状态,然后重新录制。预计需要两到三次录制。
7. 交付
- 检查连接点:在每个片段边界处运行
review.js <output> <dir> 0 at=<t-1>,<t+1>。 record.js会将之前的版本保留为*.previous.mp4。切勿覆盖不属于此文件夹自身output的视频:要添加到其他人的视频,请将其作为新文件夹的video片段添加。- 告知用户视频文件夹已准备好提交到演示仓库。仅在他们要求时才提交。
- 向用户报告:文件路径和时长、每个片段展示的内容、哪些是真实的、哪些是模拟的,以及录制遗留的状态(正在运行的服务、创建的数据、任何仍然打开的开关)以及如何撤销。
之前遇到的陷阱
- 内存: 当有动画时,CDP屏幕录制每秒最多发送60帧。将多分钟录制的帧缓冲在内存中会导致录制器被杀死且无输出。工具包会将帧流式写入磁盘,并在
ff()期间减少帧数;请保持这种方式。 - 时钟:
ff()窗口和帧时间戳都使用挂钟秒数,因此请在单个ff()调用内对整个等待时间(包括轮询)计时。 - 阻塞工作(例如
execFileSync用于数据处理脚本)在ff()内部没有问题:之后帧会恢复,并且该窗口无论如何都会被压缩。 - 端口: 在为演示启动服务之前,检查端口是否空闲;不要杀死你未启动的进程。
- 遗留状态: 一个以部署或启用某些功能结束的演示会留下这些状态。之后将其关闭或告知用户。