How to add a comic sound-effect text with Claude

A spiky yellow comic-book burst reading "POW!" slams onto the top of the frame with white motion lines shooting out, then a second burst reading "WHOOSH!" takes its place near the bottom.

Get this effect

  1. Switch to Code in your Claude Desktop App

  2. 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/skills
  3. Ask 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.

SettingDefaultWhat 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.
SIZE0.70Burst width as a share of the frame width.
SPIKES14Points 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.
SLAM0.15Seconds a burst takes to shrink from START_SCALE to its size.
START_SCALE2.2How big a burst starts, relative to its size.
EXIT0.12Seconds a burst takes to shrink away.
LINES12Motion lines around a burst as it lands.
LINES_LEN0.45Seconds 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.mp4
Show 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)

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