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 a comic sound-effect text to input.mp4 Bursts: [("POW!", 0.5, 2.0, 0.34, 0.24, -6), ("WHOOSH!", 2.6, 4.6, 0.58, 0.74, 8)]
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 a comic sound-effect text 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 |
|---|---|---|
BURSTS | [("POW!", 0.5, 2.0, 0.34, 0.24, -6), ("WHOOSH!", 2.6, 4.6, 0.58, 0.74, 8)] | Each word, the seconds it slams in and leaves, its center across and down (0-1) and its tilt in degrees. |
SIZE | 0.70 | Burst width as a share of the frame width. |
SPIKES | 14 | Points on the burst. |
BURST_COLOR | (255, 214, 0) | Burst fill. |
SHADOW_COLOR | (230, 30, 45) | Offset burst behind it. |
TEXT_COLOR | (230, 30, 45) | Word color, drawn with a white then black outline. |
SLAM | 0.15 | Seconds a burst takes to shrink from START_SCALE to its size. |
START_SCALE | 2.2 | How big a burst starts, relative to its size. |
EXIT | 0.12 | Seconds a burst takes to shrink away. |
LINES | 12 | Motion lines around a burst as it lands. |
LINES_LEN | 0.45 | Seconds the motion lines take to shoot out and fade. |
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 comic-book sound-effect text to input.mp4. Draw each word on a spiky burst: a 14-point star 70% of the frame width across and 80% as tall as it is wide, with long and short spikes alternating (short spikes at 66% of the radius), filled yellow (255, 214, 0) with a black outline 1.4% of the burst width thick, and a red (230, 30, 45) copy of the star offset 3.5% of the burst width down and to the right behind it. The word sits in the middle in Impact (or another heavy font), red (230, 30, 45) with a thin white outline inside a thicker black one, filling about 56% of the burst's width. "POW!" is centered 34% across and 24% down, tilted -6 degrees, from 0.5 to 2.0 seconds. "WHOOSH!" is centered 58% across and 74% down, tilted 8 degrees, from 2.6 to 4.6 seconds. Each burst slams in by shrinking from 2.2 times its size to full size over 0.15 seconds, then throbs gently (3% in size, 3 times a second) and shrinks away to nothing over the last 0.12 seconds before it leaves. As each one lands, 12 white motion lines with black edges shoot outward from its edge, thinning and fading over 0.45 seconds. Nothing is on screen before 0.5 seconds, between 2.0 and 2.6 seconds, or after 4.6 seconds. Draw everything with Python and Pillow (don't rely on ffmpeg's drawtext filter), keep the size and audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as comic-sound-effect-text.py and run:
python3 comic-sound-effect-text.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Comic sound-effect text: spiky comic-book bursts reading POW! and WHOOSH! slam onto the frame with motion lines.
Usage: python3 comic-sound-effect-text.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
BURSTS = [("POW!", 0.5, 2.0, 0.34, 0.24, -6), # (word, second it slams in, second it leaves,
("WHOOSH!", 2.6, 4.6, 0.58, 0.74, 8)] # center across, center down (0-1), tilt in degrees)
SIZE = 0.70 # burst width as a share of the frame width
SPIKES = 14 # points on the burst
BURST_COLOR = (255, 214, 0) # burst fill
SHADOW_COLOR = (230, 30, 45) # offset burst behind it
TEXT_COLOR = (230, 30, 45) # word color (drawn with a white then black outline)
SLAM = 0.15 # seconds a burst takes to shrink from START_SCALE to its size
START_SCALE = 2.2 # how big a burst starts, relative to its size
EXIT = 0.12 # seconds a burst takes to shrink away
LINES = 12 # motion lines around a burst as it lands
LINES_LEN = 0.45 # seconds the motion lines take to shoot out and fade
FONTS = ["/System/Library/Fonts/Supplemental/Impact.ttf", # macOS
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", # Linux
"C:/Windows/Fonts/impact.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")
def star(cx, cy, rx, ry, turn=0.0):
"""Points of a spiky burst: long and short spikes alternate, a little uneven like a hand-drawn one."""
points = []
for i in range(SPIKES * 2):
a = math.pi * i / SPIKES + turn
r = (1.0 - 0.12 * (i % 4 == 0)) if i % 2 == 0 else 0.66
points.append((cx + rx * r * math.cos(a), cy + ry * r * math.sin(a)))
return points
def burst(word):
"""The word on a yellow burst with a black outline and a red burst offset behind it."""
bw = round(w * SIZE)
bh = round(bw * 0.8)
pad = bw // 10
img = Image.new("RGBA", (bw + 2 * pad, bh + 2 * pad))
draw = ImageDraw.Draw(img)
cx, cy, rx, ry = img.width / 2, img.height / 2, bw / 2, bh / 2
edge = max(3, bw // 70)
draw.polygon(star(cx + bw * 0.035, cy + bw * 0.035, rx, ry, 0.12), fill=SHADOW_COLOR)
points = star(cx, cy, rx, ry)
draw.polygon(points, fill=BURST_COLOR)
draw.line(points + points[:1], fill="black", width=edge, joint="curve")
stroke = max(2, bw // 50)
probe_font = ImageFont.truetype(FONT_FILE, 100)
x0, y0, x1, y1 = probe_font.getbbox(word, stroke_width=stroke * 2)
font = ImageFont.truetype(FONT_FILE, round(100 * min(bw * 0.56 / (x1 - x0), bh * 0.42 / (y1 - y0))))
draw.text((cx, cy), word, font=font, anchor="mm", fill="black", stroke_width=stroke * 2, stroke_fill="black")
draw.text((cx, cy), word, font=font, anchor="mm", fill=TEXT_COLOR, stroke_width=stroke, stroke_fill="white")
return img
def motion_lines(draw, x, y, s):
"""White strokes shooting out from the burst's edge, fading as they go."""
p = s / LINES_LEN
reach = w * SIZE * 0.5
for i in range(LINES):
a = 2 * math.pi * (i + 0.5 * (i % 2)) / LINES
r0 = reach * (0.95 + 0.6 * p)
r1 = r0 + reach * (0.35 + 0.15 * (i % 3)) * (1 - p)
x0, y0 = x + r0 * math.cos(a), y + r0 * 0.8 * math.sin(a)
x1, y1 = x + r1 * math.cos(a), y + r1 * 0.8 * math.sin(a)
width = max(2, round(w * 0.016 * (1 - p)))
draw.line([(x0, y0), (x1, y1)], fill=(0, 0, 0), width=width + 4)
draw.line([(x0, y0), (x1, y1)], fill=(255, 255, 255), width=width)
def overlay(t):
img = Image.new("RGBA", (w, h))
for (word, start, end, x, y, tilt), pic in zip(BURSTS, pictures):
if not start <= t < end:
continue
s = t - start
p = min(s / SLAM, 1)
scale = START_SCALE + (1 - START_SCALE) * (1 - (1 - p) ** 2) # slams down fast, settles at full size
scale *= 1 + 0.03 * math.sin(2 * math.pi * 3 * s) * (p == 1) # a slight throb once landed
scale *= min((end - t) / EXIT, 1) # shrinks away at the end
if scale < 0.02:
continue
b = pic.resize((round(pic.width * scale), round(pic.height * scale)), Image.LANCZOS)
b = b.rotate(tilt * (1 + 0.6 * (1 - p)), resample=Image.BICUBIC, expand=True)
cx, cy = w * x, h * y
if SLAM <= s < SLAM + LINES_LEN:
motion_lines(ImageDraw.Draw(img), cx, cy, s - SLAM)
img.alpha_composite(b, (round(cx - b.width / 2), round(cy - b.height / 2)))
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)
pictures = [burst(word) for word, *_ in BURSTS]
render(src, out)





