mcpField
T
text-message-animation

text-message-animation

github.com/iart-ai创意与媒体5688 次安装18 周前更新
代码仓库

关于 text-message-animation

用于将聊天脚本制作成 Remotion 对话视频的 Skill,适合短视频创作者。它以消息数组驱动画面,并按帧控制动画与音效,便于重复渲染和批量制作。

主要功能

  1. 01由消息数组驱动视频渲染
  2. 02逐条显示气泡与输入指示
  3. 03支持 iMessage、WhatsApp 和 SMS 样式
  4. 04自动滚动、送达回执与回应
  5. 05按时间轴同步音效
  6. 06静帧与 MP4 验证工具

适用场景

  1. 01制作竖屏聊天故事视频
  2. 02将对话脚本制作成动画
  3. 03制作逐条揭示的回应视频
  4. 04从 JSON 或 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.md
references/
bubble-recipes.md
data-driven.md

详细介绍

文本消息动画

将聊天脚本变成令人忍不住停下观看的对话视频:消息气泡逐个出现,每条回复前都有跳动的输入指示器,发送时伴随提示音,聊天记录自动滚动,让最新消息始终显示在画面中。这是留存率最高的无露脸短视频形式之一——用 iMessage/WhatsApp/SMS 气泡讲述故事,让观众像在偷看别人的手机一样阅读。

适用场景

  • 为 TikTok、Reels 和 Shorts 制作“虚构短信”/聊天故事视频(9:16 竖屏)。
  • 根据脚本制作 iMessage、WhatsApp 或通用 SMS 对话动画。
  • 制作回复/评论反应视频,让聊天记录逐条揭晓包袱。
  • 使用一个模板和消息数组或 CSV 批量制作多条聊天视频。

两条不可妥协的规则

  1. 每次只揭晓一条消息,并保持节奏。 留存率来自循序渐进:输入指示器跳动一拍,然后消息气泡弹出,再留出时间阅读。切勿在第 0 帧就显示整段聊天记录——要像真实对话那样控制节奏(收到回复前显示输入指示器,每个气泡出现后留出短暂的阅读间隔)。
  2. 界面元素必须一眼看起来就像真实应用。 已发送消息在右侧,iMessage 为蓝色,SMS 为绿色,WhatsApp 为绿色;收到的消息在左侧,为灰色。连续消息中最后一个气泡要带尾巴,圆角约为 18–22px,状态栏和标题栏也要正确。如果观众在前半秒内看不出是哪款应用,幻觉就会被打破。

数据模型——先编写脚本,再渲染

所有内容都放在一个消息数组中。将故事编写为数据,渲染器只负责播放。这让这种形式易于批量制作:一个模板 × 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: "❓" },
];

根据这个数组推导每一帧的时间戳——请参阅下文“时间安排”。只将 text/from 设为必填字段,这样 CSV(from,text,typingMs,delayMs,status)就能直接映射。

气泡布局与对齐

气泡是一个最大宽度的圆角胶囊,并与所在一侧对齐。已发送消息靠右,收到的消息靠左。按发送顺序垂直堆叠;连续同一发送者的消息中,最后一个气泡带尾巴。

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,收到的消息为 #202C33,聊天背景为 #0B141A,并带有勾选标记(灰色 = 已发送,蓝色 #53BDEB = 已读)。完整匹配一种应用;不要混用 iMessage 界面元素和 WhatsApp 勾选标记。

错开显示与输入指示器

每个气泡都从略低的位置弹起,并伴随轻微缩放,动画以其自身的入场帧为锚点。收到的消息前会先显示一个带三个跳动圆点的输入气泡,随后替换为实际消息气泡。

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,这样脚本越长,视频也会自动越长。

已送达回执、时间戳和回应

  • 回执(“已送达”/“已读”)显示在最后一条已发送气泡下方,靠右对齐;气泡出现约 300 毫秒后淡入。在 WhatsApp 中,则是双勾变蓝。
  • 时间戳(“今天 9:41 PM”)是居中的灰色分隔线,位于新时间段第一条消息上方——不要给每个气泡都添加时间戳。
  • 回应 / 轻点回应在目标气泡出现约 400 毫秒后,从其角落缩放进入,并带有轻微的过冲。将它锚定到该气泡的 enterMs + 400,而不是全局帧。

自动滚动——让最新气泡保持在画面内

当消息堆栈超出可视区域后,将整个对话向上平移,让最新气泡位于下方三分之一处。每条消息出现时,都用弹簧动画将滚动偏移量移向目标位置——应平滑滑动,而不是跳动。

// 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 时间点安排一个音频提示,与视觉效果使用相同的时间基准。

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% 的宽度气泡不会贴着屏幕边缘——边距能让画面看起来像“手机”

在标题栏和输入栏之间的区域显示实时对话。让每个新气泡出现在该区域下方三分之一处(输入框上方),然后自动滚动——这是视线预期看到最新消息的位置。

输出检查清单

  • 由一个消息数组驱动所有内容;时间戳应推导得出,而不是手动放置。
  • 一次显示一条消息:按经过调整的节奏依次呈现已读间隔 → 输入中(仅接收消息)→ 弹出。
  • 从头到尾保持应用界面一致(颜色、气泡尾巴、标题栏、回执)——不要混用不同应用的样式。
  • 已发送消息在右侧,颜色为蓝色或绿色;已接收消息在左侧,颜色为灰色;每组连续消息的最后一条带气泡尾巴。
  • 回执位于最后一条已发送气泡下方;每个时间段显示一个时间戳分隔线;轻点回应带过冲效果。
  • 自动滚动让最新气泡保持在下方三分之一处,并避开安全区域。
  • 每条消息播放一个弹出声 / 叮声,并安排在帧时间线上(而不是使用计时器)。
  • 标题栏固定在状态栏下方;对话内容位于 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,并选取输入帧、中间帧和结束帧。若要快速预览且无需构建,同一布局也可以渲染为独立 HTML 页面,并通过 ?t=N 跳转到指定帧的测试工具进行预览 — 参见 references/bubble-recipes.md。

完成前:

  1. 输入帧、中间弹出帧和最后一个气泡帧都能干净地渲染静帧 — 没有缺失字体或音频。
  2. 输入帧显示收到消息一侧的输入指示器(而不是气泡);之后气泡才弹出。
  3. 每个气泡都位于正确的一侧、使用正确的颜色并带有正确的尾巴;已送达标记、时间戳和回应都显示在预期位置。
  4. 在每个检查帧上,自动滚动都能让最新气泡位于下方三分之一处,并保持在 9:16 安全区域内。
  5. 仅由帧驱动(不使用 Date.now() / Math.random() / 计时器);发布的 props 能正确渲染;生成 MP4 + 可选的 GIF。

参考文件

  • references/bubble-recipes.md — 可直接复制使用的构建方案:iMessage / WhatsApp / SMS 外观和配色、消息数据模型 → 帧时间线、弹簧气泡 + 尾巴、三点输入指示器、已读回执 / 时间戳 / 点按回应、自动滚动、逐条消息的声音调度、包含 calculateMetadata 的完整 Remotion 组合,以及带有 ?t=N 跳转到指定帧测试工具的独立 HTML 预览。
  • references/data-driven.md — 脚本 → 消息的数据结构、将对话线程编写为 JSON / CSV、将 CSV 映射到 Message[] 类型,以及使用 @remotion/renderer 从一个模板批量渲染多个视频。

评论

支持 Markdown