photo-slideshow
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSlideshow Video
幻灯片视频
Turn a set of photos into a finished video that feels shot, not assembled. The craft is giving each still its own gentle motion (Ken Burns), changing slides on the music, handling photos of different shapes gracefully, and reading a whole folder so the same template renders any set of images.
将一组照片转化为成品视频,使其看起来像是拍摄而成,而非简单拼接。关键在于为每张静态图片添加独特的轻柔运动效果(Ken Burns),让幻灯片切换与音乐同步,优雅处理不同比例的照片,并且支持读取整个文件夹,使同一模板可渲染任意图片集。
When to use
使用场景
- A folder of photos → one finished MP4 (the headline use case).
- "Memories", recap, year-in-review, wedding/trip montage with music.
- Any still-image set that needs motion, transitions, and synced pacing.
- 一个照片文件夹 → 一个成品MP4(核心使用场景)。
- “回忆”“回顾”“年度总结”“婚礼/旅行蒙太奇”等带音乐的视频制作。
- 任何需要添加运动效果、转场和同步节奏的静态图片集。
The one rule that makes a slideshow look professional
让幻灯片看起来专业的黄金法则
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.
js
// seeded PRNG → same photo always gets the same motion, but each photo differs
const mulberry32 = (a) => () => {
a |= 0; a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};任意两张幻灯片的运动方式不能相同。 业余幻灯片的标志性特征就是每张照片都做完全相同的缓慢放大。要改变每张照片的运动方向、放大/缩小模式以及初始缩放比例——但需保证运动是确定性的(由索引生成随机种子),这样重新渲染的结果完全一致,基于帧的渲染器也能保持稳定。
js
// 带种子的伪随机数生成器 → 同一张照片始终使用相同的运动效果,但每张照片的效果不同
const mulberry32 = (a) => () => {
a |= 0; a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};Per-photo Ken Burns
单张照片的Ken Burns效果
Each slide picks one move from a small vocabulary so the set has rhythm without repetition. Animate and together; never animate / (it reflows and flickers).
scaletranslatewidthheight| Knob | Range | Why |
|---|---|---|
| Zoom | in | subtle; over ~1.15 it feels like a crash-zoom |
| Pan | one of L/R/up/down/diagonal | direction chosen by seed, never all the same |
| Ease | | slow, continuous — Ken Burns has no acceleration jolt |
| Anchor | | keeps a face/horizon in frame while panning |
js
import { useCurrentFrame, interpolate, Easing } from "remotion";
const frame = useCurrentFrame(); // the ONLY clock
const p = interpolate(frame, [0, durInFrames], [0, 1], {
extrapolateRight: "clamp", easing: Easing.inOut(Easing.sin),
});
const scale = startScale + (endScale - startScale) * p; // e.g. 1.0 → 1.10
const x = panX * p, y = panY * p; // px, sign from seed
// style: transform: `scale(${scale}) translate(${x}px, ${y}px)`Always start scale ≥ so a pan never exposes an empty edge. See for the full seeded component with subject-aware anchoring.
1.05references/ken-burns.md<KenBurnsImage>每张幻灯片从少量运动模式中选择一种,让整体富有节奏感且不重复。同时动画和属性;绝不要动画/(会导致重排和闪烁)。
scaletranslatewidthheight| 参数 | 范围 | 原因 |
|---|---|---|
| 缩放 | 放大 | 效果柔和;超过1.15会显得像急推镜头 |
| 平移 | 左/右/上/下/对角线中的一种 | 方向由种子决定,绝不重复 |
| 缓动 | | 缓慢、连续——Ken Burns效果没有加速冲击 |
| 锚点 | | 平移时保持人物面部/地平线在画面内 |
js
import { useCurrentFrame, interpolate, Easing } from "remotion";
const frame = useCurrentFrame(); // 唯一的时钟
const p = interpolate(frame, [0, durInFrames], [0, 1], {
extrapolateRight: "clamp", easing: Easing.inOut(Easing.sin),
});
const scale = startScale + (endScale - startScale) * p; // 示例:1.0 → 1.10
const x = panX * p, y = panY * p; // 像素值,符号由种子决定
// 样式:transform: `scale(${scale}) translate(${x}px, ${y}px)`始终将初始缩放比例设为≥,这样平移时绝不会露出空白边缘。完整的带种子、支持主体感知锚点的组件可参考。
1.05<KenBurnsImage>references/ken-burns.mdTransitions between photos
照片间的转场效果
Pick one transition and keep it consistent — mixing wipes, spins and cubes screams "template." A 0.4–0.6s crossfade is the safe, timeless default; a soft push works for travel sequences. Overlap outgoing and incoming slides so the cut is never hard unless cutting on a beat.
jsx
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
// <TransitionSeries.Sequence durationInFrames={hold}> ...slide... </Sequence>
// <TransitionSeries.Transition timing={linearTiming({durationInFrames: 15})} presentation={fade()} />选择一种转场效果并保持一致——混合擦除、旋转和立方体转场会明显暴露“模板痕迹”。0.4–0.6秒的淡入淡出是安全且经典的默认选择;柔和推送效果适合旅行场景。让 outgoing 和 incoming 幻灯片重叠,除非在节拍点切换,否则不要硬切。
jsx
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
// <TransitionSeries.Sequence durationInFrames={hold}> ...幻灯片... </Sequence>
// <TransitionSeries.Transition timing={linearTiming({durationInFrames: 15})} presentation={fade()} />Beat / music-synced slide changes
节拍/音乐同步的幻灯片切换
A slideshow lives or dies on pacing. Lock the track first, get the beat timestamps, then snap each slide change to a beat. Detect beats once (offline) and bake them into props — never analyze audio per frame.
bash
undefined幻灯片的成败取决于节奏。先锁定音轨,获取节拍时间戳,然后将每个幻灯片切换点对齐到节拍。只需离线检测一次节拍并将其写入props——绝不要逐帧分析音频。
bash
undefinedextract beat onset times (seconds) with librosa, write to beats.json
使用librosa提取节拍起始时间(秒),写入beats.json
python3 -c "import librosa,json,sys; y,sr=librosa.load(sys.argv[1]);
_,b=librosa.beat.beat_track(y=y,sr=sr);
json.dump([float(t) for t in librosa.frames_to_time(b,sr=sr)], open('beats.json','w'))" song.mp3
_,b=librosa.beat.beat_track(y=y,sr=sr);
json.dump([float(t) for t in librosa.frames_to_time(b,sr=sr)], open('beats.json','w'))" song.mp3
Then convert beat seconds → frame numbers and assign slide boundaries. Hold each photo for a musical unit (often every 2nd or 4th beat — every beat is too frantic for photos). For the count→frame math, the rhythm-to-slide assignment, and a no-audio-analysis fallback (fixed BPM grid), see `references/timing-and-audio.md`.python3 -c "import librosa,json,sys; y,sr=librosa.load(sys.argv[1]);
_,b=librosa.beat.beat_track(y=y,sr=sr);
json.dump([float(t) for t in librosa.frames_to_time(b,sr=sr)], open('beats.json','w'))" song.mp3
_,b=librosa.beat.beat_track(y=y,sr=sr);
json.dump([float(t) for t in librosa.frames_to_time(b,sr=sr)], open('beats.json','w'))" song.mp3
然后将节拍秒数转换为帧编号,并分配幻灯片边界。每张照片保持展示一个音乐单元时长(通常是每2拍或4拍——每拍切换对照片来说过于仓促)。关于计数转帧的计算、节奏到幻灯片的分配,以及无音频分析的 fallback 方案(固定BPM网格),可参考`references/timing-and-audio.md`。Mixed aspect-ratio photos
混合宽高比的照片处理
A real folder mixes portrait phone shots and landscape cameras. Never stretch — distortion is unforgivable. Three strategies, in order of preference:
| Strategy | Look | Use when |
|---|---|---|
| Blurred pad | photo fit inside frame, a scaled+blurred copy fills the bars | mixed orientations, default choice |
| Fill (cover) | photo fills frame, edges cropped | photos roughly match the frame shape |
| Fit (letterbox) | photo fully visible on solid/gradient | every pixel matters (documents, art) |
Blurred-pad is the modern standard because it fills the frame without cropping the subject. Render the same source twice: a blurred layer behind, the sharp photo on top. See for the component and the orientation-detection logic.
covercontainreferences/aspect-and-layout.md实际的照片文件夹通常混合了竖屏手机照片和横屏相机照片。绝不要拉伸——变形是不可接受的。以下三种策略按优先级排序:
| 策略 | 效果 | 使用场景 |
|---|---|---|
| 模糊填充 | 照片适配在画面内,缩放并模糊的副本填充黑边 | 混合横竖屏照片,默认选择 |
| 填充(覆盖) | 照片填满画面,边缘被裁剪 | 照片比例与画面大致匹配 |
| 适配(留黑边) | 照片完整显示在纯色/渐变背景上 | 每一个像素都很重要(文档、艺术品) |
模糊填充是现代标准,因为它能填满画面且不裁剪主体。渲染两次相同的源:底层是模式的模糊图层,上层是模式的清晰照片。组件和方向检测逻辑可参考。
covercontainreferences/aspect-and-layout.mdCaptions, dates, intro/outro
字幕、日期、片头片尾
- Captions/dates: read from filename, EXIF, or a sidecar JSON; keep them in a fixed lower-third zone, fade in after the slide settles (~0.3s), out before it leaves.
- Intro card: title + date range, held 1.5–2s, before the first photo.
- Outro card: a closing line or a contact/CTA, held 2–3s, so the video ends on intent, not a fade to black mid-photo.
- 字幕/日期:从文件名、EXIF或辅助JSON读取;固定在画面下方区域,幻灯片稳定后(约0.3秒)淡入,切换前淡出。
- 片头卡片:标题+日期范围,展示1.5–2秒,在第一张照片之前。
- 片尾卡片:收尾语句或联系方式/行动号召,展示2–3秒,让视频有明确的结尾,而非在照片中间淡入黑场。
Templating a folder → a video
将文件夹模板化为视频
The payoff: point the composition at a folder, derive the timeline from the file list, render once. Nothing about the photos is hardcoded — count, order, captions and durations all come from data.
jsx
// data is a prop: [{ src, caption?, date? }]; the timeline is computed, not authored
export const Slideshow = ({ photos, beats, music }) => { /* maps photos → slides */ };bash
undefined最终成果:将合成指向一个文件夹,从文件列表生成时间线,一键渲染。照片的所有信息都不是硬编码的——数量、顺序、字幕和时长均来自数据。
jsx
// data是一个props: [{ src, caption?, date? }]; 时间线是计算生成的,而非手动编写
export const Slideshow = ({ photos, beats, music }) => { /* 将photos映射为slides */ };bash
// 从文件夹生成清单(按排序,包含EXIF日期),然后渲染
node scripts/build-manifest.mjs ./photos > photos.json
npx remotion render Slideshow out/slideshow.mp4 --props=photos.json将颜色、字体、转场和幻灯片时长统一放在一个主题对象中,这样每次渲染都保持一致,只有照片会变化。清单生成器(排序、EXIF日期、去重)、完整的数据驱动组件,以及无需React的纯FFmpeg方案,可参考。
<Slideshow>references/templating-folder.mdbuild the manifest from a folder (sorted, with EXIF dates), then render
输出检查清单
node scripts/build-manifest.mjs ./photos > photos.json
npx remotion render Slideshow out/slideshow.mp4 --props=photos.json
Keep colors, fonts, transition and slide duration in one theme object so every render stays consistent and only the photos change. See `references/templating-folder.md` for the manifest builder (sort, EXIF date, dedupe), the full data-driven `<Slideshow>`, and an FFmpeg-only path for when React isn't available.- 每张照片的运动效果由索引生成种子——方向/缩放各不相同,无重复。
- 所有运动均为的纯函数;初始缩放比例≥1.05。
useCurrentFrame() - 使用一种统一的转场效果(默认淡入淡出),采用重叠切换而非硬切。
- 幻灯片切换对齐节拍;每张照片展示时长≥1个音乐单元,而非每拍切换。
- 混合宽高比采用模糊填充处理——无拉伸变形。
- 字幕/日期固定在指定区域;片头片尾卡片包裹整个图片集。
- 照片、字幕和时长均来自文件夹清单——一个模板适配任意图片集。
Output checklist
交付与验证(渲染静帧→MP4)
- Every photo's motion is seeded by index — varied direction/zoom, no two identical.
- All motion is a pure function of ; start scale ≥ 1.05.
useCurrentFrame() - One consistent transition (crossfade default), overlapped not hard-cut.
- Slide changes land on beats; photos hold ≥1 musical unit, not every beat.
- Mixed aspect ratios handled by blurred-pad — nothing stretched.
- Captions/dates in a fixed zone; intro and outro cards bookend the set.
- Photos, captions and durations come from a folder manifest — one template, any set.
打包工具():使用scripts/拼接静帧,然后用scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png验证编码。详情见scripts/probe-mp4.sh out.mp4 [WxH] [fps]。scripts/README.md
这是重量级交付:成品是MP4,而非网页。幻灯片会导入用户的整个照片文件夹——验证环节主要是确认每张照片是否都正确加载且无变形地重新构图。
输出规范:
- 一个注册了合成的Remotion项目(+ zod
<Composition>+schema);Ken Burns缩放/平移、转场和节拍计时均为defaultProps的纯函数(无CSS转场、定时器、useCurrentFrame();运动效果由索引生成种子,而非Date.now())。Math.random() - 通过加载照片,使用
staticFile()/delayRender确保每张图片(及任何字幕字体)在其帧渲染前已加载完成——否则幻灯片会出现空白。时长由数据决定→在continueRender中根据照片数量计算,而非手动设置。calculateMetadata - 交付物 = 渲染后的+ 项目文件(文件夹变更时可重新渲染)。
out/*.mp4
验证流程 — 渲染静帧→检查→编码。 先渲染低成本的PNG,确认照片显示正确后再渲染视频。
bash
undefinedDeliver & verify (rendered stills → MP4)
使用要交付的清单(真实照片路径)渲染精确帧的静帧,而非defaultProps
Packaged helper (): tile your stills withscripts/, then assert the encode withscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png. Seescripts/probe-mp4.sh out.mp4 [WxH] [fps].scripts/README.md
This is heavy-tier: the deliverable is an MP4, not a live page. A slideshow ingests a whole folder of user photos — the verify pass is mostly did every photo actually load and reframe without distortion.
Output contract:
- A Remotion project with the composition registered (+ zod
<Composition>+schema); Ken Burns scale/pan, transitions and beat timing all pure functions ofdefaultProps(no CSS transitions, timers,useCurrentFrame(); motion seeded by index, notDate.now()).Math.random() - Photos loaded via , gated with
staticFile()/delayRenderso each image (and any caption font) is present before its frame renders — otherwise a slide pops in blank. Duration is data-dependent → compute it incontinueRenderfrom the photo count, not by hand.calculateMetadata - Deliverable = the rendered plus the project (re-render when the folder changes).
out/*.mp4
Verify loop — render stills → inspect → encode. Cheap PNGs first, video only once the photos land right.
bash
undefinednpx remotion still Slideshow out/f-intro.png --frame=15 --props=photos.json # 片头卡片
npx remotion still Slideshow out/f-mid.png --frame=200 --props=photos.json # 中间幻灯片(Ken Burns效果进行中)
npx remotion still Slideshow out/f-outro.png --frame=500 --props=photos.json # 片尾卡片
Frame-exact stills WITH THE MANIFEST YOU'LL SHIP (real photo paths), not defaultProps
结束帧 = durationInFrames - 1(可通过npx remotion compositions查看)
npx remotion still Slideshow out/f-intro.png --frame=15 --props=photos.json # intro card
npx remotion still Slideshow out/f-mid.png --frame=200 --props=photos.json # a mid slide mid-Ken-Burns
npx remotion still Slideshow out/f-outro.png --frame=500 --props=photos.json # outro card
检查每张PNG的**保真度**(每张幻灯片显示正确的照片;字幕/日期文本正确;模糊填充处理混合宽高比无拉伸)和**瑕疵**(图片空白/未加载、照片拉伸/变形、平移露出空白边缘——初始缩放比例需≥1.05、字幕超出下方安全区域、宽高比错误/留黑边)。
```bashend frame = durationInFrames - 1 (npx remotion compositions reads it)
仅在静帧检查无误后执行:
Inspect each PNG for **fidelity** (correct photo on each slide; caption/date text right; blurred-pad fills mixed aspect ratios without stretch) AND **artifacts** (image blank/not loaded, photo stretched/distorted, pan exposing an empty edge — start scale ≥ 1.05, caption outside the lower-third safe zone, wrong aspect/letterboxing).
```bashnpx remotion render Slideshow out/slideshow.mp4 --props=photos.json
npx remotion render Slideshow out/slideshow.gif --props=photos.json --codec=gif # README封面演示图
**批量处理(一个模板,多个文件夹):** 当用同一模板处理多个照片集时,先通过静帧验证一个代表性文件夹的清单,再批量渲染其余文件夹——一次性发现空白图片或宽高比拉伸问题,而非重复N次。
**完成前检查:**
1. 片头/中间/片尾的静帧渲染无误——无错误,照片已正确加载(非空白)。
2. 字幕/日期正确且位于固定的下方区域;混合宽高比采用模糊填充,无拉伸;平移未露出空白边缘。
3. 所有运动均由帧驱动且由索引生成种子——无CSS转场/定时器/`Date.now()`/`Math.random()`。
4. **交付的**文件夹清单渲染正确,而非仅`defaultProps`。
5. 完整MP4已编码且可正常播放;(可选)已渲染GIF用于README。Only after stills are clean:
参考文件
npx remotion render Slideshow out/slideshow.mp4 --props=photos.json
npx remotion render Slideshow out/slideshow.gif --props=photos.json --codec=gif # README first-screen proof
**Batch (one template, many folders):** when running the same template over several photo sets, verify ONE representative folder's manifest via stills before batch-rendering the rest — catch a blank-image or stretched-aspect bug once, not N times.
**Before you finish:**
1. Stills render cleanly at intro / mid / outro — no errors, photos actually loaded (not blank).
2. Captions/dates correct and in the fixed lower-third zone; mixed aspect ratios blurred-padded, nothing stretched; no empty edge from a pan.
3. All motion frame-driven and seeded by index — no CSS transitions / timers / `Date.now()` / `Math.random()`.
4. The **shipped** folder manifest renders correctly, not just `defaultProps`.
5. Full MP4 encoded and plays; (optional) GIF rendered for the README.- — 完整的带种子
references/ken-burns.mdRemotion组件:通过<KenBurnsImage>选择单张照片的运动模式、支持主体感知的mulberry32(index)、安全的初始缩放比例计算,以及FFmpeg的transform-origin等效实现。zoompan - — 使用librosa检测节拍、节拍秒数转帧、音乐单元幻灯片分配、无音频分析的BPM网格 fallback 方案,以及每张照片的节奏预算。
references/timing-and-audio.md - — 方向检测、模糊填充/覆盖/适配布局组件,以及16:9、9:16和1:1的安全区域映射,使一个主模板可适配所有平台。
references/aspect-and-layout.md - — 文件夹→清单生成器(自然排序、EXIF拍摄日期、从文件名提取字幕)、带片头片尾的完整数据驱动
references/templating-folder.md组件,以及纯FFmpeg幻灯片流程。<Slideshow>
Reference files
—
- — the complete seeded
references/ken-burns.mdRemotion component: a per-photo motion vocabulary chosen by<KenBurnsImage>, subject-awaremulberry32(index), safe start-scale math, and an FFmpegtransform-originequivalent.zoompan - — beat detection with librosa, beat-seconds → frame conversion, musical-unit slide assignment, BPM-grid fallback with no audio analysis, and pacing budgets per photo.
references/timing-and-audio.md - — orientation detection, the blurred-pad / cover / contain layouts as components, and safe-zone maps for 16:9, 9:16 and 1:1 so one master reframes to every platform.
references/aspect-and-layout.md - — the folder→manifest builder (natural sort, EXIF capture date, captions from filenames), the full data-driven
references/templating-folder.mdwith intro/outro cards, and a pure-FFmpeg slideshow pipeline.<Slideshow>
—