How to add an audio-reactive text with Claude

A yellow "GLOW UP" headline above the bottle swells to half again its size on each drum hit, then shrinks smoothly back as the sound dies away.

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 an audio-reactive text to input.mp4
    Text: GLOW UP

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 an audio-reactive text 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
TEXTGLOW UPThe headline.
X, Y0.5, 0.12Where the text is centered (0-1 across and down the frame).
SIZE0.55Text width when the music is quiet, as a share of the frame width.
PUMP0.5Extra size at the loudest moment (0.5 = 1.5 times as big).
RELEASE0.8Share of the swell kept each frame as the sound fades, so the text eases back.
COLOR, OUTLINE(255, 214, 0), (40, 10, 60)Text and outline colors.

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 audio-reactive text to input.mp4 (it has a drum loop). Draw "GLOW UP" in a heavy bold font, yellow (255, 214, 0) with a dark purple (40, 10, 60) outline 6% of the font size thick, centered 50% across and 12% down the frame. Measure the loudness at each video frame: the RMS of the mono audio during that frame, divided by the loudest frame's RMS so it runs 0 to 1, and let it fall back by multiplying by 0.8 each frame instead of dropping at once (level = max(this frame's loudness, previous level x 0.8)). Scale the text about its center so its width is 55% of the frame width x (1 + 0.5 x level): about 55% wide when quiet and about 82% wide at the loudest hit, shrinking smoothly between hits. Use Python with numpy for the loudness and Pillow to draw the text (not ffmpeg's drawtext filter), ffmpeg to put it 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 audio-reactive-text.py and run:

python3 audio-reactive-text.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Audio-reactive text: a headline swells with the loudness of the music and shrinks back as it quietens.

Usage: python3 audio-reactive-text.py input.mp4 output.mp4  (the input needs an audio track)
Needs ffmpeg, numpy and Pillow (pip install numpy pillow).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

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

TEXT = "GLOW UP"            # the headline
X, Y = 0.5, 0.12            # text center (0-1 across and down the frame)
SIZE = 0.55                 # text width at silence, as a share of the frame width
PUMP = 0.5                  # extra size at the loudest moment (0.5 = 1.5 times as big)
RELEASE = 0.8               # share of the swell kept each frame as the sound fades, so it eases back
COLOR = (255, 214, 0)       # text color
OUTLINE = (40, 10, 60)      # outline color
RATE = 48000                # audio sample rate the analysis uses
FONTS = ["/System/Library/Fonts/Supplemental/Arial Black.ttf",    # macOS
         "/System/Library/Fonts/Supplemental/Arial Bold.ttf",     # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/arialbd.ttf"]                          # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")


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 loudness_per_frame(src, frames):
    """Loudness (0-1) at each video frame: the RMS of the audio during that frame over the loudest frame's,
    held back by RELEASE so the text shrinks smoothly instead of snapping."""
    pcm = subprocess.check_output(["ffmpeg", "-v", "error", "-i", src, "-ac", "1", "-ar", str(RATE), "-f", "f32le", "-"])
    audio = np.frombuffer(pcm, np.float32)
    step = round(RATE / fps)
    rms = np.array([np.sqrt(np.mean(audio[i * step:(i + 1) * step] ** 2)) if i * step < len(audio) else 0.0
                    for i in range(frames)])
    rms /= max(rms.max(), 1e-9)
    level, prev = [], 0.0
    for r in rms:
        prev = max(r, prev * RELEASE)
        level.append(prev)
    return level


def headline():
    """The text at silence size, drawn large so scaling it up stays sharp."""
    font = ImageFont.truetype(FONT_FILE, 200)
    stroke = 12
    x0, y0, x1, y1 = font.getbbox(TEXT, stroke_width=stroke)
    img = Image.new("RGBA", (x1 - x0, y1 - y0))
    ImageDraw.Draw(img).text((-x0, -y0), TEXT, font=font, fill=COLOR, stroke_width=stroke, stroke_fill=OUTLINE)
    return img


def overlay(i):
    img = Image.new("RGBA", (w, h))
    width = round(w * SIZE * (1 + PUMP * level[i]))
    t = text.resize((width, round(text.height * width / text.width)), Image.LANCZOS)
    img.alpha_composite(t, (round(w * X - t.width / 2), round(h * Y - t.height / 2)))
    return img


def render(src, out):
    """Draw overlay(i) on every frame of src and save to out, keeping the audio."""
    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).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")


src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
frames = math.ceil(duration * fps)
level = loudness_per_frame(src, frames)
text = headline()
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