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 VHS effect to input.mp4 Label: PLAY
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 vhs effect 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 |
|---|---|---|
SOFTEN | 3 | Pixels the picture is blurred sideways, like tape's low sharpness. |
SMEAR | 9 | Pixels the color is smeared sideways. |
FRINGE | 4 | Pixels red is pushed right and blue left. |
SATURATION | 0.8 | Color left after the tape (1 keeps it all). |
WOBBLE | 2 | Pixels the lines sway sideways, in a slow wave down the frame. |
GRAIN | 6 | Tape noise strength (0 turns it off). |
TRACK_Y | 0.15 | Where the tracking band starts at 0s (0-1 down the frame). |
TRACK_SPEED | 120 | Pixels per second the band rolls down, wrapping round. |
TRACK_H | 36 | Tracking band height, in pixels. |
TRACK_TEAR | 14 | Pixels the band's lines tear sideways at most. |
HEAD_H | 14 | Rows of head-switching noise along the bottom edge. |
SPECKS | 0.12 | Share of the band and bottom edge covered in white noise streaks. |
LABEL | PLAY | The on-screen readout in the top-left corner, next to a play triangle. |
SEED | 3 | Change for a different noise pattern. |
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 input.mp4 (540x960, 30fps, 6 seconds) look like worn VHS tape playback from the first frame to the last, using Python with numpy and Pillow and ffmpeg for reading and writing frames. For each frame at time t: (1) Soften it with a 3-pixel horizontal box blur. (2) Split it into luma (0.299R + 0.587G + 0.114B) and color (RGB minus luma); smear the color with a 9-pixel horizontal box blur, multiply it by 0.8 and add it back to the luma. (3) Shift the red channel 4 pixels right and the blue channel 4 pixels left. (4) Shift every row sideways by 2 * sin(0.02 * row + 3t) pixels. (5) Tracking band: a 36-pixel-tall band whose top row is int(144 + 120t) mod 960 (so rows 264-299 at 1 second and 624-659 at 4 seconds): shift each of its rows by a random -14 to 14 pixels, brighten it by 25, and turn about 12% of it pure white (255, 255, 255) in random 6-pixel-wide horizontal streaks. (6) Head-switching noise: shift the bottom 14 rows right by 8 rising to 30 pixels and give them the same 12% white streaks. (7) Add gray tape noise with a standard deviation of 6. Nowhere else may be pure white except the readout. (8) Draw a white "PLAY" in bold sans-serif 38 pixels tall with its top-left corner at (38, 48), followed by a white play triangle (a right-pointing triangle the height of the text), both with a black drop shadow at 160/255 opacity offset 3 pixels down and right; draw it with Pillow (not ffmpeg's drawtext) and keep it crisp, without the blur, noise or shifts. Keep the 540x960 size, 30fps, 6-second length and the audio. Save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as vhs-effect.py and run:
python3 vhs-effect.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""VHS effect: soft, color-smeared tape playback with wobbling lines, a rolling tracking band and a PLAY readout.
Usage: python3 vhs-effect.py input.mp4 output.mp4
Needs ffmpeg, numpy and Pillow (pip install numpy pillow).
"""
import json
import subprocess
import sys
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw, ImageFont
SOFTEN = 3 # pixels the picture is blurred sideways, like tape's low sharpness
SMEAR = 9 # pixels the color is smeared sideways
FRINGE = 4 # pixels red is pushed right and blue left
SATURATION = 0.8 # color left after the tape (1 keeps it all)
WOBBLE = 2 # pixels the lines sway sideways, in a slow wave down the frame
GRAIN = 6 # tape noise strength (0 turns it off)
TRACK_Y = 0.15 # where the tracking band starts at 0s (0-1 down the frame)
TRACK_SPEED = 120 # pixels per second the band rolls down, wrapping round
TRACK_H = 36 # tracking band height, in pixels
TRACK_TEAR = 14 # pixels the band's lines tear sideways at most
HEAD_H = 14 # rows of head-switching noise along the bottom edge
SPECKS = 0.12 # share of the band and bottom edge covered in white noise streaks
LABEL = "PLAY" # the on-screen readout in the top-left corner, next to a play triangle
SEED = 3 # change for a different noise pattern
FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf", # macOS
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", # Linux
"C:/Windows/Fonts/arialbd.ttf"] # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")
def blur_x(img, px):
"""Box blur along each row."""
kernel = np.ones(px, np.float32) / px
pad = np.pad(img, ((0, 0), (px // 2, px - 1 - px // 2), (0, 0)), mode="edge")
return sum(pad[:, i:i + img.shape[1]] * k for i, k in enumerate(kernel))
def readout():
"""The VCR's white "PLAY" with a play triangle, with a dark drop shadow so it reads on any footage."""
img = Image.new("RGBA", (w, h))
draw = ImageDraw.Draw(img)
size = round(h * 0.04)
font = ImageFont.truetype(FONT_FILE, size)
x, y = round(w * 0.07), round(h * 0.05)
tri = x + draw.textlength(LABEL, font=font) + size * 0.4
for dx, color in ((3, (0, 0, 0, 160)), (0, (255, 255, 255, 255))):
draw.text((x + dx, y + dx), LABEL, font=font, fill=color)
draw.polygon([(tri + dx, y + size * 0.15 + dx), (tri + size * 0.75 + dx, y + size * 0.6 + dx),
(tri + dx, y + size * 1.05 + dx)], fill=color)
rgba = np.asarray(img, np.float32)
return rgba[..., :3], rgba[..., 3:] / 255
def tape(img, t):
"""Play one frame (h x w x 3, 0-255 floats) back off a worn tape."""
soft = blur_x(img, SOFTEN)
luma = soft @ np.array([0.299, 0.587, 0.114], np.float32)
chroma = blur_x(soft - luma[..., None], SMEAR) * SATURATION
pic = luma[..., None] + chroma
pic[:, FRINGE:, 0] = pic[:, :-FRINGE, 0]
pic[:, :-FRINGE, 2] = pic[:, FRINGE:, 2]
rows = np.arange(h)
shift = WOBBLE * np.sin(rows * 0.02 + t * 3)
top = int(TRACK_Y * h + TRACK_SPEED * t) % h
band = (rows >= top) & (rows < top + TRACK_H)
shift[band] += rng.uniform(-TRACK_TEAR, TRACK_TEAR, band.sum())
head = rows >= h - HEAD_H
shift[head] += np.linspace(8, 30, HEAD_H)
xs = np.clip(np.arange(w)[None, :] - shift.round().astype(int)[:, None], 0, w - 1)
pic = pic[rows[:, None], xs]
pic += rng.normal(0, GRAIN, (h, w, 1))
pic[band] += 25
noisy = band | head
streaks = np.repeat(rng.random((noisy.sum(), w // 6 + 1)), 6, axis=1)[:, :w] < SPECKS
pic[noisy] = np.where(streaks[..., None], 255, pic[noisy])
pic = pic * (1 - osd_alpha) + osd_rgb * osd_alpha
return np.clip(pic, 0, 255).astype(np.uint8)
src, out = sys.argv[1:3]
info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height", "-of", "json", src]))["streams"][0]
w, h = info["width"], info["height"]
rng = np.random.default_rng(SEED)
osd_rgb, osd_alpha = readout()
reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-r", "30", "-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", "30", "-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 True:
raw = reader.stdout.read(w * h * 3)
if len(raw) < w * h * 3:
break
frame = np.frombuffer(raw, np.uint8).reshape(h, w, 3).astype(np.float32)
writer.stdin.write(tape(frame, i / 30).tobytes())
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





