Synthetic UI Demos
没有任何内容是屏幕录制的。Claude Code 读取产品的仓库,仅重建场景显示的那些屏幕作为静态 React,然后在 Remotion 中由脚本化的光标和摄像机执行流程。每个点击都是一个帧编号,因此场景可以完全一致地重新渲染,永远不需要种子账户或实时环境,并且当产品变化时,通过编辑代码即可更新。
参考构建(与此技能一起阅读;两者都在没有产品仓库的情况下构建):
examples/ds-free-schedule-demo:一个 26 秒的流程(安排通话),带有一个旁白块。最简单的完整用例。它的src/Demo.tsx是一个单一时间线,包含每个光标关键帧、点击和摄像机移动。examples/ds-release-649:一个 80 秒的版本发布说明视频,涵盖三个应用的八个场景。templates/ds-release-notes是其可复用的形状(作为版本 650 发布)。src/Release.tsx编排场景,src/scenes/中每个发布项对应一个文件,src/room/和src/ac/是屏幕套件。
何时使用此技能 vs. Playwright
| Synthetic(此技能) | Playwright 录制(/record-demo) |
|---|---|
| 你可以读取应用的源代码 | 你只有正在运行的应用 |
| 每次发布都需要重做的版本说明和功能导览 | 一次性捕获真实行为或真实数据 |
| 流程需要你没有的数据(完整通话、其他用户、管理视图) | 真实场景易于访问且看起来已经很合适 |
| 摄像机移动、精确的点击时机、语音变更后重新渲染 | 长时间、复杂的交互,重建成本过高 |
构建块(lib/components)
import { Stage, typed, cursorAt, near } from '../../../../lib/components';
<Stage
url="app.example.com/settings" // 地址栏;主机部分颜色更深,路径部分颜色更浅
camera={CAMERA} // [{ f, z, x, y }] 画布像素坐标,缓动,限制在窗口内
path={PATH} // [{ f, x, y }] 画布像素坐标下的光标关键帧
clicks={CLICKS} // 压扁光标并抛出波纹的帧
aiNote // "显示的为 AI 生成"(或传递你自己的文本)
canvasWidth={1440} canvasHeight={810} // 默认值
>
<SettingsScreen field={typed(frame, 'Q4 Product Review', 128)} />
</Stage>
Stage在固定大小的应用画布周围绘制浏览器 chrome,应用摄像机,并将光标放在 zIndex 100 的顶层。在缩放 1 时,窗口适配(fit)为帧宽度的 0.8625 倍。关键帧重复一个值以保持该状态。Cursor和cursorAt可以单独用于非浏览器场景;near(cursor, target)驱动悬停状态。- 产品特定的屏幕套件(主题、图标、布局)保留在项目中。它们不属于库内容。
方法
1. 通过子代理从应用仓库中提取视觉规范
为每个应用或区域并行发送一个子代理,让每个子代理返回一个紧凑的规范,而不是文件转储:
- 主题: 调色板、字体栈、圆角、间距、阴影(来自主题文件或 CSS 变量)。
- 字符串: 场景显示的每个标签、菜单项、对话框和提示的精确英文本地化字符串(
public/locales/en/*.json、src/i18n/locales/en/*)。切勿意译 UI 文本。 - 图标: 如果应用使用图标字体,其 icomoon/fontello
selection.json包含每个图标的 SVG 路径。将你需要的提取到一个小的 JSON 中,并渲染为<svg><path d=...>。否则复制 SVG 组件。 - 布局和行为: 所显示屏幕的组件树、工具栏顺序、默认值、每个控件的作用。
从远程读取默认分支(git show origin/main:path,或 git fetch 然后读取 origin/<default>)。本地检出通常已过时,会给你上个季度的 UI。
对于版本说明,将每个项追溯到首先发布它的提交和组件。这样可以让你获得确切的字符串和要显示的默认值。
2. 仅在固定画布上重建场景显示的内容
- 将所有元素以绝对位置布局在固定画布上(1440x810 是一个好的默认值)。将重要的矩形导出为常量(
SAVE_BTN = { x, y, w, h }),这样点击目标就是算术运算,而非猜测。 - 只构建摄像机看到的内容。从未出现在镜头中的面板可以是一个纯色块。
- 保持静态 React:没有状态,没有副作用。每个视觉变化都是
frame的函数。 - 将套件放在自己的文件夹中(
src/ui/、src/room/),并包含一个用于常量的theme.ts。
3. 先录制语音,再确定时间线帧
- 每个场景写一行,按句子拆分(每个短语一个
.txt),并首先生成音频(voiceover.py --scene-dir ...)。录制三遍加上tools/pick_takes.py比单次录制加上--max-wpm更有效:克隆会复制其参考剪辑的节奏和平坦度,所以如果台词听起来仓促或单调,使用tools/voice_prosody.py expand修复剪辑,而不是拉伸输出(参见 CLAUDE.md 中的 "Clone Prosody")。读取测量到的时长,以及tools/captions.py用于单词时间。 - 将每个场景的大小设置为你的台词加上一个节拍,并在文件顶部保留一个注释,列出每句台词的起始位置和长度。该注释是真理的唯一来源(参见 CLAUDE.md 中的 "Audio-Anchored Timelines")。
- 场景文件顶部的一切都是帧常量:光标关键帧、
CLICKS、摄像机关键帧、UI 状态切换(frame >= C.save显示对话框)。导出点击帧,以便父级可以放置点击音效。 - 通过旁白查找帧,不要手动设置。
voice()(lib/components/cues.ts;ds-release-notes 模板有一个副本)接收场景的短语及其.captions.json和起始帧,而cue('save')返回该单词开始的帧。将每个动作锚定到它说明的单词上,并带有一个偏移量,并将每个光标和摄像机关键帧锚定到这些锚点上:重新生成或重新调整短语的时间,点击、打字、光标和摄像机都会随之移动。一个其单词不再被说出的提示会在构建时失败,并指出是哪些单词。选项:import line06a from '../../public/audio/scenes/06a_link.captions.json'; export const LINK_VOICE = voice([{ file: '06a_link', at: 10, captions: line06a }, ...]); const { cue } = LINK_VOICE; const C = { field: cue('before', { offset: -3 }), save: cue('save') }; const PATH = [{ f: C.field - 6, ...field }, { f: C.save - 24, ...saveBtn }];offset、edge: 'end'、after(一个帧,用于重复的单词)、nth、in(一个短语)。父级从LINK_VOICE.lines放置音频,因此短语的起始帧只写入一次。templates/ds-release-notes/src/scenes/FreeCustomLink.tsx是详细的示例。
4. 在停顿处拆分密集的列表行
列出多个事物的台词("日程安排、联系人、录音和抄本")一次拍摄太快。在停顿处将音频拆分为短语文件(07a、07b、...),给每个短语自己的镜头,并将每个短语放在自己的节拍上。ds-release-649 的 moreVo 字段实现了这一点。
5. 在完整渲染之前,通过联系表测量点击目标
猜测的坐标在摄像机缩放后通常偏差 20-50 像素。在每个点击帧及其前几帧渲染静态图片,将它们平铺,并检查光标尖端是否位于目标上:
i=1; for f in 46 78 122 212; do npx remotion still MyComp out/stills/s$i.png --frame=$f; i=$((i+1)); done
ffmpeg -start_number 1 -i out/stills/s%d.png -vf "scale=640:-1,tile=2x2" -frames:v 1 out/contact.png
修复常量,重新渲染静态图片,然后才渲染视频。
6. 标注屏幕上的 AI 生成人物
任何不是真实的人(方块中的 FLUX.2 肖像、头像、旁白)都要有可见的标签:Stage 上的 aiNote,或者旁白块上的 "AI" 徽章。姓名、联系人和通话数据也是虚构的,因此保持它们明显是虚构的。
7. 渲染,然后标准化
渲染前使用 tools/qc.py --project ...,然后使用 qc.py --render out/video.mp4 --fix 实现 -14 LUFS。使用 qc.py --project ... --layout 检查视频自身文本(字幕、卡片、AI 注释)的大小和对比度,并单独列出产品 UI。Stage 将其窗口标记为 data-qc="product";在外部构建的任何手机或平板模拟器上也放置相同的属性,否则其 UI 文本将被当作视频的一部分进行评判。
注意事项
- 单色 SVG 徽标渲染为黑色。
logo_mono.svg通常有黑色填充;在深色背景上使用filter: brightness(0) invert(1)。 - 已过时的本地分支。 读取
origin/<default>,而不是本地检出的任何分支。 rsync --exclude src也会删除node_modules/*/src。 复制项目或套件时,使用前导斜杠将排除项锚定到传输根目录:--exclude /src。- 摄像机会进行限制。 靠近画布边缘的摄像机中心会被拉回,以免窗口边缘显示到帧之外。如果缩放落到意外位置,就是这个原因;瞄准更靠内的点。
- 菜单和对话框必须位于画布内部。 画布会裁剪其子元素。将菜单定位为向内打开,正如真实应用那样。
- 悬停状态来自光标路径。 使用
near(cursorAt(frame, PATH), target)而不是第二组帧常量,这样移动关键帧时悬停状态也会随之移动。 - 更改的语音行会移动其后所有内容。 在提示场景中,点击、光标和摄像机会自动跟随,但场景长度和后面短语的
at仍需要检查。在手动设置的场景中,需要重新测量每个帧。无论哪种方式,都要更新头注释和场景长度,然后重新检查点击静态图片。如果由组装语音音轨驱动的口型同步旁白受到影响,也需要重新生成它。 - 套件比演示寿命更长。 重建的屏幕套件可以托管其他视频:
ds-release-notes房间变成了一个恶搞短片品牌版的舞台,其方块中有动画面孔,并且没有新的屏幕工作。将套件复制到新项目中,并将其逻辑画布缩放到帧(.claude/skills/parody-short/formats.md)。