How to add fireworks with Claude

Three rockets streak up and burst over the product one after another into gold, pink and orange shells of sparks that trail, droop and fade out.

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 fireworks to input.mp4
    Bursts: 0.8s gold at (0.25, 0.20), 1.4s pink at (0.73, 0.17), 2.0s orange at (0.50, 0.11)

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 fireworks 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
BURSTS0.8s gold at (0.25, 0.20), 1.4s pink at (0.73, 0.17), 2.0s orange at (0.50, 0.11)When each firework bursts, where (0-1 across and down) and its color. Add or remove rows for more or fewer.
RISE0.4Seconds each rocket takes to climb from the bottom edge to its burst.
SPARKS150Sparks per burst.
SPEED340How fast sparks fly out, in pixels per second.
DRAG2.0Air drag: higher stops the sparks sooner and makes the burst smaller.
GRAVITY60How fast the sparks fall, in pixels per second squared.
LIFE1.7Seconds a spark lasts before it has faded out.
TRAIL14Points in each spark's trail.
GLOW3Glow radius around each spark, in pixels.
SEED3Change for a different scatter of sparks.

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 fireworks to input.mp4. Three bursts, each preceded by a thin pale gold (255, 217, 140) rocket streak that climbs straight up from the bottom edge over 0.4 seconds, slowing as it rises: a gold (255, 190, 30) burst at 0.8 seconds centered 25% across and 20% down, a pink (255, 40, 150) burst at 1.4 seconds centered 73% across and 17% down, and an orange (255, 90, 20) burst at 2.0 seconds centered 50% across and 11% down. Each burst is 150 sparks flying out in all directions at up to 340 pixels per second, slowed by air drag so they stop about 170 pixels from the center, then sagging down under gravity (60 pixels per second squared). Each spark leaves a short trail (its positions over the last 0.17 seconds, fading toward the tail), has a white-hot core with a 3-pixel colored glow, flashes brightly for the first 0.1 seconds, flickers in its second half and has faded out completely 1.7 seconds after its burst. Lay the sparks over the shot in full color (not a screen blend, so they show on the pale backdrop). Nothing shows before the first rocket at 0.4 seconds and nothing is left after 4 seconds. Draw it with Python and numpy (no text, and 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 fireworks.py and run:

python3 fireworks.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Fireworks: three rockets streak up and burst into gold, pink and orange shells of sparks that droop and fade.

Usage: python3 fireworks.py input.mp4 output.mp4
Needs ffmpeg and numpy (pip install numpy).
"""
import json
import subprocess
import sys

import numpy as np

BURSTS = [  # (second it bursts, x, y as 0-1 across and down the frame, spark color)
    (0.8, 0.25, 0.20, (255, 190, 30)),   # gold, top left
    (1.4, 0.73, 0.17, (255, 40, 150)),   # pink, top right
    (2.0, 0.50, 0.11, (255, 90, 20)),    # orange, top middle
]
RISE = 0.4           # seconds each rocket takes to climb from the bottom edge to its burst
SPARKS = 150         # sparks per burst
SPEED = 340          # how fast sparks fly out, in pixels per second
DRAG = 2.0           # air drag: higher stops the sparks sooner and makes the burst smaller
GRAVITY = 60         # how fast the sparks fall, in pixels per second squared
LIFE = 1.7           # seconds a spark lasts before it has faded out
TRAIL = 14           # points in each spark's trail
GLOW = 3             # glow radius around each spark, in pixels
SEED = 3             # change for a different scatter of sparks


def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate", "-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)


def blur(img, r):
    """Box blur of radius r along both axes, run twice so it looks like a soft gaussian glow."""
    for _ in range(2):
        for axis in (0, 1):
            c = np.cumsum(np.pad(img, [(r + 1, r) if a == axis else (0, 0) for a in range(3)]), axis=axis)
            img = (np.take(c, range(2 * r + 1, c.shape[axis]), axis) - np.take(c, range(c.shape[axis] - 2 * r - 1), axis)) / (2 * r + 1)
    return img


def points(t):
    """Every bright point at time t: x, y, brightness and color (0-1), rockets and spark trails together."""
    xs, ys, power, colors = [], [], [], []
    for (at, bx, by, color), (vx, vy, flicker) in zip(BURSTS, shells):
        cx, cy = bx * w, by * h
        col = np.array(color, np.float32) / 255
        if -RISE < t - at < 0:  # the rocket: a pale gold streak, slowing as it nears the top
            tail = np.linspace(0, 1, 80)
            p = 1 - (np.clip(at - t + 0.08 * tail, 0, RISE) / RISE) ** 2
            xs.append(np.full(80, cx)), ys.append(h + (cy - h) * p), power.append(2 * (1 - tail))
            colors.append(np.tile((1, 0.85, 0.55), (80, 1)))
        for k in range(TRAIL):
            age = t - at - k * 0.012
            weight = 1 - k / TRAIL
            if 0 <= age < LIFE:
                spread = (1 - np.exp(-DRAG * age)) / DRAG
                fade = (1 - age / LIFE) ** 0.8 * (1 + 1.5 * np.exp(-age / 0.05))  # flash at the burst, then fade
                twinkle = np.where(age > LIFE * 0.5, 0.5 + 0.5 * ((flicker + round(t * fps)) % 3 > 0), 1)
                xs.append(cx + vx * spread), ys.append(cy + vy * spread + GRAVITY * age ** 2 / 2)
                power.append(fade * twinkle * weight), colors.append(np.repeat(col[None], SPARKS, 0))
    if not xs:
        return None
    return np.concatenate(xs), np.concatenate(ys), np.concatenate(power), np.concatenate(colors)


def lights(t):
    """The light layer at time t (0 = none, 1 and up = full color): white-hot spark cores inside a colored glow."""
    found = points(t)
    if found is None:
        return None
    x, y, p, c = found
    inside = (x >= 1) & (x < w - 1) & (y >= 1) & (y < h - 1)
    x, y, p, c = x[inside].astype(int), y[inside].astype(int), p[inside], c[inside]
    dots = np.zeros((h, w, 3), np.float32)
    hot = (0.5 * c + 0.5) * p[:, None]  # cores run toward white
    for dx, dy in ((0, 0), (1, 0), (-1, 0), (0, 1), (0, -1)):
        np.add.at(dots, (y + dy, x + dx), hot * (1 if dx == dy == 0 else 0.5))
    halo = np.zeros((h, w, 3), np.float32)
    np.add.at(halo, (y, x), c * p[:, None])
    return dots * 2 + blur(halo, GLOW) * 10


src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
rng = np.random.default_rng(SEED)
shells = []
for _ in BURSTS:
    angle = rng.uniform(0, 2 * np.pi, SPARKS)
    speed = SPEED * np.sin(rng.uniform(0.3, np.pi / 2, SPARKS)) * rng.uniform(0.9, 1.0, SPARKS)  # a sphere seen flat
    shells.append((speed * np.cos(angle), speed * np.sin(angle), rng.integers(0, 3, SPARKS)))

reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                          stdout=subprocess.PIPE)
writer = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
                           "-r", rate, "-i", "-", "-i", src, "-map", "0:v", "-map", "1:a?",
                           "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "copy", "-shortest", out],
                          stdin=subprocess.PIPE)
i = 0
while raw := reader.stdout.read(w * h * 3):
    img = np.frombuffer(raw, np.uint8).reshape(h, w, 3)
    light = lights(i / fps)
    if light is not None:
        alpha = np.minimum(light.max(axis=2, keepdims=True), 1)  # laid over the shot, so colors hold on bright backdrops
        color = light / np.maximum(light.max(axis=2, keepdims=True), 1e-6)
        img = (img * (1 - alpha) + 255 * color * alpha).astype(np.uint8)
    writer.stdin.write(img.tobytes())
    i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
    sys.exit("ffmpeg failed")

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