Communitygithub.com

lyonsv/demo-video-skill

录制精良、有脚本的Web应用演示视频(产品导览、架构前后对比动画、功能片段),输出为1440×900的MP4格式,采用一致的风格——白色标题卡片、药丸式字幕、点击涟漪效果、快进等待时间。通过Playwright驱动无头Chrome并使用ffmpeg编码。

demo-video-skill 是什么?

该技能允许AI助手(如Claude Code、Codex)录制专业、脚本化的Web应用演示视频。它使用Playwright驱动无头Chrome,通过ffmpeg编码,输出1440×900、30fps、H.264格式的MP4文件。所有输出遵循统一的“内部格式”:白色标题卡、半透明药丸式字幕、紫色点击涟漪效果、可见的打字动画以及对模型响应等慢速部分自动快进处理。该技能管理一个专用的“演示仓库”(demos repo),用于存放视频脚本和manifest,使产品代码保持整洁。还支持“场景页面”用于动画化架构图,并包含一个完整的示例视频文件夹。对于需要产品导览、架构动画或功能演示视频的任务,这是一个理想的自动化工具。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/lyonsv/demo-video-skill/tree/HEAD

在你喜欢的 AI 中提问

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

文档

演示视频

根据正在运行的真实应用录制的脚本化导览。观看者可以看到产品在执行实际工作;慢速部分(模型回复、任务)会被快进处理,每个节拍都有一个简短的字幕,提示需要关注的内容。每次录制都使用相同的格式,以便各个片段可以合并为一个视频。

内部格式

保持以下细节精确无误,以便新片段能与旧片段匹配:

  • 视频: 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.jsnode find-demos.js [root…] [--json]:列出演示仓库中的每个视频,包含片段、仓库、输出以及上次录制和提交的时间。
record.jsnode 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.jsnode encode.js <frames-dir> <timeline.json> <out.mp4>:构建MP4文件,压缩ff()窗口。
record-scenes.jsnode record-scenes.js <page.html> <out.mp4> "0:3000,1:4500,…":录制定义了window.step(n)的场景页面。
concat.jsnode concat.js <out.mp4> <a.mp4> <b.mp4> …:合并片段,重新编码为内部格式。
review.jsnode 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()内部没有问题:之后帧会恢复,并且该窗口无论如何都会被压缩。
  • 端口: 在为演示启动服务之前,检查端口是否空闲;不要杀死你未启动的进程。
  • 遗留状态: 一个以部署或启用某些功能结束的演示会留下这些状态。之后将其关闭或告知用户。

相关技能