project-demo-video
将演示视频作为代码构建,以便在任何更改后几分钟内重新渲染。屏幕上的所有内容都是真实的:正在运行的应用程序、真实的命令输出、真实的源文件。没有任何模拟。
此技能编写成让能力较弱的编码代理可以逐步跟随。请按顺序执行各个阶段。每个阶段都有一个完成条件行:在满足条件之前,请勿继续。从 templates/ 复制文件,而不是重新编写它们。
安装:将整个文件夹(SKILL.md 和 templates/)复制到代理的技能文件夹中,例如 ~/.claude/skills/project-demo-video/,或者只需将代理指向 SKILL.md。
你产出的内容
| 输出 | 备注 |
|---|---|
<name>-voiced.mp4 | 1920x1080,30 fps,H.264 + AAC,字幕烧录,带语音 |
<name>-captions-only.mp4 | 相同画面,静音音轨(用户可以选择此版本或录制自己的声音) |
<name>.srt | 字幕文件,文本内容与旁白相同 |
thumbnail.png | 1280x720 |
narration.md, shotlist.md | 带有真实时间戳的脚本;每个场景及其来源 |
frames/ | 你实际查看过的 QA 帧 |
repo/ | 已更改或新增的项目文件,在其仓库路径下镜像 |
COPY_LIST.md, README-snippet.md, REPORT.md | 要复制到哪里,一个“查看它如何工作”的 README 部分,已完成的内容以及存在风险的内容 |
0. 你必须遵守的规则
- 只读源文件。 切勿写入、提交或推送到用户的真实仓库。在副本中工作(
robocopy/cp,排除node_modules、.git、构建输出),然后在其中执行npm ci。将值得保留的更改保留在输出文件夹下,镜像仓库路径,并附上COPY_LIST.md。由其他工具或人员完成 Git 操作。 - 屏幕上要诚实。 任何模拟的内容(设备、连接器、付款、模型)都必须在视频中标记为“模拟的”。切勿暗示存在不存在的硬件、部署或用户。复制项目自己的“真实与模拟”表格;如果 README 有误,请在报告中说明,并且不要重复错误的主张。
- 屏幕上或你交付的文件中不得包含机密信息: 令牌、API 密钥、
.env内容、私钥、真实电子邮件地址。项目文档中提到的开发令牌(例如Bearer dev-alice)是可以的。展示代码时,选择排除机密信息的行范围。 - 在帧、旁白、字幕或交付的文档中,不得提及编写代码的 AI 工具或其供应商,除非用户另有说明。作为产品配置一部分的运行时模型 ID 可以作为模型名称出现。在完成前使用
grep -ri检查。 - 遵守竞赛规则。 阅读时长限制(通常“少于 3:00”;目标设定为 2:50)和所需技术规则。视频必须展示所需技术真正在运行,而不仅仅是提及它。
- 免费套餐,不部署,不购买。 请勿部署、创建云资源或花钱。如果某个步骤需要用户提供凭证或录制内容,请直接请求一次,然后继续处理所有其他内容。
- 仅向用户请求 凭证、他们自己的录制内容或真正属于他们的选择(例如使用哪种声音)。自行决定所有其他事项并告知你的决定。
1. 前提条件(检查,不要假设)
node --version # 22+(推荐 24;.mjs 文件必须是纯 JavaScript,没有 TypeScript 转换)
- 在项目副本中:必须存在
@playwright/test(安装了 Chromium)和ffmpeg-static。检查:node -e "import('@playwright/test').then(async m=>{const b=await m.chromium.launch();console.log(b.version());await b.close()})"和node -e "import('ffmpeg-static').then(m=>console.log(m.default))"。如果缺少并且项目禁止添加新依赖,请将它们放在一个单独的工具文件夹中并指向它。 - 语音:Windows PowerShell 使用
System.Speech(内置)。在 macOS 上使用say,在 Linux 上使用espeak-ng;然后你必须调整templates/lib/tts.mjs(仅synthAll和tts.ps1是操作系统相关的)。 - 没有
ffprobe随 ffmpeg-static 提供:从 WAV 标头(templates/lib/tts.mjs中的wavSeconds)和录制器自己的计时来测量时长。 - Windows shell 注意事项:对于任何包含反斜杠、正则表达式或
$的文件,最好使用写入和编辑工具。使用内联python -c/heredoc 重写 JS 会静默地损坏转义字符。前台sleep在某些代理中被阻止:使用until grep -q "done" log; do sleep 10; done来等待。
2. 阶段
阶段 A:要求和证明映射
阅读规则页面、README、项目自己的文档以及任何故事情节说明。在你的笔记中编写一个表格:
| 评委必须相信的是 | 视频中证明的地方 | 场景 |
|---|---|---|
| 所需技术在代码中被调用 | 真实调用可见:请求日志、终端、代码摘录 | ... |
| 体验有效 | 应用程序在浏览器中运行 | ... |
| 什么是真实 vs 模拟 | 项目自己的表格 | ... |
完成条件: 每一行都有对应的场景。
阶段 B:工作空间
- 将仓库复制到临时文件夹,然后安装:
npm ci。 - 将
templates/复制到副本中的video/(保持结构:video/lib、video/pages、video/scenes、video/final.mjs、video/narration.mjs,...)。将"video:final": "node video/final.mjs"添加到package.json的脚本中。不要添加依赖。 - 将
video/out/添加到副本的.gitignore中。
阶段 C:预运行真正的应用程序并查找错误(在录制任何内容之前执行此操作)
按照评委的方式启动应用程序(npm run demo 或 README 命令)在其文档记录的默认端口上,以便屏幕上显示的每个命令都是字面上的命令。使用一个小的 Playwright 脚本驱动它,在故事的每个步骤之后以 1920x1080 分辨率截取屏幕截图,并查看它们。检查:
- 登录、空白面板、红色错误横幅、控制台错误;
- 故事需要的每个状态是否实际渲染(参考项目的第一次运行显示了一个失败的登录、一个空的收件箱和一个缺失的关键视图,所有这些都被旧版本的草稿视频隐藏了);
- 文本是否重复或截断。
在副本中修复真正的错误,最小化修复,运行项目自己的检查(npm run verify、测试、类型检查),并将每个修复记录在 COPY_LIST.md 中。如果应用程序无法显示网络流量,请添加一个小的可选钩子(请求日志),而不是伪造一个。
完成条件: 整个故事连续干净地运行两次,并且项目的检查通过。
阶段 D:故事板和旁白
- 挑选场景。参考故事(约 2:50):
| 场景 | 时长 | 展示内容 |
|---|---|---|
| 钩子 | 12 秒 | 一个更改波及多个承诺(动画卡片) |
| 启动 | 6 秒 | 启动一切的一条命令,真实输出 |
| 模拟(应用程序) | 60 秒 | 8-10 个节拍;在 2 个节拍上打开请求日志 |
| 证明终端 | 17-30 秒 | 针对同一服务器的官方客户端或 CLI;身份验证检查 |
| 代码导览 | 16 秒 | 3-4 个证明所需技术的真实代码摘录 |
| 架构、云堆栈、真实 vs 模拟 | 30 秒 | 图表、堆栈来源、诚实表格、开源卡片 |
| 结束 | 12 秒 | 标签线、仓库链接、构建致谢、“X 是模拟的”页脚 |
- 编写
video/narration.mjs:scenes = [{ id, title, segs: [{ id, text }] }]。每个seg是一句口头台词和一条字幕(text按原样显示)。规则:大约每秒 2.4 个单词;总口语单词数 330-380;每行一个想法;任何屏幕上的卡片上不超过约 12 个单词;使用句子大小写;不要夸张。 - 在
say表中仅为语音引擎添加发音提示(首字母缩略词拼写出来,“Alexa+” 作为 “Alexa plus”);切勿更改字幕文本。 完成条件: 旁白读起来像一个陌生人能理解的故事,并且证明映射中的每一行都已被讲述或显示。
阶段 E:调整模板
仅编辑项目特定的内容:
| 文件 | 编辑内容 |
|---|---|
video/narration.mjs | 你的场景和台词 |
video/scenes/sim.mjs | 你的应用程序的步骤:要输入或点击的内容、选择器、相机缩放(lens(...)、zoomWire) |
video/scenes/static.mjs | 终端命令(CMD)、codeBlocks(文件 + 第一行的正则表达式 + 行数 + 高亮正则表达式)、钩子/结束页面调用 |
video/proofs.mjs、video/proof-auth.mjs | 证明你的主张的真实命令;它们仅打印状态码和错误名称 |
video/pages/*.html | 文本、颜色、磁贴、图表框、表格行、链接 |
video/lib/overlay.mjs | 针对你的应用程序布局的 SIM_CSS 选择器(参见布局规则)、滚动条的数据源 |
video/final.mjs | LIMIT 常量和场景顺序(顺序来自 narration.mjs) |
video/thumbnail.mjs、video/docs.mjs | 标题和 SHOT 表格 |
除非某些内容损坏,否则不要编辑 lib/recorder.mjs、lib/director.mjs、lib/tts.*。
阶段 F:捕获真实的证明输出
node video/proofs.mjs 启动一个新的服务器,运行真实的命令(官方检查器或客户端 CLI、身份验证检查、安装/启动输出),并将它们打印的内容保存到 video/out/proofs.json 中。终端场景回放这个捕获的输出在一个 HTML 终端中,以可读的尺寸显示。规则:显示真实的命令文本(仅缩短包路径);明显截断非常长的输出(一个暗淡的 "... N lines ..." 行),并在芯片中放入 "Recorded run";切勿打印令牌;美化 JSON,如果你这样做,请说明。
阶段 G:旁白音频
默认(无需账户):node video/final.mjs --rate 2 使用内置的 Windows 语音(Microsoft Zira)为每行生成一个 WAV 并测量每个的长度。速率 1 对于参考脚本来说大约产生 150 秒的语音,速率 2 大约 139 秒,速率 3 太快。选择能保持整个视频低于限制的最慢速率。
可选的人性化声音:参见附录 A。以下所有内容在任一种情况下工作方式相同。
阶段 H:录制和组装
node video/final.mjs --rate 2 # 所有步骤:TTS、证明、录制所有场景、组装两个版本
node video/final.mjs --rate 2 --reuse-proofs # 跳过重新运行证明
node video/final.mjs --scenes sim,close --reuse-tts --reuse-proofs # 仅重新录制某些场景
node video/final.mjs --assemble-only # 仅重新封装(字幕、音频)而不重新录制
node video/final.mjs --tts-dir video/out/tts-human --name my_video --single # 从另一个音频文件夹构建
完整运行需要 6-10 分钟;在后台运行并轮询日志。如果总时长达到 180 秒或更长,脚本会失败。目标设定为 170 秒。
重要提示:如果你更改旁白文本或语音,时长会改变,因此每个场景都必须重新录制(标记来自时长)。仅更改一个场景的视觉效果意味着 --scenes <id> --reuse-tts。
阶段 I:质量保证(绝不跳过,绝不相信日志)
- 从最终 MP4 的每个旁白行结束处提取帧:
ffmpeg -ss <markEnd-0.3> -i final.mp4 -frames:v 1 frame.png(时间线在video/out/timeline.json)。查看它们。检查 1080p 下的可读性、空白空间、裁剪的文本、字幕位置和大小、同步性(字幕的事件在屏幕上)、无机密、无供应商提及、存在诚实标签。 ffmpeg -i final.mp4 -af volumedetect -f null -:平均值约为 -22 dB,最高值低于 0 dB。- 两个版本中的时长均低于限制;两个文件都有视频和音频流。
grep -ril "<vendor words>" video/pages video/narration.mjs <output docs>返回空值,除了允许的模型 ID。- 再次运行项目的检查(
npm run verify;如果 UI 更改过,则运行 e2e 测试)。 - 修复并重新渲染直到全部通过。
阶段 J:交付物
使用 node video/docs.mjs <outdir> 生成 narration.md 和 shotlist.md(真实时间戳)。复制 MP4 文件、SRT 文件、缩略图(node video/thumbnail.mjs <png>)、QA 帧以及更改过的仓库文件(原始文件和你的副本之间所有不同的文件,在其仓库路径下)。编写 COPY_LIST.md(首先列出 Bug 修复,然后是新文件、建议的提交消息、“运行构建并在复制后验证”)、README-snippet.md(运行命令、入口文件:行、客户端文件:行、技能文件、视频链接占位符)和 REPORT.md(已完成的内容、用户必须做什么、风险)。明确说明你无法检查的内容(例如,你无法收听语音)。
3. 流水线的工作原理(在更改之前阅读)
录制器(lib/recorder.mjs)。Playwright 自己的 recordVideo 会限制比特率并使小文本模糊,因此此技能使用 Chromium 屏幕广播:JPEG 帧(质量 92)在到达时被打上时间戳、写入磁盘,并以恒定的 30 fps 使用 libx264 crf 17 进行编码。静态屏幕产生的帧很少;最后一帧会被保持。时间戳来自一个时钟(performance.timeOrigin + performance.now()),与导演使用的时钟相同。
导演(lib/director.mjs)。每个旁白行是一个片段。d.seg(id, actions, {tail}) 记录开始时间,运行屏幕上的操作,然后保持直到该行播放完毕再加上 tail * 0.45 秒(尾部会全局修剪以适应限制)。标记(id、开始、结束、文本)被保存到 video/out/<scene>.marks.json。由于音频后来在记录的标记处放置,因此画面和语音不会漂移。节拍的规则:在片段内执行操作,并确保操作在旁白结束前完成;如果行比操作短,则将其拆分为两行(这就是为什么一个故事节拍变成了 sim9 和 sim10)。
音频(final.mjs)。每个 WAV 使用 adelay 延迟到其全局开始时间,使用 amix=normalize=0 混合,使用 alimiter 限制,并使用 apad=whole_dur=<total> 填充(如果没有填充,-shortest 会切断视频末尾)。
字幕。 提示是旁白文本在句子末尾处拆分(诸如 "2.0" 之类的小数点会被保护)以及在超过 82 个字符的逗号处拆分,时间按长度比例分配。.srt 文件会被交付。烧录版本使用一个生成的 ASS 文件,其中包含 PlayResX: 1920, PlayResY: 1080 和字体大小 36。不要使用 force_style 烧录 SRT 文件:libass 会针对一个 288 像素高的画布进行缩放,导致文本大约变为四倍大。
覆盖层(lib/overlay.mjs),仅注入到录制的应用程序页面中(应用程序本身不被编辑)