---
title: "How to add an AI chat reply animation with Claude (tested prompt and script)"
description: "A chat box rises in at the bottom and types \"best serum for dull skin?\", the question flies up as a blue bubble, three dots bounce, and the AI's answer streams into a white card word by word. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/ai-chat-typing
---

# How to add an AI chat reply animation with Claude

A chat box rises in at the bottom and types "best serum for dull skin?", the question flies up as a blue bubble, three dots bounce, and the AI's answer streams into a white card word by word.

Made by the script on this page, from our own test clip.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   Install the Recharm video effects skill: download https://recharm-growth-staging.s3.us-east-2.amazonaws.com/website/downloads/recharm-video-effects.zip and unzip it into ~/.claude/skills
   ```

3. ### Ask for the effect

   ```
   Add an AI chat reply animation to input.mp4
   Question: best serum for dull skin?
   Answer: Try a daily vitamin C serum. It brightens dull skin and evens out tone in about two weeks.
   ```

## Several videos, several effects, one ask

Put your clips in the folder, mention them with @, and ask for everything at once. Claude runs one tested script after another on each clip, then joins them.

Add an ai chat reply animation to **@clip1.mp4**, put a sale badge on **@clip2.mp4**, give **@clip3.mp4** a VHS look, then join all three with crossfades.

## Settings you can change

Ask Claude to change any of these, or edit them at the top of the script.

| Setting    | Default                                                                                      | What it does                                               |
| ---------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `QUESTION` | `best serum for dull skin?`                                                                  | The question typed into the chat box.                      |
| `ANSWER`   | `Try a daily vitamin C serum. It brightens dull skin and evens out tone in about two weeks.` | The reply that streams in.                                 |
| `START`    | `0.3`                                                                                        | Second the chat box pops in.                               |
| `TYPE_AT`  | `0.6`                                                                                        | Second the typing starts.                                  |
| `CPS`      | `15`                                                                                         | Characters typed per second.                               |
| `SEND_AT`  | `2.5`                                                                                        | Second the question is sent and moves up as a bubble.      |
| `REPLY_AT` | `3.3`                                                                                        | Second the answer starts streaming, after the typing dots. |
| `WPS`      | `8`                                                                                          | Answer words per second.                                   |
| `BOX_Y`    | `0.87`                                                                                       | Chat box center, as a share of the frame height.           |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Add an AI chat reply animation to input.mp4. At 0.3 seconds a pure white (255, 255, 255) pill-shaped chat box rises in from below and fades in over 0.3 seconds: 88% of the frame width wide, 13% of the frame width tall, centered horizontally with its center 87% down the frame, with a soft dark drop shadow, gray (140, 144, 150) placeholder text "Ask anything" at its left and a blue (10, 132, 255) circular send button with a white up arrow at its right end. From 0.6 seconds "best serum for dull skin?" types into it at 15 characters per second in dark gray (32, 33, 36) regular Arial at 5% of the frame width, with a blue (10, 132, 255) blinking cursor after the last letter. At 2.5 seconds the box clears back to the placeholder and the question flies up over 0.3 seconds into a blue (10, 132, 255) rounded bubble with white text, right-aligned 6% from the right edge with its top 10% down the frame. From 2.8 to 3.3 seconds a small white bubble with three bouncing gray dots sits below it on the left, 6% from the left edge. At 3.3 seconds it becomes a white rounded card 80% of the frame width wide, and the answer "Try a daily vitamin C serum. It brightens dull skin and evens out tone in about two weeks." streams into it at 8 words per second in the same dark gray Arial, wrapped to the card, the card growing a line at a time. Everything stays to the end. Use Python with Pillow to draw it and ffmpeg to put it on the video (don't rely on ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `ai-chat-typing.py` and run:

```
python3 ai-chat-typing.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""AI chat reply: a question is typed into a chat box, sent as a blue bubble, and the answer streams in word by word.

Usage: python3 ai-chat-typing.py input.mp4 output.mp4
Needs ffmpeg and Pillow (pip install pillow).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

from PIL import Image, ImageDraw, ImageFilter, ImageFont

QUESTION = "best serum for dull skin?"
ANSWER = "Try a daily vitamin C serum. It brightens dull skin and evens out tone in about two weeks."
START = 0.3          # second the chat box pops in
TYPE_AT = 0.6        # second the typing starts
CPS = 15             # characters typed per second
SEND_AT = 2.5        # second the question is sent and moves up as a bubble
REPLY_AT = 3.3       # second the answer starts streaming (three typing dots before it)
WPS = 8              # answer words per second
MARGIN = 0.06        # side margin as a share of the frame width
TOP = 0.10           # top of the question bubble, as a share of the frame height
BOX_Y = 0.87         # chat box center, as a share of the frame height
TEXT_SIZE = 0.05     # text height as a share of the frame width
INK = (32, 33, 36)       # typed text and the answer
GREY = (140, 144, 150)   # placeholder and typing dots
BLUE = (10, 132, 255)    # question bubble, send button and cursor
FONTS = ["/System/Library/Fonts/Supplemental/Arial.ttf",          # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",       # Linux
         "C:/Windows/Fonts/arial.ttf"]                            # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No font found: add one to FONTS")

def card(cw, ch, radius, fill):
    """A rounded card with a soft drop shadow, on a transparent canvas with room for the shadow."""
    img = Image.new("RGBA", (cw + 2 * pad, ch + 2 * pad))
    shadow = Image.new("RGBA", img.size)
    ImageDraw.Draw(shadow).rounded_rectangle([pad, pad + 5, pad + cw, pad + ch + 5], radius=radius, fill=(0, 0, 0, 100))
    img.alpha_composite(shadow.filter(ImageFilter.GaussianBlur(pad / 3)))
    ImageDraw.Draw(img).rounded_rectangle([pad, pad, pad + cw, pad + ch], radius=radius, fill=fill)
    return img

def wrap(text, width):
    lines = [""]
    for word in text.split():
        trial = (lines[-1] + " " + word).strip()
        if font.getlength(trial) <= width or not lines[-1]:
            lines[-1] = trial
        else:
            lines.append(word)
    return lines

def chat_box(t):
    """The input bar: typed text and a cursor until it's sent, a placeholder before and after."""
    img = card(box_w, box_h, box_h // 2, "white")
    draw = ImageDraw.Draw(img)
    x, cy = pad + box_h * 0.45, pad + box_h / 2
    typed = QUESTION[:max(0, int((t - TYPE_AT) * CPS))] if t < SEND_AT else ""
    draw.text((x, cy), typed or "Ask anything", font=font, anchor="lm", fill=INK if typed else GREY)
    if TYPE_AT <= t < SEND_AT and int(t * 2.5) % 2 == 0:
        cx = x + font.getlength(typed) + 3
        draw.rectangle([cx, cy - font.size * 0.6, cx + max(2, w // 270), cy + font.size * 0.6], fill=BLUE)
    r = box_h * 0.32  # send button: a blue circle with a white up arrow
    bx = pad + box_w - box_h * 0.5
    draw.ellipse([bx - r, cy - r, bx + r, cy + r], fill=BLUE)
    draw.line([bx, cy + r * 0.5, bx, cy - r * 0.5], fill="white", width=max(2, w // 180))
    draw.line([bx - r * 0.4, cy - r * 0.1, bx, cy - r * 0.55, bx + r * 0.4, cy - r * 0.1], fill="white", width=max(2, w // 180))
    return img

def question_bubble():
    bw = round(font.getlength(QUESTION) + 2 * line_h * 0.7)
    img = card(bw, round(line_h * 1.9), round(line_h * 0.8), BLUE)
    ImageDraw.Draw(img).text((pad + bw / 2, pad + line_h * 0.95), QUESTION, font=font, anchor="mm", fill="white")
    return img

def typing_dots(t):
    img = card(round(line_h * 3.2), round(line_h * 1.9), round(line_h * 0.8), "white")
    draw = ImageDraw.Draw(img)
    for i in range(3):
        bounce = max(0, math.sin((t * 6 - i * 0.8) * math.pi)) * line_h * 0.18
        cx, cy, r = pad + line_h * (0.9 + i * 0.7), pad + line_h * 0.95 - bounce, line_h * 0.16
        draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=GREY)
    return img

def answer_card(t):
    """The answer so far: whole words appear WPS per second, and the card grows a line at a time."""
    left = int((t - REPLY_AT) * WPS) + 1
    shown = []
    for line in answer_lines:
        words = line.split()[:max(0, left)]
        left -= len(line.split())
        if words:
            shown.append(" ".join(words))
    img = card(card_w, round(line_h * (len(shown) + 0.9)), round(line_h * 0.8), "white")
    draw = ImageDraw.Draw(img)
    for i, line in enumerate(shown):
        draw.text((pad + line_h * 0.7, pad + line_h * (0.95 + i)), line, font=font, anchor="lm", fill=INK)
    return img

def fade(img, k):
    img = img.copy()
    img.putalpha(img.getchannel("A").point(lambda a: round(a * k)))
    return img

def ease_out(p):
    p = min(max(p, 0), 1)
    return 1 - (1 - p) ** 3

def overlay(t):
    img = Image.new("RGBA", (w, h))
    if t < START:
        return img
    p = ease_out((t - START) / 0.3)  # the chat box rises in from below
    img.alpha_composite(fade(chat_box(t), p), (round(w * MARGIN) - pad, round(h * BOX_Y - box_h / 2 + (1 - p) * h * 0.05) - pad))
    if t >= SEND_AT:
        p = ease_out((t - SEND_AT) / 0.3)  # the question flies up from the chat box to the top right
        y = round(h * TOP + (1 - p) * h * (BOX_Y - TOP - 0.05))
        img.alpha_composite(fade(bubble_img, min(1, p * 2)), (w - round(w * MARGIN) - bubble_img.width + pad, y - pad))
    reply_y = round(h * TOP + bubble_img.height - 2 * pad + line_h * 0.6) - pad
    if SEND_AT + 0.3 <= t < REPLY_AT:
        img.alpha_composite(typing_dots(t), (round(w * MARGIN) - pad, reply_y))
    elif t >= REPLY_AT:
        img.alpha_composite(answer_card(t), (round(w * MARGIN) - pad, reply_y))
    return img

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate:format=duration", "-of", "json", path]))
    s = info["streams"][0]
    num, den = s["r_frame_rate"].split("/")
    return s["width"], s["height"], s["r_frame_rate"], float(num) / float(den), float(info["format"]["duration"])

def render(src, out):
    """Draw overlay(t) on every frame of src and save to out, keeping the audio."""
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
                           "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-filter_complex", "[0:v][1:v]overlay=shortest=1,format=yuv420p[v]",
                           "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
                          stdin=subprocess.PIPE)
    for i in range(math.ceil(duration * fps)):
        ff.stdin.write(overlay(i / fps).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")

src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
pad = round(w * 0.04)  # room around each card for its shadow
font = ImageFont.truetype(FONT_FILE, round(w * TEXT_SIZE))
line_h = round(font.size * 1.45)
box_w, box_h = round(w * (1 - 2 * MARGIN)), round(w * 0.13)
card_w = round(w * 0.8)
answer_lines = wrap(ANSWER, card_w - 1.4 * line_h)
bubble_img = question_bubble()
render(src, out)
```

## More captions and text

- [Animated bar chart **Animated bar chart**](https://www.recharm.com/effects/animated-bar-chart)
- [Animated checklist **Animated checklist**](https://www.recharm.com/effects/animated-checklist)
- [Animated line graph **Animated line graph**](https://www.recharm.com/effects/animated-line-graph)
- [Animated pie chart **Animated pie chart**](https://www.recharm.com/effects/animated-pie-chart)
- [Animated underline **Animated underline**](https://www.recharm.com/effects/animated-underline)

## Find 3x _winning ads_

Recharm cuts your ad footage into tagged, searchable clips: hooks, testimonials, product shots. Find the shots that win, add the effect with Claude, and ship 3x more ad edits.

[Start 14-day free trial](https://www.recharm.com/get-started)
