How to add a muzzle flash with Claude

At 1 second a spiky orange-and-yellow flame bursts in from the left edge for three frames, then a puff of dark gray smoke rises and clears.

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 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.

SettingDefaultWhat it does
AT1.0Second the shot goes off.
X, Y0.0, 0.62Where the muzzle is, as shares of the frame width and height.
ANGLE-10Direction the flash points, in degrees (0 = right, negative = upward).
LENGTH0.75Flash 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.
SMOKE1.5Seconds 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.mp4
Show 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")

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