Remotion 产品视频
一款“无真人出镜”的产品演示(short-video-formats 中的格式 3),完全以代码构建。语音驱动时间:每一句台词被朗读,镜头首尾相接,视频的场景和字幕跟随这些时间戳。UI 使用产品自身的样式在 React 中重建,因此在任何缩放下都清晰锐利,并在产品或脚本变更时重新渲染。除了渲染出的 MP4,所有内容都存在于 Git 中,因此任何旧版本只需检出并重新渲染即可。
输入:Node 22、ffmpeg、TTS(草稿使用 edge-tts,发布前使用有许可的语音)、产品的 CSS token 和标记(通常最好使用落地页的产品演示)、以及脚本。
工作流
1. 设置
npx create-video@latest --blank reel && cd reel && npm i @remotion/google-fonts
保留一个 theme.ts,包含画框(1080×1920,30 fps)、布局条带以及从产品 CSS 复制的颜色 token。我们的条带:标题条居中于 y=118,白色 UI 条从 y=220 开始,高度 1250 像素(占高度的 65%,全宽),字幕居中于 y=1640。
2. 将脚本写成数据
script.json,每个字幕行一个条目;*星号* 标记高亮词:
{ "voice": { "name": "en-US-AndrewMultilingualNeural", "rate": "+10%" }, "gap": 0.16,
"lines": [
{ "scene": "wall", "text": "我不再只是 *提示* Claude Code 了。" },
{ "scene": "wall", "text": "而是 *雇佣* 了它。", "pause": 0.5 },
{ "scene": "end", "text": "雇佣你的第一个 *AI 员工*。", "say": "Crewmark. 雇佣你的第一个 AI 员工。" }
],
"post": { "tiktok": { "caption": "…", "hashtags": [] }, "instagram": { "caption": "…", "hashtags": [] } } }
say 用于语音与字幕不同的情况;pause 让场景在某一台词后停留更久。
3. 语音 → 时间线
一个小的 Node 脚本将每行台词朗读到自己的 WAV 文件,修剪静音,将它们在 gap/pause 之间首尾相接,混合成 public/voice.wav(使用 adelay + amix,然后为社交音量使用 loudnorm=I=-14),并写入 public/timeline.json,包含每行的 from/to(以帧为单位):
{ "durationInFrames": 959, "lines": [ { "scene": "wall", "text": "我不再只是 *提示* Claude Code 了。", "from": 12, "to": 51 } ] }
按文本和语音缓存每次朗读,这样编辑一行时无需重新朗读其余部分。合成物的 durationInFrames 来自此文件。
4. 场景跟随时间线
将连续具有相同 scene 的行分组为一个 <Sequence>;为每个场景提供其行的偏移量(“节拍”),使动画落在相应单词上。字幕是一个读取当前行的组件。
关键组件是 UI 条上的相机:以真实的 CSS 像素布局 UI,然后缩放和平移。
export function Cam({ zoom, x = 0, y = 0, children }: { zoom: number; x?: number; y?: number; children: React.ReactNode }) {
return (
<div style={{ position: "absolute", inset: 0, overflow: "hidden", background: "#fff" }}>
<div style={{ position: "absolute", width: 1080 / zoom + x, height: STAGE_H / zoom + y,
transform: `scale(${zoom}) translate(${-x}px, ${-y}px)`, transformOrigin: "0 0" }}>{children}</div>
</div>
);
}
当 zoom = 1080 / 520 时,一个 520 像素宽的布局(两个 248 像素的卡片)恰好填满宽度。在节拍之间使用缓动插值动画 zoom、x 和 y,以推入被点击的按钮。
效果良好的场景想法:卡片市场、自动填写的表单(带光标的打字效果)、卡片移动的看板、活动线程、自动绘制的图表(标注“演示数据”)、机器人提问你批准的聊天、落地页风格的结果卡。
5. 渲染并查看
node voice.mjs && npx tsc --noEmit
npx remotion still src/index.ts Reel /tmp/f0.png --frame=0 # 封面
npx remotion render src/index.ts Reel out/reel.mp4
ffmpeg -v error -y -i out/reel.mp4 -vf fps=1,scale=200:-1,tile=8x5 -frames:v 1 out/sheet.jpg
查看联系表以及每个场景的静态帧:UI 条完整,条底部无裁剪,字幕在底部条带内,每行对应正确的场景,第 0 帧不为空。
6. 版本控制
提交源代码、脚本和 voice.wav / timeline.json;将 out/ 和下载内容保留在 .gitignore 中。要找回旧版本,检出其提交并重新渲染。
检查清单
- 颜色、字体和文案取自产品自身的 CSS 和文本
- UI 条 ≥ 高度的 50%,全宽;仅上下保留条带
- 已渲染并检查第 0 帧作为封面
- 每个场景展示产品真正拥有的功能
- 演示数字已标注
- 每次渲染后复查联系表
- 源代码和语音已提交;MP4 不提交
我们遇到的陷阱
- 第一个场景的淡入导致第 0 帧(封面)为空。跳过第一个场景的进入动画。
- 相机导致内容被裁剪。 为某个缩放级别设计的图表在场景的缩放级别下超出了条带底部。检查每个相机移动结束时的静态帧,而不仅仅是开始。
- 下半部分稀疏。 在低缩放级别下,短场景使白色条带底部空白。进一步放大或在底部锚定某些内容(如消息输入框、页脚),使条带看起来像真实屏幕。
- 需要 .srt 的审查脚本。 我们旧的 ffmpeg 管道的审查工具读取 Remotion 不写入的字幕时间戳。使用
fps=1,tile的联系表可以完成相同工作。 - 一个样式对象中的重复
transform(按压动画加进入动画)会导致类型检查失败。将元素包裹起来,在每一层上放置一个变换。 - 最终版本使用 Edge TTS。 它免费且适合草稿,但听起来比任何真人语音都机械,并且不附带商业许可。在大规模发布前替换 TTS 函数。
相关技能
- short-video-formats:布局规则以及应构建哪种格式。
- social-publish:保存在
script.json中的发布文案。 - video-retro:下次渲染应更改的内容。