photo-slideshow

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Slideshow 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
scale
and
translate
together; never animate
width
/
height
(it reflows and flickers).
KnobRangeWhy
Zoomin
1.0→1.12
or out
1.12→1.0
subtle; over ~1.15 it feels like a crash-zoom
Panone of L/R/up/down/diagonaldirection chosen by seed, never all the same
Ease
easeInOutSine
slow, continuous — Ken Burns has no acceleration jolt
Anchor
transform-origin
toward the subject
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 ≥
1.05
so a pan never exposes an empty edge. See
references/ken-burns.md
for the full seeded
<KenBurnsImage>
component with subject-aware anchoring.
每张幻灯片从少量运动模式中选择一种,让整体富有节奏感且不重复。同时动画
scale
translate
属性;绝不要动画
width
/
height
(会导致重排和闪烁)。
参数范围原因
缩放放大
1.0→1.12
或缩小
1.12→1.0
效果柔和;超过1.15会显得像急推镜头
平移左/右/上/下/对角线中的一种方向由种子决定,绝不重复
缓动
easeInOutSine
缓慢、连续——Ken Burns效果没有加速冲击
锚点
transform-origin
朝向主体
平移时保持人物面部/地平线在画面内
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.md

Transitions 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
undefined

extract 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

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

然后将节拍秒数转换为帧编号,并分配幻灯片边界。每张照片保持展示一个音乐单元时长(通常是每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:
StrategyLookUse when
Blurred padphoto fit inside frame, a scaled+blurred copy fills the barsmixed orientations, default choice
Fill (cover)photo fills frame, edges croppedphotos roughly match the frame shape
Fit (letterbox)photo fully visible on solid/gradientevery 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
cover
blurred layer behind, the
contain
sharp photo on top. See
references/aspect-and-layout.md
for the component and the orientation-detection logic.
实际的照片文件夹通常混合了竖屏手机照片和横屏相机照片。绝不要拉伸——变形是不可接受的。以下三种策略按优先级排序:
策略效果使用场景
模糊填充照片适配在画面内,缩放并模糊的副本填充黑边混合横竖屏照片,默认选择
填充(覆盖)照片填满画面,边缘被裁剪照片比例与画面大致匹配
适配(留黑边)照片完整显示在纯色/渐变背景上每一个像素都很重要(文档、艺术品)
模糊填充是现代标准,因为它能填满画面且不裁剪主体。渲染两次相同的源:底层是
cover
模式的模糊图层,上层是
contain
模式的清晰照片。组件和方向检测逻辑可参考
references/aspect-and-layout.md

Captions, 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日期、去重)、完整的数据驱动
<Slideshow>
组件,以及无需React的纯FFmpeg方案,可参考
references/templating-folder.md

build 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.
  • 每张照片的运动效果由索引生成种子——方向/缩放各不相同,无重复。
  • 所有运动均为
    useCurrentFrame()
    的纯函数;初始缩放比例≥1.05。
  • 使用一种统一的转场效果(默认淡入淡出),采用重叠切换而非硬切。
  • 幻灯片切换对齐节拍;每张照片展示时长≥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
    useCurrentFrame()
    ; start scale ≥ 1.05.
  • 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项目(
    <Composition>
    + zod
    schema
    +
    defaultProps
    );Ken Burns缩放/平移、转场和节拍计时均为
    useCurrentFrame()
    的纯函数(无CSS转场、定时器、
    Date.now()
    ;运动效果由索引生成种子,而非
    Math.random()
    )。
  • 通过
    staticFile()
    加载照片,使用
    delayRender
    /
    continueRender
    确保每张图片(及任何字幕字体)在其帧渲染前已加载完成——否则幻灯片会出现空白。时长由数据决定→在
    calculateMetadata
    中根据照片数量计算,而非手动设置。
  • 交付物 = 渲染后的
    out/*.mp4
    + 项目文件(文件夹变更时可重新渲染)。
验证流程 — 渲染静帧→检查→编码。 先渲染低成本的PNG,确认照片显示正确后再渲染视频。
bash
undefined

Deliver & verify (rendered stills → MP4)

使用要交付的清单(真实照片路径)渲染精确帧的静帧,而非defaultProps

Packaged helper (
scripts/
): tile your stills with
scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png
, then assert the encode with
scripts/probe-mp4.sh out.mp4 [WxH] [fps]
. See
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 (
    <Composition>
    + zod
    schema
    +
    defaultProps
    ); Ken Burns scale/pan, transitions and beat timing all pure functions of
    useCurrentFrame()
    (no CSS transitions, timers,
    Date.now()
    ; motion seeded by index, not
    Math.random()
    ).
  • 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. Duration is data-dependent → compute it in
    calculateMetadata
    from the photo count, not by hand.
  • Deliverable = the rendered
    out/*.mp4
    plus the project (re-render when the folder changes).
Verify loop — render stills → inspect → encode. Cheap PNGs first, video only once the photos land right.
bash
undefined
npx 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、字幕超出下方安全区域、宽高比错误/留黑边)。

```bash

end 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).

```bash
npx 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.md
    — 完整的带种子
    <KenBurnsImage>
    Remotion组件:通过
    mulberry32(index)
    选择单张照片的运动模式、支持主体感知的
    transform-origin
    、安全的初始缩放比例计算,以及FFmpeg的
    zoompan
    等效实现。
  • references/timing-and-audio.md
    — 使用librosa检测节拍、节拍秒数转帧、音乐单元幻灯片分配、无音频分析的BPM网格 fallback 方案,以及每张照片的节奏预算。
  • references/aspect-and-layout.md
    — 方向检测、模糊填充/覆盖/适配布局组件,以及16:9、9:16和1:1的安全区域映射,使一个主模板可适配所有平台。
  • references/templating-folder.md
    — 文件夹→清单生成器(自然排序、EXIF拍摄日期、从文件名提取字幕)、带片头片尾的完整数据驱动
    <Slideshow>
    组件,以及纯FFmpeg幻灯片流程。

Reference files

  • references/ken-burns.md
    — the complete seeded
    <KenBurnsImage>
    Remotion component: a per-photo motion vocabulary chosen by
    mulberry32(index)
    , subject-aware
    transform-origin
    , safe start-scale math, and an FFmpeg
    zoompan
    equivalent.
  • references/timing-and-audio.md
    — 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/aspect-and-layout.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/templating-folder.md
    — the folder→manifest builder (natural sort, EXIF capture date, captions from filenames), the full data-driven
    <Slideshow>
    with intro/outro cards, and a pure-FFmpeg slideshow pipeline.