paper-collage-video — 纸片分层动画流水线
多数人做"纸片风视频",只是把一张完整图片放大缩小加点镜头移动。那样能动,但没有
层次。真正的分层动画,核心不是生成一张漂亮的图,而是让每一张图片承担明确的叙事
位置:背景、后排、主角、前景各自独立,按不同节奏运动。
本 skill 自带一个已验证可跑通的 Remotion 模板(template/),下面每条约束都是
实跑中撞出来的,不是理论。
铁律
永远不要把人物画进背景。 人物一旦和背景粘在一起,就再也不能独立飞入、缩放、
重新排序或单独给节奏。底板必须是"空场",人物必须是独立透明 PNG。
工具分工
| 工具 | 作用 | 凭证 |
|---|---|---|
Codex imagegen (game-asset-gen/scripts/gen.py) |
生成底板与角色 | codex login,不需要 API key |
| Remotion | 图层、入场、遮挡、字幕、音轨、渲染 | — |
| MiniMax | 中文旁白 TTS + 背景音乐 | MINIMAX_API_KEY(放 .env) |
| game-asset | 本地音效(impact/whoosh/tick)、BGM | 音效无需 key |
| Python + Pillow | 按 alpha 裁切图层(必需) | — |
| FFmpeg / ffprobe | 时长、音轨、抽帧验收 | — |
不要用智谱 GLM-Image 做这个。 实测它在右下角强制烙"AI生成"水印,
watermark_enabled:false 被忽略——底板直接报废,抠图时水印还会被当成主体边界。
Codex imagegen 没有这个问题。
流程(顺序不能乱)
1. script.json 先定分镜和文案,在出任何图之前
2. 底板 无人物的空场景 gen.py
3. 角色 一人一张透明 PNG gen.py --ref
4. 裁切 trim_layers.py ← 必需,别跳
5. 静态排版 先摆好,不要动画
6. lint-layout 机器检查构图
7. 动画 按 role 错峰入场
8. 旁白 tts.mjs → 旁白长度决定镜头长度
9. sync ffprobe → 时长 + 字幕时间轴
10. 渲染 + 抽帧验收
先定镜头,因为同一个人物在全景和特写里的大小、位置、朝向都不同。先随便出素材,
后面必然返工。
快速开始
本 skill 以软链方式挂在项目下(源仓库:~/code/my-skills/paper-collage-video,
GitHub lifecoder1988/my-skills),因此模板就在当前项目里:
cp -r .claude/skills/paper-collage-video/template <目标目录>
cd <目标目录> && npm install
echo "MINIMAX_API_KEY=..." > .env # 图片不需要 key,走 codex login
要在新项目里启用本 skill,照这行链过去即可(不装到全局 ~/.claude/skills):
mkdir -p <新项目>/.claude/skills
ln -s ~/code/my-skills/paper-collage-video <新项目>/.claude/skills/paper-collage-video
编辑 src/script.json(唯一手写真源),然后:
GEN=~/.claude/skills/game-asset-gen/scripts/gen.py
STYLE=$(cat prompts/_style.md)
# 底板:1536x1024,无人物
python3 $GEN --timeout 900 --size 1536x1024 --prompt-text "$STYLE ..." \
--out public/assets/plates/01-wide-bg.png
# 角色:透明,--ref 第一个角色锁风格
python3 $GEN --background transparent --size 1024x1024 \
--ref public/assets/layers/emperor.png --prompt-text "$STYLE ..." \
--out public/assets/layers/attendant.png
python3 scripts/trim_layers.py public/assets/layers # 必需
node scripts/tts.mjs # 旁白
node scripts/sync-durations.mjs # → src/script.build.json
node scripts/lint-layout.mjs # 构图体检
npm run start # Remotion Studio 逐帧预览
npm run render && node scripts/check.mjs # → out/final.mp4 + 抽帧
旁白想用自己的声音:先录一段 10s–5min 的干净人声(mp3/m4a/wav,<20MB),
克隆后 voice_id 会自动写进 src/script.json 的 voice 字段,再重跑 TTS:
node scripts/clone-voice.mjs assets/voice/my_voice.mp3 --id myvoice2026
node scripts/tts.mjs --force
注意:voice_id 必须字母开头、仅字母数字、≥8 位,否则 API 拒收;上传走
purpose=voice_clone 的 multipart,失败同样藏在 base_resp.status_code 里
(HTTP 仍 200)。
音效与 BGM:
game-asset audio sfx --preset hit --duration-ms 420 --out public/audio/sfx/impact.wav
game-asset audio sfx --preset whoosh --duration-ms 320 --out public/audio/sfx/whoosh.wav
game-asset audio sfx --preset click --duration-ms 140 --out public/audio/sfx/tick.wav
game-asset audio sfx --preset powerup --duration-ms 900 --out public/audio/sfx/riser.wav
game-asset audio bgm --prompt prompts/bgm.md --instrumental --out public/audio/bgm.mp3
图层语汇
尺寸和运动幅度同时编码叙事等级。人物不是平均分布,也不是一样大。
| role | 高度(1080画布) | 运动 | z | 音效 |
|---|---|---|---|---|
primary 主角 |
~680–820 | 位移最大、有落地感 | 最上 | impact |
secondary 配角 |
~400–520 | 从两侧进入、中等 | 中 | whoosh |
tertiary 后排 |
~330–380 | 几乎不动,不抢戏 | 最下 | tick |
背景只做 ~1% 慢速推镜。
baseline是脚底 Y,不是顶部——同一镜头所有人共用一条地面线。后排 baseline
更小(在画面更高 = 更远)且更矮。- 用
height定尺寸,不要用width(见下)。 - 纵深来自遮挡不是 3D:底板 → 后排 → 中排 → 主角 → 前排 → 字幕。前排压住主角
边缘才有层次。 - 入场必须错峰(
delay):主角先出现 → 配角补充 → 群像完成,不要一次塞满。 - 一个素材配
"flip": true复用左右两侧,不用生成镜像版。
出图提示词
prompts/_style.md 存一份共用艺术指导,每张图都要重复它——imagegen 是文生图,
风格全靠提示词复述来统一(--ref 再加一层保险)。
角色提示词必须同时锁死:时代服饰、明确朝向、完整人物不裁切头手脚、白色剪纸
描边、纯透明背景、只有一个人、无场景、无投影、无文字、无水印。
底板提示词要正向描述(见下面的坑)。
踩过的坑
底板提示词堆强调式否定会让 imagegen 卡死超时。 写
absolutely NO people, NO human figures, NO characters 那版跑满 600s 一张没出;
改成正向描述为主("An empty courtyard...", "Deserted architecture and landscape
only. No text, no watermark.")后 3 分钟出图,且真的没画人。描述画面里有什么,
而不是反复强调没有什么。
trim_layers.py 是必需步骤,不是优化。 imagegen 返回的人物浮在 1024×1024 里,
留白比例完全随机——实测人物只占图宽 33%~84%、脚下留白 2%~6%。不裁切的话
height/width 缩放的是留白而不是人物,主次大小关系会被随机破坏,人物还会
各自悬空脱离地面线。
按高度定尺寸,不是宽度。 原文用"宽 650 / 宽 575",那只在素材比例一致时成立。
alpha 裁切后各角色比例从 0.35(瘦高文官)到 0.89(带龙椅的皇帝)不等:给瘦高人物
设 width: 575 算出的高度是 1344px,超出 1080 画布,头直接被切掉。高度才是
1080 画布真正约束的维度,也才是"这个人多大"的直觉含义。跪姿人物天然约为站姿的 60%。
真人题材:先核事实,再做片子。 涉及在世真人时,先 WebSearch 核实——用户给的
前提可能本身就是错的(实测遇到过:"做拳王离婚故事",而当事人根本没离婚)。数字只
采主流媒体口径,自媒体独家、互相打架的说法一律不用,涉事陈述带出处("据公开报道"
"某某在节目里说")。不生成未成年人形象。另外:imagegen 不会拒绝点名真人,但也
画不出本人——给的是通用长相,所以"画得像本人"这条在本流水线里做不到,按角色
特征写提示词即可。
渲染前先跑 lint-layout.mjs。 渲染要几分钟,构图错误不该等渲完才发现。它检查:
头/脚出画、后排被主角完全吞掉(白生成)、主次大小倒挂、入场帧撞车、脚底压在字幕带
下面。上面每一条都是实际撞到过的。
等 gen.py 进程退出,不要等文件出现。 codex 先把原图写到磁盘,gen.py 随后才做
_force_size 后处理。如果你用 until [ -f x.png ] 之类的循环一看到文件就去 trim,
_force_size 会把你已经裁好的图再 resize() 成 --size 的尺寸——而且它
不保宽高比,人物直接被拉变形。实测踩过:wife.png 被压成 1024×1024 正方形,
lint 报 790x790 才发现。要等就等 job/进程结束。
同理,--size 只是"强制拉伸到这个尺寸",不是"按比例缩放"。底板给 1536x1024
(codex 本来就出这个比例)是安全的;给一个和出图比例不符的 --size 会挤压画面。
gen.py 超时会漏 codex 孤儿进程。 超时只杀直接子进程,codex 本体会继续在后台空转
抢资源。清理:
ps -o pid,etime,command -ax | grep "[c]odex exec" # macOS 的 ps 没有 etimes
kill -9 <老进程 pid>
MiniMax 用 https://api.minimax.io(不是 api.minimax.chat / api.minimaxi.com,
那两个返回 invalid api key)。TTS 返回的音频是 hex 编码(不是 base64),且失败时
HTTP 仍是 200,真实状态在 base_resp.status_code 里。node scripts/tts.mjs --voices
可列出全部音色(332 个,中文搜 Chinese (Mandarin)_)。
出图很慢:每张 2–4 分钟,底板可能更久。丢后台并行跑,别串行等。
验收清单
npm run start 逐帧预览 + check.mjs 抽帧,重点看:
- 人物头、手、脚有没有被裁掉
- 朝向是否符合场景关系(两侧人物应朝向中心)
- 主角和配角大小是否拉开
- 前后排有没有错误遮挡(后排是否被完全吞掉)
- 人物有没有同时出现
- 字幕是否挡住脚部和关键道具
- 音效是否和人物入场帧对齐
check.mjs 会自动验证:分辨率/帧率、音轨是否存在、时长、是否静音
(mean_volume < -60dB 判死),并抽 5 帧供肉眼检查。
这套方法不限于历史题材
历史人物、知识科普、商业故事、人物关系、城市发展——只要画面能拆成"背景、主角、
配角、前景",流程完全一样。改 script.json 的文案与图层、换 prompts/_style.md
的艺术指导即可,Remotion 组件不用动。