wrapped-video

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Wrapped Video

Wrapped视频

Build a "Spotify Wrapped"-style recap: take a row of data about one person (or account, team, year) and turn it into a punchy, shareable vertical video. The core idea is one template × a data table → many personalized videos. Write the template once, then render a unique film for every row.
制作「Spotify Wrapped」风格的回顾视频:提取单个人(或账户、团队、年份)的一行数据,将其转换为生动、可分享的竖版视频。核心思路是一个模板 × 一张数据表 → 多个个性化视频。只需编写一次模板,即可为每一行数据渲染出独特的视频。

When to use

适用场景

  • Year-in-review / "your 2026 wrapped" recaps for any product with per-user stats.
  • Personalized data videos: fitness year, reading year, spending recap, gaming stats, sales rep recap, student progress.
  • Any time the deliverable is "the same video, but with each person's numbers" — at 1 or 100,000 copies.
This is a data → share-bait pattern, not a hand-edited film. If there is no data table (or no per-record data), use a different skill.
  • 为拥有用户专属统计数据的产品制作年度回顾/「你的2026 Wrapped」视频。
  • 个性化数据视频:健身年度总结、阅读年度回顾、消费复盘、游戏统计、销售代表业绩回顾、学生进度报告。
  • 任何需要产出「同款视频,但替换每个人的专属数据」的场景——无论是1个还是10万个视频副本。
这是一种数据→爆款分享内容模式,而非手动剪辑的影片。如果没有数据表(或无单条记录数据),请使用其他技能。

The two non-negotiables

两大核心原则

  1. Data drives everything. Every headline, number, name, and color comes from props, never hardcoded. A scene that can't be filled from a data row does not belong in a Wrapped.
  2. Built to be screenshotted. Each scene must read in under 2 seconds and look good frozen — that frozen frame is what gets shared to a story. Design for the pause, not the play.
  1. 数据驱动一切。所有标题、数字、名称和颜色均来自props,绝不硬编码。无法通过数据行填充的场景不应出现在Wrapped视频中。
  2. 为截图而生。每个场景必须在2秒内清晰可读,且静态画面美观——用户会将这张静态截图分享到动态中。要为暂停状态设计,而非播放状态。

The Wrapped scene grammar

Wrapped场景语法

A Wrapped is a fixed sequence of short scene types, each ~2.5–4s. Pick 5–7 and order them as a build. Same grammar every year; only the data and palette change.
Scene typeJobData shape
Intro / "Your 2026, wrapped"Brand the moment, set palettename, year
Big-number revealOne hero stat, counts up hugeone number + unit + label
Top-X listRanked 1→5, staggered inarray of
{rank, label, value}
Superlative / persona"You're in the top 1%", an archetypecomputed tier/label
Comparison"more than 92% of listeners"percentile or ratio
Time/heatmap"your busiest month was March"series or peak
Outro / share cardLogo + handle + CTA, holds stillname, handle
Order as a crescendo: small context first, biggest/most personal stat as the climax, then the still share card. See
references/scene-grammar.md
for a full 7-scene storyboard with timings.
Wrapped视频由一系列固定的短场景类型组成,每个场景时长约2.5–4秒。选择5–7种场景并按递进顺序排列。每年使用相同的语法,仅更换数据和配色方案。
场景类型作用数据格式
开场/「你的2026 Wrapped」强化品牌场景,设定配色姓名、年份
大额数字展示单个核心统计数据,动态递增至目标值单个数字 + 单位 + 标签
Top-X榜单按1→5排名,依次入场
{rank, label, value}
数组
最高级/用户画像「你跻身前1%」,定义用户类型计算得出的层级/标签
对比数据「超过92%的听众」百分位数或比例
时间/热力图「你最忙碌的月份是3月」序列数据或峰值数据
结尾/分享卡片品牌Logo + 账号 + 行动号召,保持静态姓名、账号
按高潮递进顺序排列:先展示小范围背景信息,将最重磅/最个性化的统计数据作为高潮,最后展示静态分享卡片。详见
references/scene-grammar.md
中的完整7场景故事板及时间安排。

The recap story arc

回顾视频的叙事弧线

The scene grammar is the skeleton; the arc is what makes a Wrapped feel like a gift instead of a dashboard. A recap is a tiny five-beat drama about one viewer — sequence it so each reveal feels bigger and more personal than the last.
BeatJobMaps to
Build-upBrand the moment, promise it's theirsIntro: "{name}, your {year}"
Escalating revealsStack stats that rise in stakesTop-X, genres, time patterns
The "big number"One hero stat, max scale, held longestBig-number / count-up climax
Personalized superlativeName who they are, not just what they didPersona/percentile tier
Shareable payoffA still poster they want to postOutro share card
Sequence stats for rising impact — smallest first, biggest last. Rank every stat by emotional payload (raw size, rarity, how flattering) and play them in ascending order. Never open on the hero number: there's nowhere to climb after it, and the rest of the film feels like a comedown. Hold the climax stat largest and longest; everything before it is set-up.
The hook is "this is about YOU." A Wrapped wins because the viewer is the protagonist. Earn that in the first 2 seconds: lead with their name, use their per-user
accent
, and write every line in second person ("You listened to 412 artists"). Generic copy ("Top genre: Indie") breaks the spell — reframe as "You're an Indie kind of person." If a frame would read identically for two different users, it isn't pulling its weight.
Design the final share-frame as the destination. The whole arc exists to deliver a poster worth posting. The outro is not a credits roll — it's the payoff: the headline superlative or hero number restated, name + @handle, logo, one short CTA, holding completely still ≥2s so a screenshot or auto-loop lands clean. Decide this frame first and build the crescendo toward it. Copywriting tiers and the full storyboard are in
references/scene-grammar.md
.
场景语法是骨架,而叙事弧线是让Wrapped视频感觉像一份礼物而非仪表盘的关键。回顾视频是围绕单个观众展开的微型五幕剧——场景顺序需让每一次数据展示都比上一次更重磅、更具个性化。
阶段作用对应场景
铺垫强化品牌场景,承诺这是用户专属内容开场:「{name},你的{year}」
递进式展示依次展示重要性递增的统计数据Top-X榜单、品类、时间规律
「核心数字」单个核心统计数据,最大尺寸展示,时长最久大额数字/递增高潮场景
个性化最高级描述定义用户的身份,而非仅描述行为用户画像/百分位层级
可分享回报用户愿意分享的静态海报结尾分享卡片
按影响力递增顺序排列统计数据——从小到大,最后展示最重磅的内容。按情感权重(数值大小、稀有度、讨好程度)为所有统计数据排序,按升序播放。绝不能以核心数字开场:否则后续内容会显得平淡,影片整体效果会大打折扣。核心统计数据要以最大尺寸展示最久时间;所有前置内容都是铺垫。
钩子是「这是专属于你的内容」。Wrapped视频成功的原因在于观众是主角。在前2秒就要抓住用户:以他们的姓名开场,使用用户专属的
accent
配色,所有文案使用第二人称(「你收听了412位艺术家的作品」)。通用文案(「顶级品类:独立音乐」)会打破沉浸感——要重构为「你是独立音乐爱好者」。如果两个不同用户的画面文案完全相同,那这个画面就是无效的。
以最终分享画面为目标进行设计。整个叙事弧线的存在都是为了产出值得分享的海报。结尾不是片尾字幕——而是回报:重申核心最高级描述或核心数字,加上姓名 + @账号、品牌Logo、简短行动号召,完全静止≥2秒,确保截图或自动循环播放时画面清晰。先确定这个画面,再围绕它构建递进式叙事。文案层级和完整故事板详见
references/scene-grammar.md

Data → video shape

数据→视频格式

Define a typed schema for one record. The whole video is a pure function of it.
ts
// src/schema.ts
import { z } from "zod";
export const wrappedSchema = z.object({
  name: z.string(),
  year: z.number(),
  minutesListened: z.number(),
  topArtists: z.array(z.object({ rank: z.number(), label: z.string(), value: z.number() })),
  topGenre: z.string(),
  percentile: z.number(), // 0–100, "top X%"
  accent: z.string(),     // per-user palette, e.g. "#1DB954"
});
export type Wrapped = z.infer<typeof wrappedSchema>;
Register it as
defaultProps
+
schema
on the composition so each render just swaps props.
tsx
// src/Root.tsx
import { Composition } from "remotion";
import { Wrapped as Recap } from "./Wrapped";
import { wrappedSchema } from "./schema";

export const Root = () => (
  <Composition
    id="Wrapped"
    component={Recap}
    schema={wrappedSchema}
    durationInFrames={30 * 22}  // 22s @ 30fps
    fps={30}
    width={1080} height={1920}  // 9:16 vertical — the share format
    defaultProps={{ name: "Sam", year: 2026, minutesListened: 41203,
      topArtists: [{ rank: 1, label: "Phoebe Bridgers", value: 312 }],
      topGenre: "Indie", percentile: 3, accent: "#1DB954" }}
  />
);
为单条记录定义类型化schema。整个视频是该schema的纯函数。
ts
// src/schema.ts
import { z } from "zod";
export const wrappedSchema = z.object({
  name: z.string(),
  year: z.number(),
  minutesListened: z.number(),
  topArtists: z.array(z.object({ rank: z.number(), label: z.string(), value: z.number() })),
  topGenre: z.string(),
  percentile: z.number(), // 0–100, "top X%"
  accent: z.string(),     // per-user palette, e.g. "#1DB954"
});
export type Wrapped = z.infer<typeof wrappedSchema>;
将其注册为合成组件的
defaultProps
+
schema
,这样每次渲染只需替换props即可。
tsx
// src/Root.tsx
import { Composition } from "remotion";
import { Wrapped as Recap } from "./Wrapped";
import { wrappedSchema } from "./schema";

export const Root = () => (
  <Composition
    id="Wrapped"
    component={Recap}
    schema={wrappedSchema}
    durationInFrames={30 * 22}  // 22s @ 30fps
    fps={30}
    width={1080} height={1920}  // 9:16 vertical — the share format
    defaultProps={{ name: "Sam", year: 2026, minutesListened: 41203,
      topArtists: [{ rank: 1, label: "Phoebe Bridgers", value: 312 }],
      topGenre: "Indie", percentile: 3, accent: "#1DB954" }}
  />
);

The signature move: the big-number counter

标志性效果:大额数字计数器

The count-up is the heartbeat of every Wrapped. Drive it from
useCurrentFrame()
(never
setState
/
setInterval
— that flickers on render), ease it with a spring, and format with
toLocaleString()
.
tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";

export const BigNumber: React.FC<{ value: number; label: string; accent: string }> =
({ value, label, accent }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const progress = spring({ frame, fps, config: { damping: 200 } }); // 0→1, settles
  const shown = Math.round(interpolate(progress, [0, 1], [0, value]));
  const pop = interpolate(progress, [0, 1], [0.6, 1]); // overshoot-free scale-in
  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "center",
        justifyContent: "center", height: "100%", transform: `scale(${pop})` }}>
      <span style={{ fontSize: 220, fontWeight: 900, color: accent, lineHeight: 1,
          fontVariantNumeric: "tabular-nums" }}>{shown.toLocaleString()}</span>
      <span style={{ fontSize: 48, color: "#fff", marginTop: 24 }}>{label}</span>
    </div>
  );
};
Use
tabular-nums
so digits don't jitter width as they roll. For the top-X list, reuse one spring per item with a staggered
delay
(rank 1 first) — see
references/remotion-recipes.md
.
数字递增动画是每个Wrapped视频的核心。通过
useCurrentFrame()
驱动(绝不要使用
setState
/
setInterval
——这会导致渲染闪烁),使用弹簧动画缓动,并用
toLocaleString()
格式化。
tsx
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";

export const BigNumber: React.FC<{ value: number; label: string; accent: string }> =
({ value, label, accent }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const progress = spring({ frame, fps, config: { damping: 200 } }); // 0→1, settles
  const shown = Math.round(interpolate(progress, [0, 1], [0, value]));
  const pop = interpolate(progress, [0, 1], [0.6, 1]); // overshoot-free scale-in
  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "center",
        justifyContent: "center", height: "100%", transform: `scale(${pop})` }}>
      <span style={{ fontSize: 220, fontWeight: 900, color: accent, lineHeight: 1,
          fontVariantNumeric: "tabular-nums" }}>{shown.toLocaleString()}</span>
      <span style={{ fontSize: 48, color: "#fff", marginTop: 24 }}>{label}</span>
    </div>
  );
};
使用
tabular-nums
确保数字滚动时宽度不会抖动。对于Top-X榜单,为每个条目复用一个弹簧动画并设置交错
delay
(排名第1的先入场)——详见
references/remotion-recipes.md

Vertical 9:16, designed for the pause

9:16竖版画幅,为暂停状态设计

Render 1080×1920. Stories/Reels/TikTok crop and overlay UI, so keep all type and key numbers inside the center 80% height, clear of the top 12% and bottom 18%. One bold idea per frame, oversized type, high-contrast accent on a flat/gradient background. Per-user
accent
color makes each share feel personal. Full safe-area map in
references/scene-grammar.md
.
渲染分辨率为1080×1920。Stories/Reels/TikTok会裁剪并叠加UI,因此所有文字和关键数字要放在中间80%的高度范围内,避开**顶部12%底部18%**的区域。每帧一个核心创意,大字号文字,高对比度强调色搭配纯色/渐变背景。用户专属的
accent
配色让每个分享内容都更具个性化。完整安全区域图详见
references/scene-grammar.md

Batch: one template → many videos

批量处理:一个模板→多个视频

The payoff. Render every data row to its own MP4 with
renderMedia
, passing the row as
inputProps
. Generate a CSV of jobs, loop, name files per record.
ts
// render-all.ts  — run with: npx tsx render-all.ts
import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
import { wrappedSchema } from "./src/schema";
import users from "./users.json"; // array of rows matching the schema

const serveUrl = await bundle({ entryPoint: "./src/index.ts" });
for (const user of users) {
  const props = wrappedSchema.parse(user);            // validate the row
  const comp = await selectComposition({ serveUrl, id: "Wrapped", inputProps: props });
  await renderMedia({
    composition: comp, serveUrl, codec: "h264",
    inputProps: props,
    outputLocation: `out/wrapped-${user.id}.mp4`,
  });
  console.log("rendered", user.id);
}
For thousands of rows, fan out across machines/Lambda and dedupe identical prop sets. Pipeline, scaling, and a Node + CLI batch variant are in
references/batch-pipeline.md
.
这是核心价值所在。使用
renderMedia
将每一行数据渲染为独立的MP4,将该行数据作为
inputProps
传入。生成任务CSV,循环处理,按记录命名文件。
ts
// render-all.ts  — run with: npx tsx render-all.ts
import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
import { wrappedSchema } from "./src/schema";
import users from "./users.json"; // array of rows matching the schema

const serveUrl = await bundle({ entryPoint: "./src/index.ts" });
for (const user of users) {
  const props = wrappedSchema.parse(user);            // validate the row
  const comp = await selectComposition({ serveUrl, id: "Wrapped", inputProps: props });
  await renderMedia({
    composition: comp, serveUrl, codec: "h264",
    inputProps: props,
    outputLocation: `out/wrapped-${user.id}.mp4`,
  });
  console.log("rendered", user.id);
}
对于数千行数据,可跨机器/Lambda分布式处理,并去重相同的props集合。流水线、扩展方案及Node + CLI批量处理变体详见
references/batch-pipeline.md

Build checklist

构建检查清单

  • Every visible string/number comes from props; nothing hardcoded.
  • 5–7 scenes, crescendo order, biggest/most personal stat as the climax.
  • Numbers count up via
    useCurrentFrame
    + spring,
    tabular-nums
    , locale-formatted.
  • 9:16 1080×1920; key content in center 80%, clear of top 12% / bottom 18%.
  • Each scene reads in <2s and looks good frozen (it will be screenshotted).
  • One schema validates every row before render; batch script names files per record.
  • 所有可见文字/数字均来自props,无硬编码内容。
  • 5–7个场景,按高潮递进顺序排列,最重磅/最个性化的统计数据作为高潮。
  • 数字通过
    useCurrentFrame
    + 弹簧动画递增,使用
    tabular-nums
    和本地化格式。
  • 9:16 1080×1920分辨率;核心内容位于中间80%高度,避开顶部12%/底部18%区域。
  • 每个场景可在<2秒内清晰可读,且静态画面美观(会被截图分享)。
  • 单个schema在渲染前验证所有行数据;批量脚本按记录命名文件。

Deliver & verify (rendered stills → MP4)

交付与验证(渲染静态帧→MP4)

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
.
A Wrapped is a Remotion composition rendered per data row — frame-deterministic, so any exact frame renders headlessly with no seek harness. The deliverable is an MP4 (often many) carrying each person's exact numbers; verify one representative row by stills before you batch.
Output contract:
  • A Remotion project with the composition registered (
    <Composition>
    + zod
    schema
    +
    defaultProps
    ), all motion frame-driven (no timers /
    Date.now()
    /
    Math.random()
    — count-ups via
    useCurrentFrame
    + spring).
  • Deliverable = the rendered
    out/wrapped-*.mp4
    per row (plus the project, so any row re-renders).
  • Per-user stats baked into props and validated by the schema before render; 9:16 1080×1920.
  • Duration data-dependent? compute it in
    calculateMetadata
    , not by hand.
Verify loop — stills of ONE row → inspect → batch. Render a representative user's frames first (cheap, no encode); catch a layout/data bug once instead of N times.
bash
undefined
封装工具
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
Wrapped视频是基于每行数据渲染的Remotion合成组件——帧确定性,因此任何精确帧都可无头渲染,无需寻道工具。交付物是包含每个人专属数据的MP4(通常是多个);批量渲染前先验证一条代表性数据行的静态帧。
输出规范:
  • 一个Remotion项目,已注册合成组件(
    <Composition>
    + zod
    schema
    +
    defaultProps
    ),所有动画由帧驱动(无计时器/
    Date.now()
    /
    Math.random()
    ——数字递增通过
    useCurrentFrame
    + 弹簧动画实现)。
  • 交付物 = 每行数据对应的渲染后文件
    out/wrapped-*.mp4
    (附带项目文件,以便重新渲染任意行数据)。
  • 用户专属统计数据嵌入props,渲染前通过schema验证;分辨率为9:16 1080×1920。
  • 时长依赖数据?使用
    calculateMetadata
    计算,而非手动设置。
验证流程——单条数据行的静态帧→检查→批量渲染。先渲染一个代表性用户的帧(成本低,无需编码);一次性发现布局/数据错误,避免重复N次错误。
bash
undefined

Frame-exact stills at start / mid / end for ONE representative row — pass that row as props

为一条代表性数据行渲染起始/中间/结尾的精确帧——传入该行数据作为props

npx remotion still Wrapped out/f-start.png --frame=0 --props='{...one user...}' npx remotion still Wrapped out/f-mid.png --frame=N --props='{...one user...}' npx remotion still Wrapped out/f-end.png --frame=L --props='{...one user...}' # L = durationInFrames - 1
npx remotion still Wrapped out/f-start.png --frame=0 --props='{...one user...}' npx remotion still Wrapped out/f-mid.png --frame=N --props='{...one user...}' npx remotion still Wrapped out/f-end.png --frame=L --props='{...one user...}' # L = durationInFrames - 1

Inspect: every visible string/number comes from that row and is EXACT (big-number reveal lands on the

检查:所有可见文字/数字均来自该行数据且完全准确(大额数字展示最终停留在真实值,Top-X排名/标签正确);核心内容位于中间80%高度,避开顶部12%/底部18%区域。

real value, top-X ranks/labels correct); key content inside center 80%, clear of top 12% / bottom 18%.

只有在代表性静态帧检查通过后,再批量渲染所有行数据:

Only after the representative stills check out, batch-render every row:

npx tsx render-all.ts

- `npx remotion compositions` reads `durationInFrames`/`fps` to pick the end frame and the big-number's settle frame.
- **README demo GIF for free**: `npx remotion render Wrapped out/demo.gif --codec=gif --props='{...}'`.

**Before you finish:**
1. `npx remotion still` renders cleanly at frame 0, mid, and last for the representative row — no errors, no missing assets/fonts.
2. Big-number reveal lands on the EXACT prop value at its settle frame; top-X ranks/labels/values match the row.
3. 9:16: key numbers/type inside center 80%, clear of top 12% / bottom 18%; each checked frame reads frozen.
4. Frame-driven only — no `Date.now()` / `Math.random()` / timers; schema validates every row before batch.
5. Representative MP4 encoded and plays; then batch all rows (file per record); (optional) GIF for the README.
npx tsx render-all.ts

- `npx remotion compositions`读取`durationInFrames`/`fps`以选择结束帧和大额数字的稳定帧。
- **免费生成README演示GIF**:`npx remotion render Wrapped out/demo.gif --codec=gif --props='{...}'`。

**完成前检查:**
1. `npx remotion still`可成功渲染代表性数据行的起始、中间和最后一帧——无错误,无缺失资源/字体。
2. 大额数字展示在稳定帧时准确显示props中的真实值;Top-X排名/标签/数值与数据行一致。
3. 9:16分辨率:关键数字/文字位于中间80%高度,避开顶部12%/底部18%区域;所有检查帧的静态画面清晰可读。
4. 仅由帧驱动——无`Date.now()`/`Math.random()`/计时器;批量渲染前schema验证所有行数据。
5. 代表性MP4编码完成且可正常播放;然后批量渲染所有行数据(按记录命名文件);(可选)为README生成GIF。

Reference files

参考文件

  • references/scene-grammar.md
    — full 7-scene storyboard with frame timings, the build/crescendo logic, superlative & percentile copywriting patterns, and the 9:16 safe-area map.
  • references/remotion-recipes.md
    — runnable scene components: staggered top-X list, percentile bar, scene sequencing with
    <Series>
    , transitions, fonts, and the no-flicker animation rules.
  • references/batch-pipeline.md
    — CSV/JSON → many MP4s: schema validation, the render loop, concurrency, Lambda fan-out, and file naming.
  • references/scene-grammar.md
    — 完整7场景故事板及帧时间安排、递进/高潮逻辑、最高级/百分位文案模式,以及9:16安全区域图。
  • references/remotion-recipes.md
    — 可运行的场景组件:交错入场Top-X榜单、百分位进度条、使用
    <Series>
    进行场景排序、转场效果、字体,以及无闪烁动画规则。
  • references/batch-pipeline.md
    — CSV/JSON→多个MP4:schema验证、渲染循环、并发处理、Lambda分布式处理,以及文件命名规则。