text-message-animation について
チャットの台本をRemotionの会話動画にするスキルです。ショート動画制作者向けに、メッセージ配列とフレーム単位のタイミングで、再現可能なレンダリングを支援します。
主な機能
- 01メッセージ配列による動画生成
- 02吹き出しと入力中表示の段階的表示
- 03iMessage、WhatsApp、SMSのスタイル
- 04自動スクロール、既読表示、リアクション
- 05タイムラインに同期した音声
- 06静止画とMP4の検証用スクリプト
活用シーン
- 01縦型チャットストーリー動画の制作
- 02台本から会話アニメーションを作成
- 03反応を順番に見せる動画の制作
- 04JSONやCSVから動画を一括生成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ iart-ai/ text-message-video-skills --skill text-message-animationインストール後、Claude Code で/text-message-animationを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.mdreferences/bubble-recipes.mddata-driven.md詳細
テキストメッセージのアニメーション
チャットの台本を、スクロール中の目を引く会話動画に変えます。吹き出しが1つずつ表示され、返信の前には入力中インジケーターが点滅し、送信時に効果音が鳴り、最新のメッセージが画面内に収まるようにスレッドが自動スクロールします。これは、顔出しなしのショート動画で最も視聴維持率が高いフォーマットの1つです。iMessage/WhatsApp/SMSの吹き出しで語られるストーリーを、視聴者は誰かのスマートフォンをのぞき見するように読みます。
使用する場面
- TikTok、Reels、Shorts向けの「偽のテキストメッセージ」やチャットストーリー動画(縦型9:16)。
- 台本からiMessage、WhatsApp、または汎用SMSの会話をアニメーション化する場合。
- スレッドを通じてオチが1メッセージずつ明かされる、返信/コメントへのリアクション動画。
- 1つのテンプレートとメッセージ配列またはCSVから、多数のチャット動画をまとめて制作する場合。
絶対に守るべき2つのルール
- メッセージはリズムをつけて、1つずつ表示します。 視聴維持率を生むのは、小出しにすることです。入力の間を置いてから吹き出しが表示され、次に読むための間を設けます。スレッド全体をフレーム0で一気に表示してはいけません。実際の会話のようなテンポにします(受信した返信の前には入力中インジケーターを表示し、吹き出しごとに読むための短い間を設けます)。
- アプリの画面デザインは、ひと目で本物とわかる必要があります。 送信メッセージは右側で、iMessageなら青、SMSなら緑、WhatsAppなら緑です。受信メッセージは左側で、灰色にします。連続する吹き出しの最後にはしっぽを付け、角丸は約18~22pxにし、ステータスバーとヘッダーも正しく表示します。最初の0.5秒でどのアプリか視聴者に伝わらなければ、リアルさが損なわれます。
データモデル — まず台本、次にレンダリング
すべてのデータをメッセージ配列1つにまとめます。ストーリーをデータとして作成し、レンダラーはそれを再生するだけです。これにより、このフォーマットをまとめて制作できます。1つのテンプレート × N本の台本 → N本の動画です。
type Message = {
from: "me" | "them"; // me = sent (right/blue), them = received (left/gray)
text: string;
typingMs?: number; // show typing indicator this long before the bubble (received only)
delayMs?: number; // read-gap AFTER the previous bubble, before this one
status?: "delivered" | "read"; // receipt under the last sent bubble
reaction?: "❤️" | "👍" | "😂" | "‼️" | "❓"; // tapback, lands ~400ms after the bubble
};
const thread: Message[] = [
{ from: "them", text: "wait you're WHERE right now", typingMs: 900 },
{ from: "me", text: "outside your house 🙂", delayMs: 600, status: "read" },
{ from: "them", text: "...", typingMs: 1400, reaction: "❓" },
];
配列からすべてのフレーム時刻を導きます。詳しくは、下記の「タイミング」を参照してください。CSV(from,text,typingMs,delayMs,status)をそのまま取り込めるように、必須フィールドはtextとfromだけにします。
吹き出しのレイアウトと配置
吹き出しは、画面の左右どちらかに寄せた最大幅のピル形状です。送信メッセージは右端に、受信メッセージは左端に寄せます。送信順に縦に積み重ね、同じ送信者の連続したメッセージでは最後の吹き出しにしっぽを付けます。
const BUBBLE: React.CSSProperties = {
maxWidth: "74%", // never full-width — leaves the "this is a phone" gutter
padding: "14px 18px",
borderRadius: 22,
fontSize: 34, // ~iMessage proportions at 1080px wide
lineHeight: 1.25,
wordBreak: "break-word",
};
const SENT: React.CSSProperties = {
...BUBBLE, alignSelf: "flex-end", background: "#0A84FF", color: "#fff", // iMessage blue
};
const RECEIVED: React.CSSProperties = {
...BUBBLE, alignSelf: "flex-start", background: "#3B3B3D", color: "#fff", // dark-mode gray
};
配色の早見表:iMessageの送信メッセージは#0A84FF、ライト背景は#1C1C1Eです。受信メッセージは、ライトモードでは背景が#E9E9EBでテキストが#000、ダークモードでは#3B3B3Dです。SMSの送信メッセージは緑の#37C24Aです。WhatsAppの送信メッセージは#005C4B、受信メッセージは#0B141Aのチャット壁紙上に表示する#202C33で、チェックマークも付けます(灰色=送信済み、青の#53BDEB=既読)。アプリは1つ選び、画面デザインを完全に合わせます。iMessageの画面デザインにWhatsAppのチェックマークを混ぜてはいけません。
時差表示と入力中インジケーター
各吹き出しは、少し下から小さくポップしながら、独自の表示フレームを基点として現れます。受信メッセージの前には、3つの点が点滅する入力中の吹き出しを表示し、その後、本物のメッセージに置き換えます。
import { useCurrentFrame, useVideoConfig, spring, interpolate, Sequence } from "remotion";
const Bubble: React.FC<{ enterFrame: number; sent: boolean; children: React.ReactNode }> =
({ enterFrame, sent, children }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const p = spring({ frame: frame - enterFrame, fps, config: { damping: 14, mass: 0.7 } });
return (
<div style={{
alignSelf: sent ? "flex-end" : "flex-start",
transform: `translateY(${interpolate(p, [0, 1], [24, 0])}px) scale(${interpolate(p, [0, 1], [0.85, 1])})`,
opacity: interpolate(p, [0, 1], [0, 1]),
transformOrigin: sent ? "bottom right" : "bottom left",
}}>{children}</div>
);
};
// three-dot typing indicator — dots breathe on a sine, frame-driven (no CSS animation timers)
const Typing: React.FC = () => {
const frame = useCurrentFrame();
const dot = (i: number) => 0.4 + 0.6 * (0.5 + 0.5 * Math.sin((frame / 8) - i * 0.9));
return (
<div style={{ ...RECEIVED, display: "flex", gap: 8, padding: "18px 20px" }}>
{[0, 1, 2].map(i => (
<span key={i} style={{ width: 12, height: 12, borderRadius: 6, background: "#aaa", opacity: dot(i) }} />
))}
</div>
);
};
入力中インジケーターは、吹き出しの<Sequence>の直前に<Sequence from={typingStart} durationInFrames={typingLen}>として配置します。先に表示されてから、本物の吹き出しに切り替わります。入力中の表示を行うのは受信メッセージだけです。送信メッセージはそのままポップ表示します(自分が入力する様子を見ることはありません)。
タイミング — 配列をフレームに変換する
メッセージを順にたどり、カーソルにミリ秒単位の時間を加算します。これを唯一の基準として、スプリングによる表示、入力中の時間、効果音、自動スクロールのタイミングを決めます。
const f = (ms: number) => Math.round((ms / 1000) * fps);
let cursor = 0;
const timeline = thread.map((m) => {
cursor += m.delayMs ?? 500; // read-gap before this message
const typingLen = m.from === "them" ? (m.typingMs ?? 800) : 0;
const typingStart = cursor;
cursor += typingLen; // typing plays during the gap
const enterMs = cursor; // bubble pops the instant typing ends
cursor += 250; // settle before the next read-gap
return { ...m, typingStartMs: typingStart, typingLenMs: typingLen, enterMs };
});
const durationMs = cursor + 1000; // tail pad so the last bubble is readable
テンポを調整します。キビキビした勢いのあるスレッドでは、delayMsを300~500、typingMsを500~900にします。サスペンスを持たせてオチを明かす場合は、決めの一言の前のtypingMsを1500~2500に延ばします。calculateMetadataでdurationMsからdurationInFramesを計算すれば、台本が長くなると動画も自動的に長くなります。
既読通知、タイムスタンプ、リアクション
- 通知(「配信済み」/「既読」)は、最後の 送信 吹き出しの下に小さく右寄せで表示します。吹き出しが表示されてから約300ms後にフェードインさせます。WhatsAppでは、代わりにダブルチェックが青に変わります。
- タイムスタンプ(「今日 9:41 PM」)は、新しい時間帯の最初のメッセージの上に、中央揃えのグレーの区切りとして表示します。吹き出しごとにタイムスタンプを付けないでください。
- リアクション/タップバックは、吹き出しが表示されてから約400ms後に、対象の吹き出しの角に重なる位置から少しオーバーシュートさせながら拡大表示します。グローバルフレームではなく、その吹き出しの
enterMs + 400を基準に配置します。
自動スクロール — 最新の吹き出しを画面内に表示する
吹き出しの積み重ねが表示領域を超えたら、スレッド全体を上に移動させ、最新の吹き出しが下から3分の1の位置にくるようにします。メッセージが表示されるたびに、スプリングアニメーションでスクロールオフセットを目標値へ移動させます。急に飛ぶのではなく、滑らかに動かしてください。
// targetY = total height of messages above the viewport floor, recomputed as bubbles enter
const scroll = spring({ frame: frame - lastEnterFrame, fps, config: { damping: 200 } });
const y = prevY + (targetY - prevY) * scroll; // ease from old offset to new
return <div style={{ transform: `translateY(${-y}px)` }}>{/* bubbles */}</div>;
通知や入力中のドットがその下に表示されるよう、最新の吹き出しが下部のセーフエリアに重ならないようにしてください(下記参照)。
サウンド同期(メッセージごとのポップ音)
メッセージを送信するたびに軽快な「シュッ/ポン」という音、受信するたびに控えめな「チン」という音を鳴らすことが、この形式をリアルに感じさせる理由の半分を占めています。各メッセージの enterMs に、ビジュアルと同じ時間軸で音声キューを1つ配置します。
import { Audio, staticFile, Sequence } from "remotion";
{timeline.map((m, i) => (
<Sequence key={i} from={f(m.enterMs)}>
<Audio src={staticFile(m.from === "me" ? "send.mp3" : "receive.mp3")} />
</Sequence>
))}
必要に応じて、入力中の各ウィンドウの下に小さなキーボード音を追加します。早いテンポのスレッドで音が重ならないよう、音声キューは短くしてください(300ms未満)。タイマーで音を鳴らさないでください。ヘッドレスレンダリングでも維持できるよう、フレームのタイムラインに沿ってスケジュールします。
9:16(1080×1920)のセーフエリア
| 領域 | 空けておく範囲 | 理由 |
|---|---|---|
| 上部のステータスバー | 約120px | ここには偽のステータスバー/ノッチが配置されます。吹き出しをその下に置かないでください |
| アプリヘッダー | 約150px(アバター+名前) | 連絡先ヘッダーがアプリらしさを演出します。固定し、ほかの要素で隠さないでください |
| 下部の入力欄+UI | 約360px | 偽の入力バーとプラットフォームのキャプション/CTA/音声UIの両方が下部に重なるためです |
| 左右の余白 | 幅の中央約88% | 吹き出しは画面端に触れません。この余白が「スマートフォンらしさ」を生み出します |
ヘッダーと入力バーの間の帯状の領域で会話を進めます。新しい吹き出しをその領域の下から3分の1の位置(入力欄の上)に表示してから、自動スクロールさせます。視線は最新のメッセージをそこに探すためです。
出力チェックリスト
- すべてを1つのメッセージ配列で制御し、タイムスタンプは手動で配置せず、導出する。
- メッセージを1つずつ、調整したリズムで表示する:既読間隔 → 入力中(受信のみ)→ ポップ。
- アプリの外観を最初から最後まで正しく再現する(色、吹き出しのしっぽ、ヘッダー、通知)。複数のアプリを混在させない。
- 送信側は右側/青または緑、受信側は左側/グレーに配置し、連続する吹き出しの最後にしっぽを付ける。
- 最後の送信吹き出しの下に通知を表示する。時間帯ごとにタイムスタンプの区切りを表示し、タップバックはオーバーシュートさせる。
- 自動スクロールで最新の吹き出しを下から3分の1の位置に表示し、セーフエリアに重ならないようにする。
- メッセージごとにポップ音/チンという音を1回鳴らし、タイマーではなくフレームのタイムラインに沿ってスケジュールする。
- ヘッダーをステータスバーの下に固定し、会話を9:16のセーフエリア内に表示する。
配信と検証(レンダリングした静止画 → MP4)
同梱ヘルパー(
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を参照してください。
Remotion のコンポジション(<Composition> + zod の schema + defaultProps)として提供され、props はメッセージ配列です。吹き出しの表示、タイピングのパルス、スクロールオフセット、サウンドの合図はすべて useCurrentFrame() に基づいてフレーム駆動されます。Date.now()/Math.random()/CSS アニメーションのタイマーは使わないため、ヘッドレスでもどのフレームも同じようにレンダリングされます。成果物は out/*.mp4 +プロジェクトです(新しいスクリプトで再レンダリングできます)。デフォルトは縦型の 9:16(1080×1920)です。長さはデータに応じて変わるため、手動で設定せず、タイムラインカーソルから calculateMetadata 内で durationInFrames を計算してください。
検証ループ — 静止画をレンダリング → 確認 → エンコード。 問題になりやすいのは表示タイミングとスクロール位置です。エンコードに時間をかける前に、正確なフレームを確認してください。
# Stills WITH SHIPPED PROPS (the real thread) at: a typing beat / a mid pop / the last bubble
npx remotion still TextThread out/f-typing.png --frame=24 --props='{"threadSrc":"thread.json"}'
npx remotion still TextThread out/f-mid.png --frame=90 --props='{"threadSrc":"thread.json"}'
npx remotion still TextThread out/f-end.png --frame=N --props='{"threadSrc":"thread.json"}' # N = durationInFrames-1
# Inspect each PNG:
# - frame 24 shows the three-dot typing indicator on the LEFT (received), no real bubble yet
# - at the mid frame the just-landed bubble is on the correct side/color with its tail; receipt/reaction placed right
# - thread is auto-scrolled so the newest bubble sits in the lower third, NOT hidden under the compose bar
# - app chrome consistent (status bar, header) and inside the 9:16 safe band; nothing under the top notch
npx remotion render TextThread out/thread.mp4 --props='{"threadSrc":"thread.json"}' # encode once stills are right
npx remotion render TextThread out/demo.gif --codec=gif # README proof clip
npx remotion compositions を使って durationInFrames/fps を確認し、タイピング中・中盤・最後のフレームを選びます。ビルド不要の簡易プレビューとして、同じレイアウトを ?t=N シーク用ハーネス付きのスタンドアロン HTML ページとしてレンダリングできます。references/bubble-recipes.md を参照してください。
完了前に確認すること:
- タイピング中のフレーム、中盤の表示、最後の吹き出しで、静止画が問題なくレンダリングされること(フォント/音声が欠落していないこと)。
- タイピング中のフレームでは、受信側にインジケーターが表示され(吹き出しではない)、その後に吹き出しが表示されること。
- 各吹き出しの側と色が正しく、しっぽも適切であること。開封通知/タイムスタンプ/リアクションが想定どおりの位置に表示されること。
- 確認したすべてのフレームで、自動スクロールにより最新の吹き出しが下部 3 分の 1 に収まり、9:16 のセーフエリア内に表示されること。
- フレーム駆動のみであること(
Date.now()/Math.random()/タイマーを使わないこと)。同梱の props が正しくレンダリングされ、MP4 +任意の GIF が出力されること。
参照ファイル
references/bubble-recipes.md— そのまま使えるビルド例:iMessage/WhatsApp/SMS のクロームと配色、メッセージのデータモデルからフレームタイムラインへの変換、スプリング式の吹き出し+しっぽ、3 点タイピングインジケーター、開封通知/タイムスタンプ/タップバック、自動スクロール、メッセージごとのサウンドスケジュール設定、calculateMetadataを使った Remotion コンポジション全体、および?t=Nシーク用ハーネス付きのスタンドアロン HTML プレビュー。references/data-driven.md— スクリプトからメッセージへのデータ形式、JSON/CSV によるスレッドの作成、CSV からMessage[]型へのマッピング、@remotion/rendererを使って 1 つのテンプレートから複数の動画を一括レンダリングする方法。