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 an eye blink transition Seconds the eyelids take to shut over clip A: 0.3 Seconds the screen stays black between the clips: 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 an eye blink 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 |
|---|---|---|
CLOSE | 0.3 | Seconds the eyelids take to shut over clip A. |
SHUT | 0.1 | Seconds the screen stays black between the clips. |
OPEN | 0.4 | Seconds the eyelids take to open onto clip B. |
CURVE | 0.45 | How much the lids dip toward the sides of the frame; 0 gives straight edges. |
SOFT | 10 | Blur on the lid edges, in pixels. |
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 eye blink transition starting 0.8 seconds before A ends (so the result is 11.2 seconds for two 6 second clips). Build the blink frames with Python, numpy and Pillow. The picture shows through a gap between two black eyelids: at column x, with dx = (x - (width-1)/2) / (width/2) running from -1 to 1, a pixel is visible when its distance from the frame's vertical center is less than o * G * (1 - 0.45*dx^2), where G = (height/2 + 20) / (1 - 0.45) so the frame is fully clear at o = 1. Blur that mask with a 10 pixel Gaussian blur so the lid edges are soft, and multiply the frame by it. Over the first 0.3 seconds the lids close over clip A (o = 1 - s(p), p from 0 to 1, with smoothstep s(p) = p*p*(3-2p)); for the next 0.1 seconds the screen is fully black; over the last 0.4 seconds they open onto clip B (o = s(p)), 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 eye-blink-transition.py and run:
python3 eye-blink-transition.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Eye blink: black eyelids with curved edges close over clip A, stay shut a moment, then open onto clip B.
Usage: python3 eye-blink-transition.py clipA.mp4 clipB.mp4 output.mp4 (both clips need an audio track)
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import subprocess
import sys
import numpy as np
from PIL import Image, ImageFilter
CLOSE = 0.3 # seconds the lids take to shut over clip A
SHUT = 0.1 # seconds the screen stays black
OPEN = 0.4 # seconds the lids take to open onto clip B
CURVE = 0.45 # how much the lids dip toward the corners (0 = straight edges)
SOFT = 10 # blur on the lid edges, in pixels
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 = CLOSE + SHUT + OPEN
offset = float(info["format"]["duration"]) - D
n = round(D * FPS)
# the gap between the lids is widest in the middle and narrows toward the sides, like an eye;
# fully open, it is tall enough that even the corners (plus the blurred edge) are clear
dx = (np.arange(w) - (w - 1) / 2) / (w / 2)
shape = 1 - CURVE * dx ** 2
widest = (h / 2 + 2 * SOFT) / (1 - CURVE)
rows = np.abs(np.arange(h) - (h - 1) / 2)[:, None]
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 smoothstep(p):
return p * p * (3 - 2 * p)
def openness(t):
"""1 = eye fully open, 0 = shut."""
if t < CLOSE:
return 1 - smoothstep(t / CLOSE)
if t < CLOSE + SHUT:
return 0.0
return smoothstep((t - CLOSE - SHUT) / OPEN)
def blink_frame(picture, o):
gap = Image.fromarray(np.where(rows < o * widest * shape, 255, 0).astype(np.uint8))
gap = np.asarray(gap.filter(ImageFilter.GaussianBlur(SOFT)), np.float32)[:, :, None] / 255
return (picture * gap).astype(np.uint8).tobytes()
tail, head = frames(a_path, offset), frames(b_path, 0)
middle = b"".join(blink_frame(tail[i] if i / FPS < CLOSE + SHUT else head[i], openness(i / FPS)) for i in range(n))
# A up to the blink, then the blink 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)





