motion-reels 是做什么的?
代码驱动的竖版 Reel 动态图形。每个 Reel 由一个或多个 block 组成。Block 是一个小型 JavaScript 文件,用于在画面中放置标题、绘图、照片和样本,并指定每个元素出现的时机。每个时机来自文字时间线:要么是节拍表(无配音),要么是语音录音中的文字时间。引擎在独立的浏览器页面中逐帧渲染,检查布局,最后与语音和/或音乐合成为最终视频。
页面所有者决定外观、文字和发布内容。Claude 从不发布任何内容。
环境设置(一次)
- Python 3.10+ 以及 ffmpeg/ffprobe 在 PATH 中。然后运行仓库中的
setup.ps1(Windows)或setup.sh(macOS/Linux),或者pip install -r requirements.txt+python -m playwright install chromium(或设置环境变量MG_CHROME指向已有的 Chrome)。 - 仅语音功能:
pip install -r requirements-voice.txt(faster-whisper;首次运行会下载语音模型)。 - 测试:运行
python engine/selftest.py必须输出SELFTEST OK(检查示例通过,负面测试按预期失败,阿拉伯语字幕正确分割,短渲染+故障扫描+合成达到 -14 LUFS)。--quick跳过渲染。
引擎文件(全部在 engine/ 目录中):render.py(检查/分镜/静帧/视频),beats.py,words.py,frames.py,faultscan.py,assemble.py,storyboard_sheet.py,make_grain.py,templates/(外壳,reel.js,reel.css,字体)。
每个 Reel 一个文件夹
<reel name>/
brief.md 该 Reel 的目的、核心想法、行动号召、可用照片
beats.txt 屏幕上的文字,每行一个节拍(或 script.txt + take.wav 用于有配音的情况)
words.json 时间线(由 beats.py 或 words.py 生成)
photos/ 所有者的照片(仅限其拥有使用权的照片)
A.js, B.js ... block 文件
build/ 检查 JSON、分镜静帧、block 视频、render.json 文件、联系表
<Reel>_REV01_<日期>.mp4 交付文件;后续修复版另存为 _REV02,从不覆盖之前的版本
关卡(绝不跳过或重排;在每个 ⛔ 处暂停等待所有者确认)
- Brief。 每个 Reel 一个核心想法(房间展示、材料故事、平面变化、技巧)。索要照片和社交媒体账号。列出每张照片展示的内容。在前 2 秒内抓住观众注意力。
- 节拍表(
beats.txt):屏幕上精确的文字,按顺序,每行一个节拍。保持节拍简短(2–7 个词)。列表中的每一项单独一行,让每句话都有停留时间。⛔ 所有者确认文字。 确认后改动文字意味着需要重新调整 block 的时间线(如果有配音还需重新录制):改动前务必说明。 - 时间线。
- 无配音:
python engine/beats.py beats.txt words.json(默认节奏 2.6 词/秒 + 每个节拍后停留 0.7 秒;行末的[+1.5]表示延长停留时间;[pause 2]表示停留但无新内容)。 - 有语音(任何 TTS 或录音,每个 block 一次录制,不编辑):
python engine/words.py take.wav words.json,然后将转录文本与脚本逐词比对。画面根据录音调整,而非相反。
- 无配音:
- ⛔ 分镜静帧。 构建 block 布局(仅位置即可),运行
check,然后render.py A.js words.json storyboard build/sb和storyboard_sheet.py build/storyboard.png build/sb。自行审阅联系表(见下方清单),然后发送出去。在所有者在布局上确认同意之前,不要制作任何动画。 - 构建动画。 每个元素在其名字对应的词出现时进场(规则见下方)。每次改动后运行
check。 - 渲染。 每个 block 运行
render.py A.js words.json video build/A.mp4 --workers 4(发送给所有者的文件绝不使用--draft)。每个 block 的build/A.mp4.render.json必须包含"capture": "fresh"且浏览器与其他 block 一致;如果浏览器改变,重新渲染所有 block。 - 故障扫描。 对每个 block 运行
faultscan.py build/A.mp4 --sheet build/A_contact.png,然后对最终 Reel 再运行一次。检查所有被标记的时间点。黑条或单帧闪烁属于故障:停止并修复。深色照片内容不算问题。 - 合成。
assemble.py blocks.json <Reel>_REV01_<日期>.mp4 [--music bed.mp3]。它会输出测量响度;必须达到目标值(默认 -14 LUFS),真峰值应低于或等于 -1.5 dBTP。 - ⛔ 审核。 发送 Reel 及其联系表。仅画面修复只需重新渲染改动过的 block,并作为下一个 _REV 版本交付。所有者负责发布。
动画规则(原因:营造平静、高端的感觉;无抖动,无难读元素)
- 文字绝不移动。 文字淡入(0.28 秒)后保持静止。文本上无滑动、缩放、弹跳或漂移效果。
- 标题:
hl(el, text, cue(...))使每个词在其对应的时间线词上逐词淡入。当标题转述语音时,使用{stream:true}:文字从 cue 开始,从左到右依次出现,间隔 0.1 秒。 - 相机: 镜头保持时相机完全静止;仅在镜头之间滑动(0.8 秒,ease-in-out 缓动)。在 9:16 画面中,将镜头垂直堆叠在画面中(镜头 2 在 y 1920,镜头 3 在 y 3840),然后向下滑动:
SHOTS=[[t,540,960,1],[t2,540,2880,1],...]。 - 照片可缓慢推近:
push:[1,1.04]= 恒定速度,不超过 +6%(MOTION.PUSH_MAX;引擎会拒绝超出值)。带有文字的照片上不能推近。 - 绘图在被命名时逐渐绘制出来,先绘制参考元素:墙在窗户和门之前,地板线在从其测量高度之前。
at('#walls', cue('Walls'), null, 'draw', 0.9);每个元素 0.6–1.0 秒。 - 同一比例的尺寸标注。 如果一个平面图显示两个尺寸,两者必须使用同一比例。
- 为文字中提到的所有内容添加标注;每个数字都附带单位(“3.2 m”,而不是单独的“3.2”)。
- 任何地方不使用弹跳、过冲、弹性或弹簧缓动效果。 所有时间参数来自
reel.js中的MOTION;请在那里修改,绝不在 block 中写死。 - 直接传给
at()的裸虚线 SVG 元素会在其 cue 之前显示:将其包裹在<g>中。
时间令牌(MOTION in reel.js)
| 令牌 | 值 | 用途 |
|---|---|---|
| WORD_FADE / WORD_LEAD | 0.28 秒 / 0.08 秒 | 词淡入;在词开始之前一点开始 |
| STREAM | 0.10 秒 | 流式模式标题单词间距 |
| REVEAL / EXIT | 0.5 秒 / 0.4 秒 | 块、芯片、样本、照片的入场/出场 |
draw (at(...,'draw',dur)) | 0.6–1.0 秒 | 每个绘制元素 |
| CAM_SLIDE / CAM_LEAD | 0.8 秒 / 0.35 秒 | 相机滑动;在镜头 cue 之前开始 |
| PUSH_MAX | 0.06 | 照片推近限制 |
| WIPE | 1.2 秒 | 前后对比擦除 |
| 缓动函数:入场使用 ease-out cubic,滑动、绘制和擦除使用 ease-in-out cubic。 |
布局与检查
- 安全区(
reel.js中的SAFE):文字、标签和字幕必须保持在 1080x1920 帧的 x 65–1015、y 269–1248 范围内(顶部 14%,底部 35%,两侧 6%)。这些数据广泛用于 Reels 的第三方指南,并非 Meta 发布的原生帖规则:如果应用更改,从已发布 Reel 的手机截图中重新测量并设置safeZone({...})。照片和绘图可以超出安全区,但文字不可。 render.py <block> words.json check build/<block>.check.json是每次渲染前必须执行的步骤。它每 0.25 秒采样一次,检查不同元素(文字、绘图、芯片、样本、照片、字幕)之间是否有重叠持续 0.5 秒或以上,以及相机保持时是否有文字超出安全区。检查失败则必须修复布局,绝不允许渲染。通过引入一个已知故障的负面测试来证明新规则(检查必须失败)。- 属于照片上的文字应放在照片元素内部(
parent: photoDiv),其后添加scrim(),或者将照片作为背景(bg:true)。否则检查会报告文字覆盖照片的问题,这正是你想要的。 - 检查不会检测画面边缘被裁切的部分:这由自行审阅负责。
构建 Block(reel.js)
boot(()=>{ // 每个 block 以 boot(...) 开始
handle('@yourstudio'); // 安全区顶部的小手柄
$('world').insertAdjacentHTML('beforeend', `<div id="h1" class="abs serif" style="left:90px;top:360px;width:900px;font-size:104px"></div>`);
hl('h1', 'A small room, *re-planned.*', cue('A small')); // *...* = 强调斜体;~word = 不在时间线中
at('#walls', cue('Walls'), null, 'draw', 0.9); // fx: 'fade' | 'pop' | 'wipe' | 'draw'
photo({src:rel('photos/living.jpg'), x:0, y:1920, w:1080, h:1350, a:cue('Light'), push:[1,1.04]});
beforeAfter({before:rel('photos/before.jpg'), after:rel('photos/after.jpg'), x:90, y:2440, w:900, h:900, a:cue('Before'), t:cue('after')});
swatch({name:'Oak', note:'floor', color:'#B08A62', x:90, y:4480, size:270, a:cue('Oak')}); // 或 src: rel('photos/oak.jpg')
clip({dir:rel('clips/walk'), n:240, x:0, y:5760, w:1080, h:1920, a:cue('Walk'), t0:cue('Walk')}); // frames.py 生成的帧
captions(1120, 3); // 从时间线烧录的字幕(有配音的 Reel)
SHOTS=[[0,540,960,1],[cue('Light'),540,2880,1]];
});
rel(path) 解析与 block 同目录的文件。cue('words', after) 在词不在时间线中时会报错,因此输入错误会立即暴露。完整示例:examples/demo/demo.js。
阿拉伯语及其他从右向左的 Reel
- 在
boot()中首先调用rtl()。标题使用 Noto Naskh Arabic,文本使用 IBM Plex Sans Arabic(OFL);拉丁字母回退到默认字体。阿拉伯语中不要使用斜体、字间距或大写(字母必须连接);强调词仅改变颜色。 - 文本框保持
left+width;文字靠右对齐。列表从右向左排列(第一个命名的在最右边)。 - Cue 忽略变音符号、塔特威尔、alef/yaa/taa-marbuta 变体,并转换阿拉伯-印度数字;不含字母的 cue 会报错。将 ة 和 ه 视为同一字母,因此如果两个词仅在此处不同,请选择更长的短语作为 cue。数字词仅匹配英文:在阿拉伯语中,cue 数字或转写后的词(
words.py --lang ar选择多语言模型)。 - 字幕在 Arabic punctuation ,;؟ 和 Latin punctuation 处换行,并且不显示这些标点。
- 示例:
examples/arabic/。
外观
主题仅存在于 engine/templates/reel.css(颜色定义为 CSS 变量,字体)和字体文件中。默认“暖色极简”:纸张 #F4EFE8,墨水 #2B2622,强调色 #9C6B4E,标题 Cormorant Garamond + 文本 Jost(均为 SIL OFL 1.1)。
要更改外观:编辑变量,或将 OFL 字体添加到 engine/fonts/ 目录(附带许可证文件)并在 fonts.css 和 window.FONTS(reel.js)中列出。先向所有者索要 3–5 张其网格截图,然后进行匹配。
自行审阅(Claude,在发送任何联系表或渲染文件之前)
- 每个元素在其对应的词开始时出现;参考线先绘制。
- 文字落地后保持静止;镜头保持时相机静止;滑动结束时没有跳动。
- 没有任何内容在帧边缘被裁切;所有文字在安全区内;没有重要内容在底部 35% 区域。
- 同一比例的尺寸标注;每个数字都有单位;拼写与已签字的节拍表一致。
- 每个 block 的检查都通过;
render.json显示capture: fresh,所有 block 使用相同浏览器。 - 故障扫描无问题,或者每个标记的时间点都已检查并说明。
assemble.py输出的响度和真峰值达到目标。
权利与隐私
- 仅使用所有者拥有使用权的照片。客户房屋:未经同意不得出现人脸、地址或文件。
- 音乐:仅使用所有者已获许可的音轨,或者发布时在应用中添加的平台自有曲库。
- 不声称所有者未提出的主张(价格、时间线、奖项)。
故障排查
cue not found: "...":该词不在words.json中的相应顺序里;修复节拍表或 cue。ERR reqfail:file:///...jpg:照片或剪辑路径错误;使用rel()并检查文件名。- 之前绘图的幽灵副本或黑条:你使用了
--draft渲染。重新渲染时不加该参数。 - 渲染缓慢:在具有相应核心数的机器上使用
--workers 4(最多 8)。检查和分镜很快,视频渲染则不然。