1实现原理 · 为什么它能做到
唯一铁律:没有两张照片可以用同一种运动,且这种差异必须是由索引播种的确定性伪随机。
**No two slides may move the same way.** The instant-giveaway of an amateur slideshow is every photo doing the identical slow zoom-in. Vary direction, zoom in vs. out, and start scale per photo — but make it **deterministic** (seeded by index), so re-renders are identical and a frame-based renderer stays stable.
播种机制是 mulberry32 + 索引播种:同一张照片永远同一运动,相邻照片不同。
`mulberry32` is a tiny, fast, well-distributed 32-bit PRNG. Seed it with the photo index so photo 0 always gets the same move, but adjacent photos differ.
Ken Burns 只动 transform,且起始 scale ≥1.05,保证平移不露白边。
Always start scale ≥ `1.05` so a pan never exposes an empty edge.
混合比例照片用 blurred-pad(模糊底 + contain 前景原图),绝不拉伸。
Blurred-pad wins for mixed folders: it fills the frame, never crops the subject, and the soft blur reads as intentional depth rather than empty bars.
配乐先行:离线测一次 beat 烘进 props,绝不在渲染时逐帧做音频分析。
The rule: **lock the music first, derive slide boundaries from it.** Detect beats once, offline, and bake them into props — audio analysis per frame is non-deterministic and slow.
文件夹 → manifest:自然排序、EXIF 拍摄时间、文件名提取 caption,全部派生自数据。
// natural sort so photo2 < photo10 (default lexical sort breaks this)
图片必须加载门控(staticFile + delayRender/continueRender),否则某帧会空白弹出。
- Photos loaded via `staticFile()`, gated with `delayRender`/`continueRender` so each image (and any caption font) is present before its frame renders — otherwise a slide pops in blank.
验证环重点是「每张照片是否真的加载并正确重构图」,而不是画面美感。
A slideshow ingests a whole folder of user photos — the verify pass is mostly *did every photo actually load and reframe without distortion*.
转场只选一种(默认 0.4–0.6s 交叉溶解),且要重叠而不是硬切,除非切在拍点。
Pick **one** transition and keep it consistent — mixing wipes, spins and cubes screams "template."
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| package | remotion(frame/interpolate/Easing/Img/Audio/Sequence) |
| package | @remotion/transitions(TransitionSeries + fade) |
| package | sharp + exifr(读图像尺寸与 EXIF DateTimeOriginal) |
| package | librosa + soundfile(pip 安装;离线测 beat 与 onset) |
| cli | ffmpeg(blurred-pad、zoompan、xfade 与仓库共享 contact-sheet.sh) |
| network | iart.ai(reference 尾部推广链接,具 utm 参数;仅读者点击才请求) |
4风险提醒 风险提醒:橙色 · 评估后使用
- reference 与 SKILL.md 的加载门控口径不一致 — 照抄 reference 会绕过 staticFile/delayRender,仍可能出现空白帧与相对路径问题。
- 文档引用的助手脚本未随包 — build-manifest.mjs 与 beats.py 只有内联源码,仓库无对应文件;照抄命令行会找不到文件。
- 依赖面最宽且跨生态 — npm(remotion/sharp/exifr)+ pip(librosa/soundfile)+ ffmpeg 三套供应链,任一环节版本不兼容都会卡住产线。
- 不可信二进制输入通道 — 照片与音频由本地解析库处理,恶意构造文件属解析器漏洞面;skill 无沙箱或校验建议。
- 隐私未讨论 — EXIF 日期会被上屏、manifest 保留文件路径,全文未提 EXIF/GPS 与输出元数据策略。
- 商业引流位 — reference 尾部固定 iart.ai 推广段与 utm 链接。
5第二遍独立确认
- [discrepancy] 图片加载门控口径是否实现一致 — SKILL.md 要求 staticFile() + delayRender/continueRender 门控,但 templating-folder.md 的 Slideshow 直接把 manifest 里的文件系统路径(join(dir, f))交给 Img/KenBurnsImage,既未用 staticFile 也无 delayRender——照抄 reference 会拿不到 Remotion public/ 之外的资源,且仍可能出现空白帧。
- [discrepancy] 被引用的「打包脚本」是否真的存在 — reference 以 `scripts/build-manifest.mjs` 文件名给出源码、SKILL.md 也写 `node scripts/build-manifest.mjs ./photos`,但仓库根 scripts/ 只有 seek-shot.sh / contact-sheet.sh / probe-mp4.sh,没有 build-manifest.mjs;`python beats.py` 同理(脚本内联在文档里)。使用前必须自行落盘。
- [ok] 起始 scale 与运动确定性口径 — SKILL.md 的 start scale ≥ 1.05、seeded by index 与 ken-burns.md 的 `const base = 1.05;`、`mulberry32(index * 2654435761)` 完全一致。
- [ok] 适配策略与判据是否自洽 — aspect-and-layout.md 的三策略表与 fitStrategy 阈值(<0.15 → cover)一致;SKILL.md 的 never stretch 与 blurred-pad 默认选择一致。
- [ok] 换片节奏的量化声明 — timing-and-audio.md 的每张 3–6s、低于约 2.5s 会显仓促、每 2/4 拍换片,与 SKILL.md 的 photos hold ≥1 musical unit, not every beat 一致。
- [unlocatable] 隐私面:照片元数据是否被提示处理 — manifest 读取 EXIF DateTimeOriginal 并把日期上屏,但全文未讨论照片 GPS/相机信息的处理与输出视频的元数据策略;源码内定位不到任何隐私处理声明,记 unlocatable(能力缺口,不作为档位依据)。
6结论
58ffd9961bdf764e…2bd0dd19c9