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
Join clipA.mp4 and clipB.mp4 with a seamless hand-cover transition Seconds the hand takes to slide over the lens, slowing as it lands: 0.35 Seconds the lens stays fully covered; the cut to clip B happens halfway through: 0.1
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.
Put a sale badge on @clip1.mp4, give @clip2.mp4 a VHS look, then join them with a seamless hand-cover transition.
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 |
|---|---|---|
IN | 0.35 | Seconds the hand takes to slide over the lens, slowing as it lands. |
HOLD | 0.1 | Seconds the lens stays fully covered; the cut to clip B happens halfway through. |
OUT | 0.35 | Seconds the hand takes to speed off to the left. |
DRIFT | 0.08 | How far the hand keeps creeping left while it covers the lens, as a share of the frame width. |
FINGERS | (0.5, 0.6, 0.56, 0.42) | How far each finger reaches past the palm, as a share of the frame width, top to bottom. |
SOFT | 0.03 | Blur on the hand's edge as a share of the frame width; more looks closer to the lens. |
SKIN | (24, 16, 14) | Color of the hand in its own shadow, which fills the screen at the cut. |
RIM | (150, 98, 76) | Warmer color along the hand's edges, where light catches the skin. |
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
Join clipA.mp4 and clipB.mp4 with a 0.8 second hand-cover transition that starts 0.8 seconds before clipA.mp4 ends (11.2 seconds in all for two 6 second clips), as if a hand passed right in front of the lens. Build the transition frames with Python, numpy and Pillow; W and H are the frame width and height (540x960), and X is the left edge of the hand drawing. Draw the hand into a mask with Pillow: a palm, a rounded rectangle from X+0.75W to X+2.0W across and from -0.2H to 1.2H down with a corner radius of 0.3W; four fingers pointing left, rounded rectangles 0.17H tall with fully rounded ends, top edges at 0.10H, 0.27H, 0.44H and 0.61H, each running from X+0.95W left to X+0.75W minus 0.50W, 0.60W, 0.56W and 0.42W (top to bottom); and a thumb, a rounded rectangle from X+0.35W to X+1.05W across and 0.78H to 0.984H down (corner radius 0.102H), rotated 25 degrees counterclockwise about the point (X+1.05W, 0.78H). Blur the mask with a Gaussian blur of radius 0.03W so the hand looks out of focus. Color the hand from a second copy of the mask blurred by 0.06W: with k = clip((blurred/255 - 0.5) * 2, 0, 1), the color is (150, 98, 76) * (1 - k) + (24, 16, 14) * k, so its edges look like warm skin and its middle is near black. Paste it over the frame with the soft mask. Move it with t in seconds from the start of the transition and cover = -0.835W: for t < 0.35, X = W + (cover - W) * (1 - (1 - p)^2) with p = t / 0.35 (the hand slows as it lands); from 0.35 to 0.45, X creeps linearly from cover to cover - 0.08W while the lens is fully covered; from 0.45 to 0.8, X = cover - 0.08W + (-2.1W - cover + 0.08W) * p^2 with p = (t - 0.45) / 0.35 (it speeds off to the left). Show clip A under the hand up to 0.4 seconds into the transition and clip B after that, with B playing from its first frame at the start of the transition. Don't use ffmpeg's drawtext. Use ffmpeg for the rest: match B's size and frame rate to A (30fps), crossfade the audio over the same 0.8 seconds, keep the audio and save it as output.mp4 (H.264, yuv420p).Run it without Claude
This is the exact script we tested. Save it as seamless-hand-cover-transition.py and run:
python3 seamless-hand-cover-transition.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Hand cover: an out-of-focus hand swipes in from the right over clip A, blacks out the lens, then swipes off to the left onto clip B.
Usage: python3 seamless-hand-cover-transition.py clipA.mp4 clipB.mp4 output.mp4 (both clips need an audio track)
Needs ffmpeg, Pillow and numpy (pip install pillow numpy). Filmed hand covers look best; this fakes one for any two clips.
"""
import json
import subprocess
import sys
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
IN = 0.35 # seconds the hand takes to slide over the lens (slowing as it lands)
HOLD = 0.1 # seconds the lens stays covered; the cut to B happens halfway through
OUT = 0.35 # seconds the hand takes to slide off to the left (speeding up)
DRIFT = 0.08 # how far the palm keeps creeping left while it covers the lens, as a share of the width
FINGERS = (0.5, 0.6, 0.56, 0.42) # how far each finger reaches past the palm, as a share of the width, top to bottom
SOFT = 0.03 # blur on the hand's edge as a share of the width (it's too close to the lens to be sharp)
SKIN = (24, 16, 14) # color of the hand in its own shadow
RIM = (150, 98, 76) # warmer color where light catches the hand's edges
FPS = 30
a_path, b_path, out_path = sys.argv[1:4]
info = json.loads(subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height:format=duration",
"-of", "json", a_path], capture_output=True, check=True).stdout)
w, h = info["streams"][0]["width"], info["streams"][0]["height"]
D = IN + HOLD + OUT
offset = float(info["format"]["duration"]) - D
n = round(D * FPS)
def hand():
"""The hand as an RGB image and a soft-edged mask: a palm 1.25 frames wide and taller than the frame, with four
fingers pointing left (the way it moves) and a thumb below them. Returns them with the palm's center (x)."""
hw, hh = round(w * 2.1), round(h * 1.4)
palm_left, palm_right = round(w * 0.75), round(w * 2.0)
mask = Image.new("L", (hw, hh))
draw = ImageDraw.Draw(mask)
draw.rounded_rectangle([palm_left, 0, palm_right, hh], radius=round(w * 0.3), fill=255)
finger = round(h * 0.17)
for i, length in enumerate(FINGERS): # index finger at the top, little finger at the bottom
y = round(h * 0.3) + i * finger
draw.rounded_rectangle([palm_left - w * length, y, palm_left + w * 0.2, y + finger - 2], radius=finger // 2, fill=255)
thumb = Image.new("L", (hw, hh))
y = round(h * 0.3) + 4 * finger
ImageDraw.Draw(thumb).rounded_rectangle([palm_left - w * 0.4, y, palm_left + w * 0.3, y + finger * 1.2],
radius=round(finger * 0.6), fill=255)
mask.paste(255, mask=thumb.rotate(25, center=(palm_left + w * 0.3, y), resample=Image.BICUBIC))
alpha = mask.filter(ImageFilter.GaussianBlur(w * SOFT))
# deep inside the hand is near black; toward its edges it warms up to RIM, like skin catching the light
inside = np.asarray(mask.filter(ImageFilter.GaussianBlur(w * 0.06)), np.float32)[:, :, None] / 255
inside = np.clip((inside - 0.5) * 2, 0, 1)
rgb = np.array(RIM, np.float32) * (1 - inside) + np.array(SKIN, np.float32) * inside
return Image.fromarray(rgb.astype(np.uint8)), alpha, (palm_left + palm_right) / 2
def hand_x(t):
"""Left edge of the hand image: from just off the right edge, to the palm centered over the lens, to off the left edge."""
cover = w / 2 - palm_mid + w * DRIFT / 2
if t < IN:
p = t / IN
return w + (cover - w) * (1 - (1 - p) ** 2)
if t < IN + HOLD:
return cover - w * DRIFT * (t - IN) / HOLD
p = (t - IN - HOLD) / OUT
gone = -skin.width # far enough left that the hand has left the frame
return cover - w * DRIFT + (gone - cover + w * DRIFT) * p ** 2
def frames(path, start):
"""The n frames from `start`, fitted to A's size, as RGB arrays."""
raw = subprocess.run(
["ffmpeg", "-v", "error", "-ss", str(start), "-i", path, "-frames:v", str(n),
"-vf", f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS}",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"], capture_output=True, check=True).stdout
return np.frombuffer(raw, np.uint8).reshape(-1, h, w, 3)
def cover_frame(picture, t):
img = Image.fromarray(picture)
img.paste(skin, (round(hand_x(t)), round(-h * 0.2)), alpha)
return img.tobytes()
skin, alpha, palm_mid = hand()
tail, head = frames(a_path, offset), frames(b_path, 0)
cut = IN + HOLD / 2
middle = b"".join(cover_frame(tail[i] if i / FPS < cut else head[i], i / FPS) for i in range(n))
# A up to the hand, then the hand frames from stdin, then the rest of B; the sound crossfades underneath
subprocess.run(
["ffmpeg", "-y", "-v", "error", "-i", a_path, "-i", b_path,
"-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", str(FPS), "-i", "-",
"-filter_complex",
f"[0:v]fps={FPS},trim=end_frame={round(offset * FPS)},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[a];"
f"[2:v]format=yuv420p,setsar=1[m];"
f"[1:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS},"
f"trim=start_frame={n},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[b];"
f"[a][m][b]concat=n=3:v=1:a=0[v];[0:a][1:a]acrossfade=d={D}[au]",
"-map", "[v]", "-map", "[au]", "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "aac", out_path],
input=middle, check=True)





