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 muzzle flash to input.mp4 Second the shot goes off: 1.0 Where the muzzle is, as shares of the frame width and height: 0.0, 0.62
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 muzzle flash 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 |
|---|---|---|
AT | 1.0 | Second the shot goes off. |
X, Y | 0.0, 0.62 | Where the muzzle is, as shares of the frame width and height. |
ANGLE | -10 | Direction the flash points, in degrees (0 = right, negative = upward). |
LENGTH | 0.75 | Flash length as a share of the frame width. |
FLASH | [1.0, 0.85, 0.5] | Strength on each frame of the burst; add values for a longer flash. |
FIRE, CORE | (255, 135, 25), (255, 240, 175) | Outer flame and white-hot core colors. |
SMOKE | 1.5 | Seconds the smoke takes to rise and clear. |
SMOKE_COLOR | (110, 106, 102) | Smoke gray. |
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 a muzzle flash to input.mp4, as if a gun just off the left edge fires once at 1.0 seconds. The muzzle is at the left edge, 62% down the frame, and the flash points right, tilted 10 degrees upward. Draw it with Python, Pillow and numpy: a diamond-shaped flame 75% of the frame width long and 22% of the frame width wide at its widest (30% of the way along), plus six pointed spikes fanning out from the muzzle at +-30, +-55 and +-80 degrees from the flash direction, each 40-60% of the flame length (shorter the wider the angle). Fill the flame orange (255, 135, 25) with a pale yellow (255, 240, 175) core 60% of the flame length, and soften all edges with a Gaussian blur of 1% of the frame width. Show it only on the three frames at 1.0s, 1.033s and 1.067s, at full, 85% and 50% strength, each frame 15% shorter than the last and with the spikes jittered a few degrees so it flickers. On those frames also screen-blend a soft orange glow centered on the muzzle (radius 60% of the frame width, 80% strength at the first frame). From 1.0s to 2.5s draw a dark gray (110, 106, 102) smoke puff from overlapping soft circles along the first 40% of the flash's path, starting at 80% opacity and growing, rising and fading to nothing by 2.5s. Leave the video untouched before 1.0s and after 2.5s. Don't use ffmpeg's drawtext; keep the size, 30fps, length and audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as muzzle-flash.py and run:
python3 muzzle-flash.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Muzzle flash: a three-frame orange burst shoots in from the left edge, lights up the scene, then leaves a puff of gray smoke.
Usage: python3 muzzle-flash.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import math
import random
import subprocess
import sys
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
AT = 1.0 # second the shot goes off
X, Y = 0.0, 0.62 # muzzle position (0-1 across and down the frame); the flash points away from it
ANGLE = -10 # direction the flash points, in degrees (0 = right, negative = upward)
LENGTH = 0.75 # flash length as a share of the frame width
FLASH = [1.0, 0.85, 0.5] # flash strength on each frame from AT (three frames = 0.1s)
FIRE = (255, 135, 25) # outer flame color
CORE = (255, 240, 175) # white-hot core color
SMOKE = 1.5 # seconds the smoke takes to drift up and clear
SMOKE_COLOR = (110, 106, 102) # smoke gray (darker reads better on bright footage)
def along(d, side):
"""A point d frame-widths along the flash and side frame-widths across it, in pixels."""
a = math.radians(ANGLE)
return (mx + w * (d * math.cos(a) - side * math.sin(a)), my + w * (d * math.sin(a) + side * math.cos(a)))
def flash_masks(k):
"""Soft masks (0-1) for the whole flame and its core on the k-th frame of the shot."""
rng = random.Random(k) # a new spike pattern each frame, so the burst flickers
size = LENGTH * (1 - 0.15 * k)
flame, core = Image.new("L", (w, h)), Image.new("L", (w, h))
fd, cd = ImageDraw.Draw(flame), ImageDraw.Draw(core)
fd.polygon([along(0, 0.06), along(size * 0.3, 0.11), along(size, 0), along(size * 0.3, -0.11), along(0, -0.06)], fill=255)
for spread in (30, -30, 55, -55, 80, -80):
a = math.radians(spread + rng.uniform(-6, 6))
reach = size * rng.uniform(0.4, 0.6) * (1 - abs(spread) / 160)
fd.polygon([along(-0.05 * math.sin(a), 0.05 * math.cos(a)), along(reach * math.cos(a), reach * math.sin(a)),
along(0.05 * math.sin(a), -0.05 * math.cos(a))], fill=255)
cd.polygon([along(0, 0.04), along(size * 0.2, 0.06), along(size * 0.6, 0), along(size * 0.2, -0.06), along(0, -0.04)], fill=255)
blur = ImageFilter.GaussianBlur(w * 0.01)
return (np.asarray(flame.filter(blur), np.float32) / 255)[..., None], (np.asarray(core.filter(blur), np.float32) / 255)[..., None]
def smoke_mask(age):
"""Gray puffs that grow, rise and thin out as `age` goes from 0 to 1."""
small = Image.new("L", (w // 4, h // 4))
draw = ImageDraw.Draw(small)
rng = random.Random(7)
for i in range(8):
cx, cy = along(0.04 + 0.05 * i + 0.06 * age, rng.uniform(-0.04, 0.04))
cy -= h * 0.06 * age * (1 + i * 0.1)
r = w * (0.06 + 0.012 * i) * (1 + 1.2 * age)
draw.ellipse([(cx - r) / 4, (cy - r) / 4, (cx + r) / 4, (cy + r) / 4], fill=255)
soft = small.filter(ImageFilter.GaussianBlur(w * 0.01)).resize((w, h), Image.BILINEAR)
return (np.asarray(soft, np.float32) / 255)[..., None] * 0.8 * (1 - age) ** 1.2
def screen(base, light):
return 255 - (255 - base) * (255 - light) / 255
def apply(img, t):
"""The frame at time t with the flash, its light on the scene and the smoke."""
if t < AT or t >= AT + SMOKE:
return img
out = img.astype(np.float32)
smoke = smoke_mask((t - AT) / SMOKE)
out = out * (1 - smoke) + np.array(SMOKE_COLOR, np.float32) * smoke
k = int((t - AT) * fps + 0.01)
if k < len(FLASH):
strength = FLASH[k]
glow = np.exp(-((xx - mx) ** 2 + (yy - my) ** 2) / (w * 0.6) ** 2)[..., None] * 0.8 * strength
out = screen(out, np.array(FIRE, np.float32) * glow)
flame, core = flash_masks(k)
light = np.array(FIRE, np.float32) * flame + (np.array(CORE, np.float32) - FIRE) * core
out = out * (1 - flame * strength) + light * strength
return out.clip(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,r_frame_rate:format=duration", "-of", "json", path]))
s = info["streams"][0]
num, den = s["r_frame_rate"].split("/")
return s["width"], s["height"], float(num) / float(den)
src, out = sys.argv[1:3]
w, h, fps = probe(src)
mx, my = X * w, Y * h
yy, xx = np.mgrid[0:h, 0:w].astype(np.float32)
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", str(fps),
"-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):
writer.stdin.write(apply(np.frombuffer(raw, np.uint8).reshape(h, w, 3), i / fps).tobytes())
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





