Get this effect
Switch to Code in your Claude Desktop App
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/skillsAsk for the effect
Add a disintegration to input.mp4 Second the shot freezes and the subject starts to crumble: 1.0 Seconds the crumbling takes to run from the top of the subject to the bottom: 1.4
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 disintegration 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.
| Setting | Default | What it does |
|---|---|---|
START | 1.0 | Second the shot freezes and the subject starts to crumble. |
SWEEP | 1.4 | Seconds the crumbling takes to run from the top of the subject to the bottom. |
LIFE | (1.0, 1.6) | Shortest and longest time a speck of dust lasts before it has faded out, in seconds. |
GRAIN | 2 | Size of a speck of dust in pixels. |
WIND | (260, -70) | Dust speed in pixels a second (right, down); each speck gets 0.4x to 1.6x of it. |
GUST | 220 | Extra push to the right in pixels a second, every second. |
ASH | (60, 52, 62) | Ash color the edge darkens to and the dust is tinted with. |
CHAR | 0.03 | Seconds the edge darkens to ash before it breaks off. |
SEED | 7 | Change for a different scatter. |
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, the camera circling it) disintegrate into dust. Play the clip untouched until 1.0 seconds, then freeze on the frame at 1.0 seconds and hold that frozen frame to the end. Cut the bottle out of the frozen frame with rembg (u2net model). Make an empty backdrop by painting the bottle out: dilate the mask (alpha above 0.1) by 7 pixels each way, inpaint at half size with OpenCV's Telea inpainting (radius 9), scale back up, blur it (Gaussian sigma 6) and blend it in through the hole blurred with sigma 4. Give every bottle pixel a break-off time of 1.0 + 1.4 x (0.7 x d + 0.3 x n) seconds, where d runs from 0 at the top of the bottle to 1 at its bottom and n is blotchy 0-1 noise (80% random noise blurred with sigma 9 plus 20% blurred with sigma 1.5, each stretched to 0-1), so it crumbles from the cap down in ragged clumps. Until its time a pixel shows the frozen bottle (darkening 70% toward ash (60, 52, 62) over its last 0.03 seconds); after it, the empty backdrop. Each 2x2 pixel cell of the bottle becomes a 2x2 speck of dust at its break-off time, colored 65% its own color and 35% ash, moving right at 260 and up at 70 pixels a second times a random 0.4 to 1.6 (plus random up/down drift with a 40 px/s spread), with an extra 220 px/s² push to the right and a 6 pixel side-to-side wobble, fading out (alpha (1 - age/life)^1.5) over a random 1.0 to 1.6 seconds, so all dust is gone by 4.1 seconds. There is no text, so nothing needs drawing with Pillow or ffmpeg's drawtext. Use Python with numpy, OpenCV and rembg, and ffmpeg to read and write the video; 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 disintegration.py and run:
python3 disintegration.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Disintegration: the shot freezes and the product crumbles from the top down into dust that blows away.
Usage: python3 disintegration.py input.mp4 output.mp4
Needs ffmpeg, numpy, Pillow, OpenCV and rembg to cut the product out of the frozen frame
(pip install numpy pillow opencv-python-headless "rembg[cpu]"; rembg downloads its u2net model on first use).
"""
import json
import subprocess
import sys
import cv2
import numpy as np
from PIL import Image
from rembg import new_session
START = 1.0 # second the shot freezes and the subject starts to crumble
SWEEP = 1.4 # seconds the crumbling takes to run from the top of the subject to the bottom
LIFE = (1.0, 1.6) # seconds each speck of dust lasts before it has faded out (shortest, longest)
GRAIN = 2 # size of a speck of dust in pixels
WIND = (260, -70) # dust speed in pixels a second (right, down), up to 1.6x for the fastest specks
GUST = 220 # extra push to the right in pixels a second, every second
ASH = (60, 52, 62) # dark ash color the edge turns just before it breaks off
CHAR = 0.03 # seconds the edge darkens to ash before it breaks off
SEED = 7 # change for a different scatter
def plate(still, mask):
"""The frozen frame with the subject painted out, so the backdrop shows where it stood."""
hole = cv2.dilate((mask > 0.1).astype(np.uint8), np.ones((15, 15), np.uint8))
small = cv2.resize(still, (w // 2, h // 2), interpolation=cv2.INTER_AREA).astype(np.uint8)
small_hole = cv2.resize(hole, (w // 2, h // 2), interpolation=cv2.INTER_NEAREST)
filled = cv2.inpaint(small, small_hole, 9, cv2.INPAINT_TELEA)
filled = cv2.GaussianBlur(cv2.resize(filled, (w, h), interpolation=cv2.INTER_CUBIC), (0, 0), 6)
soft = cv2.GaussianBlur(hole.astype(np.float32), (0, 0), 4)[:, :, None]
return still * (1 - soft) + filled * soft
def break_times(mask):
"""Second each pixel breaks off: top first, with a blotchy edge so it goes in clumps."""
ys, xs = np.nonzero(mask > 0.5)
top, bottom = ys.min(), ys.max()
blotch = sum(k * norm(cv2.GaussianBlur(rng.random((h, w)).astype(np.float32), (0, 0), size))
for k, size in ((0.8, 9), (0.2, 1.5)))
down = np.clip((np.arange(h)[:, None] - top) / (bottom - top), 0, 1)
return START + SWEEP * (0.7 * down + 0.3 * blotch)
def norm(a):
"""Stretch an array to run from 0 to 1."""
return (a - a.min()) / (a.max() - a.min())
def dust(still, mask, when):
"""One speck per GRAIN x GRAIN cell of the subject: where it starts, when it goes, how it moves, its color."""
ys, xs = np.nonzero(mask[::GRAIN, ::GRAIN] > 0.5)
ys, xs = ys * GRAIN, xs * GRAIN
n = len(xs)
speed = rng.uniform(0.4, 1.6, n)
return {"x": xs.astype(np.float32), "y": ys.astype(np.float32), "t": when[ys, xs],
"vx": WIND[0] * speed, "vy": WIND[1] * speed + rng.normal(0, 40, n),
"life": rng.uniform(*LIFE, n), "phase": rng.uniform(0, 6.3, n),
"color": still[ys, xs] * 0.65 + np.array(ASH) * 0.35}
def freeze(rgb):
"""Everything the crumbling needs from the frozen frame: the subject's mask, the empty backdrop, the dust."""
still = rgb.astype(np.float32)
mask = np.asarray(session.predict(Image.fromarray(rgb))[0], np.float32) / 255
when = break_times(mask)
return {"still": still, "mask": mask, "when": when, "backdrop": plate(still, mask), "dust": dust(still, mask, when)}
def frame_at(scene, t):
"""The frozen frame: subject still standing where it hasn't broken off, ashen at the edge, dust on top."""
still, mask, when, d = scene["still"], scene["mask"], scene["when"], scene["dust"]
left = (mask * (when > t))[:, :, None]
char = np.clip(1 - (when - t) / CHAR, 0, 1)[:, :, None] * 0.7
img = scene["backdrop"] * (1 - left) + (still * (1 - char) + np.array(ASH) * char) * left
age = t - d["t"]
live = (age > 0) & (age < d["life"])
a = age[live]
x = d["x"][live] + d["vx"][live] * a + 0.5 * GUST * a * a + 6 * np.sin(5 * a + d["phase"][live])
y = d["y"][live] + d["vy"][live] * a
alpha = (1 - a / d["life"][live])[:, None] ** 1.5
on = (x >= 0) & (x < w - GRAIN) & (y >= 0) & (y < h - GRAIN)
x, y, alpha, color = x[on].astype(int), y[on].astype(int), alpha[on], d["color"][live][on]
for dy in range(GRAIN):
for dx in range(GRAIN):
img[y + dy, x + dx] = img[y + dy, x + dx] * (1 - alpha) + color * alpha
return img.clip(0, 255).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):
"""Pass frames through until START, then draw the frozen, crumbling frame to the end, 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, scene = 0, None
while raw := reader.stdout.read(w * h * 3):
rgb = np.frombuffer(raw, np.uint8).reshape(h, w, 3)
if i / fps >= START:
scene = scene or freeze(rgb)
rgb = frame_at(scene, i / fps)
writer.stdin.write(rgb.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)
rng = np.random.default_rng(SEED)
session = new_session("u2net")
render(src, out)





