How to add a pulsing cover art with Claude

A white-framed square cover of the product sits over a blurred, dimmed copy of the video and swells 15% on every kick drum before easing back.

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 pulsing cover art to input.mp4
    Cover width at rest as a share of the frame width: 0.60
    How much bigger the cover gets on a full-strength kick: 0.15

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 pulsing cover art 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
SIZE0.60Cover width at rest as a share of the frame width.
PULSE0.15How much bigger the cover gets on a full-strength kick.
RELEASE0.80Share of the swell kept each frame, so it eases back.
LOW_HZ, HIGH_HZ30, 150Bass band that drives the pulse.
COVER_AT1.0Second of the video used as the cover when no image is given.
BLUR, DIM, VIVID30, 0.55, 2.0Background blur radius, brightness and saturation.

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
Make a pulsing cover art video from input.mp4 (it has a drum loop). Background: the video itself with a 30 pixel box blur, saturation doubled and brightness multiplied by 0.55. Cover: a square crop from the center of the video's frame at 1.0 second, drawn 60% of the frame width across (white frame included) with a white (255, 255, 255) border 2.5% of that width and a soft dark drop shadow, centered on the frame. Drive its size from the bass: for each video frame take an FFT of the audio around it, sum the 30-150 Hz band, divide by the loudest frame's value, and let it fall back by multiplying by 0.8 each frame instead of dropping at once; scale the cover by 1 + 0.15 times that, about its center, so it is 15% bigger on each kick and back to its rest size between kicks. Use Python with Pillow and numpy to build the frames and ffmpeg to put them on the video, keep the audio, and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as pulsing-cover-art.py and run:

python3 pulsing-cover-art.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Pulsing cover art: a white-framed square cover swells on every kick drum and eases back, over a blurred copy of the video.

Usage: python3 pulsing-cover-art.py input.mp4 [cover.png] output.mp4  (the input needs an audio track;
without cover.png, a square from the video at COVER_AT seconds is the cover)
Needs ffmpeg, numpy and Pillow (pip install numpy pillow).
"""
import json
import math
import subprocess
import sys

import numpy as np
from PIL import Image, ImageDraw, ImageFilter

SIZE = 0.60                 # cover width at rest as a share of the frame width, white frame included
X, Y = 0.5, 0.5             # cover center (0-1 across and down the frame)
PULSE = 0.15                # how much bigger the cover gets on a full-strength kick (0.15 = 15%)
RELEASE = 0.80              # how much of the swell carries over to the next frame, so it eases back
LOW_HZ, HIGH_HZ = 30, 150   # the bass band that drives the pulse
BORDER = 0.025              # white frame width as a share of the cover width
COVER_AT = 1.0              # second of the video used as the cover when no image is given
BLUR = 30                   # background blur radius in pixels
DIM = 0.55                  # background brightness (1 = unchanged)
VIVID = 2.0                 # background saturation (1 = unchanged), so the blur keeps its color
RATE = 48000                # audio sample rate the analysis uses


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 bass_per_frame(src, frames):
    """Bass loudness (0-1) at each video frame: jumps up on a kick, then falls back by RELEASE each frame."""
    pcm = subprocess.check_output(["ffmpeg", "-v", "error", "-i", src, "-ac", "1", "-ar", str(RATE), "-f", "f32le", "-"])
    audio = np.frombuffer(pcm, np.float32)
    n = 2048
    window = np.hanning(n)
    freqs = np.fft.rfftfreq(n, 1 / RATE)
    band = (freqs >= LOW_HZ) & (freqs < HIGH_HZ)
    padded = np.pad(audio, (n // 2, n))
    level = np.array([np.abs(np.fft.rfft(padded[round(i / fps * RATE):][:n] * window))[band].sum()
                      for i in range(frames)])
    level /= max(level.max(), 1e-9)
    for i in range(1, frames):
        level[i] = max(level[i], level[i - 1] * RELEASE)
    return level


def cover_art(path):
    """The cover as a square RGBA image with a white frame and a soft drop shadow around it."""
    if path:
        img = Image.open(path).convert("RGB")
    else:
        raw = subprocess.check_output(["ffmpeg", "-v", "error", "-ss", str(COVER_AT), "-i", src, "-frames:v", "1",
                                       "-f", "rawvideo", "-pix_fmt", "rgb24", "-"])
        img = Image.frombytes("RGB", (w, h), raw)
    side = min(img.size)
    img = img.crop(((img.width - side) // 2, (img.height - side) // 2,
                    (img.width + side) // 2, (img.height + side) // 2))
    d = round(w * SIZE)
    b = round(d * BORDER)
    framed = Image.new("RGB", (d, d), "white")
    framed.paste(img.resize((d - 2 * b, d - 2 * b), Image.LANCZOS), (b, b))
    pad = d // 8
    shadow = Image.new("RGBA", (d + 2 * pad, d + 2 * pad))
    ImageDraw.Draw(shadow).rectangle([pad, pad + d // 40, pad + d, pad + d + d // 40], fill=(0, 0, 0, 160))
    card = shadow.filter(ImageFilter.GaussianBlur(d // 30))
    card.paste(framed, (pad, pad))
    return card


def overlay(level):
    img = Image.new("RGBA", (w, h))
    scale = 1 + PULSE * level
    c = card.resize((round(card.width * scale), round(card.height * scale)), Image.LANCZOS)
    img.alpha_composite(c, (round(w * X - c.width / 2), round(h * Y - c.height / 2)))
    return img


def render(src, out):
    """Blur and dim the video, draw the pulsing cover on every frame and save to out, keeping the audio."""
    frames = math.ceil(duration * fps)
    bass = bass_per_frame(src, frames)
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
                           "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-filter_complex", f"[0:v]boxblur={BLUR},eq=saturation={VIVID},colorchannelmixer=rr={DIM}:gg={DIM}:bb={DIM}[bg];"
                                              "[bg][1:v]overlay=shortest=1,format=yuv420p[v]",
                           "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
                          stdin=subprocess.PIPE)
    for i in range(frames):
        ff.stdin.write(overlay(bass[i]).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")


src, *cover_file, out = sys.argv[1:]
w, h, rate, fps, duration = probe(src)
card = cover_art(cover_file[0] if cover_file else None)
render(src, out)

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