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 text slam to input.mp4 Words: [("GLASS", 0.4, 0.14), ("SKIN", 0.9, 0.265)]
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 text slam 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 |
|---|---|---|
WORDS | [("GLASS", 0.4, 0.14), ("SKIN", 0.9, 0.265)] | Each word, the second it lands and where its center sits (0 = top, 1 = bottom). |
DROP | 0.1 | Seconds a word takes to fall into place. |
START_SCALE | 2.6 | How big a word starts its drop, relative to its final size. |
WIDTH | 0.8 | A word's final width as a share of the frame width. |
COLOR | (255, 214, 0) | Text color. |
OUTLINE | 0.025 | Black outline thickness as a share of the frame width. |
FLASH | 0.9 | White flash strength on impact (1 = solid white). |
FLASH_LEN | 0.2 | Seconds the flash takes to fade. |
SHAKE | 14 | How far the frame jumps on impact, in pixels. |
SHAKE_LEN | 0.4 | Seconds until the shake settles. |
PUNCH | 0.08 | Extra zoom on impact, easing back to none. |
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 a text slam to input.mp4: two huge words in yellow (255, 214, 0) with a black outline 2.5% of the frame width thick, each 80% of the frame width wide and centered horizontally. "GLASS" lands at 0.4 seconds centered 14% of the way down, then "SKIN" lands at 0.9 seconds centered 26.5% of the way down; both stay to the end. Each word falls in over the 0.1 seconds before it lands, shrinking from 2.6 times its size while fading in and speeding up as it drops. On each landing, flash the whole frame toward white (90% white, fading to nothing over 0.2 seconds) and shake the frame, words included: jolt it about 14 pixels in changing directions with an extra 8% zoom that both die down to still over 0.4 seconds, never showing an edge. Nothing changes before 0.3 seconds. Draw the text with Python and Pillow (don't rely on ffmpeg's drawtext filter), keep the size and audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as text-slam.py and run:
python3 text-slam.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Text slam: huge words drop onto the screen one at a time, each landing with a white flash and a screen shake.
Usage: python3 text-slam.py input.mp4 output.mp4
Needs ffmpeg and Pillow (pip install pillow).
"""
import json
import math
import subprocess
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
WORDS = [("GLASS", 0.4, 0.14), # (word, second it lands, where its center sits: 0 = top, 1 = bottom)
("SKIN", 0.9, 0.265)]
DROP = 0.1 # seconds a word takes to fall from big to its place
START_SCALE = 2.6 # how big a word starts its drop, relative to its final size
WIDTH = 0.8 # a word's final width as a share of the frame width
COLOR = (255, 214, 0) # text color
OUTLINE = 0.025 # black outline thickness as a share of the frame width
FLASH = 0.9 # white flash strength on impact (1 = solid white)
FLASH_LEN = 0.2 # seconds the flash takes to fade
SHAKE = 14 # how far the frame jumps on impact, in pixels
SHAKE_LEN = 0.4 # seconds until the shake settles
PUNCH = 0.08 # extra zoom on impact, easing back to none (also hides the shake's edges)
FONTS = ["/System/Library/Fonts/Supplemental/Arial Black.ttf", # macOS
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", # Linux
"C:/Windows/Fonts/ariblk.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 word_image(word):
"""One word in color with a black outline, sized to fill WIDTH of the frame."""
stroke = round(w * OUTLINE)
probe_font = ImageFont.truetype(FONT_FILE, 100)
x0, _, x1, _ = probe_font.getbbox(word, stroke_width=stroke)
font = ImageFont.truetype(FONT_FILE, round(100 * w * WIDTH / (x1 - x0)))
x0, y0, x1, y1 = font.getbbox(word, stroke_width=stroke)
img = Image.new("RGBA", (x1 - x0, y1 - y0))
ImageDraw.Draw(img).text((-x0, -y0), word, font=font, fill=COLOR, stroke_width=stroke, stroke_fill="black")
return img
def add_text(img, t):
for (word, land, y), pic in zip(WORDS, pictures):
if t < land - DROP:
continue
p = min((t - (land - DROP)) / DROP, 1)
scale = START_SCALE + (1 - START_SCALE) * p * p # speeds up as it falls, like a dropped weight
pic_t = pic.resize((round(pic.width * scale), round(pic.height * scale)), Image.LANCZOS)
if p < 1:
pic_t.putalpha(pic_t.getchannel("A").point(lambda a: a * p))
img.alpha_composite(pic_t, (round(w / 2 - pic_t.width / 2), round(h * y - pic_t.height / 2)))
return img
def last_hit(t):
"""Seconds since the latest word landed, or None before the first."""
since = [t - land for _, land, _ in WORDS if t >= land]
return min(since) if since else None
def shake(img, t):
s = last_hit(t)
if s is None or s >= SHAKE_LEN:
return img
k = 1 - s / SHAKE_LEN # 1 on impact, 0 once settled
zoom = 1 + PUNCH * k
dx, dy = SHAKE * k * math.sin(t * 83), SHAKE * 0.8 * k * math.cos(t * 61)
cw, ch = w / zoom, h / zoom
x0, y0 = (w - cw) / 2 + dx / zoom, (h - ch) / 2 + dy / zoom
return img.resize((w, h), Image.BICUBIC, box=(x0, y0, x0 + cw, y0 + ch))
def flash(img, t):
s = last_hit(t)
if s is None or s >= FLASH_LEN:
return img
return Image.blend(img, Image.new("RGB", img.size, "white"), FLASH * (1 - s / FLASH_LEN))
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):
"""Read every frame of src, add the words, shake and flash, and save to out with the audio."""
reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-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", rate, "-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)
for i in range(math.ceil(duration * fps)):
raw = reader.stdout.read(w * h * 3)
if len(raw) < w * h * 3:
break
t = i / fps
img = add_text(Image.frombuffer("RGB", (w, h), raw).convert("RGBA"), t).convert("RGB")
writer.stdin.write(flash(shake(img, t), t).tobytes())
writer.stdin.close()
reader.wait()
if writer.wait():
sys.exit("ffmpeg failed")
src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
pictures = [word_image(word) for word, _, _ in WORDS]
render(src, out)





