Skip to content
Public template

About

Remotion 讲解视频模板:[[cue]] 标记 + edge-tts 逐词时间戳驱动字幕与动画

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

explainer-kit

用 Remotion 做讲解视频和短片广告的模板。讲解片的旁白写在 tts/script.json 里,用 [[cue]] 标出动画触发点;edge-tts 合成语音时记下每个词的出口时间,生成 src/timings.json。场景代码只问「这个 cue 在第几帧」「这个词在第几帧」,不写死任何秒数,所以改旁白、重跑语音之后,动画和字幕自动跟上。没有旁白的 15 秒短片和广告走另一条时间线:promo/promo.json 写节拍和小节,画面的每个重拍和配乐的鼓点落在同一张节拍网格上(见「短片模式」)。

仓库自带一支约 3 分钟的演示片(9 场),每场换一套风格包:film(电影)、slides(幻灯片 + KaTeX 公式)、paper(手绘笔记)、neon(霓虹终端)、editorial(杂志排版)、math(数学推导)、keynote(发布会)、pixel(像素游戏)、ink(水墨)。演示片的时间轴和音频是用 --mock 生成的静音占位,克隆下来不用联网合成就能预览。另有一支 15 秒竖屏短片 Promo(7 场,128 BPM,8 小节),用 punch 风格包给模板自己做的宣传片,node tools/promo.mjs 一条命令出带配乐、母带处理过的成片。

先读 docs/principles.md:八条原理讲清楚这个模板为什么这样设计,看懂它,后面的组件和工具都能推出来。

快速开始

npm i
npm run studio          # 打开 Remotion Studio,直接看演示片(静音占位音轨)

想要真实配音和正式字体:

npm run fonts                       # 下载 Noto Sans SC / Noto Serif SC 可变字体到 public/fonts/(OFL,不进仓库)
pip install -r requirements.txt     # edge-tts、numpy、scipy、Pillow、pytest(potracer 只在描 logo 时用)
python tts/gen.py                   # 真 edge-tts,需要联网和 ffmpeg;写 public/audio/*.mp3 + src/timings.json

不下载字体也能跑:字体栈会退回系统字体(微软雅黑、苹方等),日志里那两条 fonts/*.ttf 404 属于预期。

目录

路径 作用
kit.config.json 讲解片的唯一配置:合成 ID、尺寸、帧率、旁白音色与语速、引语音色、停顿时长、字幕行长、--mock 语速、每场前后留白、品牌色、画幅、音效音量、配乐节拍、响度、字体
promo/promo.json 短片的唯一配置:尺寸、bpm、调性、响度目标、品牌文案、每场小节数 / 能量 / 转场 / cue(「小节:拍」)
tts/script.json 旁白稿,每场一个 {id, chapter, text}
tts/gen.py 旁白 → 音频 + src/timings.json;--mock 离线估算
src/core/ 与风格无关的核心:时间轴 API、动效词汇(motion.ts)、跟旁白同步的文字(Spoken)、镜头与转场(Shots)、Video 外壳、布局助手、Tex、配色与字体栈
src/film/ 电影风格包(可删)
src/slides/ 幻灯片风格包(可删)
src/paper/ 手绘笔记风格包(可删)
src/neon/ 霓虹终端风格包(可删)
src/editorial/ 杂志排版风格包(可删)
src/math/ 数学推导风格包(可删)
src/keynote/ 发布会风格包(可删)
src/pixel/ 像素游戏风格包(可删)
src/ink/ 水墨风格包(可删)
src/punch/ 动态排版风格包:砸字、拆色故障、跑马灯、环形徽章、商品轮播、图案填字、logo 揭示、CTA 条(可删)
src/promo/ 短片:节拍网格(grid.ts)、外壳和转场(Promo.tsx)、演示短片的 7 场(scenes/)
src/brand/logo.json 品牌 logo 的矢量部件(tools/trace-logo.py 生成;仓库里是手画的演示标志)
src/scenes/ 你的场景;index.ts 是登记表
tools/ 一键出片、新建场景/风格包、审图、拼帧、听音效、配乐、混音、母带、响度报告、音效合成、描 logo、广告文案检查、下载字体
public/fx/ 胶片颗粒和纸张纹理
public/sfx/ 音效库(tools/sfx.py 合成,无版权问题)
.claude/skills/ 给 Claude Code 的项目 skill:写一场、审片、新风格包、出片、做短片

配置 kit.config.json

字段 含义
id width height fps 合成参数。npm run render 默认输出 out/<id>.mp4
voice rate 旁白音色和语速,例如 zh-CN-YunjianNeural、+10%
voices 引语音色表,键名就是 `<<key
pause `
maxLine 字幕每行最多字数
mockCharsPerSec --mock 估算语速,默认 4.2 字/秒
pace.lead / pace.tail 每场旁白前(放章节卡)和旁白后的留白帧数;leadOverride / tailOverride 按场覆盖
brand name(片尾、跑马灯里的名字);accent / accent2(#rrggbb,填了就替换所有风格包的主色和副色,null 保留各包原色)
theme 按包微调单个颜色,例如 {"neon": {"cyan": "#00FFC2"}},键名见各包调色板(C P N E M K PX I)
formats 额外画幅,每个会多一个合成 <id>-<键名>:默认 9x16(1080×1920)和 1x1(1080×1080);zoom > 1 时放大画面、裁掉两侧;loudness 覆盖这个画幅的响度目标(默认竖屏 -14)
sfx.volume 音效总音量(0–1)
music.bpm 配乐节拍;场景里 useBeat() 用同一个节拍网格
loudness tools/mix.py 的母带目标:target(LUFS,默认 -16)和限幅天花板 ceiling(dBFS,默认 -2.0,给 AAC 编码留余量)
fonts sans / serif / mono 的字体名、文件、下载地址和后备字体

src/Root.tsx、tools/*.mjs、tools/*.py、tts/gen.py 都从这里读,不要在别处写死这些值。

旁白标记

{"id": "s01", "chapter": "第一场 · 旁白就是时间轴",
 "text": "[[open]]做讲解视频……[[quote]]有位剪辑师说得好:<<q|别对齐画面,对齐声音。>>||[[ask]]画面只需要问一句……"}
标记 含义
[[name]] cue:动画或镜头切换点,记录的是它后面第一个词的出口时间。名字只能是字母、数字、下划线;end 是保留名
|| 戏剧停顿,时长取 pause,可以连写叠加
<<key|……>> 这一段换成 voices[key] 的音色读;字幕里这一行会用衬线体加「」

chapter 用 · 分成小标题和大标题,显示在章节卡上。lead 不超过 30 帧的场景不出章节卡(适合冷开场)。

时间轴 API

场景里调用 useScene(),返回的帧数都以本场开头为 0:

函数 返回
c('cue') 这个 cue 所在的帧
w('词', n?) 旁白第 n 次(从 0 数)说到这个词的帧
rel('词', 'cue') 从这个 cue 到它之后第一次说到这个词,隔了多少帧
f total end lead 当前帧、本场总帧数、旁白结束帧、开场留白帧数

配套的动画助手:ease(f, at, dur) 缓动 0→1,pop(f, at) 带回弹,fu(p) 上浮淡入样式,lerp(p, a, b),window_(f, a, b) 在 a..b 之间可见并带淡入淡出,num(v) 千分位。更完整的动效词汇见下面的「动效」。

const {f, c, w, rel} = useScene();
const title = ease(f, c('idea'));               // cue 一到,标题淡入
const stampAt = w('重新对一遍');                 // 说到这个词时盖章
// 在 film 的 Shots 里,镜头内时间 t 从 cue 起算,所以用 rel:
<Stamp t={t} at={rel('重新对一遍', 'old')} text="全部返工" x={1180} y={610} />

w() 找不到词、c() 找不到 cue 都会直接抛错,改稿后 Studio 里会立刻报出来。

动效

讲解视频的画面最容易「死」:元素淡入以后一动不动,下一个镜头靠叠化硬切过去,屏幕上的大字按固定速度打出来、跟嘴型对不上。src/core 提供一套跟风格无关的动效词汇来避开这些问题,所有风格包都能用。

进场和退场(motion.ts)

函数 作用
tween(f, at, dur, ease) 0→1,ease 可选 out(进场)、in(退场)、inOut(运镜、转场)、expo(大标题)、back(回弹)
life(f, at, out) 返回 {p, q}:进场进度和退场进度。要离场的元素都应该给 out
move(kind, p, q) 把进度变成样式:rise fade scale blur left right drop。退场沿进场方向继续走(升上来的就升出去),不会原路倒回
stagger(at, i, gap) 第 i 个元素的起始帧,用于错峰
springAt(f, at, kind) 弹簧:soft 无回弹、snappy 一点、bouncy 很多
drift(f, seed) 缓慢的有机漂移(CSS transform)。元素落定后加一点,就不会冻住
pulse(f, period) 0..1 的正弦呼吸
punch(f, at) 说到某个词时放大一下再回落
shake(f, at) 衰减的镜头震动(盖章、砸数字)
noise(seed, x) rng(seed) 确定性的平滑噪声和随机数
const {p, q} = life(f, c('cards'), c('formula'));      // 卡片在 formula 时离场
<At x={160} y={330} style={move('rise', p, q)}>…</At>

跟旁白同步的字(Spoken.tsx)

<Spoken text="让旁白自己当时间轴" mode="blur" /> 让每个字在旁白念到它的那一帧出现,用的就是字幕的逐词时间,不再是固定的每秒几个字。text 必须是旁白里原样出现的一段(可以加 \n 换行);找不到会抛错。mode:rise blur fade scale drop pop(弹出),或 ink(先显示暗字,念到才点亮)。hot={['关键词']} 让关键词念到时变色。useSpoken().chars(text) 返回每个字的帧,span(text) 返回整段 [开始, 结束]。film 的 BigQuote 加 spoken 就按引语的配音逐字出现。

镜头和转场(shots.tsx)

Shots 按 cue 切镜头,每个镜头自带本地时间 t 和长度 d。转场 tr:

转场 效果
fade / slow 叠化(12 / 26 帧)——两个相似的画面叠化会有重影,换别的
cut / flash 硬切 / 白闪切
black 先落到底色再出来
push / up 新镜头把旧镜头横向 / 向上推走
zoom 旧镜头冲过镜头,新镜头落定
wipe 带亮边的划像
whip 快速甩镜,带运动模糊
iris 新镜头从画面中心的圆里打开

dur 可以按镜头覆盖转场时长。Shots 原来在 film 包里,现在在 core,film 继续从原位置导出。

几条动效原则(多数来自整理 Opus 5.5 生成视频提示词的 awesome-opus5-5-videos 里反复出现的要求):

  • 不要死帧:超过 1 秒什么都不动,观众就会走神。落定的元素加 drift,背景自己带一点不停的动作。
  • 能变形就别切:同一个形状改尺寸、圆角、颜色,内容跟着换(keynote 包的 Morph),比叠化到另一张图更连贯。
  • 容器先动,内容后进;内容先走,容器再变。这样文字永远不会和下一个状态叠在一起。
  • 弹簧只要一点点回弹;大回弹留给少数需要「砸」的时刻。
  • 镜头跟着重点走:推近到正在讲的那一块(keynote 的 ZoomCam、film 的 Cam)。
  • 每个画面变化都落在旁白的 cue 或词上;有配乐的片子,也可以落在节拍上。

音效和节拍(Sfx.tsx)

<Sfx at={帧} name="pop" volume={0.6} /> 在本场某一帧播放 public/sfx/pop.wav;同一个声音打在多帧用 <SfxAt frames={[...]} name="tick" />。声音库:click pop whoosh whip swish thud slam tick type coin levelup glitch chime draw,都由 python tools/sfx.py 用 numpy 合成(改配方后重跑)。放在场景最外层,不要放进 Shots 的镜头里,镜头切走会把声音截断。Shots 会给移动类转场自动配声音(push/up/zoom 配 whoosh,whip 配 whip,wipe/iris 配 swish),sfx={false} 关掉。音效跟画面一起进渲染出来的音轨,tools/mix.py 再把配乐压在下面。

配乐(tools/music.py)在 music.bpm 的节拍网格上加了很轻的鼓点。场景里 const beat = useBeat():beat.next(帧) 把事件吸附到下一拍,beat.nearest(帧) 吸附到最近一拍,beat.pulse() 每拍一次从 1 衰减到 0(neon 的地平线、pixel 小人原地踏步都用它)。

审动态:静帧只能看到元素落定后的样子,看不到它怎么动。node tools/strip.mjs s01:quote 把 cue 前 6 帧到后 84 帧、每 6 帧一张拼成一张图(场景:cue:起:止:步长,最多 16 格),一次渲染出结果,输出 out/strips/<scene>_<cue>.png。重点看三件事:cue 之后有没有一两秒什么都没有;元素是不是落定之后就再也不动;两个镜头叠化时有没有重影。

工作流

  1. 写稿:编辑 tts/script.json。
  2. 合成:python tts/gen.py(只重做某几场:python tts/gen.py s02;离线占位:python tts/gen.py --mock)。
  3. 预览:npm run studio。
  4. 审图:node tools/stills.mjs s01 s02 在每个 cue 后约 2.5 秒出一张图(OFF=30 node tools/stills.mjs 改偏移),python tools/sheet.py s01 拼成 2×2 的检查表;只看某几帧用 node tools/pick.mjs s01:quote:40 s02:end:-10(场景:cue:偏移帧,end 表示旁白结束);看动态用 node tools/strip.mjs s01:quote(见「动效」)。输出在 out/stills、out/sheets、out/pick、out/strips。 听音效:node tools/listen.mjs s05 渲染这一场的音轨,列出每个声音离哪个 cue 多少帧;看全片:node tools/overview.mjs(--format 9x16 看竖屏)。
  5. 渲染:npm run render(带 --gl=angle)→ out/<id>.mp4;竖屏 npm run render -- 9x16 → out/<id>-9x16.mp4。
  6. 配乐:python tools/music.py → out/music.wav。按 timings 每场一段和声,每个章节开头一声轻击,重配音后重跑即可对齐。
  7. 混音:python tools/mix.py → out/<id>-mixed.mp4(竖屏 python tools/mix.py 9x16)。配乐低通后按旁白做 sidechain 压缩,再交给 tools/master.py 做母带:测响度 → 线性加增益 → 前瞻限幅,到 loudness.target(横版 -16、竖版 -14 LUFS),并在编码后的文件上确认真峰值 ≤ -1 dBTP(见「声音」)。
  8. 听报告:python tools/audio-report.py out/<id>-mixed.mp4 打印整体和每一场的响度,画出波形和频谱(out/audio/)。

交付前:npm run verify。依次检查类型、源文件一致(旁白稿 ↔ 时间轴 ↔ 音频 ↔ 登记表 ↔ 音效库,短片的节拍网格 ↔ 登记表 ↔ 广告文案)、每场在每种画幅下都能渲染、同一批帧渲染两次逐字节一致,再对短片做同样两关(每个 cue 渲染一帧拼成 out/verify/promo.png,两帧各渲染两次比对)。--quick 只跑前两项;示意图在 out/verify/。

第 2、5、6、7 步可以一条命令跑完:node tools/publish.mjs(--formats 9x16,1x1 或 all 同时出其他画幅,--skip-tts 沿用现有配音,--mock 离线跑通流程)。

短片模式:没有旁白,按节拍走

讲解片的时间来自旁白;15 秒的短视频、广告、品牌片没有旁白,时间来自配乐的节拍。这一块是从两条真实的水果店广告里提炼出来的:整条片子按 128 BPM、8 小节设计成正好 15 秒,每一下砸字、每一次硬切、每一个转场都落在鼓点上,音效跟着画面的动作走,最后用 logo 揭示和行动号召收尾。

node tools/promo.mjs          # 验收(quick + 安全区) → 渲染 Promo → 按节拍写配乐 → 母带到 -14 LUFS → 响度报告 → 导出封面
node tools/promo-sheet.mjs    # 按手机上看到的样子审片:第 0 帧、每个 cue 定住后、转场中点、封面,
                              # 平台 UI 遮挡区涂红,出安全区的字框红 → out/promo/sheet.png
npm run studio                # 合成 Promo 就是短片

成品是 out/Promo-mixed.mp4。

写稿 promo/promo.json:bpm、key(配乐调性,Am、C……)、width/height(默认竖屏 1080×1920)、loudness、brand(名字、标语、CTA),以及每一场:

{"id": "p4", "bars": 1, "energy": "break", "in": {"tr": "flood", "beats": 1, "color": "#FF5B1F"},
 "cues": {"col": "0:0", "say": "0:1", "tag": "0:2"}}
字段 含义
bars 这一场几小节(4 拍一小节;可以是 1.5,但必须是整拍数)
energy 配乐在这一场怎么编:intro(暗的 pad,每个 cue 一下底鼓加人声切片)、groove(四踩、拍手、贝斯、拨弦主旋律)、break(抽掉底鼓,让下一个重拍更响)、build(军鼓滚奏 + 滤波打开,最后一拍留空)、drop(完整律动加人声切片,全片最后一拍收在长和弦上)
in 进这一场的转场:cut、iris(圆形展开)、wipe(斜切)、flood(液体从下往上涨满,液面在前、新画面跟在后面)、up。转场在重拍之前播 beats 拍,重拍那一帧正好切完;外壳自动配声音
cues 「小节:拍」,都从 0 数,拍可以有小数("0:2.5" 是第 3 拍的后半拍)

写场景:src/promo/scenes/p1.tsx……在 src/promo/scenes/index.ts 登记。场景里:

const {f, cue, at, beat, len, every, pulse} = usePromo();
<Slam at={cue('c')} size={300} split={[PU.sage, PU.sky]}>不将就</Slam>   // 恰好在这一拍砸到位,带拆色故障
<Sfx at={cue('c')} name="impact" />                                    // 同一帧的声音
<SfxAt frames={every(0.25)} name="tick" />                             // 每个 16 分音符一下

f 以这一场的重拍为 0;转场播放时,外壳把下一场冻结在第 0 帧显示,所以进场动画从 0 开始写。整片跨场的东西(进度条)用 abs / total。

素材从哪来:logo 用 python tools/trace-logo.py logo.png(截图也行,自动去掉窗口边框;--split 把字标拆成一个个字)描成 src/brand/logo.json,<LogoReveal> 先描边、再填色泛光、指定部件从上方落下。不要找字体去代替 logo 里的字。广告文案先过 python tools/copy_lint.py:「最好」「第一」「唯一」「100%」这类绝对化用语,《广告法》第九条不允许用(只是词表,产地、价格、「每日上新」这类事实还要人确认)。

声音:按交付标准量出来

画面有逐帧检查,声音也要有数(docs/principles.md 原理 7)。

工具 做什么
tools/sfx.py 音效库,numpy 合成,无版权问题。这次新增给短片用的 impact(重拍)、riser(1.5 秒上升音,结束在重拍上:at = 重拍 - 45)、revcrash(0.5 秒反向镲:at = 重拍 - 15)、shimmer(闪光琶音)、rush(液体涌入,flood 转场自带)
tools/synth.py 配乐的乐器和总线:分层底鼓、拍手、开闭镲、军鼓、七声部超锯齿、带滤波包络的拨弦、正弦 sub 加锯齿上层的贝斯、共振峰合成的人声切片(ah/oh/eh)、能量归一的卷积混响、按底鼓做的侧链
tools/score.py 短片配乐:按 promo.json 的节拍、调性和每场 energy 编曲,写 out/promo-score.wav,并按段打印每条总线的 RMS
tools/master.py 母带:测响度 → 线性增益 → 前瞻限幅 → 再测,直到差 0.3 LU 以内;编码成 AAC 后再测真峰值,超过 -1 dBTP 就降天花板重做。mix.py 和 promo.mjs 都用它
tools/audio-report.py 整体响度、响度范围、真峰值,每一段的响度条,波形图和频谱图;--target 偏离 1 LU 以上或真峰值超标就报错

怎么读:总线 RMS 里混响或 pad 比鼓还响,声音一定是糊的;波形是一条平带说明压过头了;铺垫段应该凹下去,重拍应该跳起来;某一场比相邻的响 3 LU,就是那一场的音效没配平。

画幅和品牌

场景永远按主画幅(1920×1080)写。竖屏和方屏由外壳排版:16:9 画面放在中间,上面是章节标题,下面是按行长自动放大的字幕,最底下一条全片进度条,背景用这一场风格包的底纹铺满。内容集中在画面中间的场景,可以在登记表里放大裁边:s07: {component: S07, look: keynote, portrait: {zoom: 1.3}}(focus 0–1 决定裁剪时保留哪一侧)。

换品牌色只改 kit.config.json:"brand": {"name": "你的产品", "accent": "#FF5A1F", "accent2": "#7B61FF"},九套风格包的主色、副色一起换掉;单个颜色再用 theme 微调。每个包把哪些颜色当作主色,写在它调色板的 themed() 第三个参数里。

自动化和 skill

反复手动做的事都收成了脚本:

命令 做什么
node tools/new-scene.mjs s10 --look paper --chapter "…" --text "[[a]]……" 追加旁白、生成能直接跑的场景文件(每个 cue 一段跟着旁白出现的字)、登记、跑 mock 时间轴
node tools/new-pack.mjs chalk --base "#1F2B26" --accent "#F2C14E" 生成一个新风格包骨架:接好品牌色的调色板、会动的背景、章节卡、组件、Look
node tools/publish.mjs --formats all 配音 → 验收 → 各画幅渲染 → 配乐 → 混音 + 母带
node tools/promo.mjs 短片:验收 + 安全区 → 渲染 → 节拍配乐 → 母带 → 响度报告 → 封面 out/<id>-cover.png
node tools/promo-sheet.mjs 短片审片:第 0 帧、每个 cue 定住后、转场中点、封面各一帧,量出安全区外和被切掉的字
python tools/trace-logo.py logo.png --split 把 logo 图片描成矢量部件(src/brand/logo.json)
python tools/copy_lint.py 广告文案的绝对化用语检查
python tools/audio-report.py 文件 --target -14 响度、真峰值、分段响度、波形和频谱
npm run verify 交付前的验收:类型、源文件一致、每场可渲染、渲染结果可复现,短片同样两关
node tools/strip.mjs / overview.mjs / listen.mjs 审动态 / 看全片 / 听音效

.claude/skills/ 里有五个项目 skill,在 Claude Code 里打开这个仓库就能用:new-scene(从文稿到一场)、review(审片清单)、new-style-pack(新风格包的要求)、publish(出片和踩过的坑)、new-promo(做一条按节拍走的短片或广告)。

风格包

外壳 src/core/Video.tsx 按场景登记表里的 look 给每一场「穿衣服」:底色、背景层、画面上的叠加层(颗粒、暗角)、最上层的框(黑边)、章节卡、字幕样式。核心代码从不 import 风格包,不用的包整个删掉即可。

  • film(src/film/):Shots 按 cue 切镜头(转场见「动效」),Cam 缓推运镜(hand 加手持晃动),Graded 调色(past/warm/cold/dusk/neutral),Grain、Vignette、Letterbox(2.2:1),Place 年份地点字幕,Tag、Line、BigQuote,Paper、Doc、Stamp、Mark、InkBars、Count(Doc 的行可以 strike: 帧号,到时用红笔划掉),背景 Sky、Glow、Void、Desk,章节卡 Chapter。字幕放在下黑边里。

  • slides(src/slides/):Panel、Chip、Kicker、H,点阵背景和简单章节卡;字幕是半透明底条,按逐词时间点亮。

  • paper(src/paper/):手绘笔记。米色方格纸底;RoughBox、RoughCircle、RoughArrow、RoughLine、Squiggle、LabelBox 是会一笔一笔画出来的手绘线条(放在 <Svg> 里,p 是绘制进度),画完以后每 4 帧重抖一次(BOIL),像手绘动画那样不会僵住;Note 便利贴带回弹落下、可以 out 撕走;Marker 荧光笔;Hand 手写感标题。适合讲流程、因果。

  • neon(src/neon/):深色科技。会一直往镜头滚的透视网格地面、星空、扫描线;Neon 霓虹字(像灯管一样闪着亮起);Term 终端窗口逐字敲命令;Net 节点图,节点带波纹弹出、连线画出后有数据脉冲一直流动;Glitch 包住整个镜头做故障切换;Brackets HUD 角框。适合讲系统、架构。

  • math(src/math/):数学推导,Manim 的路子。深色底;Axes 网格从原点展开、坐标轴画出来;Plot 把函数曲线一笔描上去;Tangent 切线和点贴着曲线走;Readout 实时数值;Steps 一行一行推导 KaTeX 公式,最新一步点亮、旧的变暗。坐标映射用 Plane + px()/py()。适合讲数学、物理。

  • keynote(src/keynote/):发布会 / 产品演示。暖灰底加缓慢漂移的色块;Morph 是贯穿全场的一个形状,按状态表在 logo、按钮、进度条、玻璃卡片之间弹簧变形,内容带短模糊切换;Cursor 光标滑到目标并点击(带按下和波纹);ZoomCam 屏幕录制式推拉镜头;Rise 文字从遮罩线下升起。chain() 把多次改变的弹簧叠加起来,中途改目标也不会跳。适合讲产品、做功能演示。

  • pixel(src/pixel/):像素游戏。所有东西画在 320×180 的 canvas 上再无平滑放大 6 倍,连中文也先栅格化再二值化成硬像素。drawWorld 画天空、云、两层视差山和草地(scroll 跟随镜头);hero 会走路的小人、coin 旋转金币、bar 分格经验条、box RPG 对话框;精灵用字符串画。场景里用 <PixelCanvas draw={(g, f) => …}> 逐帧画,对话框的字用 useSpoken().chars() 跟旁白出现。适合轻松科普、儿童向内容。

  • ink(src/ink/):水墨。宣纸底;Mountains 几层远山各自从雾里晕开(先模糊后清晰)并缓慢漂移,Mist 雾带,Birds 飞鸟;Brush 带飞白的毛笔笔触;InkBlot 墨滴在纸上洇开;VText 竖排文字(配 Spoken 念到哪里墨落到哪里,\n 换列);Seal 红色印章按下。适合讲历史、诗词、传统文化。

  • editorial(src/editorial/):杂志排版 / 动态文字。新闻纸底色、黑红两色;MaskText 大标题逐行从遮罩下升起(也能逐行升出去);Slam 大数字砸进画面,hits 在说到某个词时再顶一下;Block 色块推进来再从另一侧推出去(用来遮住换版);Rule 画线;Label 小标签;Ticker 跑马灯。适合讲观点、金句、数据。

  • punch(src/punch/):现代动态排版,给短片和广告用,也能放进讲解片的一场。深墨绿底加缓慢漂移的网格;Slam 大字在指定帧砸到位(加速进场 + 落地回弹,split 加两层拆色故障);Mono 等宽小标签;Hud 裁切角标、标签和进度线;Marquee 斜着横穿画面的跑马灯条,能沿自身方向冲进来、再折叠收起;Badge 绕圈转的环形文字;CardStrip 商品卡片轮播,按给定的帧一张张滑过去,高速时带水平运动模糊、停下时清晰;PatternText 用流动的图案填满大字;Wipe 擦入;LogoReveal 描边 → 填色泛光 → 部件落下;CtaBar 从底部推上来的行动号召跑马灯。适合广告、短视频开场、金句。

所有包共用 src/core 的 At(center 时居中位移会和你传入的 transform 叠加)、Full、Svg、DrawLine、Tex、Shots、Spoken 和动效函数。film 和 slides 的配色在 src/core/theme.ts,新包各自带调色板(P、N、E、M、K、PX、I)。

字幕样式由风格包的 look.subtitles 决定(浅色底的包用 boxColor 换成深色底条),单场可以在登记表里覆盖,例如 s01: {component: S01, look: film, subtitles: {hideQuotes: true}}:这一场已经把引语大字放在画面上,就不再给引语配字幕。

选哪套:叙事、人物用 film;概念、API 用 slides;流程、因果、拆解步骤用 paper;系统、架构、数据流用 neon;观点、金句、关键数字用 editorial;公式推导、函数图像用 math;产品介绍、操作演示用 keynote;轻松科普、儿童向用 pixel;历史、诗词、传统文化用 ink;广告、短视频、金句砸字用 punch。同一支片子可以按场混用。

写一个新风格包

node tools/new-pack.mjs <名字> --base <底色> --accent <主色> 生成骨架,再照着 src/paper/index.tsx 这类成熟的包扩展:调色板走 themed()(品牌色才能替换)、背景带一点不停的小动作(滚动、闪烁、纸纹跳动)、章节卡、几个道具组件,最后导出一个 Look。道具只用 useCurrentFrame() 和传进来的帧号,不要用 Math.random()。要求清单见 .claude/skills/new-style-pack/SKILL.md。

加一个场景

最快:node tools/new-scene.mjs s10 --look slides --chapter "第十场 · 标题" --text "[[a]]……[[b]]……",下面三步它都会做好。手动的话:

  1. 在 tts/script.json 里加一项,比如 {"id": "s03", "chapter": "第三场 · 标题", "text": "[[a]]……[[b]]……"},跑 python tts/gen.py s03(或 --mock)。

  2. 新建 src/scenes/s03.tsx:

    import React from 'react';
    import {At, ease, fu, useScene} from '../core';
    import {H, Panel} from '../slides';
    
    export const S03: React.FC = () => {
      const {f, c, w} = useScene();
      return (
        <At x={960} y={540} center style={fu(ease(f, c('a')))}>
          <Panel><H>{f >= w('关键词') ? '说到了' : '还没说到'}</H></Panel>
        </At>
      );
    };
  3. 在 src/scenes/index.ts 登记:s03: {component: S03, look: slides}。场景顺序以 tts/script.json 为准。

踩过的坑

  • 渲染一定带 --gl=angle。走 GPU 合成;纯软件 GL 大约慢 5 倍(上一个 21 分钟的片子:25 分钟对两个多小时)。npm run render 已经带上。
  • 超过 10 分钟左右的渲染要脱离终端跑,看日志,别挂在会超时的后台 shell 上,超时会把渲染一起杀掉。Git Bash:nohup npm run render > out/render.log 2>&1 &;PowerShell:Start-Process npm.cmd -ArgumentList 'run','render' -RedirectStandardOutput out/render.log -RedirectStandardError out/render.err -WindowStyle Hidden,然后看 out/render.log。
  • edge-tts 原始输出只有 -24 LUFS 左右,直接发布会明显偏小声。发布前一定跑 tools/mix.py(或至少做一遍 loudnorm)。
  • 一场的第一个 cue 太晚,开场会空 3–6 秒。镜头从 cue 才开始画的话,章节卡淡出后到第一个 cue 之间什么都没有。第一个 cue 放在文稿最前面,或者让第一个镜头从第 0 帧开始(演示片 s01 就是这样)。
  • 标题用 Spoken 等念到才出,前面那半句也会空。「换一套衣服:手绘笔记风」里标题要等一秒多才念到。给开头那半句也配上字(演示片 s03–s05 的小标签),或者把标题提前放好、用 mode="ink" 念到再点亮。
  • 相似画面别用 fade 转场。两张纸、两组卡片叠化时会叠出重影;用 push,或者先让旧元素 life(..., out) 离场、再让新元素进场(演示片 s02)。
  • 只看静帧审不出动效问题。静帧默认在 cue 后 2.5 秒,那时一切都已落定。用 tools/strip.mjs 看 cue 前后一段。
  • 没装 Remotion 自带浏览器的环境(比如云端沙箱):tools/ 里的渲染脚本会自动用 /opt/pw-browsers 里预装的 headless shell,也可以设 REMOTION_BROWSER=/path/to/chrome-headless-shell 指定。
  • 音效放进 Shots 的镜头里会被截断:镜头一切走就卸载了。<Sfx> 放在场景最外层。
  • 多了几个合成以后,直接 remotion render src/index.ts 会问你渲染哪一个。用 npm run render(tools/render.mjs),它按 kit.config.json 选好合成和输出路径。
  • zh-CN-YunyangNeural 在 +6% 时只有约 3.3 字/秒,听着拖。用它的话语速调到 +15% 左右。
  • --mock 的时间只是估算(每字 1/4.2 秒),用来搭画面足够,但换成真配音后每个 cue 的位置都会变;按 cue 和词取帧的写法不受影响,写死的帧数会错位。
  • 只重做某几场时,其他场沿用旧 timings:python tts/gen.py s02 不会动 s01 的音频和时间。
  • cue 名 end 是保留的(工具里表示旁白结束),gen.py 遇到会直接报错。
  • 场景里别用 Math.random(),要随机就用 film 包的 rng(seed) 或 Remotion 的 random(seed)。渲染是多进程逐帧并行的,同一帧必须画出同样的东西。
  • edge-tts 偶尔断线,gen.py 每段会重试 10 次,间隔递增。
  • Python wave 模块写的 WAV 会让 Windows 上的渲染崩溃(Remotion 4.0.532)。它写的是最简 44 字节头,data 紧跟在 fmt 后面;Remotion 一拉这种文件做 <Audio> 就失败,报错只剩一句看不出原因的 kill EBADF,而且只在渲染范围里有音频时才出现。同样的采样加一个 LIST/INFO 块(ffmpeg 写 WAV 就带)就正常。所以场景要播的 WAV 一律经 tools/kit.py 的 wav_bytes() 写(sfx.py、gen.py --mock 已改),tests/test_promo.py 会检查 public/ 里每个 WAV 都带这个块。
  • 审片拼图不能用 <Freeze> 冻结带预卷转场的外壳:短片的转场在重拍前播放,冻结整片会差出几帧,审到的不是成片里那一帧。短片的 promo-sheet 和 verify 逐帧直接渲染。
  • Windows 下把 Python 输出重定向到文件时,默认编码是 cp1252,打印 → 这类字符会崩。新加的工具开头都有 sys.stdout.reconfigure(encoding="utf-8")。
  • 展示字体要先确认每个字都认得出。方块、黄油这类风格化中文字体,个别字会认不出来(站酷庆科黄油体的「多」「水」就是),只看字体样张发现不了。上线前把文案里用到的每个字都渲染出来看一遍,认不出的换成思源黑体粗体。
  • 八分音符一张的轮播,卡片要停得住:滑动只给 4 帧,留 3 帧静止、清晰。整段都在动,观众一个字也读不到。

English

explainer-kit is a Remotion template for narrated explainer videos and, since this iteration, for short beat-timed promos without narration. Narration lives in tts/script.json with inline markers: [[cue]] marks an animation/shot trigger, || inserts a dramatic pause, and <<key|...>> reads a quote with an alternate voice. tts/gen.py synthesizes each scene with edge-tts, records word boundaries and writes public/audio/<scene>.mp3 plus src/timings.json. Scenes read frames through useScene() — c('cue'), w('word'), rel('word', 'cue') — so durations and animations follow the audio after any rewrite.

Quick start: npm i && npm run studio shows the bundled ~3 min, nine-scene demo (one scene per style pack), whose timings and silent audio were produced by python tts/gen.py --mock (no network; word times estimated from character count). npm run fonts downloads the Noto Sans SC / Noto Serif SC variable fonts (OFL) into public/fonts/; without them the stacks fall back to system fonts. All size, fps, voice, pace and font settings live in kit.config.json. Nine optional style packs are included: film (letterbox, grain, grade, cue-driven shots, paper props), slides (panels, chips, karaoke subtitles), paper (hand-drawn strokes that draw on and boil, sticky notes, highlighter), neon (scrolling grid floor, scanlines, glowing type, terminal, node graph with flowing pulses, glitch), editorial (masked kinetic headlines, slammed numbers, colour-block wipes, ticker), math (Manim-style axes, traced plots, sliding tangent, step-by-step TeX), keynote (one morphing shape, cursor clicks, liquid glass, screen-studio zoom), pixel (320×180 canvas upscaled with hard pixels, walking hero, coins, RPG dialog) and ink (ink-wash mountains bleeding through mist, brush strokes, vertical calligraphy, red seal). src/core/motion.ts holds a shared motion vocabulary (enter/exit with life + move, springs, idle drift, punch, shake), <Spoken> reveals on-screen text exactly as the narrator says it, and Shots cuts on cues with fade/push/up/zoom/wipe/whip/iris/black/flash transitions. docs/principles.md explains the eight principles behind the design (one time source — the narration or the beat grid, frames as pure functions, enter/breathe/exit, looks as clothing, one canvas for all formats, nothing derived is hand-edited, sound measured to delivery spec, accept the frame as the viewer sees it on a phone); npm run verify is the delivery gate (types, source consistency, every scene renders in every format, two renders are byte-identical) and publish runs it before rendering. Sound effects are synthesized by tools/sfx.py into public/sfx/ and placed with <Sfx at={frame} name="pop" /> (moving Shots transitions add their own); the score pulses on a music.bpm grid that scenes can follow with useBeat(). brand.accent/accent2 in kit.config.json recolour every pack, theme.<pack> overrides single keys, and formats adds portrait (9:16) and square (1:1) compositions that lay the 16:9 picture out with a title band, large captions and a progress bar. tools/publish.mjs runs TTS → render (all formats) → score → mix in one go; tools/new-scene.mjs and tools/new-pack.mjs scaffold scenes and style packs; .claude/skills/ holds Claude Code skills for writing a scene, reviewing, building a pack and publishing. Review with tools/stills.mjs, tools/pick.mjs, tools/sheet.py, tools/strip.mjs (a filmstrip of frames around a cue, to judge motion rather than end states), tools/overview.mjs (one frame per scene) and tools/listen.mjs (renders a range's audio and lists sound events by cue); render with npm run render (uses --gl=angle); add a synthesized score with tools/music.py and mix with tools/mix.py, which now masters with tools/master.py (measure → linear gain → look-ahead limiter, repeated to within 0.3 LU; true peak checked on the AAC-encoded file and the ceiling lowered if it lands above -1 dBTP) to loudness.target (-16 LUFS; a format can override it, 9x16 uses -14).

Promo mode: promo/promo.json sets bpm, key, size, loudness and, per scene, a length in bars, an energy (intro/groove/break/build/drop), an incoming transition (cut/iris/wipe/flood/up — played before the downbeat so it completes on it) and cues as bar:beat positions. Scenes in src/promo/scenes/ read frames with usePromo() (cue(), at(), every(), pulse()); land()/squash() in motion.ts make a hit arrive exactly on its frame, <Burst> throws deterministic ballistic particles, and the new punch style pack provides slammed type with an RGB split, marquee bands, a rotating text badge, a motion-blurred card carousel, pattern-filled type, LogoReveal (outline → fill with bloom → parts dropping in) and a CTA bar. tools/score.py (with tools/synth.py: layered drums, supersaws, plucks, sub bass, formant vocal chops, an energy-normalized convolution reverb, kick sidechain) writes the score on the same grid and prints per-bus RMS per section; node tools/promo.mjs renders, scores, masters to -14 LUFS and runs tools/audio-report.py (loudness per section, waveform and spectrogram, fails on true peak above -1 dBTP or loudness off target). tools/trace-logo.py traces a logo image (screenshots included) into vector parts in src/brand/logo.json; tools/copy_lint.py flags absolute claims that China's Advertising Law forbids in ad copy; verify adds two promo stages. The promo is checked as it is watched: promo.json declares the platform safe area (safe: status bar, caption line, button rail) and an upload cover; src/core/probe.tsx measures every readable text box on the rendered frame (after transforms, masks and opacity) and outlines copy outside the safe area or cut by the frame edge; verify and tools/promo-sheet.mjs probe frame 0 (which must already carry copy), every cue once settled, transition midpoints and the cover, and promo.mjs exports out/<id>-cover.png. On-screen counts (「十套风格包」, 06 OF 10) are derived from PACKS, which a test checks against the style packs on disk. Python's wave module writes minimal-header WAVs that crash Remotion 4.0.532 renders on Windows (kill EBADF); every scene WAV is now written with a LIST/INFO chunk via wav_bytes() in tools/kit.py, and a test enforces it.

License

MIT,见 LICENSE。Noto 字体不随仓库分发,由 npm run fonts 下载,遵循 SIL Open Font License。

About

Remotion 讲解视频模板:[[cue]] 标记 + edge-tts 逐词时间戳驱动字幕与动画

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages