How to add a product drop-in with Claude

The acrylic block stands empty until the serum bottle falls in from above the frame, squashes flat as it hits, hops up once and settles before the camera starts to orbit.

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 a product drop-in to input.mp4
    Second the product starts falling; the set is empty before this: 0.4
    Seconds the fall takes: 0.5

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 product drop-in 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
START0.4Second the product starts falling; the set is empty before this.
DROP0.5Seconds the fall takes.
SQUASH0.12Seconds of the landing squash.
FLATTEN0.18How much shorter the product gets at the deepest squash (0.18 = 18%).
BOUNCE0.05Height of the bounce, as a share of the frame height.
HOP0.28Seconds the bounce takes.
SETTLE0.10Seconds of the second, smaller squash.
SHADOW0.45Darkness of the contact shadow under the product (0-1).

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 the serum bottle in input.mp4 (540x960, 30fps, 6 seconds) drop into the shot. Take the first frame, cut the bottle out with rembg (u2net model) and paint it out of that frame with OpenCV's inpaint (dilate the mask by 12 pixels first) to get an empty set: the clear acrylic block with nothing on it. From 0 to 0.4 seconds show only the empty set. From 0.4 to 0.9 seconds the cut-out bottle falls straight down from fully above the top of the frame to its original place, accelerating like gravity (distance grows with time squared), stretched 8% taller and 4% narrower by the end of the fall. From 0.9 to 1.02 seconds it squashes, anchored at its base and centered horizontally where it stood: height down by up to 18% and width up by 0.65 times that, peaking half-way. From 1.02 to 1.3 seconds it hops straight up in a parabola 5% of the frame height (48 pixels) high and lands again, then from 1.3 to 1.4 seconds does a smaller squash (7.2% shorter at the peak). Under the bottle draw a soft dark purple (40, 30, 60) contact shadow on the block: an ellipse 1.1 times the bottle's width and 26 pixels tall at its base, blurred with a 9-pixel Gaussian blur, 45% opaque when the bottle is down and fading to nothing as it rises 30% of the frame height. From 1.4 seconds play input.mp4 from its first frame, so the camera starts to orbit from where the bottle landed, and cut the result at 6 seconds. Use Python with Pillow, numpy, OpenCV and rembg for the frames and ffmpeg to read and write the video (don't use ffmpeg's drawtext filter), keep the original audio, 30fps and size, and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as product-drop-in.py and run:

python3 product-drop-in.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Product drop-in: the product falls into an empty set, squashes as it lands, bounces once and settles.

Usage: python3 product-drop-in.py input.mp4 output.mp4
Needs ffmpeg, Pillow, numpy, OpenCV and rembg: rembg cuts the product out of the first frame and OpenCV
paints the empty set behind it (pip install pillow numpy opencv-python-headless "rembg[cpu]";
rembg downloads its u2net model on first use).
"""
import json
import math
import subprocess
import sys

import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
from rembg import new_session

START = 0.4       # second the product starts falling (the set is empty before this)
DROP = 0.5        # seconds the fall takes, speeding up like it's dropped
SQUASH = 0.12     # seconds of the landing squash
FLATTEN = 0.18    # how much shorter the product gets at the deepest squash (0.18 = 18%)
BOUNCE = 0.05     # height of the bounce, as a share of the frame height
HOP = 0.28        # seconds the bounce takes
SETTLE = 0.10     # seconds of the second, smaller squash
SHADOW = 0.45     # darkness of the contact shadow under the product (0-1)
LAND = START + DROP
LIVE = LAND + SQUASH + HOP + SETTLE  # the video plays on from its first frame here


def cut_out(first):
    """The product cut out of the first frame, where it sits in the frame, and the set with it painted out."""
    alpha = np.asarray(new_session("u2net").predict(Image.fromarray(first))[0])
    ys, xs = np.nonzero(alpha > 20)
    box = (xs.min(), ys.min(), xs.max() + 1, ys.max() + 1)
    sprite = Image.fromarray(np.dstack([first, alpha])).crop(box)
    hole = cv2.dilate((alpha > 8).astype(np.uint8) * 255, np.ones((25, 25), np.uint8))
    plate = Image.fromarray(cv2.inpaint(first, hole, 9, cv2.INPAINT_TELEA))
    return sprite, box, plate


def squash(p, amount):
    """Width and height scale for a squash that peaks half-way through (p from 0 to 1)."""
    a = amount * math.sin(math.pi * p)
    return 1 + a * 0.65, 1 - a


def pose(t):
    """How far above its resting place the product's base is (pixels) and its width and height scale at t."""
    if t < LAND:  # falling from just above the frame, stretched a little by the speed
        p = (t - START) / DROP
        return box[3] * (1 - p * p), 1 - 0.04 * p, 1 + 0.08 * p
    if t < LAND + SQUASH:
        return (0, *squash((t - LAND) / SQUASH, FLATTEN))
    if t < LAND + SQUASH + HOP:
        p = (t - LAND - SQUASH) / HOP
        return h * BOUNCE * 4 * p * (1 - p), 1, 1
    return (0, *squash((t - LAND - SQUASH - HOP) / SETTLE, FLATTEN / 2.5))


def intro_frame(t):
    """The empty set, a contact shadow that darkens as the product nears, and the product at its pose."""
    img = plate.copy()
    if t < START:
        return img
    lift, sx, sy = pose(t)
    x0, y0, x1, y1 = box
    sw, sh = round(sprite.width * sx), round(sprite.height * sy)
    near = max(0.0, 1 - lift / (h * 0.3))
    shade = Image.new("L", (w, h))
    ImageDraw.Draw(shade).ellipse([(x0 + x1) / 2 - sw * 0.55, y1 - 22, (x0 + x1) / 2 + sw * 0.55, y1 + 4],
                                  fill=round(255 * SHADOW * near))
    img.paste((40, 30, 60), mask=shade.filter(ImageFilter.GaussianBlur(9)))
    b = sprite.resize((sw, sh), Image.LANCZOS)
    img.paste(b, (round((x0 + x1 - sw) / 2), round(y1 - lift - sh)), b)
    return img


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):
    """The drawn intro, then src from its first frame, cut to src's length and keeping its audio."""
    intro = round(LIVE * fps)
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error",
                           "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-t", f"{duration - intro / fps:.3f}", "-i", src, "-i", src,
                           "-filter_complex", "[0:v][1:v]concat=n=2:v=1,format=yuv420p[v]",
                           "-map", "[v]", "-map", "2:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy",
                           "-t", f"{duration:.3f}", out], stdin=subprocess.PIPE)
    for i in range(intro):
        ff.stdin.write(intro_frame(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)
first = np.frombuffer(subprocess.check_output(["ffmpeg", "-v", "error", "-i", src, "-frames:v", "1",
                                               "-f", "rawvideo", "-pix_fmt", "rgb24", "-"]), np.uint8).reshape(h, w, 3)
sprite, box, plate = cut_out(first)
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