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
Join clipA.mp4 and clipB.mp4 with a burning paper transition Ember colors: near white, orange, dark red
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.
Put a sale badge on @clip1.mp4, give @clip2.mp4 a VHS look, then join them with a burning paper transition.
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 |
|---|---|---|
D | 1.5 | Length of the transition in seconds. |
CENTER | (0.5, 0.5) | Where the hole starts, as fractions across and down the frame. |
RAGGED | 0.12 | How far the noise pushes the burn edge in and out, as a share of the frame height. |
EMBER | 0.014 | Width of the glowing edge, as a share of the frame height. |
CHAR | 0.045 | Width of the scorched brown band just outside the glowing edge. |
GLOW | 0.03 | How far the orange glow reaches either side of the edge. |
EMBER_COLORS | near white, orange, dark red | The glowing edge's colors from the burned side outward. |
CHAR_COLOR | (35, 20, 12) | Color of the scorched paper next to the edge. |
GLOW_COLOR | (255, 110, 20) | Color of the light the edge throws on both clips. |
SEED | 3 | Change for a different burn shape. |
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
Join clipA.mp4 and clipB.mp4 with a 1.5 second burning paper transition that starts 1.5 seconds before the end of A, so the output is 10.5 seconds long. Match B's size to A (scale to cover and center-crop) and output 30fps. Do it in Python with numpy and Pillow: read both clips' frames as RGB through ffmpeg pipes, composite each frame of the transition in numpy and pipe the result back to ffmpeg. Outside the 1.5 second window the picture must be untouched. Build a static burn map once, with h the frame height: dist = (distance in pixels from the frame center) / h + 0.24 * (n - 0.5), where n = 0.55*N(4x7) + 0.3*N(10x18) + 0.15*N(40x70) and N(cols x rows) is a fixed grid of uniform random values (0-1) upscaled to the frame with bicubic resampling. With EMBER = 0.014, CHAR = 0.045 and GLOW = 0.03, the burn front r runs linearly from min(dist) - EMBER - CHAR - 3*GLOW at the start of the transition to max(dist) + 3*GLOW at the end. For each pixel let e = dist - r. Where e < 0 show clip B, otherwise clip A. Where EMBER <= e < EMBER + CHAR, blend A toward the scorched color RGB (35, 20, 12) by clip(1 - (e - EMBER)/CHAR, 0, 1)^1.5. Where 0 <= e < EMBER, paint the glowing edge, interpolating e/EMBER = 0, 0.5, 1 to RGB (255, 235, 170), (255, 140, 30), (150, 40, 5). Then add an orange glow on both sides of the edge: add RGB (255, 110, 20) * 0.6 * exp(-(e/GLOW)^2) and clip to 0-255. Crossfade the audio over the same 1.5 seconds and keep it. Output H.264 yuv420p, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as burning-paper-transition.py and run:
python3 burning-paper-transition.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Burning paper: a hole burns open in the middle of clip A and spreads out on a ragged, glowing edge to uncover clip B.
Usage: python3 burning-paper-transition.py clipA.mp4 clipB.mp4 output.mp4 (both clips need an audio track)
Needs ffmpeg, numpy and Pillow (pip install numpy pillow). The burn edge is drawn from random noise, so no stock overlay is needed.
"""
import json
import subprocess
import sys
import numpy as np
from PIL import Image
D = 1.5 # transition length in seconds
CENTER = (0.5, 0.5) # where the hole starts (0-1 across and down the frame)
RAGGED = 0.12 # how far the noise pushes the burn edge in and out, as a share of the frame height
EMBER = 0.014 # width of the glowing edge, as a share of the frame height
CHAR = 0.045 # width of the scorched brown band beyond it
GLOW = 0.03 # reach of the orange glow either side of the edge
EMBER_COLORS = [(255, 235, 170), (255, 140, 30), (150, 40, 5)] # glowing edge, inside to outside: near white, orange, dark red
CHAR_COLOR = (35, 20, 12) # color of the scorched paper next to the edge
GLOW_COLOR = (255, 110, 20) # orange light the edge throws on both clips
SEED = 3 # change for a different burn shape
def noise(cols, rows):
"""Smooth random field in 0-1: a cols x rows grid of random values scaled up to the frame."""
grid = np.random.default_rng([SEED, cols]).random((rows, cols)).astype(np.float32)
return np.asarray(Image.fromarray(grid).resize((w, h), Image.BICUBIC))
def burn(fa, fb, u):
"""Clip A frame fa burned away into clip B frame fb, u (0-1) of the way through the transition."""
e = dist - (start + (end - start) * u) # each pixel's distance from the burn edge, into the unburned paper
out = np.where((e < 0)[..., None], fb, fa).astype(np.float32)
char = np.clip(1 - (e - EMBER) / CHAR, 0, 1) ** 1.5 * (e >= EMBER)
out += (np.array(CHAR_COLOR) - out) * char[..., None]
ember = (e >= 0) & (e < EMBER)
k = e[ember] / EMBER
out[ember] = np.stack([np.interp(k, [0, 0.5, 1], [c[i] for c in EMBER_COLORS]) for i in range(3)], axis=1)
out += np.array(GLOW_COLOR) * 0.6 * np.exp(-(e / GLOW) ** 2)[..., None]
return np.clip(out, 0, 255).astype(np.uint8)
def probe(path):
info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height:format=duration", "-of", "json", path]))
return info["streams"][0]["width"], info["streams"][0]["height"], float(info["format"]["duration"])
def frames(path, vf):
"""Read a clip's frames as RGB arrays."""
ff = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf", vf, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
stdout=subprocess.PIPE)
while data := ff.stdout.read(w * h * 3):
yield np.frombuffer(data, np.uint8).reshape(h, w, 3)
a, b, out = sys.argv[1:4]
w, h, len_a = probe(a)
len_b = probe(b)[2]
fps = 30
offset = len_a - D
# distance from CENTER in frame heights, pushed in and out by noise so the hole's edge is ragged like burning paper
ys, xs = np.mgrid[0:h, 0:w]
n = 0.55 * noise(4, 7) + 0.3 * noise(10, 18) + 0.15 * noise(40, 70) # big lobes, smaller bites and a crinkled edge
dist = np.hypot(xs - CENTER[0] * w, ys - CENTER[1] * h) / h + RAGGED * 2 * (n - 0.5)
start = dist.min() - EMBER - CHAR - 3 * GLOW # nothing shows yet at the first frame
end = dist.max() + 3 * GLOW # all of clip B, glow gone, at the last
ra = frames(a, f"fps={fps}")
rb = frames(b, f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={fps}")
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", str(fps),
"-i", "-", "-i", a, "-i", b, "-filter_complex", f"[1:a][2:a]acrossfade=d={D}[au]",
"-map", "0:v", "-map", "[au]", "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "aac", out],
stdin=subprocess.PIPE)
for i in range(round((len_a + len_b - D) * fps)):
t = i / fps
if t < offset:
frame = next(ra)
elif t < len_a:
frame = burn(next(ra), next(rb), (t - offset) / D)
else:
frame = next(rb)
ff.stdin.write(frame.tobytes())
ff.stdin.close()
sys.exit("ffmpeg failed" if ff.wait() else 0)





