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 TV channel-change transition Output frame rate: 30 Frames of clip A rolling up and breaking into static: 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.
Put a sale badge on @clip1.mp4, give @clip2.mp4 a VHS look, then join them with a tv channel-change 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 |
|---|---|---|
FPS | 30 | Output frame rate. |
OUT_FRAMES | 4 | Frames of clip A rolling up and breaking into static. |
STATIC_FRAMES | 5 | Frames of full-screen static. |
BLACK_FRAMES | 4 | Frames of black between the two channels. |
IN_FRAMES | 5 | Frames of clip B rolling in as the static clears. |
GRAIN | 2 | Size of one static speck, in pixels. |
ROLL | 0.3 | Furthest the picture slips up while it changes, as a share of its height. |
BAR | 24 | Height of the black bar where the slipping picture wraps round, in pixels. |
HISS | 0.15 | Loudness of the static's hiss, 0 to 1. |
SEED | 3 | Random seed; change it for a different but repeatable pattern of static. |
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.6 second TV channel-change transition that starts 0.6 seconds before the end of A, so the output is 11.4 seconds long and B starts playing (from its first frame) at 5.4s. Use Python with numpy and ffmpeg, working frame by frame at 30fps and fitting B to A's size first. The 18 transition frames are: 4 frames of A breaking up, 5 frames of full-screen static, 4 frames of pure black (0,0,0), then 5 frames of B coming in. Static is black-and-white snow: a random gray value from 0 to 255 for every 2x2 pixel block, new each frame, multiplied by 0.75 plus 0.4 times a horizontal Gaussian band (centered at a random height, width 8% of the frame height) so one bright stripe crosses it, with every odd row darkened to 80% for scan lines. A's 4 frames use mix p = 0.2, 0.4, 0.6, 0.8 and B's 5 frames use p = 5/6, 4/6, 3/6, 2/6, 1/6: roll the picture up by round(p * 0.3 * height) pixels with the top wrapping round to the bottom, paint a 24 pixel tall black bar at the wrap line, and blend it with a fresh static frame as (1 - p) * picture + p * static. Leave every frame outside the transition untouched. Sound: A's audio until 5.7s (0.03s fade out), a white-noise hiss at amplitude 0.15 from 5.4s to 5.7s (0.1s fade in), silence during the black, and B's audio (in sync with its picture) fading in over 0.03s at 5.833s; mix them without normalizing. Use a fixed random seed so it repeats. Save it as output.mp4 (H.264, yuv420p).Run it without Claude
This is the exact script we tested. Save it as tv-channel-change-transition.py and run:
python3 tv-channel-change-transition.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""TV channel-change transition: clip A rolls and dissolves into hissing static, the screen goes black for a beat, then clip B rolls in through the static.
Usage: python3 tv-channel-change-transition.py clipA.mp4 clipB.mp4 output.mp4 (both clips need an audio track)
Needs ffmpeg and numpy (pip install numpy).
"""
import json
import subprocess
import sys
from itertools import islice
import numpy as np
FPS = 30 # output frame rate
OUT_FRAMES = 4 # frames of A rolling up and breaking into static
STATIC_FRAMES = 5 # frames of full-screen static
BLACK_FRAMES = 4 # frames of black between the channels
IN_FRAMES = 5 # frames of B rolling in as the static clears
GRAIN = 2 # size of one static speck, in pixels
ROLL = 0.3 # furthest the picture slips up while it changes, as a share of its height
BAR = 24 # height of the black bar between the slipping picture and its wrapped-round top, in pixels
HISS = 0.15 # loudness of the static's hiss (0-1)
SEED = 3 # change for a different (but repeatable) pattern of static
def probe(path):
info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height:format=duration", "-of", "json", path]))
return info["streams"][0]["width"], info["streams"][0]["height"], float(info["format"]["duration"])
def frames(path, count=None):
"""Decode a clip (or its first count frames) as w x h RGB frames at FPS, cropping it to fill the frame if its shape differs."""
limit = ["-frames:v", str(count)] if count else []
ff = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf",
f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS}", *limit,
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
while True:
data = ff.stdout.read(w * h * 3)
if len(data) < w * h * 3:
return
yield np.frombuffer(data, np.uint8).reshape(h, w, 3).astype(np.float32)
def static():
"""One frame of black-and-white snow with scan lines and a bright band across it at a random height."""
specks = rng.integers(0, 256, (h // GRAIN + 1, w // GRAIN + 1)).astype(np.float32)
snow = specks.repeat(GRAIN, 0).repeat(GRAIN, 1)[:h, :w]
rows = np.arange(h)[:, None]
band = np.exp(-((rows - rng.integers(0, h)) / (h * 0.08)) ** 2)
snow = snow * (0.75 + 0.4 * band) * np.where(rows % 2, 0.8, 1.0)
return np.repeat(np.clip(snow, 0, 255)[:, :, None], 3, axis=2)
def tuning(img, p):
"""A picture mid-change: slipped up by p * ROLL of its height, with a black bar where it wraps round,
and mixed p of the way into static."""
slip = round(p * ROLL * h)
rolled = np.roll(img, -slip, axis=0)
rolled[h - slip:h - slip + BAR] = 0
return (1 - p) * rolled + p * static()
A, B, out = sys.argv[1:4]
w, h, length = probe(A)
n = OUT_FRAMES + STATIC_FRAMES + BLACK_FRAMES + IN_FRAMES # frames in the transition
start = length - n / FPS # where the transition starts; B starts here too
hiss_end = start + (OUT_FRAMES + STATIC_FRAMES) / FPS # A's sound and the hiss stop as the screen goes black
b_sound = hiss_end + BLACK_FRAMES / FPS # B's sound comes in with its picture
rng = np.random.default_rng(SEED)
# Sound: A until the black gap, white-noise hiss over the static, a moment of silence, then B
sound = (f"[1:a]atrim=end={hiss_end},afade=t=out:st={hiss_end - 0.03}:d=0.03[a];"
f"anoisesrc=d={hiss_end - start}:c=white:a={HISS}:r=48000,afade=t=in:d=0.1,adelay={start * 1000:.0f}:all=1[hiss];"
f"[2:a]adelay={start * 1000:.0f}:all=1,afade=t=in:st={b_sound}:d=0.03[b];"
f"[a][hiss][b]amix=inputs=3:duration=longest:normalize=0[au]")
enc = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
"-r", str(FPS), "-i", "-", "-i", A, "-i", B, "-filter_complex", sound,
"-map", "0:v", "-map", "[au]", "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p",
"-c:a", "aac", out], stdin=subprocess.PIPE)
def write(img):
enc.stdin.write(img.astype(np.uint8).tobytes())
clip_a, clip_b = frames(A, round(start * FPS) + OUT_FRAMES), frames(B) # A stops where the static takes over
for a in islice(clip_a, round(start * FPS)):
write(a)
for i, a in enumerate(islice(clip_a, OUT_FRAMES)):
write(tuning(a, (i + 1) / (OUT_FRAMES + 1)))
for _ in range(STATIC_FRAMES):
write(static())
black = np.zeros((h, w, 3), np.float32)
for _ in range(BLACK_FRAMES):
write(black)
# B's frames under the static and black are skipped so it stays in sync with its sound
for i, b in enumerate(islice(clip_b, OUT_FRAMES + STATIC_FRAMES + BLACK_FRAMES, n)):
write(tuning(b, 1 - (i + 1) / (IN_FRAMES + 1)))
for b in clip_b:
write(b)
enc.stdin.close()
if enc.wait():
sys.exit("ffmpeg failed")





