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 embers and sparks to input.mp4 Second the first sparks enter at the bottom edge: 0.5 New sparks per second: 36
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 embers and sparks 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 |
|---|---|---|
START | 0.5 | Second the first sparks enter at the bottom edge. |
RATE | 36 | New sparks per second. |
LIFE | (2.8, 4.2) | Seconds a spark glows before it has cooled away. |
RISE | (190, 340) | Upward speed of the slowest and fastest sparks, in pixels per second. |
SWIRL | (12, 40) | Sideways swing of each spark, in pixels. |
SIZE | (1.6, 3.4) | Core radius of the smallest and largest sparks, in pixels. |
TRAIL | 0.07 | Seconds of motion each spark smears into a streak. |
HEAT | (255, 215, 90), (255, 140, 30), (200, 45, 10) | Spark color at birth, halfway through its life and as it dies. |
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 rising embers and sparks to input.mp4 (540x960). From 0.5 seconds, 36 new sparks per second enter just below the bottom edge (10 px under it) at random positions between 5% and 95% across, until the end; before 0.5 seconds the frame is untouched. Each spark rises at a random 190 to 340 px/s, speeding up by 18 px/s every second (y = 970 - speed*age - 18*age^2), drifts sideways with a random wind of -25 to 25 px/s and swings side to side by 12 to 40 px on a 1.2 to 2.6 second cycle. It lives 2.8 to 4.2 seconds: its color goes from hot yellow (255, 215, 90) at birth to orange (255, 140, 30) halfway to deep red (200, 45, 10) at the end, it stays solid for the first half and then fades to nothing, flickering by 20%. Draw each spark as a Gaussian core with a radius of 1.6 to 3.4 px plus a halo four times as wide at about 20% strength, smeared into a streak along the last 0.07 seconds of its path and dimmer toward its tail. Paint the sparks over the footage with normal alpha (opaque cores, so they show on a light background), not a screen blend. Use Python with numpy and ffmpeg (don't use ffmpeg's drawtext filter for anything), keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as embers-and-sparks.py and run:
python3 embers-and-sparks.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Embers and sparks: glowing sparks rise from the bottom edge and swirl up the frame, cooling from yellow to orange to red as they fade.
Usage: python3 embers-and-sparks.py input.mp4 output.mp4
Needs ffmpeg and numpy (pip install numpy).
"""
import json
import subprocess
import sys
import numpy as np
START = 0.5 # second the first sparks enter at the bottom edge
RATE = 36 # new sparks per second
LIFE = (2.8, 4.2) # seconds a spark glows before it has cooled away
RISE = (190, 340) # upward speed of the slowest and fastest sparks, in pixels per second
SWIRL = (12, 40) # sideways swing of each spark, in pixels
SIZE = (1.6, 3.4) # core radius of the smallest and largest sparks, in pixels
TRAIL = 0.07 # seconds of motion each spark smears into a streak
STEPS = 12 # dots that make up each streak
HEAT = [(255, 215, 90), # color when the spark is born: hot yellow
(255, 140, 30), # halfway through its life: orange
(200, 45, 10)] # as it dies: deep red
SEED = 7 # change for a different arrangement
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 position(s, age):
"""Where a spark is `age` seconds after it entered: rising, drifting with the wind and swinging side to side."""
x = s["x"] + s["wind"] * age + s["swirl"] * np.sin(2 * np.pi * age / s["period"] + s["phase"])
y = h + 10 - s["rise"] * age - 18 * age ** 2 # speeds up a little as the hot air carries it
return x, y
def heat_color(p):
"""Color at life fraction p (0 = just born, 1 = cooled): yellow, then orange, then red."""
a, b = (HEAT[0], HEAT[1]) if p < 0.5 else (HEAT[1], HEAT[2])
f = p * 2 if p < 0.5 else p * 2 - 1
return np.array(a) + (np.array(b) - np.array(a)) * f
def sparks(t):
"""The sparks at time t: their color and how much each pixel is covered (0-1)."""
paint = np.zeros((h, w, 3), np.float32)
cover = np.zeros((h, w), np.float32)
for s in embers:
age = t - s["born"]
p = age / s["life"]
if age < 0 or p >= 1:
continue
glow = min(1, 2 * (1 - p)) * (0.8 + 0.2 * np.sin(age * s["flicker"])) # solid, then fades as it cools
color = heat_color(p)
r = s["size"]
for k in range(STEPS): # a short streak behind the spark, dimmer toward its tail
x, y = position(s, max(age - TRAIL * k / STEPS, 0))
weight = glow * (1 - k / STEPS) * 4 / STEPS
rad = int(r * 6) + 1
x0, y0, x1, y1 = max(int(x) - rad, 0), max(int(y) - rad, 0), min(int(x) + rad + 1, w), min(int(y) + rad + 1, h)
if x0 >= x1 or y0 >= y1:
continue
yy, xx = np.mgrid[y0:y1, x0:x1].astype(np.float32)
d2 = ((xx - x) ** 2 + (yy - y) ** 2) / r ** 2
spot = weight * (1.6 * np.exp(-d2) + 0.35 * np.exp(-d2 / 16)) # a solid core in a halo four times as wide
paint[y0:y1, x0:x1] += spot[..., None] * color
cover[y0:y1, x0:x1] += spot
color = paint / np.maximum(cover, 1e-6)[..., None]
return color, np.minimum(cover, 1)[..., None]
src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
rng = np.random.default_rng(SEED)
embers = []
for born in np.arange(START, duration, 1 / RATE):
embers.append({"born": born + rng.uniform(0, 1 / RATE), "life": rng.uniform(*LIFE),
"x": rng.uniform(0.05, 0.95) * w, "wind": rng.uniform(-25, 25),
"rise": rng.uniform(*RISE), "size": rng.uniform(*SIZE),
"swirl": rng.uniform(*SWIRL), "period": rng.uniform(1.2, 2.6), "phase": rng.uniform(0, 2 * np.pi),
"flicker": rng.uniform(15, 30)})
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", "16", "-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)
if i / fps >= START:
color, cover = sparks(i / fps)
img = (img * (1 - cover) + color * cover).astype(np.uint8)
writer.stdin.write(img.tobytes())
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





