How to add shoppable price tags with Claude

White dots pop onto the serum bottle and its acrylic stand, and a line runs from each to a dark card with the item's name and a yellow price.

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 shoppable price tags to input.mp4
    Tags: Glow Serum $34 at 0.8s, Acrylic Riser $12 at 1.6s

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 shoppable price tags 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
TAGSGlow Serum $34 at 0.8s, Acrylic Riser $12 at 1.6sEach tag's name, price, start second, dot position and card position.
CARD(24, 20, 34, 235)Card, rim and line color.
PRICE_COLOR(255, 214, 0)Price text color.
PULSE1.2Seconds between pulses of the ring round each dot.

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 two shoppable price tags to input.mp4. Each tag is a white dot 1.6% of the frame width in radius with a dark (24, 20, 34) rim, a 0.6%-wide dark (24, 20, 34) line from the dot to the center of a card, and a rounded card filled with (24, 20, 34) at 92% opacity (corner radius 2.4% of the width, padding 2.8%) showing the item name in white bold at 4% of the width over the price in yellow (255, 214, 0) bold at 6% of the width. Tag 1: "Glow Serum" / "$34", dot at 52% across and 58% down (on the bottle's label), card centered at 22% across and 40% down, starting at 0.8 seconds. Tag 2: "Acrylic Riser" / "$12", dot at 78% across and 85% down (on the acrylic block), card centered at 76% across and 69% down, starting at 1.6 seconds. For each tag the dot pops in over 0.3 seconds with a slight overshoot, the line then draws out to the card over 0.2 seconds, and the card then fades in while growing from 80% to full size over 0.25 seconds; once in, a thin white ring pulses out from the dot every 1.2 seconds, growing and fading. Both tags stay to the end. Use Python with Pillow to draw the tags and their text and ffmpeg to put them on the video (don't rely on 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 shoppable-price-tags.py and run:

python3 shoppable-price-tags.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Shoppable price tags: pulsing white dots pop onto items in the shot, each linked to a dark card with its name and price.

Usage: python3 shoppable-price-tags.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

# name, price, second it appears, dot center, card center (0-1 across and down the frame)
TAGS = [("Glow Serum", "$34", 0.8, (0.52, 0.58), (0.22, 0.40)),
        ("Acrylic Riser", "$12", 1.6, (0.78, 0.85), (0.76, 0.69))]
CARD = (24, 20, 34, 235)       # card fill (RGBA)
PRICE_COLOR = (255, 214, 0)    # price text color
POP = 0.3                      # seconds the dot takes to pop in
LINK = 0.2                     # seconds the line takes to reach the card, once the dot is in
FADE = 0.25                    # seconds the card takes to grow in, once the line is there
PULSE = 1.2                    # seconds between pulses of the ring round each dot
SS = 4                         # supersampling, for smooth circles and corners
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 card(name, price):
    """The name over the price on a rounded dark card, drawn large and shrunk so the edges are smooth."""
    name_font = ImageFont.truetype(FONT_FILE, round(w * 0.040) * SS)
    price_font = ImageFont.truetype(FONT_FILE, round(w * 0.060) * SS)
    pad, gap = round(w * 0.028) * SS, round(w * 0.010) * SS
    nw, nh = name_font.getbbox(name, anchor="lt")[2:]
    pw, ph = price_font.getbbox(price, anchor="lt")[2:]
    cw, ch = max(nw, pw) + 2 * pad, nh + gap + ph + 2 * pad
    img = Image.new("RGBA", (cw, ch))
    draw = ImageDraw.Draw(img)
    draw.rounded_rectangle([0, 0, cw - 1, ch - 1], radius=round(w * 0.024) * SS, fill=CARD)
    draw.text((pad, pad), name, font=name_font, anchor="lt", fill="white")
    draw.text((pad, pad + nh + gap), price, font=price_font, anchor="lt", fill=PRICE_COLOR)
    return img.resize((cw // SS, ch // SS), Image.LANCZOS)


def dot(scale, pulse):
    """A white dot with a dark rim, and a ring that grows and fades as pulse goes 0 -> 1."""
    r = w * 0.016 * scale
    size = round(w * 0.12)
    img = Image.new("RGBA", (size * SS, size * SS))
    draw = ImageDraw.Draw(img)
    c = size * SS / 2

    def circle(radius, **style):
        draw.ellipse([c - radius * SS, c - radius * SS, c + radius * SS, c + radius * SS], **style)

    if scale >= 1:
        circle(r * (1.3 + 2.2 * pulse), outline=(255, 255, 255, round(220 * (1 - pulse))), width=round(w * 0.005) * SS)
    circle(r * 1.35, fill=CARD)
    circle(r, fill="white")
    return img.resize((size, size), Image.LANCZOS)


def overlay(t):
    img = Image.new("RGBA", (w, h))
    draw = ImageDraw.Draw(img)
    for (name, price, start, (dx, dy), (cx, cy)), sprite in zip(TAGS, cards):
        if t < start:
            continue
        dot_xy, card_xy = (w * dx, h * dy), (w * cx, h * cy)
        link = clamp((t - start - POP) / LINK)
        if link > 0:
            end = (dot_xy[0] + (card_xy[0] - dot_xy[0]) * link, dot_xy[1] + (card_xy[1] - dot_xy[1]) * link)
            draw.line([dot_xy, end], fill=CARD, width=round(w * 0.006))
        grow = clamp((t - start - POP - LINK) / FADE)
        if grow > 0:
            s = 0.8 + 0.2 * grow
            c = sprite.resize((round(sprite.width * s), round(sprite.height * s)), Image.LANCZOS)
            c.putalpha(c.getchannel("A").point(lambda a: round(a * grow)))
            img.alpha_composite(c, (round(card_xy[0] - c.width / 2), round(card_xy[1] - c.height / 2)))
        pop = ease_out_back(clamp((t - start) / POP))
        if pop > 0.05:
            d = dot(pop, ((t - start - POP) / PULSE) % 1 if t >= start + POP else 0)
            img.alpha_composite(d, (round(dot_xy[0] - d.width / 2), round(dot_xy[1] - d.height / 2)))
    return img


def clamp(p):
    return min(max(p, 0), 1)


def ease_out_back(p):
    """0 -> 1 with a small overshoot past 1 before settling, which reads as a 'pop'."""
    return 1 + 2.70158 * (p - 1) ** 3 + 1.70158 * (p - 1) ** 2


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)
cards = [card(name, price) for name, price, *_ in TAGS]
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