Get this effect
Switch to Code in your Claude Desktop App
Install the skill (once)
Show the prompt
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/skillsAsk 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.
Show the prompt
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.mp4Show the script
#!/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)





