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 ransom note text to input.mp4 Lines: ["NEW", "DROP"] Papers: white, yellow, black, red, newsprint
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 ransom note text 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 |
|---|---|---|
LINES | ["NEW", "DROP"] | The words, one line each. |
START | 0.5 | Second the first letter is pasted on. |
STEP | 0.15 | Seconds between letters. |
SIZE | 0.15 | Letter size as a share of the frame width (each letter varies a little). |
JITTER_FPS | 8 | How often the letters twitch per second. |
JITTER | (3, 2.5) | Most each letter moves (pixels) and turns (degrees) per twitch. |
PAPERS | white, yellow, black, red, newsprint | The scrap and letter colors, used in turn. |
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
Add ransom note text to input.mp4: the words "NEW" on one line and "DROP" on the line below, each letter cut out like a magazine clipping. Give each letter its own scrap of paper, cycling through these (paper, letter) colors in order: white (250, 248, 240) with near-black (20, 20, 20), yellow (255, 214, 0) with near-black, near-black with white (250, 248, 240), red (215, 25, 35) with white, and newsprint (238, 228, 200) with red (215, 25, 35). Use a different bold font for each letter in turn (Arial Black, Impact, Times New Roman Bold, Georgia Bold, Courier New Bold), with letters between 13% and 16.5% of the frame width tall. Each scrap is the letter's box plus 18% of the letter size of paper on every side, with its four corners nudged a few pixels at random so it looks hand-cut, a soft drop shadow 3 pixels right and 4 down at 50% black, and a fixed tilt between -7 and 7 degrees. Lay each line out left to right with neighboring scraps overlapping by about 8%, center the lines across the frame, and put the line centers at 10% and 22% down the frame. Paste the letters on one at a time, reading order, the first at 0.5 seconds and one every 0.15 seconds after that (the last, P, at 1.4 seconds), with nothing on screen before 0.5 seconds. To make it feel like stop-motion, 8 times a second give every letter a new random offset of up to 3 pixels in each direction and an extra turn of up to 2.5 degrees, holding it between those updates. Keep the letters on to the end. Use Python with Pillow to draw it and ffmpeg to put it on the video (don't rely on ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as ransom-note-text.py and run:
python3 ransom-note-text.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Ransom note text: "NEW DROP" is pasted on letter by letter, each cut from a different magazine, twitching like stop-motion.
Usage: python3 ransom-note-text.py input.mp4 output.mp4
Needs ffmpeg and Pillow (pip install pillow).
"""
import json
import math
import random
import subprocess
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageFont
LINES = ["NEW", "DROP"] # one line of letters each
START = 0.5 # second the first letter is pasted on
STEP = 0.15 # seconds between letters
SIZE = 0.15 # letter size as a share of the frame width
X, Y = 0.5, 0.16 # center of the whole note (0-1 across and down the frame)
LINE_GAP = 0.12 # distance between the two lines' centers, as a share of the frame height
JITTER_FPS = 8 # how often the letters twitch, for a stop-motion feel
JITTER = (3, 2.5) # most each letter moves (pixels) and turns (degrees) per twitch
PAPERS = [((250, 248, 240), (20, 20, 20)), # (scrap color, letter color): white paper
((255, 214, 0), (20, 20, 20)), # yellow
((20, 20, 20), (250, 248, 240)), # black
((215, 25, 35), (250, 248, 240)), # red
((238, 228, 200), (215, 25, 35))] # newsprint
FONTS = ["/System/Library/Fonts/Supplemental/Arial Black.ttf", # macOS
"/System/Library/Fonts/Supplemental/Georgia Bold.ttf",
"/System/Library/Fonts/Supplemental/Impact.ttf",
"/System/Library/Fonts/Supplemental/Courier New Bold.ttf",
"/System/Library/Fonts/Supplemental/Times New Roman Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", # Linux
"/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf",
"C:/Windows/Fonts/ariblk.ttf", "C:/Windows/Fonts/georgiab.ttf", # Windows
"C:/Windows/Fonts/impact.ttf", "C:/Windows/Fonts/courbd.ttf"]
FONT_FILES = [f for f in FONTS if Path(f).exists()] or sys.exit("No fonts found: add some to FONTS")
def scrap(letter, i):
"""One letter on a roughly cut piece of paper with a soft shadow; scrap i gets its own paper, font and size."""
rnd = random.Random(i)
paper, ink = PAPERS[i % len(PAPERS)]
font = ImageFont.truetype(FONT_FILES[i * 2 % len(FONT_FILES)], round(w * SIZE * rnd.uniform(0.85, 1.1)))
left, top, right, bottom = font.getbbox(letter)
pad = round(font.size * 0.18)
sw, sh = right - left + 2 * pad, bottom - top + 2 * pad
edge = pad // 2 # room round the scrap for its jagged corners and shadow
corners = [(edge + rnd.uniform(-1, 1) * pad / 3 + x * sw, edge + rnd.uniform(-1, 1) * pad / 3 + y * sh)
for x, y in [(0, 0), (1, 0), (1, 1), (0, 1)]]
size = (sw + 2 * edge + 6, sh + 2 * edge + 6)
cut = Image.new("L", size)
ImageDraw.Draw(cut).polygon(corners, fill=255)
shadow = cut.transform(size, Image.AFFINE, (1, 0, -3, 0, 1, -4)).filter(ImageFilter.GaussianBlur(3))
img = Image.new("RGBA", size, (0, 0, 0, 0))
img.putalpha(shadow.point(lambda a: a // 2))
img.paste(paper + (255,), mask=cut)
ImageDraw.Draw(img).text((edge + pad - left, edge + pad - top), letter, font=font, fill=ink)
return img, rnd.uniform(-7, 7)
def layout():
"""Each letter's scrap, base tilt and center, laid out left to right in centered lines."""
letters, i = [], 0
for row, line in enumerate(LINES):
scraps = [scrap(ch, i + k) for k, ch in enumerate(line)]
width = sum(s.width for s, _ in scraps) * 0.92 # scraps overlap a little, like a pasted collage
x = w * X - width / 2
cy = h * (Y + (row - (len(LINES) - 1) / 2) * LINE_GAP)
for s, tilt in scraps:
letters.append((s, tilt, (x + s.width * 0.46, cy)))
x += s.width * 0.92
i += len(line)
return letters
def overlay(t):
img = Image.new("RGBA", (w, h))
tick = math.floor(t * JITTER_FPS)
for i, (s, tilt, (cx, cy)) in enumerate(letters):
if t < START + i * STEP:
break
rnd = random.Random(tick * 100 + i) # the same twitch for every frame of one tick
move, turn = JITTER
r = s.rotate(tilt + rnd.uniform(-turn, turn), resample=Image.BICUBIC, expand=True)
img.alpha_composite(r, (round(cx - r.width / 2 + rnd.uniform(-move, move)),
round(cy - r.height / 2 + rnd.uniform(-move, move))))
return img
def probe(path):
info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height,r_frame_rate:format=duration", "-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), float(info["format"]["duration"])
def render(src, out):
"""Draw overlay(t) on every frame of src and save to out, keeping the audio."""
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
"-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
"-filter_complex", "[0:v][1:v]overlay=shortest=1,format=yuv420p[v]",
"-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
stdin=subprocess.PIPE)
for i in range(math.ceil(duration * fps)):
ff.stdin.write(overlay(i / fps).tobytes())
ff.stdin.close()
if ff.wait():
sys.exit("ffmpeg failed")
src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
letters = layout()
render(src, out)





