---
title: "How to add a text on beat with Claude (tested prompt and script)"
description: "The words \"Brighter skin in just 7 days\" slam onto the screen above the bottle one at a time, each landing on a drum beat and swapping between yellow and white. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/text-on-beat
---

# How to add a text on beat with Claude

The words "Brighter skin in just 7 days" slam onto the screen above the bottle one at a time, each landing on a drum beat and swapping between yellow and white.

Made by the script on this page, from our own test clip.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   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 text on beat to input.mp4
   Words: BRIGHTER, SKIN, IN, JUST, 7, DAYS
   ```

## 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 text on beat 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                                                                 |
| -------------- | ----------------------------------- | ---------------------------------------------------------------------------- |
| `WORDS`        | `BRIGHTER, SKIN, IN, JUST, 7, DAYS` | One word per beat; the last one stays to the end.                            |
| `START`        | `1.0`                               | The first beat at or after this second gets the first word.                  |
| `BPM_GUESS`    | `120`                               | Rough tempo of the music, so the beat tracker doesn't skip every other beat. |
| `SLAM, SETTLE` | `1.3, 0.15`                         | How much bigger a word lands, and the seconds it takes to settle.            |
| `COLORS`       | `(255, 214, 0), (255, 255, 255)`    | Colors the words alternate between.                                          |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Add text on beat to input.mp4 (it has a 120 BPM drum loop). Find the beats with librosa: onset strength with hop_length 256, then librosa.beat.beat_track with start_bpm 120 and tightness 400, so it finds a beat every 0.5 seconds. From the first beat at or after 1.0 seconds, show one word per beat, each replacing the last: BRIGHTER, SKIN, IN, JUST, 7, DAYS, and keep DAYS on screen to the end. Nothing shows before that first beat. Draw each word in a heavy bold font with a font size 20% of the frame width (shrink a word to 75% of the frame width if it's wider), with a dark purple (40, 10, 60) outline 6% of the font size thick, centered 50% across and 12% down the frame. Alternate the colors: the 1st, 3rd and 5th words yellow (255, 214, 0), the others white (255, 255, 255). Each word lands 1.3 times its size on the beat and shrinks to its normal size over 0.15 seconds, easing out (scale = 1 + 0.3 x (1 - progress)^3). Use Python with librosa for the beats 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 `text-on-beat.py` and run:

```
python3 text-on-beat.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Text on beat: the words of a slogan slam onto the screen one at a time, each landing on a beat of the music.

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

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

WORDS = ["BRIGHTER", "SKIN", "IN", "JUST", "7", "DAYS"]  # one word per beat; the last one stays to the end
START = 1.0                 # first beat at or after this second gets the first word
BPM_GUESS = 120             # rough tempo, so the beat tracker doesn't lock onto every other beat
X, Y = 0.5, 0.12            # word center (0-1 across and down the frame)
SIZE = 0.2                  # font size as a share of the frame width
MAX_WIDTH = 0.75            # long words shrink to fit this share of the frame width
SLAM = 1.3                  # a word lands this much bigger and settles to its size...
SETTLE = 0.15               # ...over this many seconds
COLORS = [(255, 214, 0), (255, 255, 255)]  # words alternate yellow and white
OUTLINE = (40, 10, 60)      # outline color
RATE = 22050                # audio sample rate the beat tracker 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 beat_times(src):
    """Seconds of each beat in the soundtrack, from librosa's beat tracker."""
    pcm = subprocess.check_output(["ffmpeg", "-v", "error", "-i", src, "-ac", "1", "-ar", str(RATE), "-f", "f32le", "-"])
    audio = np.frombuffer(pcm, np.float32)
    strength = librosa.onset.onset_strength(y=audio, sr=RATE, hop_length=256)
    _, beats = librosa.beat.beat_track(onset_envelope=strength, sr=RATE, hop_length=256, units="time",
                                       start_bpm=BPM_GUESS, tightness=400)
    return [b for b in beats if b >= START - 0.05][:len(WORDS)]

def word_image(word, color):
    """One word with a dark outline, drawn large so scaling it stays sharp."""
    font = ImageFont.truetype(FONT_FILE, round(w * SIZE))
    stroke = round(w * SIZE * 0.06)
    x0, y0, x1, y1 = font.getbbox(word, stroke_width=stroke)
    img = Image.new("RGBA", (x1 - x0, y1 - y0))
    ImageDraw.Draw(img).text((-x0, -y0), word, font=font, fill=color, stroke_width=stroke, stroke_fill=OUTLINE)
    if img.width > w * MAX_WIDTH:
        img = img.resize((round(w * MAX_WIDTH), round(img.height * w * MAX_WIDTH / img.width)), Image.LANCZOS)
    return img

def overlay(t):
    img = Image.new("RGBA", (w, h))
    landed = [b for b in beats if b <= t]
    if landed:
        word = words[len(landed) - 1]
        p = min((t - landed[-1]) / SETTLE, 1)
        scale = 1 + (SLAM - 1) * (1 - p) ** 3   # eases out: big on the beat, settled by SETTLE
        s = word.resize((round(word.width * scale), round(word.height * scale)), Image.LANCZOS)
        img.alpha_composite(s, (round(w * X - s.width / 2), round(h * Y - s.height / 2)))
    return img

def render(src, out):
    """Draw overlay(t) 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(math.ceil(duration * fps)):
        ff.stdin.write(overlay(i / fps).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")

src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
beats = beat_times(src)
words = [word_image(word, COLORS[i % len(COLORS)]) for i, word in enumerate(WORDS)]
render(src, out)
```

## More music-synced visuals

- [AI voice orb **AI voice orb**](https://www.recharm.com/effects/ai-voice-orb)
- [Audio-reactive blob **Audio-reactive blob**](https://www.recharm.com/effects/audio-reactive-blob)
- [Audio-reactive particles **Audio-reactive particles**](https://www.recharm.com/effects/audio-reactive-particles)
- [Audio-reactive text **Audio-reactive text**](https://www.recharm.com/effects/audio-reactive-text)
- [Audio waveform **Audio waveform**](https://www.recharm.com/effects/audio-waveform)

## 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](https://www.recharm.com/get-started)
