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 animated feature callouts to input.mp4 Callouts: Glass dropper, 15% vitamin C, Fragrance free
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 animated feature callouts 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 |
|---|---|---|
CALLOUTS | Glass dropper, 15% vitamin C, Fragrance free | Each label, the point on the product, the elbow, the line end and the second it starts. |
COLOR | (235, 40, 120) | Color of the lines, dots and label boxes. |
DOT, DRAW, LABEL | 0.15, 0.4, 0.25 | Seconds the dot pop, the line draw and the label fade take. |
TEXT | 0.037 | Text height as a share of the frame width. |
LINE | 3 | Line width in pixels. |
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 three animated feature callouts to input.mp4 (540x960) in pink (235, 40, 120). Each one has a dot on the product (radius 7 px, with a thinner ring at radius 14 px), a 3 px line from the dot to an elbow and then flat to a label, and the label: white bold text 20 px high on a pink pill with 12 px padding left and right, 10 px top and bottom, and fully rounded ends. Positions as fractions of the frame (across, down): 1) "Glass dropper": dot at (0.54, 0.27), elbow (0.62, 0.20), line ends at (0.68, 0.20) with the label's left edge there, starting at 0.5 s. 2) "15% vitamin C": dot (0.45, 0.45), elbow (0.38, 0.40), line ends at (0.32, 0.40) with the label's right edge there, starting at 1.0 s. 3) "Fragrance free": dot (0.47, 0.75), elbow (0.38, 0.80), line ends at (0.32, 0.80) with the label's right edge there, starting at 1.5 s. For each: the dot pops in over 0.15 s, then the line draws out from the dot over 0.4 s, then the label fades in while sliding 12 px toward its final spot over 0.25 s, all eased out; everything then stays to the end. Nothing is on screen before 0.5 s. Draw the overlay with Python and Pillow (not ffmpeg's drawtext filter) and composite it with ffmpeg, keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as animated-feature-callouts.py and run:
python3 animated-feature-callouts.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Animated feature callouts: pink lines draw out from the dropper, the glass and the base to labels beside the bottle.
Usage: python3 animated-feature-callouts.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, ImageFont
# label, point on the product, elbow where the line turns flat, end of the line (the label's near edge),
# second it starts. Positions are 0-1 across and down the frame; a label sits right of its line end if the
# line heads right, left of it if the line heads left.
CALLOUTS = [("Glass dropper", (0.54, 0.27), (0.62, 0.20), (0.68, 0.20), 0.5),
("15% vitamin C", (0.45, 0.45), (0.38, 0.40), (0.32, 0.40), 1.0),
("Fragrance free", (0.47, 0.75), (0.38, 0.80), (0.32, 0.80), 1.5)]
COLOR = (235, 40, 120) # lines, dots and label boxes
DOT = 0.15 # seconds the dot takes to pop onto the product
DRAW = 0.4 # seconds the line takes to draw out
LABEL = 0.25 # seconds the label takes to slide and fade in
TEXT = 0.037 # text height as a share of the frame width
LINE = 3 # line width in pixels
FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf", # macOS
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", # Linux
"C:/Windows/Fonts/arialbd.ttf"] # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")
SS = 2 # draw at twice the size and shrink, for smooth lines and circles
def progress(t, start, length):
p = min(max((t - start) / length, 0), 1)
return 1 - (1 - p) ** 3 # ease out
def label(text):
"""A rounded box with the label in white, at SS times the size."""
font = ImageFont.truetype(FONT_FILE, round(w * TEXT * SS))
pad = round(w * 0.022 * SS)
tw, th = font.getlength(text), font.size
img = Image.new("RGBA", (round(tw + 2 * pad), round(th + 1.6 * pad)))
ImageDraw.Draw(img).rounded_rectangle([0, 0, img.width - 1, img.height - 1], radius=img.height // 2, fill=COLOR)
ImageDraw.Draw(img).text((img.width / 2, img.height / 2), text, font=font, anchor="mm", fill="white")
return img
def along(points, p):
"""The polyline cut off after share p of its length."""
lengths = [math.dist(a, b) for a, b in zip(points, points[1:])]
left = p * sum(lengths)
out = [points[0]]
for (a, b), n in zip(zip(points, points[1:]), lengths):
f = min(left / n, 1)
out.append((a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f))
left -= n
if left <= 0:
break
return out
def overlay(t):
img = Image.new("RGBA", (w * SS, h * SS))
draw = ImageDraw.Draw(img)
for (text, *spots, start), box in zip(CALLOUTS, boxes):
if t < start:
continue
anchor, elbow, end = [(x * w * SS, y * h * SS) for x, y in spots]
r = progress(t, start, DOT) * 7 * SS
draw.ellipse([anchor[0] - 2 * r, anchor[1] - 2 * r, anchor[0] + 2 * r, anchor[1] + 2 * r], outline=COLOR + (150,), width=2 * SS)
draw.ellipse([anchor[0] - r, anchor[1] - r, anchor[0] + r, anchor[1] + r], fill=COLOR)
drawn = progress(t, start + DOT, DRAW)
if drawn > 0:
draw.line(along([anchor, elbow, end], drawn), fill=COLOR, width=LINE * SS, joint="curve")
shown = progress(t, start + DOT + DRAW, LABEL)
if shown > 0:
heads = 1 if end[0] > elbow[0] else -1
x = end[0] - (12 * SS * (1 - shown) if heads > 0 else box.width - 12 * SS * (1 - shown))
faded = box.copy()
faded.putalpha(faded.getchannel("A").point(lambda a: round(a * shown)))
img.alpha_composite(faded, (round(x), round(end[1] - box.height / 2)))
return img.resize((w, h), Image.LANCZOS)
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)
boxes = [label(text) for text, *_ in CALLOUTS]
render(src, out)





