How to add a background replacement with Claude

At one second the holographic backdrop and acrylic stand dissolve away, leaving the purple serum bottle standing on a coral studio floor in front of a glowing cream sun.

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 background replacement to input.mp4
    Wall: (250, 150, 105) to (255, 205, 160)
    Floor: (240, 175, 135) to (205, 130, 95)

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 background replacement 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
START1.0Second the new background starts to dissolve in.
FADE0.5Seconds the dissolve takes.
WALL(250, 150, 105) to (255, 205, 160)Wall gradient, top to floor line.
SUN(255, 238, 210)Color of the soft disc behind the product.
SUN_Y, SUN_R0.40, 0.42Disc center down the frame and radius as a share of the width.
FLOOR_Y0.78Where the floor starts, 0-1 down the frame; set it at the product's base.
FLOOR(240, 175, 135) to (205, 130, 95)Floor gradient, back to front.
SHADOW0.45Darkness of the contact shadow under the product.

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
Replace the background of input.mp4 (540x960, the camera circles a serum bottle whose base sits about 79% down the frame). Build the new background once with Python, Pillow and numpy: a wall that is a vertical gradient from (250, 150, 105) at the top to (255, 205, 160) at 78% down the frame, then a floor from 78% down to the bottom that goes from (240, 175, 135) to (205, 130, 95), plus a soft cream (255, 238, 210) disc on the wall centered horizontally and 40% down, with a radius of 42% of the frame width and edges blurred by 4% of the width (keep it off the floor). Leave the video untouched before 1.0 second. From 1.0 second, on every frame cut the bottle out with rembg (u2net model), dissolve the old background into the new one over 0.5 seconds with a smoothstep ease, add a soft dark contact shadow (45% darkness, a blurred ellipse as wide as 1.2 times the bottle) under the bottle's base that follows it, and put the cut-out bottle back on top unchanged. Use ffmpeg to read and write the frames (no text is drawn; if you add any, draw it with Pillow, not ffmpeg's drawtext), keep the size, 30fps and the audio, and save it as output.mp4.

Run it without Claude

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

python3 background-replacement.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Background replacement: the product stays put while everything behind it dissolves into a new warm studio scene.

Usage: python3 background-replacement.py input.mp4 output.mp4
Needs ffmpeg, Pillow, numpy and rembg to cut the product out of every frame
(pip install pillow numpy "rembg[cpu]"; rembg downloads its u2net model on first use).
"""
import json
import subprocess
import sys

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

START = 1.0                  # second the new background starts to dissolve in
FADE = 0.5                   # seconds the dissolve takes
WALL = ((250, 150, 105),     # wall color at the top...
        (255, 205, 160))     # ...and where it meets the floor
SUN = (255, 238, 210)        # soft disc on the wall behind the product
SUN_Y, SUN_R = 0.40, 0.42    # disc center (0-1 down the frame) and radius (share of the frame width)
FLOOR_Y = 0.78               # where the floor starts, 0-1 down the frame (at the product's base)
FLOOR = ((240, 175, 135),    # floor color at the back...
         (205, 130, 95))     # ...and at the front
SHADOW = 0.45                # darkness of the contact shadow under the product (0-1)


def gradient(top, bottom, rows):
    p = np.linspace(0, 1, rows)[:, None]
    return np.array(top) * (1 - p) + np.array(bottom) * p


def scene():
    """The new background: a wall gradient with a soft disc, over a floor that starts at FLOOR_Y."""
    split = round(h * FLOOR_Y)
    img = np.zeros((h, w, 3))
    img[:split] = gradient(*WALL, split)[:, None]
    img[split:] = gradient(*FLOOR, h - split)[:, None]
    disc = Image.new("L", (w, h))
    r = w * SUN_R
    ImageDraw.Draw(disc).ellipse([w / 2 - r, h * SUN_Y - r, w / 2 + r, h * SUN_Y + r], fill=255)
    disc = np.asarray(disc.filter(ImageFilter.GaussianBlur(w * 0.04)), np.float32)[:, :, None] / 255
    disc[split:] = 0
    return img * (1 - disc) + np.array(SUN) * disc


def shadow(subject):
    """A soft dark ellipse under the product's base, following it as the camera moves."""
    ys, xs = np.nonzero(subject[:, :, 0] > 0.5)
    layer = Image.new("L", (w, h))
    if len(xs):
        base, half = ys.max(), (xs.max() - xs.min()) * 0.6
        cx = xs[ys > base - h * 0.03].mean()
        ImageDraw.Draw(layer).ellipse([cx - half, base - h * 0.012, cx + half, base + h * 0.018], fill=255)
    return np.asarray(layer.filter(ImageFilter.GaussianBlur(w * 0.025)), np.float32)[:, :, None] / 255 * SHADOW


def composite(rgb, t):
    """The new scene, dissolved in over FADE, with the product cut out of the frame on top."""
    if t < START:
        return rgb
    subject = np.asarray(session.predict(Image.fromarray(rgb))[0], np.float32)[:, :, None] / 255
    p = min((t - START) / FADE, 1)
    p = p * p * (3 - 2 * p)
    back = rgb * (1 - p) + backdrop * (1 - shadow(subject)) * p
    return (back * (1 - subject) + rgb * subject).round().astype(np.uint8)


def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate", "-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)


def render(src, out):
    """Read src frame by frame, composite each one and save to out, keeping the audio."""
    reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                              stdout=subprocess.PIPE)
    writer = subprocess.Popen(["ffmpeg", "-y", "-v", "error",
                               "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                               "-i", src, "-map", "0:v", "-map", "1:a?", "-c:v", "libx264", "-crf", "18",
                               "-pix_fmt", "yuv420p", "-c:a", "copy", "-shortest", out], stdin=subprocess.PIPE)
    i = 0
    while raw := reader.stdout.read(w * h * 3):
        rgb = np.frombuffer(raw, np.uint8).reshape(h, w, 3)
        writer.stdin.write(composite(rgb, i / fps).tobytes())
        i += 1
    writer.stdin.close()
    if reader.wait() or writer.wait():
        sys.exit("ffmpeg failed")


src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
backdrop = scene()
session = new_session("u2net")
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