text-message-animation
This skill should be used when the user asks to "make a fake text message video", "animate an iMessage conversation", "create a text story video", "do a chat reply animation", "WhatsApp/SMS conversation video", "typing indicator animation", "render a chat thread to a video", or "batch text-message videos from a script/CSV". Covers sent/received bubble layout, staggered appear timing with typing indicators, read receipts/timestamps, per-message pop sound-sync, 9:16 safe-area, and data-driven render from a messages array.
1How It Works & Mechanism
Non-negotiable rule 1: Reveal only one message at a time with rhythm—type first, pop bubble second, leave reading pause third; never dump the entire conversation at frame 0.
1. **One message reveals at a time, on a rhythm.** The retention comes from the drip: a beat of typing, then a bubble pops in, then a pause to read. Never dump the whole thread at frame 0 — pace it like a real conversation (a typing indicator before received replies, a short read-gap after each bubble).
Non-negotiable rule 2: Chrome must be recognizable as the specific app within half a second (colors, bubbles, tails, receipts, and headers all aligned); no mix-and-matching allowed.
If a viewer can't tell which app it is in the first half-second, the illusion breaks.
Data model first: The entire video is driven by a single Message[] array; plot is expressed as data, while the renderer only handles playback.
Everything is one array of messages. Author the story as data; the renderer just plays it.
Derive timeline via array cursor: Traverse messages and accumulate cursor to produce typingStartMs / typingLenMs / enterMs, which all springs, sound effects, and scrolling read from.
const durationMs = cursor + 1000; // tail pad so the last bubble is readable
Delegate duration calculation to calculateMetadata via cursor; hardcoded frame counts are forbidden.
Compute `durationInFrames` from `durationMs` in `calculateMetadata` so a longer script makes a longer video automatically.
Typing indicators only appear before received messages, using sine breathing and frame-driven timing (no CSS animation timers).
Only received messages type; sent messages just pop (you don't watch yourself type).
Auto-scrolling uses springs to approach target offset, landing the latest bubble in the lower third of the viewport rather than snapping.
Animate the scroll offset toward the target with a spring each time a message lands — it should glide, not jump.
Schedule a sound effect per message (different for send/receive) directly onto the frame timeline rather than via event callbacks.
Never trigger sound off a timer — schedule it on the frame timeline so it survives headless render.
Vertical safe area defined across four margins (top status bar ~120px, header ~150px, bottom input + platform UI ~360px, side gutters).
| Bottom input + UI | ~360px | Fake compose bar AND the platform's caption/CTA/audio UI both crowd the bottom |
Validation loop: Produce three still frames first (typing frame, midpoint frame, end frame); criteria include "typing frame shows only three dots on the left, no real bubble" and "latest bubble not covered by input bar".
At a typing frame the indicator (not the bubble) shows on the received side; the bubble pops only after.
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| cli | npx remotion still / render / compositions |
| package | @remotion/bundler + @remotion/renderer(批产路线:bundle 一次、renderMedia 多次) |
| cli | scripts/contact-sheet.sh + scripts/probe-mp4.sh(仓库根共享验证工具箱,SKILL.md 明列) |
| cli | scripts/seek-shot.sh(Light tier:驱动单文件 HTML 的 `?t=N` harness 截图) |
| network | (可选,reference 建议)Remotion Lambda 并行渲染上百条片 |
4Recommended Governance & Evidence
- scripts/: Local script or command execution: (Validation loop explicitly listed in SKILL.md) `npx remotion still TextThread out/f-{typing,mid,end}.png
- 网络端点: External network access declaration: `npx remotion …` and batch stage `@remotion/bundler`/`@remotion/renderer`: fetched via official npm registry on first run
- 本地工作目录: File I/O and artifact output: Remotion projects and out/thread.mp4 (or out/chat-001.mp4 … out during batch runs) generated by agent following this skill
5Second-Pass Verification
- [ok] 「一次一条 + 打字只出现在收到的消息」是否真在代码里成立(防第一遍只读散文) — reference §5/§9 的组件把每条消息包在 `<Sequence from={f(m.typingStartMs)}>` 内,仅当 `m.typingLenMs > 0`(由 `m.from === "them" ? (m.typingMs ?? 800) : 0` 决定)才渲染 `<Typing />`,随后 `<Sequence from={f(m.enterMs - m.typingStartMs)}>` 弹泡;SKILL.md 也明写 "Only received messages type; sent messages just pop"。声明与实现一致。
- [ok] 数据驱动时长(calculateMetadata)是否真能自动出正确长度 — SKILL.md 给 `durationMs = cursor + 1000` 并写 `Compute durationInFrames from durationMs in calculateMetadata`;reference §9 的 Composition 把 `durationInFrames={300}` 标为默认值并由 `calculateMetadata` 覆盖为 `Math.ceil((durationMs / 1000) * 30)`,§4 再次强调 "never hand-set the duration"。三处自洽。
- [discrepancy] reference 示例 import 的模块是否随包分发(第一遍未察觉) — data-driven.md §5 的批渲染脚本写着 `import { csvToThreads } from "./csvToThreads";`,但仓库内并不存在 csvToThreads 文件(全仓仅 .md 与 .sh/.git)。属示意代码:照抄会直接报模块不存在,读者需按 §3 的 CSV 列约定自写映射。同型问题在 CSV 示例里也可见(示例行 `2,Alex,me,no,,400,read` 省略了线程首行)。对 pin commit 有效的真实出入,非虚假能力声明。
- [discrepancy] 音效能力落地是否需要用户自备资产 — SKILL.md 与 reference §8 都用 `staticFile(m.from === "me" ? "send.mp3" : "receive.mp3")`,但仓库内不含任何 mp3;data-driven.md 只在 Tips 里提醒 "Keep `send.mp3` / `receive.mp3` / fonts in `public/`"。结论:音效同步是能力,但前置资产(发/收提示音、字体、头像)必须由用户提供——SKILL.md 的输出清单「One pop/ding per message」在缺资产时会静默退化。
- [ok] 「轻量预览(`?t=N`)」与仓库 Light tier 脚本的契约是否真的对齐 — bubble-recipes.md §10 的 HTML 里实现了 `const t = new URLSearchParams(location.search).get("t"); if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); }`,与 scripts/README.md 描述的 seek-shot.sh 契约(页面 load 时 `tl.pause(); tl.seek(t)`)逐字对应。三包中只有本 skill 真能用上 seek-shot.sh。
- [unlocatable] 「one of the highest-retention faceless short-form formats」是否有据 — SKILL.md 开头断言本格式为最高留存的 faceless 形式之一,仓库内无数据、无引用、无来源标注;本次为只读侦查,不验证该断言,仅记为文档声明。
- [discrepancy] 包间一致性:同为 2026-06-22 发布的三个 iart 包,skill 目录结构是否统一 — tiktok 包四个 skill 各带人读 README(1377–1494 字节);本仓库的 text-message-animation 与 youtube 包的两个 skill 的 README 均为 **0 字节**。功能不受影响(README 非 skill 加载所必需),但包间文档完整度不一致,属可核查的结构出入。
6Verdict & Scenarios
fdd9560a3ce85697…