ndemo 是做什么的?
创建带旁白的网页应用屏幕录制演示视频。
设置
首次使用时,必须先构建工具包再运行任何命令。
检查 ${CLAUDE_SKILL_DIR}/dist/cli.js 是否存在。如果不存在,请运行:
cd ${CLAUDE_SKILL_DIR} && npm install && npm run build
然后根据需要安装 Playwright 浏览器:
cd ${CLAUDE_SKILL_DIR} && npx playwright install chromium
验证设置:
${CLAUDE_SKILL_DIR}/ndemo doctor
命令
所有命令均通过 ${CLAUDE_SKILL_DIR}/ndemo 运行:
| 命令 | 作用 |
|---|---|
${CLAUDE_SKILL_DIR}/ndemo open <playbook> | 启动浏览器守护进程,导航到应用 |
${CLAUDE_SKILL_DIR}/ndemo close | 关闭浏览器守护进程 |
${CLAUDE_SKILL_DIR}/ndemo reset | 导航回应用 URL(全新状态) |
${CLAUDE_SKILL_DIR}/ndemo page-state | 打印当前页面的无障碍树 |
${CLAUDE_SKILL_DIR}/ndemo page-state --screenshot | 同上 + 保存截图至 .ndemo/screenshot.png |
${CLAUDE_SKILL_DIR}/ndemo play <playbook> | 播放所有片段 |
${CLAUDE_SKILL_DIR}/ndemo play <playbook> --segment <id> | 播放单个片段(先回退) |
${CLAUDE_SKILL_DIR}/ndemo play <playbook> --from <id> | 从指定片段播放至结尾 |
${CLAUDE_SKILL_DIR}/ndemo play <playbook> --to <id> | 在该片段后停止 |
${CLAUDE_SKILL_DIR}/ndemo play <playbook> --audio | 播放时附带 TTS 旁白音频 |
${CLAUDE_SKILL_DIR}/ndemo render <playbook> | 完整流水线:TTS → 回放 → 合并 → mp4 |
${CLAUDE_SKILL_DIR}/ndemo render <playbook> --output <path> | 渲染到指定的输出文件 |
${CLAUDE_SKILL_DIR}/ndemo doctor | 检查依赖关系 |
工作流程
第 1 步 — 创建 Playbook
每个 Playbook 位于用户项目的 demo/ 目录下的独立子目录中。子目录名称与 playbook 名称一致。
demo/
my-feature/
my-feature.yaml ← playbook
fixtures/ ← 设置时要恢复的文件
audio/ ← 生成的 TTS 文件(自动)
video-raw/ ← 原始录制(自动)
demo.mp4 ← 最终输出(自动)
在编写 playbook 之前,请思考应用需要处于什么状态才能使演示可靠且可重复:
- 用户是否需要登录?→ 添加条件登录步骤
- 演示是否会修改需要恢复的文件/数据?→ 将原始文件复制到 playbook 的
fixtures/目录,以便设置时恢复 - 演示是否依赖于特定内容的存在?→ 在 fixtures 中创建或通过设置 Shell 命令创建
将演示期间会被修改的任何文件 复制到 playbook 目录的 fixtures/ 子目录中。设置步骤会在每次运行前将它们复制回去,确保演示始终从干净状态开始。
创建目录和 YAML 文件。完整的 playbook 模式支持以下顶级部分:
# demo/edit-page/edit-page.yaml
app:
url: http://localhost:8080/wiki
viewport: # 可选,默认 1920x1080
width: 1920
height: 1080
scale: 2 # 设备缩放因子(默认 2)
zoom: 1.25 # CSS 缩放级别(默认 1.25)
colorScheme: light # "light" 或 "dark"(默认 "light")
setup:
# 恢复演示期间修改的文件
- run: cp demo/edit-page/fixtures/page.txt data/pages/page.txt
# 清理之前运行产生的工件
- run: rm -f data/cache/*.tmp
# 如果需要则登录(条件 — 如果已登录则跳过)
- type: click
target: { role: link, name: "Login" }
if:
hidden: ".user-info"
- type: type
target: { role: textbox, name: "Username" }
text: admin
if:
visible: ".login-form"
- type: type
target: { role: textbox, name: "Password" }
text: password
if:
visible: ".login-form"
- type: click
target: { role: button, name: "Sign in" }
if:
visible: ".login-form"
done:
visible: ".user-info"
titleCard: # 可选的标题卡片(作为第一帧显示)
title: "Editing a Wiki Page" # 突出显示
subtitle: "A quick tour of the editor" # 可选的副标题
duration: 3000 # 保持显示的毫秒数(默认 3000)
tts: # 可选的 TTS 配置
provider: openai # "openai"(默认)或 "elevenlabs"
voice: alloy # TTS 语音名称(默认 "alloy")
speed: 1.0 # 语速倍数(默认 1.0)
recording: # 可选的录制设置
outputDir: "." # 相对于 playbook 的输出目录(默认 ".")
fps: 30 # 视频帧率(默认 30)
segments:
- id: intro
narration: "Welcome to our wiki. Let's edit a page."
intent: "show the wiki start page"
timing: after # "after"(默认)或 "parallel"
actions:
- type: wait
duration: 2000
- id: open-editor
narration: "Click the edit button to open the editor."
intent: "click the edit button"
actions: []
片段时序 控制操作相对于旁白的执行时机:
after(默认)— 先播放旁白,然后执行操作parallel— 旁白播放的同时执行操作
首先为所有片段编写旁白和意图。将操作留作空数组。将 Playbook 路径传递给 ndemo 命令时使用绝对路径。
第 2 步 — 打开浏览器
${CLAUDE_SKILL_DIR}/ndemo open /absolute/path/to/demo/my-demo/my-demo.yaml
第 3 步 — 编写每个片段
对于操作为空的每个片段:
a) 读取当前页面状态:
${CLAUDE_SKILL_DIR}/ndemo page-state
b) 查看无障碍树输出。找到片段意图中引用的元素。使用树中的信息将操作写入 playbook YAML。
如何编写目标 — 使用 page-state 输出的信息:
如果 page-state 显示 [button "Settings"]:
target: { role: button, name: "Settings" }
如果 page-state 显示 [searchbox "Search reports" value=""]:
target: { role: searchbox, name: "Search reports" }
其他目标字段 — 使用最能标识元素的字段:
target: { selector: "#my-element" } # CSS 选择器
target: { testId: "submit-btn" } # data-testid 属性
target: { label: "Email" } # aria-label
target: { placeholder: "Search..." } # 占位符文本
target: { text: "Click me" } # 可见文本内容
至少需要一个目标字段。可以组合多个字段以缩小匹配范围。
提示: 网页应用的源代码在项目仓库中可用。当无障碍树不够用时,请查看组件源码以获取 data-testid 属性、类名或 ID。
如何编写完成条件:
done:
visible: ".settings-panel" # 元素出现
hidden: ".loading-spinner" # 元素消失
networkIdle: true # 没有待处理的请求
stable: 500 # DOM 500 毫秒内未改变
url: "**/settings" # URL 已更改
text: # 元素包含文本
selector: ".results"
has: "Q3 Revenue"
attribute: # 元素具有属性
selector: html
name: data-theme
value: dark
timeout: 10000 # 覆盖默认超时时间(毫秒)
每个更改页面的操作都必须有完成条件。 如果没有,下一个操作可能会在页面准备好之前执行。
在可见更改后添加 wait 操作,以便观众能够看到发生了什么:
- type: wait
duration: 2000 # 2 秒
c) 测试片段:
${CLAUDE_SKILL_DIR}/ndemo play /absolute/path/to/demo/my-demo/my-demo.yaml --segment <id>
d) 如果失败,运行 ${CLAUDE_SKILL_DIR}/ndemo page-state 查看当前屏幕内容,调整操作,然后重试。
e) 片段工作正常后,在编写下一个片段之前再次读取 page-state — 页面已经发生了变化。
第 4 步 — 审查
${CLAUDE_SKILL_DIR}/ndemo play /absolute/path/to/demo/my-demo/my-demo.yaml
在浏览器中观看完整序列。询问用户是否看起来正确。
要附带 TTS 旁白进行审查(需要 OPENAI_API_KEY 和 ffplay):
${CLAUDE_SKILL_DIR}/ndemo play /absolute/path/to/demo/my-demo/my-demo.yaml --audio
这会为每个片段生成 TTS 音频(按内容哈希缓存),将音频与浏览器操作一起播放,并调整时序使操作与旁白时长匹配。如果旁白文本发生更改,音频会自动重新生成,旧文件会被删除。
第 5 步 — 迭代
用户可能会要求更改。常见模式:
| 用户说 | 该怎么做 |
|---|---|
| "点了错误的按钮" | 运行 page-state,找到正确的元素,更新目标 |
| "太快" / "太慢" | 调整等待时长,使用 --audio 重放以检查时序 |
| "将旁白改为..." | 编辑旁文字段 |
| "添加一个展示 X 的步骤" | 插入新片段,编写其操作 |
| "删除那个步骤" | 从 YAML 中删除该片段 |
| "从片段 X 开始重播" | ${CLAUDE_SKILL_DIR}/ndemo play --from <id> |
| "重新开始" | ${CLAUDE_SKILL_DIR}/ndemo reset 然后重播 |
每次更改后,重新播放受影响的片段以验证。
第 6 步 — 渲染
当用户批准后:
${CLAUDE_SKILL_DIR}/ndemo render /absolute/path/to/demo/my-demo/my-demo.yaml
这将生成带有 TTS 旁白的最终 mp4 文件。
设置步骤参考
设置步骤在演示开始前运行(在 open、reset、play 和 render 时)。它们可以是 Shell 命令或浏览器操作。
Shell 命令相对于项目工作目录执行(即调用 ndemo 的目录)。run 命令中的所有路径都应相对于项目根目录。
Shell 命令 — 对文件操作使用 run:
setup:
- run: cp demo/my-feature/fixtures/original.txt data/page.txt
- run: rm -f data/cache/*.tmp
- run: ./scripts/reset-db.sh
浏览器操作 — 与片段操作语法相同:
setup:
- type: click
target: { role: button, name: "Login" }
- type: type
target: { role: textbox, name: "Username" }
text: admin
条件步骤 — 添加 if 以在不满足条件时跳过:
setup:
# 仅在未登录时登录
- type: click
target: { role: link, name: "Sign in" }
if:
hidden: ".user-menu" # 如果用户菜单可见则跳过
# 仅在特定页面运行
- type: click
target: { role: button, name: "Reset" }
if:
url: "**/admin/settings" # 如果不在该页面则跳过
条件字段:
visible: "<selector>"— 仅当选择器匹配可见元素时运行步骤hidden: "<selector>"— 仅当选择器不匹配任何可见元素时运行步骤url: "<pattern>"— 仅当当前 URL 匹配时运行步骤(**匹配任何路径)
操作类型参考
| 类型 | 必填字段 | 注释 |
|---|---|---|
| click | target | |
| type | target, text | 可选的 delay(毫秒)按键间隔(默认 80) |
| hover | target | |
| scroll | target | 将元素滚动到视图中 |
| wait | duration (毫秒) | 让观众暂停(默认 1000 毫秒) |
| press | key | 键盘按键,例如 "Enter"、"Escape" |
| select | target, option | 下拉选择 |
片段 ID 规则
- 小写字母数字加连字符:
open-settings、toggle-dark - 在 playbook 内必须唯一
- 用作音频/视频的文件名,请保持简短
故障排除
- 浏览器无响应:先运行
${CLAUDE_SKILL_DIR}/ndemo close,再运行open。检查.ndemo/daemon.log以查看浏览器守护进程输出。 - 找不到元素:运行
${CLAUDE_SKILL_DIR}/ndemo page-state查看当前页面内容,确保元素存在于无障碍树中。可能需要等待页面加载完成。 - TTS 失败:确保
OPENAI_API_KEY环境变量已设置。运行${CLAUDE_SKILL_DIR}/ndemo doctor检查所有依赖项。 - 渲染失败:检查磁盘空间和文件权限。渲染会消耗大量临时空间。