/screen-record — 脚本化浏览器操作演示视频
使用时机
用户希望制作一个网站操作的精致视频,通常用于 LinkedIn、Twitter、销售演示、投资者更新或入门文档。用户描述屏幕上应显示的内容、光标操作以及旁白的大致内容。您编写脚本;引擎负责处理光标动画、通过 ElevenLabs 进行语音合成、背景音乐、点击音效和字幕嵌入。
不要将此技能用于:正在进行的实时屏幕录制(请使用 macOS 的屏幕截图或 QuickTime)、GIF(请使用 GIF 工具)或需要真人配音的视频(您仍可以编写脚本,然后让他们重新录制)。
工作原理
引擎位于 engine.mjs。它导出 runDemo(config),该函数:
- 使用 ElevenLabs 为每个提示生成一个 TTS 音频片段。
- 使用 ffmpeg 生成柔和的背景音乐和 UI 点击音效。
- 启动无头 Chromium,分辨率为 1920×1080,录制视频。
- 运行您的
scenes(helpers)函数,该函数驱动浏览器操作。 - 混合音频(旁白响亮 + 音乐轻柔 + 点击声轻微),并烧录 ASS 格式字幕。
- 返回最终
.mp4、.webm、.srt、.ass文件的路径。
工作流程
第一步 — 阅读用户的请求
确定以下内容:要录制的 URL、流程(光标应如何操作)、语调(正式/随意)、时长目标。如果缺少任何信息,请提出一个有针对性问题,而不是猜测。
第二步 — 检查 API 密钥
按以下顺序查找 ELEVENLABS_API_KEY:
$CLAUDE_PLUGIN_ROOT/.env.local(或技能文件夹自身的.env.local)- 当前项目的
.env.local process.env
如果缺失,请告诉用户:
我需要一个 ElevenLabs API 密钥。请从 https://elevenlabs.io/app/settings/api-keys 免费获取一个(每月 10,000 个字符,足以录制约 15 个演示)。您可以将其粘贴到聊天中,我会将其保存到技能的
.env.local,或者您也可以自己添加。
如果密钥存在但引擎抛出 ApiKeyDeadError(HTTP 401/402/403),请告诉用户:
您的 ElevenLabs 密钥已被拒绝(已撤销、过期或配额不足)。请到 https://elevenlabs.io/app/settings/api-keys 轮换密钥,然后更新
.env.local。在您完成之前,我不会重试。
不要默默重试,除非用户明确要求,否则不要回退到 macOS 的 say 命令。
第三步 — 选择语音
默认是 Liam (TX3LPaxmHKxFdv7VOQHJ) — 充满活力的美国男性,现代风格。比 George(英国男性)和 Sarah(美国女性)更轻快。engine.mjs 中的 FREE_TIER_VOICES 下还有其他免费层级的语音。如果用户说“使用 X 语音”,请在目录中找到该语音。如果他们说的是“深沉/厚重/轻柔/温暖/女性”,请相应地选择。
库语音(Charlotte、Rachel、Adam 等)需要付费的 ElevenLabs 订阅。如果用户要求使用付费语音但他们是免费层级,您将收到 HTTP 402 错误 — 回退到最接近的免费层级语音并告知用户。
第四步 — 探查目标页面(仅在需要时)
如果您不了解页面的结构,请使用 Playwright 或 curl 检查 DOM 并找到选择器。不要猜测 — 选择器很重要,不明确的匹配会导致录制超时。
对于具有动态内容的 SPA,优先使用 aria-label、role 或文本选择器,而不是类名。使用 page.locator(s).first() 语义(引擎的助手已经这样做)。
第五步 — 编写场景文件
在 /tmp/screen-record-<name>.mjs 创建一个文件(不要污染用户的仓库),该文件导入引擎并调用 runDemo。模板如下:
import { runDemo } from '/Users/<you>/Desktop/Projects/skill/screen-record-skills/engine.mjs';
// 或者,如果通过符号链接安装:
// import { runDemo } from '/Users/<you>/.claude/skills/screen-record/engine.mjs';
await runDemo({
baseUrl: 'http://localhost:3000',
outputDir: './output/onboarding-demo',
outputName: 'onboarding',
splash: {
label: '访问我们',
url: 'www.yourapp.com',
footer: 'YourApp — 入门流程',
},
cues: [
{ id: 'intro', text: '欢迎使用 YourApp。让我带您看看。' },
{ id: 'signup', text: '点击“注册”创建一个账户。' },
{ id: 'form', text: '输入您的电子邮件和密码。' },
{ id: 'done', text: '就这样 — 您已成功进入。' },
],
// 可选:模拟后端,使演示具有确定性。
routes: [
{ match: /\/api\/signup$/, fulfill: { status: 200, body: { ok: true } } },
],
// 可选:预填充已认证用户,使演示跳过登录。
// initialAuth: { id: 'u1', email: '[email protected]', name: 'Demo User' },
scenes: async ({ page, baseUrl, sleep, speak, awaitCue,
clickAt, typeInto, moveCursorTo, hideSplash, typeSplashUrl,
configSwitch, showSwitch, hideSwitch, setAuth }) => {
// 场景 0:URL 启动画面(引擎已在镜头外导航到 baseUrl)。
speak('intro');
await typeSplashUrl('www.yourapp.com');
await awaitCue('intro');
await hideSplash();
await sleep(750); // 匹配启动画面的淡出
// 场景 1:点击注册
speak('signup');
await clickAt('button:has-text("注册")');
await awaitCue('signup');
// 场景 2:填写表单
speak('form');
await typeInto('input[type="email"]', '[email protected]');
await typeInto('input[type="password"]', 'CorrectHorseBattery');
await awaitCue('form');
// 场景 3:提交
speak('done');
await clickAt('button[type="submit"]');
await sleep(1000);
await awaitCue('done');
},
});
第六步 — 运行它
cd /Users/<you>/Desktop/Projects/skill/screen-record-skills
# 仅第一次执行:
npm install --silent
# 每次执行:
node /tmp/screen-record-<name>.mjs
每次运行大约需要最终视频时长加上约 10 秒的额外时间。
第七步 — 汇报
告诉用户:
- 最终
.mp4文件的路径(已针对 LinkedIn 优化,1920×1080,H.264 + AAC) .srt文件的路径(LinkedIn 会自动将其作为隐藏式字幕导入)- 在 macOS 上提供使用
afplay <path>播放的选项
助手参考(您的场景可以调用的内容)
| 助手 | 功能 |
|---|---|
page | 原始的 Playwright Page 对象,如果您需要它 |
baseUrl | 配置的基础 URL |
sleep(ms) | await sleep(800) |
moveCursorTo(x, y, durationMs?) | 平滑移动到坐标 |
moveToSelector(selector, durationMs?, offset?) | 平滑移动到元素中心 |
clickAt(selector, durationMs?) | 移动 + 脉冲 + 点击(记录点击时间用于音效) |
typeInto(selector, text, perChar?) | 点击 + 清除 + 逐字符输入 |
recordClick() | 手动记录点击时间戳(用于音效) |
speak(cueId) | 标记提示开始。结束时间为 start + audioDuration + 0.35s |
awaitCue(cueId) | 休眠直到该提示的旁白播放完毕 |
typeSplashUrl(text, perChar?) | 在启动画面 URL 栏中动画打字 |
hideSplash() | 淡出启动画面(之后总是休眠约 750ms) |
configSwitch({ fromInitial, fromLabel, toInitial, toLabel, title, sub }) | 设置账户切换卡片的内容 |
showSwitch() / hideSwitch() | 切换账户切换覆盖层的显示 |
setAuth(user) | 将 localStorage[authStorageKey] 设置为新用户(与 showSwitch 配对用于账户交换演示) |
节奏经验法则
- 每个提示一句话。保持在约 12 个词以内。
- 对于 UI 操作,在操作之前
speak(cue),以便在光标移动时播放旁白。 - 对于打字密集的场景,打字时间比旁白时间长 — 使用
sleep而不是awaitCue来控制节奏。 - 视频总时长通常为
sum(audioDurations) + sum(sleeps)。对于 LinkedIn,目标时长为 30-75 秒。
常见易失效的选择器
- 单独使用
text=Foo可能匹配多个元素。使用page.locator(...).first()这也是引擎已经做的,或者使用button:has-text("Foo")将范围限定到特定标签。 - Tailwind 类名很脆弱。优先使用
aria-label、role、语义标签 + 文本。 - 模态框/对话框:在点击内部之前,等待
[role="dialog"]或模态框的标题文本。
禁止事项
- 不要将实际的旁白文本写入场景文件 — 将旁白保留在
cues[].text中,以便字幕和音频保持同步。 - 不要将光标烧录到页面 CSS 中 — 引擎会注入它。
- 不要在场景中间
await page.screenshot()— 您会捕获到光标在不合适的位置;如果需要,事后使用extractFrames。 - 不要提交
.env.local(它在.gitignore中)。