How to add numbered step labels with Claude

A dark pill with a yellow numbered circle slides in at the top left reading "Step 1 of 3: Apply 3 drops", then gives way to step 2 and step 3 as the demo moves on.

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 numbered step labels to input.mp4
    Steps: 0.5 Apply 3 drops, 2.3 Massage in, 4.1 Follow with SPF

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 numbered step labels 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
STEPS0.5 Apply 3 drops, 2.3 Massage in, 4.1 Follow with SPFEach step's start second and its text.
IN, OUT0.35, 0.25Seconds a label takes to slide in, and to fade out before the next one.
X, Y0.06, 0.12The label's left edge and vertical center, as shares of the frame.
HEIGHT0.178Label height as a share of the frame width.
YELLOW(255, 200, 40)Number badge and "STEP 1 OF 3" color.
CARD(20, 20, 30, 220)Label background color and opacity.

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 numbered step labels to input.mp4 (540x960), one at a time at the top left. Three steps: "Apply 3 drops" from 0.5 s, "Massage in" from 2.3 s and "Follow with SPF" from 4.1 s; each label shows until the next step starts and the last stays to the end. Nothing is on screen before 0.5 s. Each label is a pill 96 px tall with fully rounded ends, filled dark (20, 20, 30) at 86% opacity, its left edge 6% across the frame (32 px) and its center 12% down (115 px). Inside it, 16 px from the left, a yellow (255, 200, 40) circle 64 px across holds the step number in dark (20, 20, 30) bold text 38 px high. 16 px right of the circle, two lines: "STEP 1 OF 3" (with the right number) in yellow bold 18 px, centered 36% down the pill, and the step's text in white bold 30 px, centered 66% down the pill. The pill is as wide as its text plus 38 px. Each label slides 40 px in from the left while fading in over 0.35 s, eased out, and fades out over the 0.25 s before the next step starts. Draw the labels with Python and Pillow (not ffmpeg's drawtext filter), composite them with ffmpeg, keep the audio, and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as numbered-step-labels.py and run:

python3 numbered-step-labels.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Numbered step labels: a "Step 1 of 3" label with a yellow number badge slides in at the top as each stage begins.

Usage: python3 numbered-step-labels.py input.mp4 output.mp4
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

STEPS = [(0.5, "Apply 3 drops"),     # (second the step starts, what it says)
         (2.3, "Massage in"),
         (4.1, "Follow with SPF")]
IN, OUT = 0.35, 0.25     # seconds a label takes to slide in, and to fade out before the next one
SLIDE = 0.075            # how far a label slides in from the left, as a share of the frame width
X, Y = 0.06, 0.12        # the label's left edge and vertical center (0-1 across and down the frame)
HEIGHT = 0.178           # label height as a share of the frame width
YELLOW = (255, 200, 40)  # number badge and "STEP 1 OF 3" color
CARD = (20, 20, 30, 220) # label background (red, green, blue, opacity 0-255)
FONTS = ["/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 label(n, text):
    """The pill for step n: a yellow number badge, then "STEP n OF 3" over the step's text."""
    ph = round(w * HEIGHT)
    pad, d = round(ph * 0.17), round(ph * 0.67)
    big = ImageFont.truetype(FONT_FILE, round(ph * 0.31))
    small = ImageFont.truetype(FONT_FILE, round(ph * 0.19))
    num = ImageFont.truetype(FONT_FILE, round(d * 0.6))
    kicker = f"STEP {n} OF {len(STEPS)}"
    text_w = max(big.getlength(text), small.getlength(kicker))
    pw = round(pad + d + pad + text_w + ph * 0.4)
    img = Image.new("RGBA", (pw, ph))
    draw = ImageDraw.Draw(img)
    draw.rounded_rectangle([0, 0, pw - 1, ph - 1], radius=ph // 2, fill=CARD)
    draw.ellipse([pad, (ph - d) / 2, pad + d, (ph + d) / 2], fill=YELLOW)
    draw.text((pad + d / 2, ph / 2), str(n), font=num, anchor="mm", fill=CARD[:3])
    tx = pad + d + pad
    draw.text((tx, ph * 0.36), kicker, font=small, anchor="lm", fill=YELLOW)
    draw.text((tx, ph * 0.66), text, font=big, anchor="lm", fill="white")
    return img


def overlay(t):
    img = Image.new("RGBA", (w, h))
    for i, (start, _) in enumerate(STEPS):
        end = STEPS[i + 1][0] if i + 1 < len(STEPS) else math.inf
        if not start <= t < end:
            continue
        p = ease_out((t - start) / IN)
        fade = min(p, (end - t) / OUT, 1)
        pill = labels[i].copy()
        pill.putalpha(pill.getchannel("A").point(lambda a: round(a * fade)))
        x = round(w * X - w * SLIDE * (1 - p))
        img.alpha_composite(pill, (x, round(h * Y - pill.height / 2)))
    return img


def ease_out(p):
    p = min(max(p, 0), 1)
    return 1 - (1 - p) ** 3


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."""
    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)
labels = [label(i + 1, text) for i, (_, text) in enumerate(STEPS)]
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