Communitygithub.com

HusseinRagab/motion-reels

使用 Playwright 和 ffmpeg 逐帧渲染,为室内设计页面制作 9:16 竖版动态图形 Reel(1080×1920),包括标题、平面图绘制、前后对比擦除、照片推近、材质样本等。当要求制作、分镜、渲染或修复 Reel、Shorts 或竖版动态视频时使用。

motion-reels 是什么?

这是一个用于生成代码驱动的竖版动态图形 Reel 的技能,专为室内设计页面量身定制。它通过将 Reel 分解为多个 JavaScript 模块(Block),每个模块控制标题、照片、绘图和材质样本等元素的布局与动画时间线。时间线基于文字节拍表(beats.txt)或语音录音时间戳,使用 Playwright 逐帧截图渲染,并用 ffmpeg 最终合成为 1080×1920 的 MP4 视频。技能强调极简、平稳、高级的视觉效果:文字从不移动,相机只在镜头切换时滑动,照片推近不超过 6%,所有元素严格遵守安全区规则,并提供强制性的布局检查。用户拥有最终发布权,技能仅负责本地生成。支持英语和阿拉伯语(RTL),可通过 CSS 变量调整视觉主题。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/HusseinRagab/reel-motion-kit/tree/HEAD/motion-reels

在你喜欢的 AI 中提问

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

文档

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,从不覆盖之前的版本

关卡(绝不跳过或重排;在每个 ⛔ 处暂停等待所有者确认)

  1. Brief。 每个 Reel 一个核心想法(房间展示、材料故事、平面变化、技巧)。索要照片和社交媒体账号。列出每张照片展示的内容。在前 2 秒内抓住观众注意力。
  2. 节拍表(beats.txt):屏幕上精确的文字,按顺序,每行一个节拍。保持节拍简短(2–7 个词)。列表中的每一项单独一行,让每句话都有停留时间。⛔ 所有者确认文字。 确认后改动文字意味着需要重新调整 block 的时间线(如果有配音还需重新录制):改动前务必说明。
  3. 时间线。
    • 无配音: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,然后将转录文本与脚本逐词比对。画面根据录音调整,而非相反。
  4. ⛔ 分镜静帧。 构建 block 布局(仅位置即可),运行 check,然后 render.py A.js words.json storyboard build/sb 和 storyboard_sheet.py build/storyboard.png build/sb。自行审阅联系表(见下方清单),然后发送出去。在所有者在布局上确认同意之前,不要制作任何动画。
  5. 构建动画。 每个元素在其名字对应的词出现时进场(规则见下方)。每次改动后运行 check。
  6. 渲染。 每个 block 运行 render.py A.js words.json video build/A.mp4 --workers 4(发送给所有者的文件绝不使用 --draft)。每个 block 的 build/A.mp4.render.json 必须包含 "capture": "fresh" 且浏览器与其他 block 一致;如果浏览器改变,重新渲染所有 block。
  7. 故障扫描。 对每个 block 运行 faultscan.py build/A.mp4 --sheet build/A_contact.png,然后对最终 Reel 再运行一次。检查所有被标记的时间点。黑条或单帧闪烁属于故障:停止并修复。深色照片内容不算问题。
  8. 合成。 assemble.py blocks.json <Reel>_REV01_<日期>.mp4 [--music bed.mp3]。它会输出测量响度;必须达到目标值(默认 -14 LUFS),真峰值应低于或等于 -1.5 dBTP。
  9. ⛔ 审核。 发送 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_LEAD0.28 秒 / 0.08 秒词淡入;在词开始之前一点开始
STREAM0.10 秒流式模式标题单词间距
REVEAL / EXIT0.5 秒 / 0.4 秒块、芯片、样本、照片的入场/出场
draw (at(...,'draw',dur))0.6–1.0 秒每个绘制元素
CAM_SLIDE / CAM_LEAD0.8 秒 / 0.35 秒相机滑动;在镜头 cue 之前开始
PUSH_MAX0.06照片推近限制
WIPE1.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,在发送任何联系表或渲染文件之前)

  1. 每个元素在其对应的词开始时出现;参考线先绘制。
  2. 文字落地后保持静止;镜头保持时相机静止;滑动结束时没有跳动。
  3. 没有任何内容在帧边缘被裁切;所有文字在安全区内;没有重要内容在底部 35% 区域。
  4. 同一比例的尺寸标注;每个数字都有单位;拼写与已签字的节拍表一致。
  5. 每个 block 的检查都通过;render.json 显示 capture: fresh,所有 block 使用相同浏览器。
  6. 故障扫描无问题,或者每个标记的时间点都已检查并说明。
  7. assemble.py 输出的响度和真峰值达到目标。

权利与隐私

  • 仅使用所有者拥有使用权的照片。客户房屋:未经同意不得出现人脸、地址或文件。
  • 音乐:仅使用所有者已获许可的音轨,或者发布时在应用中添加的平台自有曲库。
  • 不声称所有者未提出的主张(价格、时间线、奖项)。

故障排查

  • cue not found: "...":该词不在 words.json 中的相应顺序里;修复节拍表或 cue。
  • ERR reqfail:file:///...jpg:照片或剪辑路径错误;使用 rel() 并检查文件名。
  • 之前绘图的幽灵副本或黑条:你使用了 --draft 渲染。重新渲染时不加该参数。
  • 渲染缓慢:在具有相应核心数的机器上使用 --workers 4(最多 8)。检查和分镜很快,视频渲染则不然。

相关技能