1仕組みと動作原理
妥協不可ルール1:メッセージは必ず1件ずつリズミカルに表示すること。タイピング表示 → バブルポップ → 読了の間を順に挟み、フレーム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).
妥協不可ルール2:0.5秒以内にどのアプリのUIか即座に識別できること(色・バブル・尾部・既読表示・ヘッダーを完全一致させ、混在は禁止)。
If a viewer can't tell which app it is in the first half-second, the illusion breaks.
データモデル優先:動画全体を1つのMessage[]配列で駆動。筋書きをデータとして記述し、レンダラーは再生処理のみを担当。
Everything is one array of messages. Author the story as data; the renderer just plays it.
タイムラインは配列カーソルから自動導出:メッセージを走査してカーソルを加算し、typingStartMs / typingLenMs / enterMsを算出。スプリング、効果音、スクロールのすべてがこれを参照。
const durationMs = cursor + 1000; // tail pad so the last bubble is readable
動画全体の尺はカーソル値からcalculateMetadataで算出し、フレーム数のハードコードを禁止。
Compute `durationInFrames` from `durationMs` in `calculateMetadata` so a longer script makes a longer video automatically.
タイピングインジケーターは受信メッセージの直前にのみ表示し、正弦波ブレス・フレーム駆動で制御(CSSアニメーションタイマーは使用禁止)。
Only received messages type; sent messages just pop (you don't watch yourself type).
自動スクロールはスプリングで目標変位に収束させ、最新バブルを瞬間移動ではなく視野の下部1/3に自然に着地させる。
Animate the scroll offset toward the target with a spring each time a message lands — it should glide, not jump.
メッセージごとに1つの効果音(送信/受信で別音)を割り当て、イベントコールバックではなくフレームタイムラインに直接配置。
Never trigger sound off a timer — schedule it on the frame timeline so it survives headless render.
縦型セーフエリアを4段階の余白で定義(上部ステータスバー約120px、ヘッダー約150px、下部入力部+プラットフォームUI約360px、サイド余白)。
| Bottom input + UI | ~360px | Fake compose bar AND the platform's caption/CTA/audio UI both crowd the bottom |
検証ループ:最初にタイピング中・中間・末尾の3枚の静止フレームを出力。「タイピングフレーム左側には3点リーダーのみで実バブルがないこと」「最新バブルが入力欄に隠れていないこと」などを判定基準とする。
At a typing frame the indicator (not the bubble) shows on the received side; the bubble pops only after.
2コア機能
3外部依存関係
| 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 并行渲染上百条片 |
4推奨統制措置と判定根拠
- scripts/: ローカルスクリプトまたはコマンドの実行: (SKILL.mdに明記された検証ループ)`npx remotion still TextThread out/f-{typing,mid,end}.png
- 网络端点: 外部ネットワークアクセスの宣言: `npx remotion …` および量産フェーズの `@remotion/bundler`/`@remotion/renderer`:初回実行時にnpm公式レジストリ経由で取得
- 本地工作目录: ファイル読み書きと成果物出力: エージェントが本スキルに基づき構築するRemotionプロジェクトおよびout/thread.mp4(または一括生成時のout/chat-001.mp4 … out)
5二重目の独立検証
- [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 加载所必需),但包间文档完整度不一致,属可核查的结构出入。
6結論
fdd9560a3ce85697…