Communitygithub.com

delacournz/demo-video

在真实模拟器上生成产品演示视频,附带打字终端模拟。适用于 UI 变更后重新录制、文档嵌入以及定义 `*.demo.ts` 编辑。

demo-video 是什么?

该技能是一个为 AI 代理(如 Claude Code、Cursor、Codex)设计的完整演示视频制作工作流,能够在托管的 iOS 模拟器上录制操作,并自动渲染出 1920x1080 的高清视频。视频布局为左侧标题、中间终端(逐字键入真实的 `warden` 命令并显示输出)和右侧圆角手机模拟屏幕。它支持多个平台(iOS 模拟器)和丰富的自定义选项,包括定义文件格式(`*.demo.ts`)、录制循环(`bun run demo`)以及诚实规则——确保每个显示的命令都真实执行过,没有虚假合成。该技能还提供了 X 导出规范(H.264 编码、30 fps、无音频)、文档嵌入(通过 MDX 的 `DemoClip` 组件)以及高级工作流(如 `warden dev` 仪表盘录制和终端密集型剪辑),自动处理状态栏、字体、权限提示等细节,并集成 `maskSecrets` 进行隐私保护。

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

在你喜欢的 AI 中提问

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

文档

demo-video 是做什么的?

bun run demo <feature>/<clip> 在托管的 iOS 模拟器上录制一段视频,然后渲染出 1920x1080 的视频:左侧为标题,中间是终端(逐字键入每个真实的 warden 命令,并在步骤结束时打印输出),右侧为手机(圆角,3px 描边)。工具包:scripts/demo/(demo.ts CLI、record.ts、render.py、timeline.ts、demo.types.ts)。定义文件:apps/example/demos/<feature>/<clip>.demo.ts(name 必须等于 <feature>/<clip>);资源文件:apps/docs/public/demos/<feature>/<clip>.mp4|jpg。每个功能一个文件夹(biometrics、network、location、clock),小写,连字符。内部使用:无 changeset。

1. 何时制作

  • 效果在屏幕上可见且几秒内发生(如生物识别、提示、手势)。不适用于日志、性能数据或表格更佳的内容。
  • 当应用 UI 或命令行为变更后:重新录制,绝不编辑 mp4 文件。
  • 每个剪辑一个故事,时长 10-25 秒,3-6 个带说明步骤。

需要 ffmpeg、ffprobe、uv(brew install ffmpeg uv)、Xcode,以及 node_modules/@expo-google-fonts 中的字体(bun install)。

2. 编写定义

导出 demo: DemoDefinition(scripts/demo/demo.types.ts)。复制 apps/example/demos/biometrics/faceid.demo.ts。

字段含义
title / subtitle / terminalTitle / footer静态文本;标题是主张,副标题一行
phoneCaption手机下方的说明文字,默认为 real simulator · not an animation;"" 隐藏它
claim{ biometrics?, profile? } -> warden claim ios --json --label demo-<feature>-<clip> --ttl 30m [--biometrics]
appbundleId、kind(默认 simulator)、project(相对于仓库的 cwd)、deepLink(使用 --settle 打开)、appearance(light/dark,通过 simctl ui 在托管的 udid 上设置,录制后恢复)
setup应用打开后、深度链接前运行的 warden 参数(在镜头外);["ui","network","set","online","--relaunch"] 为 iOS dylib 启动网络/时钟步骤提供所需。每个条目是一个参数数组或 { argv, optional: true }:可选的步骤退出非零时会被记录(optional setup warden … -> <code>)而不是导致录制失败(例如权限提示,可能不会显示)
ready深度链接后、视频开始前必须通过的 warden ui wait 选择器(例如 id=bio-screen);步骤不会按下未挂载的屏幕
leadInMs / tailHoldMs步骤 1 之前的空闲时间/最后一帧保持时间,由渲染器填充(录制尾部为 1 秒)
steps[]argv(warden 之后;录制器附加 --udid)、output(ok 绘制矢量勾选,plain 静默,lines 打印多条静默行)、pauseAfterMs、shown?、hidden?、quote?
outro最后输出后键入的 # comment 行

output: { kind: "lines", text: [...] } 每行静默打印一条(保持约 6 条;每条超出第一条会消耗布局中 0.4 个条目的间距),适用于显示文件中的代码或文本。配合 quote: { file }(相对于 app.project 的路径):步骤后录制器会检查每个打印的行(去空格后)是否出现在该文件中,否则抛出 quoted line not in <file>: <line> 错误,因此引用的行永远不会被虚构。行内容仍会经过 maskSecrets。

时间从不手动编写:录制器根据视频时钟测量每个步骤的开始和结束,输出行在步骤结束时显示。

3. 循环

bun run demo <feature>/<clip> --record-only      # 声明、9:41 状态栏、启动、录制、按 id 释放
bun run demo <feature>/<clip> --render-only --sheet   # 生成 mp4 + jpg,写入 <work>/sheet.jpg
bun run demo <feature>/<clip>          # 两者都做

工作目录:$TMPDIR/warden-demo/<feature>/<clip>(--out dir):take.mp4、steps.json、plan.json、sheet.jpg;录制失败时会有 failure-snapshot.txt(错误信息 + ui snapshot -i)。这些内容都不会提交。

  1. 录制。
  2. 读取 sheet.jpg(4x4 网格,每秒一张,pts 烧印)。对照日志检查:每个输出行出现在手机可见反应时;没有帧显示无关的提示、键盘、红框或错误状态栏;最后一帧显示结束状态。
  3. 如需更仔细查看:ffmpeg -ss 7 -i take.mp4 -frames:v 1 f.png 并 Read 它。
  4. 在定义中修复(pauseAfterMs、删除/隐藏步骤、shown)并 --render-only;仅在录制本身出错时重新录制。批量修复:每轮一次录制。
  5. 将 mp4 + jpg 提交到 apps/docs/public/demos/<feature>/。

4. 诚实规则

  • 每个打印的命令都实际运行过,针对真实应用。shown 与 argv 含义不同(它只修剪了诸如 --timeout 的标志)。
  • 输出文本是手机实际做的,而不是命令打印的内容。如果一次按下没有可见效果(例如在 sheet 的进入动画期间丢失,或者什么也没变),不要为其添加说明:使用 hidden: true,或者通过更长的前一个 pauseAfterMs 调整时机。
  • 首次运行的权限提示只在新安装时显示;声明会复用热构建,因此除非录制确实显示了提示,否则不要为其添加说明。
  • 没有应用侧绕过或模拟提示:标题中的声明必须在录制中真实成立。
  • 秘密信息:maskSecrets 隐藏 key=value、ghp_/sk- 风格令牌和打印行中的长 hex/base64。仍要检查手机上是否有令牌、邮箱、设备名称。
  • 真实模拟器,真实 iOS UI。绝不合成虚假帧。

5. 注意事项(已编码在工具包中)

  • simctl 视频是 VFR 且会丢弃空闲尾部:渲染器进行 fps 归一化并填充最后一帧(tpad)以达到计划长度。
  • 视频时间与命令日志的时间差在约 0.5 秒内;录制器在 video start 后立即启动时钟。
  • Martian Mono 没有勾选字形:render.py 将其绘制为矢量。
  • 帧使用文档 Device Hub 深色配色(Mona Sans + Martian Mono);设置 app.appearance: "dark" 以使手机匹配。录制器之后会恢复模拟器的外观。
  • 终端溢出:computeTerminalLayout 将字号和间距缩小到 17 px,然后滚动移除最早的条目。
  • 设备视频限制 3 分钟;无论如何请保持录制在 40 秒以内。
  • 状态栏:允许在托管的 udid 上使用 xcrun simctl status_bar <udid> override;录制器之后会清除它。

6. X 导出

1920x1080,H.264 High,yuv420p,30 fps,+faststart,无音频,时长 <= 2:20,小文件(工具包通过 CRF 阶梯 24..36 的目标是 <= 2 MB)。检查:

ffprobe -v error -show_entries stream=codec_name,profile,width,height,r_frame_rate -show_entries format=duration,size apps/docs/public/demos/<feature>/<clip>.mp4

7. 资源和文档嵌入

输出:apps/docs/public/demos/<feature>/<clip>.mp4 + <clip>.jpg(海报 = 最后一帧),通过 /demos/<feature>/<clip>.mp4 提供(Vite public/)。使用 DemoClip MDX 组件(apps/docs/src/components/demo-clip.tsx)嵌入:自动播放静音并循环,在减少动效设置下停留在海报图。src 没有扩展名;label 说明剪辑显示的内容(与说明文字匹配)。原始的 <video> 标签不会通过 apps/docs/src/lib/source.test.ts 测试。

<DemoClip src="/demos/<feature>/<clip>" label="warden ui … 在托管的模拟器上执行 …" />

添加一句包含命令名称的说明文字。批量轮播 apps/docs/public/demos/e2e/batch.*(使用 <DemoReel> 嵌入)是一个不同的流程(bun run demo:compose)。

8. warden dev 轮播(dev/hero)

第三个流程:warden 真实的 DEV 仪表盘(左侧,在终端卡片内)旁边是模拟器,warden dev 自己进行声明(右侧)。没有任何内容被输入或伪造:dev-take.ts 在 PTY 中生成 warden dev ios --label demo-dev-<clip> --headless --no-shutdown(Bun.spawn({ terminal }))并镜像到 @xterm/headless(帧读取用于等待),并将每个 PTY 块写入 tui.cast;时钟起点是生成时刻。它轮询 warden ls --json 以获取标记的设备租约,然后设置深色 + 9:41,卸载应用并启动 ui screen video(手机在此之前是黑屏)。dev 子进程有自己的 WARDEN_SESSION_ID:warden dev 采用调用者会话持有的设备租约,并且每个合作代理共享一个,因此没有它的话录制可能会驱动别人的模拟器。

bun run demo:dev dev/hero --record-only        # 声明、卸载应用、9:41 + 深色、录制、按 id 释放
bun run demo:dev dev/hero --render-only --sheet # 聚合 + ffmpeg -> apps/docs/public/demos/dev/hero.{mp4,webm,jpg}、<work>/sheet.jpg
bun run demo:dev dev/hero                      # 两者都做
  • 工具包:dev-demo.ts(CLI)、dev-take.ts(录制器,runBeats)、dev-render.ts(聚合 + 合成,DEV_LAYOUT)、dev-frame.py(卡片、描边、蒙版)、dev-timeline.ts(投射文本、斜坡分段、说明文字)。类型:DevDemoDefinition / DevBeat 在 demo.types.ts 中。定义:apps/example/demos/dev/<clip>.demo.ts;工作目录 $TMPDIR/warden-demo/dev/<clip>(take.mp4、tui.cast、dev.json、sheet.jpg、failure-frame.txt)。
  • 一个 beat = waitScreen(TUI 帧上的正则) + waitPhone(ui wait 选择器) -> 动作(向 TUI 输入按键,和/或对仓库文件进行真实的 edit) -> afterPhone 选择器(热重新加载落地) -> pauseAfterMs。dev.json 保存每个 beat 在视频时钟上的开始/就绪/结束时间;说明文字从 beat 的动作运行到下一个 beat 的动作。
  • 编辑是对源文件的真实编辑(welcome.tsx 的 body 字符串),在 finally 中逐字节还原;录制后检查 git diff apps/example/src 是否为空。
  • uninstallFirst 从托管的模拟器中移除应用,以便 warden dev 在镜头前从缓存安装开发构建。第一次录制是本地构建(几分钟,dev build 行编译):先录制一次以预热缓存,然后重新录制。warden app ensure --kind dev 不会预热 warden dev 读取的缓存。
  • 速度斜坡:DevBeat.ramp: N 将该 beat 的等待加速 N 倍(保持 1.2 秒头部和 1 秒尾部以原始速度),轮播会绘制一个 × N speed 标签。如果预热录制在开发客户端等待中速度较慢,则进行斜坡加速。按键和热重新加载始终是实时。
  • 诚实:<DemoReel> 说明文字中的事实来自录制的 TUI 行(dev build 3.5s、Metro 3.9s、dev client 161ms)和 dev.json(编辑 -> ui wait 看到新文本:0.76 秒)。每条重新录制后重新读取它们;它们会变化。
  • 限制:仪表盘没有单独的声明行(它在 preparing the ios dev build 内部);--headless 因此没有 Device Hub 窗口打开;100x26 PTY(100 列是 HUD 的最小值)。带有 stopVideoFirst 的 beat 会先停止视频再执行动作(q 结束租约,然后 warden ui 会拒绝)。在 q 之后,录制验证 warden ls 是否持有该会话的租约(否则失败),并且投射在仪表盘离开备选屏幕的地方被截断,以便轮播以其最后一帧结束。开发启动器的白色加载屏幕和蓝色的 Refreshing… 横幅是真实应用;Metro 重新加载也可能来自另一个代理编辑 apps/example/src。
  • 斜坡:ramp 仅当等待时间减去其 1.2 秒头部和 1 秒尾部后至少为 6 秒(RAMP_MIN_S)时才应用。英雄轮播将其开发客户端等待加速 3 倍并带有标签。
  • devEnv 向 warden dev 子进程添加环境变量;英雄轮播设置 EXPO_NO_DEPENDENCY_VALIDATION=1(@expo/cli checkDependenciesOnStart)以去除 Expo 的“有更新可用”通知。仅用于噪音,绝不用于行为。
  • 使用 <DemoReel src="/demos/dev/hero" command="warden dev ios" facts="…" label="…" /> 嵌入(落地页英雄、sims/dev.mdx、getting-started/quickstart.mdx)。海报 = posterBeat 动作帧 + 1 秒(HUD 开启,所有行完成,应用运行中)。

9. 终端密集型剪辑(VID-6)

所有命令都是真实命令;每个轮播只显示实际运行的内容。一次一个剪辑,每次按标签释放。

剪辑流程命令
e2e/parallelbatch-take.ts:warden e2e <suite> --count N --record --headless 到 <work>/rec,在启动时设置每个托管模拟器为深色 + 9:41,然后 compose.ts --height 1080bun run demo:batch e2e/parallel --suite example --specs devtools,gestures,onboarding,sign-in,todo-detail --count 2(--record-only / --compose-only;头部显示规格数量;当规格与任务不同时,计数器显示 N/total shards done)
dev/worktreesLanesDemoDefinition:每个车道并发执行一个 recordDev,下方有一个 shell 条(warden ls)bun run demo:lanes dev/worktrees(lanes.ts、lanes-frame.py、lanes-demo.ts;车道通过 DevTake.startedAtS 共享一个时钟)
golden/cloneDevDemoDefinition.script:一个 shell 脚本在 PTY 中运行,而不是 warden devbun run demo:dev golden/clone
debug/failurescript + rawVideobun run demo:dev debug/failure --sheet
  • script 模式:不卸载,不检查释放;录制以 release --mine --label L --shutdown 和 sims delete <udid> --yes 结束。带有 waitBoot 的 beat 会轮询 simctl io screenshot 直到 PNG 超过 100 KB(HOME_SCREEN_MIN_BYTES):一个刚刚克隆的无头模拟器在 claim 返回后 10 多秒才显示苹果标志。对该等待进行斜坡加速(3 倍,带标签)。
  • rawVideo:使用 simctl io recordVideo 录制(通过 SIGINT 停止),因为 warden 的 ui screen video 助手与 warden e2e 在一个设备上冲突。
  • --record 只录制开始时存在的设备;弹性后期加入者没有视频。主机容量门决定有多少个可以容纳,因此要求不超过启动时启动的模拟器数量(这里最多 2 个)。
  • debug/failure 的临时规格写入 apps/example/e2e/debug-demo.e2e.ts,并由 shell trap 移除;之后检查 git status(被杀的录制可能留下它)。
  • 使用 <DemoReel src="/demos/<feature>/<clip>" command="…" facts="…" label="…" /> 嵌入;事实来自录制,每次重新录制后重新读取。
  • demo:batch 使用轮询器设置模拟器样式(styleWhenBooted:通过标签 warden ls --json,simctl list 查找 Booted,然后 ui appearance dark + status_bar override 9:41),因为 warden e2e 没有按设备钩子;它在 finally 中释放 --mine --label demo-<feature>-<clip> --shutdown。使用在干净模拟器上通过的规格(perf、settings、todos 不可靠);合成 --height 1080 标志给出 16:9 布局(终端

…

[已针对翻译截断]

相关技能