How to add a music player overlay with Claude

A dark now-playing card slides up with cover art, "Golden Hour" by Recharm Beats, a pink progress bar creeping across and level bars bouncing on every beat.

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 music player overlay to input.mp4
    Title, artist: Golden Hour, Recharm Beats

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 music player overlay 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
TITLE, ARTISTGolden Hour, Recharm BeatsThe track name and artist on the card.
START0.4Second the card starts sliding up.
Y0.82Card center, as a share of the frame height.
ACCENT(255, 64, 129)Color of the progress fill and the level bars.
COVER_AT1.0Second of the video used as the cover art.
DECAY0.80How much of a beat's bounce the level bars keep each frame.

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 now-playing music card to input.mp4 (540x960), with beat times in beats.json ("beats": list of seconds). The card is 475x203 pixels, color (18, 18, 22) at 88% opacity with 22-pixel rounded corners, centered across with its center 82% down the frame. It slides up from 240 pixels lower and fades in, starting at 0.4 seconds and taking 0.45 seconds with an ease-out, then stays. Inside, 19 pixels from the card's top-left: a 108-pixel square of cover art with 11-pixel rounded corners, cut as the center 324x324 square of the video's frame at 1.0 second. Right of it, starting 146 pixels into the card: the title "Golden Hour" in bold white 27-pixel text (baseline 60 pixels from the card top) and "Recharm Beats" in regular 20-pixel gray (170, 170, 178) text (baseline 95 pixels down). Below the cover, 149 pixels from the card top: a 6-pixel-tall rounded progress bar from 19 to 456 pixels across the card, gray (70, 70, 78) track and pink (255, 64, 129) fill reaching (current time / video length) of the way across, with a 12-pixel white knob at the end of the fill. Under the bar, elapsed time ("0:02") at the left and the video length ("0:06") at the right in 16-pixel gray. In the card's top-right corner, 19 pixels in: four pink (255, 64, 129) level bars 6 pixels wide and 4 pixels apart, bottom-aligned, up to 32 pixels tall; on each beat time they jump to (0.2 + 0.8 x weight) of that height, weights 0.7, 1.0, 0.5, 0.85, and between beats they fall back to 20% by keeping 80% of the bounce each frame. Draw it with Python and Pillow and put it on the video with ffmpeg (don't use ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as music-player-overlay.py and run:

python3 music-player-overlay.py input.mp4 beats.json output.mp4
Show the script
#!/usr/bin/env python3
"""Music player overlay: a dark now-playing card slides up with cover art, the track title and a pink progress bar.

Usage: python3 music-player-overlay.py input.mp4 beats.json output.mp4
beats.json holds the track's beat times in seconds, for example from librosa:
  {"beats": [0.0, 0.5, 1.0, ...]}
The progress bar runs from the start to the end of the video. Needs ffmpeg and Pillow (pip install pillow).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

TITLE, ARTIST = "Golden Hour", "Recharm Beats"
START = 0.4                  # second the card starts sliding up
SLIDE = 0.45                 # seconds the slide takes
WIDTH = 0.88                 # card width as a share of the frame width
Y = 0.82                     # card center (0 = top, 1 = bottom), centered across
CARD = (18, 18, 22, 225)     # card color and opacity
ACCENT = (255, 64, 129)      # progress fill and the bouncing level bars
COVER_AT = 1.0               # second of the video used as the cover art
COVER_CROP = 0.6             # cover side as a share of the frame width, cut from the frame center
DECAY = 0.80                 # how much of a beat's bounce carries over to the next frame
BOLD = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf",     # macOS
        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
        "C:/Windows/Fonts/arialbd.ttf"]                          # Windows
REGULAR = ["/System/Library/Fonts/Supplemental/Arial.ttf",
           "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
           "C:/Windows/Fonts/arial.ttf"]
BOLD_FILE = next((f for f in BOLD if Path(f).exists()), None) or sys.exit("No bold font found: add one to BOLD")
REGULAR_FILE = next((f for f in REGULAR if Path(f).exists()), None) or sys.exit("No font found: add one to REGULAR")


def card_base():
    """The parts of the card that don't change: background, rounded cover art, title and artist."""
    cw, pad, cover = round(w * WIDTH), round(w * 0.035), round(w * 0.2)
    ch = pad + cover + round(w * 0.105) + pad
    img = Image.new("RGBA", (cw, ch))
    draw = ImageDraw.Draw(img)
    draw.rounded_rectangle([0, 0, cw - 1, ch - 1], radius=round(w * 0.04), fill=CARD)
    raw = subprocess.check_output(["ffmpeg", "-v", "error", "-ss", str(COVER_AT), "-i", src, "-frames:v", "1",
                                   "-f", "rawvideo", "-pix_fmt", "rgb24", "-"])
    frame = Image.frombytes("RGB", (w, h), raw)
    side = round(w * COVER_CROP)
    art = frame.crop(((w - side) // 2, (h - side) // 2, (w + side) // 2, (h + side) // 2)).resize((cover, cover), Image.LANCZOS)
    mask = Image.new("L", (cover, cover))
    ImageDraw.Draw(mask).rounded_rectangle([0, 0, cover - 1, cover - 1], radius=round(cover * 0.1), fill=255)
    img.paste(art, (pad, pad), mask)
    text_x = pad * 2 + cover
    draw.text((text_x, pad + cover * 0.38), TITLE, font=ImageFont.truetype(BOLD_FILE, round(w * 0.05)),
              anchor="ls", fill="white")
    draw.text((text_x, pad + cover * 0.70), ARTIST, font=ImageFont.truetype(REGULAR_FILE, round(w * 0.037)),
              anchor="ls", fill=(170, 170, 178))
    return img


def card_at(t, level):
    """The card at time t: progress bar, elapsed and total time, and level bars bouncing by `level` (0-1)."""
    img = base.copy()
    draw = ImageDraw.Draw(img)
    cw, pad, cover = img.width, round(w * 0.035), round(w * 0.2)
    bar_y, bar_h = pad + cover + round(w * 0.04), max(4, round(w * 0.011))
    draw.rounded_rectangle([pad, bar_y, cw - pad, bar_y + bar_h], radius=bar_h // 2, fill=(70, 70, 78))
    fill_x = pad + (cw - 2 * pad) * min(t / duration, 1)
    draw.rounded_rectangle([pad, bar_y, max(fill_x, pad + bar_h), bar_y + bar_h], radius=bar_h // 2, fill=ACCENT)
    draw.ellipse([fill_x - bar_h, bar_y - bar_h / 2, fill_x + bar_h, bar_y + bar_h * 1.5], fill="white")
    small = ImageFont.truetype(REGULAR_FILE, round(w * 0.03))
    draw.text((pad, bar_y + bar_h * 2.5), clock(t), font=small, anchor="lt", fill=(170, 170, 178))
    draw.text((cw - pad, bar_y + bar_h * 2.5), clock(duration), font=small, anchor="rt", fill=(170, 170, 178))
    bar_w, gap, tall = round(w * 0.012), round(w * 0.008), round(w * 0.06)
    for i, weight in enumerate((0.7, 1.0, 0.5, 0.85)):
        x = cw - pad - (4 - i) * (bar_w + gap)
        height = tall * (0.2 + 0.8 * level * weight)
        draw.rectangle([x, pad + tall - height, x + bar_w, pad + tall], fill=ACCENT)
    return img


def clock(seconds):
    s = int(seconds)
    return f"{s // 60}:{s % 60:02d}"


def beat_levels(beats, frames):
    """Bounce (0-1) at each frame: 1 on a beat, then falling back by DECAY each frame."""
    level = [0.0] * frames
    for b in beats:
        if 0 <= round(b * fps) < frames:
            level[round(b * fps)] = 1.0
    for i in range(1, frames):
        level[i] = max(level[i], level[i - 1] * DECAY)
    return level


def overlay(t, level):
    img = Image.new("RGBA", (w, h))
    if t < START:
        return img
    p = min((t - START) / SLIDE, 1)
    ease = 1 - (1 - p) ** 3
    card = card_at(t, level)
    if p < 1:
        card.putalpha(card.getchannel("A").point(lambda a: round(a * ease)))
    y = round(h * Y - card.height / 2 + (1 - ease) * h * 0.25)
    img.paste(card, ((w - card.width) // 2, y))  # paste clips the part still below the frame
    return img


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 render(src, out):
    """Draw overlay(t) on every frame of src and save to out, keeping the audio."""
    frames = math.ceil(duration * fps)
    levels = beat_levels(json.loads(Path(beats_file).read_text())["beats"], 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", "[0:v][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(i / fps, levels[i]).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")


src, beats_file, out = sys.argv[1:4]
w, h, rate, fps, duration = probe(src)
base = card_base()
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