Communitygithub.com

GetOccasion/demo-video-skill

生成带有旁白和字幕的屏幕录制演示视频:通过 agent-browser 录制脚本化的浏览器交互,使用本地 Kokoro 语音合成,并用 ffmpeg 烧录字幕。

demo-video-skill 是什么?

该技能能够将 Web 应用功能转化为一段简短、专业且完全本地的演示视频,包含真实的浏览器交互、语音旁白和字幕。其核心创新在于“旁白即是时钟”:先渲染语音,测量每个场景的时长,然后精确编排每个浏览器动作,使其在语音提到相关内容时恰好执行。技能流程包括理解功能、编写脚本、渲染语音、录制浏览器操作以及最终合成视频。它支持自定义旁白语音(如 `af_heart`、`am_michael`、`bf_emma`),可以适应客户演示或内部团队演示,并提供脚本审核环节以确保故事准确性。整个流程可在 macOS/Linux 上运行,使用 agent-browser、Kokoro、ffmpeg 和 Pillow,无需依赖云服务。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add GetOccasion/demo-video-skill

在你喜欢的 AI 中提问

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

文档

演示视频

将功能转化为一段短视频:真实的浏览器交互、解释屏幕内容的旁白以及字幕。所有操作都在本地完成:agent-browser 录制,Kokoro 朗读,ffmpeg 和 Pillow 合成。

使其工作的核心理念:旁白就是时钟。 先渲染语音,测量每个场景的时长,然后将每个浏览器动作编排到对应词句上。永远不要让旁白去迁就录制的节奏;录制的节奏会漂移,而 WAV 不会。录制本身不会等待旁白:动作连续执行,并在合成时在旁白需要更多时间的位置冻结画面。

工具

本技能的脚本位于其基础目录下的 scripts/ 中,该目录在加载时显示为“本技能的基础目录”;示例场景位于其中的 references/。每个 shell 命令都是全新启动的,因此在一个命令中导出的变量在下一个命令中会消失:每个需要它们的命令都必须以 export SCRIPTS=<基础目录>/scripts DEMO=<演示目录>; 开头,如下面的预检命令:

export SCRIPTS=<基础目录>/scripts; bash $SCRIPTS/preflight.sh

它会检查 agent-browser 及其所需的 Chrome、ffmpeg/ffprobe 及 libx264、Kokoro 的 Python 环境和模型:要么已预安装(设置了 DEMO_VIDEO_VENV),要么有 uv(用于在 Python 3.12 上运行 Kokoro 和 Pillow,无论系统 Python 版本是什么)以及 Hugging Face 的访问权限。如果它退出时返回非零值,请停止:告诉用户缺少什么以及如何安装,不要开始编写脚本、准备数据或录制。未经询问,不要替换其他 TTS 引擎、录制器或无声视频——结果不会是用户想要的效果。它的警告(没有 espeak-ng,没有系统字体)不会阻塞;只需提及即可。

$SCRIPTS/python 在技能 uv.lock 锁定的环境中运行以下 Python 脚本;场景也会读取 $SCRIPTS。将所有工作文件保存在临时存储区中的一个演示目录(DEMO,绝对路径)内,位于仓库之外。如果你只能写入仓库内部,或者视频要附加到该仓库的拉取请求中,则使用一个被 git 忽略的文件夹:在 git check-ignore -q tmp/demo-video 成功时使用 tmp/demo-video/<名称>,否则使用另一个它确认被忽略的文件夹,以确保其中的内容不会被提交。未经询问,不要为此编辑 .gitignore。

工作流程

1. 理解功能并选择故事

阅读分支差异、计划或 PR 描述以及功能面向用户的文案(i18n 文件、工具提示、帮助文本)。工具提示通常能比任何其他东西都更好地定义列和选项——旁白应该与它们逐字一致,说明某个数字的含义。

选择一个主角(一个真正会使用该功能的虚构企业)和一个该功能为他们回答的问题。演示一个从问题到答案的具体场景胜过浏览所有控件。

只询问用户那些会改变视频的内容:观众(客户还是团队)、目标时长、语音,以及他们是否希望在录制任何内容之前阅读脚本。默认值:客户、约 3 分钟、af_heart。跳过他们已经回答或已经变得无关的问题——那些要求从头到尾完成且无需检查,或者显然不在场回答的人,直接使用默认值,不进行脚本审核。

2. 制作能讲述故事的演示数据

空数据或随机数据会使功能看起来有缺陷。构建数据,使其中的每个数字在屏幕上都显而易见:在第一屏就能看到的边缘情况,当选项切换时总量会明显变化。

  • 隔离数据。 为演示数据创建一个单独的数据库,并在自己的端口上运行应用;切勿复用用户的开发数据。首先查找项目自身的约定(CLAUDE.md、关于工作树和开发服务器的指南、种子数据目录)。如果项目有种子框架,编写一个可复用的、按行业划分的演示种子(例如 db/seeds/demos/bike_shop.rb),而不是一次性脚本——这样下一个演示就不需要额外成本。在提交之前先询问。
  • 将所有日期设为相对于今天,这样演示永远不会过时。
  • 历史数据需要时间旅行。 应用会拒绝创建过去的订单或预订;在记录应该真实创建的时刻创建每条记录(例如 Rails 的 travel_to)。注意那些忽略时间旅行的状态:在旅行前加载的实例上记忆的值,以及在创建时计算的状态列。在时间旅行块内重新加载并刷新它们。
  • 对空模式进行快照(例如 createdb -T),这样修复后重新播种就是一次删除和复制,而不是运行迁移。
  • 然后打开页面,检查你打算提到的每个数字。

3. 编写脚本

编写 DEMO/scenes.json:[{"id": "01_intro", "text": "..."}, ...]。每个场景一个想法,共 10–15 个场景。Id 按播放顺序排序。

  • 时长: Kokoro 的语速约为每分钟 150 个单词,每个场景会增加约 1–3 秒的动作和停顿。对于 3 分钟的视频,编写大约 450 个单词;420 个单词大约会生成 2:40 的视频。
  • 按照观众看到的顺序,说出他们正在看的内容。用屏幕上的标签来命名控件。
  • 为耳朵而写:短句,如果数字读起来很奇怪则拼写出来(“百分之二十五”)。
  • 在旁白中遵循项目的词汇规则(词汇表、保留字),就像在代码中一样。
  • 当内容出现在屏幕上时,说出它的名字。 在观众能看到过滤值、菜单选项或记录之前,不要提及它们(当员工列表还关闭时,不要说“Sam 卖出的所有东西”)。在名称之前放置打开菜单的词语,以便场景可以先打开菜单,并在说出该选项时将指针定位到它上面。
  • 以功能自身的问题开头。 第一句话必须是只有这个功能才能回答的问题。“我们这个月卖了多少钱?”也描述了相邻的报告;“哪家商店卖出的入门套件最多?”则不会。在确定问题之前,检查应用的同级功能(其他报告、其他设置页面)。
  • 为指定的受众而写。 客户演示展示他们能做什么;面向营销或客户管理的内部演示还需要涵盖它如何决策、如何启用、它的限制以及如何回答客户的问题。

如果用户要求审核脚本,请在渲染音频之前在此处停止并显示脚本:首先是简短的大纲(每个场景一行:显示的内容和它提出的一个主张),加上你不确定的任何主张,后面是完整的文本供需要的人查看。稍后更改一个句子意味着重新渲染该场景、重新编排场景并重新录制片段,因此这是更改故事成本最低的时机。否则继续;在最终报告中说明脚本位于 DEMO/scenes.json 中。

4. 渲染旁白

export SCRIPTS=… DEMO=…; bash $SCRIPTS/python $SCRIPTS/narrate.py $DEMO

写入 tts/<id>.wav 和包含逐词时间戳的 tts/<id>.words.json,并打印每个场景的时长。使用 --only <id> ... 重新渲染单个场景。语音:af_heart(最自然的美国口音)、am_michael、bf_emma(英国口音)。语音的第一个字母选择语言。

5. 试运行每个交互

在录制之前,使用 agent-browser 驱动每个步骤一次并截取屏幕截图。这可以找出会在镜头中出问题的地方:错误的路由、被覆盖层遮挡的元素、关闭时提交的菜单、当徽章出现时布局的变化。快照引用(@e12)在每次页面加载时都会改变,因此请在场景中使用稳定的 CSS 选择器(id、dom_ids、name=)。优先使用它们而不是 demo.click_role:助手在操作前会将指针滑动到选择器的元素上,但 agent-browser 不会给出角色的位置,因此角色点击会使指针跳跃。

隐藏仅用于开发的覆盖层(分析器、调试栏),并通过在录制开始前加载页面两次来清除一次性闪屏,例如“已登录为…”(demo.prime(url))。

6. 编写场景并录制

场景是 DEMO 中的一个简短 Python 脚本,使用 SCRIPTS/recorder.py。从 references/example_scenario.py 开始。使用 s.at("word") 将动作编排到词语上。标点符号有自己的标记,因此请将提示词设为 "October" 而不是 "October,",并在一个词出现两次时使用 nth=。

只能通过助手移动。 原始的 agent-browser 指针命令会传送指针,鼠标滚轮会跳动页面,这在镜头中看起来像是一次剪辑。demo.move、demo.hover 和 demo.click 将指针滑动到那里,并在到达后返回;demo.scroll 动画化滚动,并在页面静止后返回。因此,每个动作都会完整地出现在录制中,然后下一次冻结会固定一帧。

让观众能够阅读每个菜单。 一个在一秒钟内打开和关闭的列表什么也看不到。使用助手,它们会在打开的菜单上停留,并在选择选项之前将指针放在选项上:

  • 对于自定义 HTML 菜单,使用 demo.pick(open_trigger, option_selector)。
  • 向任一助手传递 scene=s, cue="<word>",以便在旁白说出该选项的名称时恰好将指针放在选项上;菜单大约在两秒前打开。这需要在同一场景中,在提示词之前有两秒的旁白,并且中间不能有慢动作——合成会打印 warning: … runs …s late 当它无法满足时,修复方法在脚本中(“该菜单中的第三个选项将其拆分…”而不是“或者将其拆分…”)或将慢动作移至上一个场景。
  • 对于原生 <select>,使用 demo.pick_native(select_selector, label)。无头录制永远不会绘制原生 select 的弹出窗口,因此它会根据 select 的真实选项绘制一个占位列表,指向选项,然后选择它。

每个片段一个视口。 一个在录制过程中视口发生变化的片段会产生变形的画面。将手机部分录制为自己的片段(demo.viewport(390, 844));合成会将其适配到画布上。之后用一个简短的结尾片段恢复到桌面。

录制每个片段:export SCRIPTS=… DEMO=…; python3 -I $DEMO/scenario.py desktop(以及 phone、outro等)。录制是未计时的:s.at() 和 s.until() 不会休眠,而是记录一个暂停点,因此片段只运行其动作所需的时间(一个 2 分钟的视频在不到一分钟内录制完成),并且损坏的选择器或旁白中缺失的提示词会在几秒钟内失败。这使得单独的试运行变得不必要;直接在片段上迭代。每个片段写入 <take>.mp4 和 <take>.holds.json;合成会在每个暂停点冻结画面,直到旁白达到其提示词。如果某个步骤在片段中途失败,修复场景并重新运行整个片段;暂停点必须来自一次干净的运行。

每个动作必须在下一次提示词之前使页面稳定,因为最后冻结的就是那一帧:在重新加载或提交的菜单之后保留 time.sleep,否则冻结的框架会显示一个半加载的页面。任何按自身时钟运行的内容(淡出的提示、旋转器、倒计时)只有在场景正在执行动作时才会推进,因此暂停会将其冻结在中途。要避免出现在镜头中,在下一个提示词之前等待它完成,就像 prime 对登录通知所做的那样。要显示它,例如出现并淡出的“已保存”提示,在动作后放置一个覆盖整个效果的 time.sleep,使其在下一个暂停之前播放完毕。对于空闲时连续动画的页面,使用 DEMO_TIMED=1 对该片段进行计时录制,它会在录制时休眠直到每个提示词,并写入 <take>.timeline.json。

7. 合成

首先关闭浏览器(agent-browser close):Chrome 和 ffmpeg 的编码器一起可能会超出小型机器或容器的内存。

export SCRIPTS=… DEMO=…; bash $SCRIPTS/python $SCRIPTS/assemble.py $DEMO --takes desktop phone outro --out <名称>

生成包含烧录字幕、软字幕轨道和旁白的 DEMO/build/<名称>.mp4,以及 <名称>.srt。

8. 交付前检查

你可以看到视频,但听不到音频。检查你能做到的事情:

export DEMO=…; ffmpeg -v error -y -i $DEMO/build/<名称>.mp4 -vf "fps=1/6,scale=480:-1,tile=4x8" -frames:v 1 $DEMO/build/grid.png   # 阅读它
export DEMO=…; ffmpeg -i $DEMO/build/<名称>.mp4 -map 0:a -af volumedetect -f null - 2>&1 | grep volume                              # 不是静音

在网格图中,确认每个场景都显示其旁白描述的内容,字幕从未覆盖被指向的内容,并且没有帧捕捉到半加载的页面或错误。在提示时间点(-ss <t>)抓取关键单帧。

然后停止开发服务器并将视频交付:在用户自己的机器上,将其复制到他们能找到的地方并 open;在其他地方,给出其路径,或者如果你的工具支持,将其附加到拉取请求。当你报告时,说明你无法验证的内容(通过耳朵听音频)并列出任何采取的捷径,例如占位菜单。

已经解决的陷阱

  • 当页面仍在加载时,agent-browser click 会失败并显示“被覆盖”。recorder.ab 会重试;在重试后仍然失败的才是真正的失败,因此修复选择器。
  • 关闭时提交的菜单,例如在 Escape 时提交的复选框列表,需要几秒钟重新加载。在下次点击之前等待它完成,否则该点击会落在一个移动的按钮上。
  • Overmind 拒绝在过时的 .overmind.sock 上启动。仅在确认没有进程响应它之后才移除该 socket(overmind status)。
  • 过长的临时区路径会超出 Unix socket 限制;不要将 socket 指向那里。
  • Kokoro 会将长文本分割成块;narrate.py 会偏移每个块的单词时间戳,因此请按单词进行提示,切勿按猜测的秒数。

相关技能