How to add a carousel swipe with Claude

Four product photos on rounded cards swipe left one at a time over a blurred backdrop, with a row of page dots lighting up underneath like a phone photo carousel.

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

    Make a carousel swipe from clipA.mp4 and clipB.mp4
    Which clip and second each photo is taken from, in carousel order: [(1, 0.5), (1, 5.0), (2, 2.5), (2, 5.5)]
    Second each swipe to the next photo starts: [1.0, 2.3, 3.6]

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 carousel swipe 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
PHOTOS[(1, 0.5), (1, 5.0), (2, 2.5), (2, 5.5)]Which clip and second each photo is taken from, in carousel order.
SWIPES[1.0, 2.3, 3.6]Second each swipe to the next photo starts.
SWIPE0.6Seconds a swipe takes.
CARD_W, CARD_H0.80, 0.60Card size as a share of the frame width and height.
GAP0.045Space between cards, as a share of the frame width.
BLUR, DARKEN24, 0.40Background blur and how much darker the background gets.

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
Make a photo carousel from clipA.mp4 and clipB.mp4, keeping clipA's size (540x960), 30fps, 6 seconds and its audio. Background: clipA with a Gaussian blur (sigma 24) and every color channel multiplied by 0.6. Photos: four stills, taken from clipA at 0.5s and 5.0s and from clipB at 2.5s and 5.5s, each cropped to the card's box from the full frame (x 54 to 486, y 163 to 739, so 432x576, 80% of the width and 60% of the height, centered horizontally with its center 47% down). Draw each card with 27-pixel rounded corners and a soft black shadow (alpha 140, Gaussian blur 14, 8 pixels lower). Cards sit in a row 24 pixels apart, so the neighbors peek in at the edges. Start on the first photo, then swipe the row left by one card (456 pixels) at 1.0s, 2.3s and 3.6s, each swipe taking 0.6 seconds with a smoothstep ease in and out, ending on the fourth photo. Below the card, centered at y 777, draw four page dots of radius 7.5 pixels, 27 pixels apart center to center, centered horizontally: the dot of the photo on screen solid white, the others white at 40% opacity, cross-fading as the cards swipe. Draw the cards and dots with Python and Pillow (not ffmpeg's drawtext) and composite them over the background with ffmpeg. Save it as output.mp4 (H.264, yuv420p).

Run it without Claude

This is the exact script we tested. Save it as carousel-swipe.py and run:

python3 carousel-swipe.py clipA.mp4 clipB.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Carousel swipe: four product photos on rounded cards swipe left one at a time, like a phone photo carousel.

Usage: python3 carousel-swipe.py clipA.mp4 clipB.mp4 output.mp4
The photos are stills taken from the two clips; clipA also plays blurred and darkened behind the cards, with its sound.
Needs ffmpeg and Pillow (pip install pillow).
"""
import json
import math
import subprocess
import sys

from PIL import Image, ImageDraw, ImageFilter

PHOTOS = [(1, 0.5), (1, 5.0), (2, 2.5), (2, 5.5)]  # (clip 1 or 2, second) of each photo, in carousel order
SWIPES = [1.0, 2.3, 3.6]       # second each swipe to the next photo starts
SWIPE = 0.6                    # seconds a swipe takes
CARD_W, CARD_H = 0.80, 0.60    # card size as a share of the frame width and height
Y = 0.47                       # card center (0 = top, 1 = bottom)
GAP = 0.045                    # space between cards, as a share of the frame width
RADIUS = 0.05                  # corner rounding, as a share of the frame width
DOT = 0.014                    # page dot radius, as a share of the frame width
BLUR, DARKEN = 24, 0.40        # background blur (Gaussian sigma, pixels) and how much darker it gets (0-1)


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 still(path, t):
    """The frame of path at t seconds, fitted to the output size, cropped to the card's box and rounded off."""
    raw = subprocess.check_output(["ffmpeg", "-v", "error", "-ss", str(t), "-i", path, "-frames:v", "1",
                                   "-vf", f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h}",
                                   "-f", "rawvideo", "-pix_fmt", "rgb24", "-"])
    frame = Image.frombytes("RGB", (w, h), raw)
    card = frame.crop((card_x, card_y, card_x + card_w, card_y + card_h)).convert("RGBA")
    mask = Image.new("L", card.size)
    ImageDraw.Draw(mask).rounded_rectangle([0, 0, card_w - 1, card_h - 1], round(w * RADIUS), fill=255)
    card.putalpha(mask)
    return card


def shadow():
    """A soft dark shape the size of a card, drawn under each one."""
    pad = 30
    img = Image.new("RGBA", (card_w + 2 * pad, card_h + 2 * pad))
    ImageDraw.Draw(img).rounded_rectangle([pad, pad + 8, pad + card_w, pad + card_h + 8], round(w * RADIUS),
                                          fill=(0, 0, 0, 140))
    return img.filter(ImageFilter.GaussianBlur(14)), pad


def position(t):
    """How many photos along the carousel is at t: 0 on the first photo, 1.5 halfway from the second to the third."""
    return sum(ease((t - start) / SWIPE) for start in SWIPES)


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


def overlay(t):
    img = Image.new("RGBA", (w, h))
    pos = position(t)
    step = card_w + round(w * GAP)
    for i, card in enumerate(cards):
        x = card_x + round((i - pos) * step)
        if -card_w < x < w:
            img.alpha_composite(drop, (x - pad, card_y - pad))
            img.alpha_composite(card, (x, card_y))
    draw = ImageDraw.Draw(img)
    r, spacing = w * DOT, w * DOT * 3.6
    first = w / 2 - spacing * (len(cards) - 1) / 2
    dot_y = card_y + card_h + w * 0.07
    for i in range(len(cards)):
        lit = max(0.0, 1 - abs(i - pos))  # the photo on screen gets the solid white dot
        x = first + i * spacing
        draw.ellipse([x - r, dot_y - r, x + r, dot_y + r], fill=(255, 255, 255, round(255 * (0.4 + 0.6 * lit))))
    return img


def render(src, out):
    """Blur and darken src, draw overlay(t) on every frame 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", f"[0:v]gblur=sigma={BLUR},colorchannelmixer=rr={1 - DARKEN}:gg={1 - DARKEN}:bb={1 - DARKEN}[bg];"
                                              "[bg][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")


clips, out = sys.argv[1:3], sys.argv[3]
w, h, rate, fps, duration = probe(clips[0])
card_w, card_h = round(w * CARD_W), round(h * CARD_H)
card_x, card_y = (w - card_w) // 2, round(h * Y - card_h / 2)
cards = [still(clips[n - 1], t) for n, t in PHOTOS]
drop, pad = shadow()
render(clips[0], 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