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 size-emphasis captions to input.mp4 Keywords: {"SERUM", "TWO", "WEEKS"}
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 size-emphasis captions 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 |
|---|---|---|
KEYWORDS | {"SERUM", "TWO", "WEEKS"} | Words drawn big; have Claude pick them from the transcript. |
SMALL | 0.075 | Height of the other words, as a share of the frame width. |
BIG | 0.22 | Height of the key words, shrunk so a line fits in 90% of the width. |
Y | 0.70 | Where the caption block's center sits (0 = top, 1 = bottom). |
MAX_WORDS | 3 | Most words in one phrase. |
POP | 0.15 | Seconds a key word takes to grow to full size. |
STROKE | 0.08 | Black outline, as a share of the text height. |
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 captions to input.mp4 where the key words are far bigger than the rest. Transcribe its voice with faster-whisper (model base.en, device cpu, compute_type int8, word_timestamps=True) to get each word's start and end time, then uppercase the words and strip punctuation. The key words are SERUM, TWO and WEEKS. Split the words into phrases of at most 3 words, ending a phrase early right after a key word when the next word isn't one. Within a phrase, each run of consecutive key words gets its own line and each run of other words gets its own line, stacked in spoken order. Other words are 7.5% of the frame width tall; key words are 22% of the frame width tall, shrunk until their line fits in 90% of the width. Use Arial Black (or another heavy bold font), pure white (255, 255, 255) text with a black outline 8% of the text height thick. Center each line horizontally, make each line 1.15 times its text height tall, and center the phrase's block of lines 70% of the way down the frame. Lay out the whole phrase up front, but draw each word only from its own start time; a key word grows from 60% to full size over 0.15 seconds (ease out) when it appears. A phrase stays up from its first word's start to its last word's end, and nothing shows after the last word. Use Python with Pillow to draw the text (don't use ffmpeg's drawtext filter) and ffmpeg to put it on the video, keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as size-emphasis-captions.py and run:
python3 size-emphasis-captions.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Size-emphasis captions: spoken words appear in small white capitals, and the key words land three times bigger.
Usage: python3 size-emphasis-captions.py input.mp4 output.mp4
Whisper transcribes the voice for the word timings, so no subtitle file is needed. Put the words worth shouting
(the product, numbers, the claim) in KEYWORDS; ask Claude to pick them from the transcript. Needs ffmpeg,
faster-whisper and Pillow (pip install faster-whisper pillow); the first run downloads Whisper's base.en model
(about 140 MB).
"""
import json
import math
import subprocess
import sys
from itertools import groupby
from pathlib import Path
from faster_whisper import WhisperModel
from PIL import Image, ImageDraw, ImageFont
KEYWORDS = {"SERUM", "TWO", "WEEKS"} # words drawn big
SMALL = 0.075 # height of the other words, as a share of the frame width
BIG = 0.22 # height of the key words (shrinks so a line fits in 90% of the width)
Y = 0.70 # where the caption block's center sits (0 = top, 1 = bottom)
MAX_WORDS = 3 # most words in one phrase; a phrase also ends after its key words
POP = 0.15 # seconds a key word takes to grow from 60% to full size
STROKE = 0.08 # black outline, as a share of the text height
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 transcribe(path):
"""Every spoken word with its start and end time, uppercased and without punctuation."""
model = WhisperModel("base.en", device="cpu", compute_type="int8")
segments, _ = model.transcribe(path, word_timestamps=True)
return [{"text": w.word.strip().strip(".,!?;:\"").upper(), "start": w.start, "end": w.end}
for s in segments for w in s.words]
def phrases(words):
"""Split the words into phrases of up to MAX_WORDS, each ending after a run of key words."""
groups, current = [], []
for i, word in enumerate(words):
current.append(word)
nxt = words[i + 1]["text"] if i + 1 < len(words) else None
ends_keywords = word["text"] in KEYWORDS and nxt not in KEYWORDS
if ends_keywords or len(current) == MAX_WORDS or nxt is None:
groups.append(current)
current = []
return groups
def font(size):
return ImageFont.truetype(FONT_FILE, max(12, round(size)))
def lines_of(group):
"""Runs of key words and runs of other words each get a line: [(words, is_key, font), ...]."""
lines = []
for key, run in groupby(group, key=lambda word: word["text"] in KEYWORDS):
run = list(run)
text = " ".join(word["text"] for word in run)
size = w * (BIG if key else SMALL)
while font(size).getlength(text) > w * 0.9 and size > 12:
size -= 2
lines.append((run, key, font(size)))
return lines
def draw_phrase(draw, group, t):
lines = lines_of(group)
heights = [f.size * 1.15 for _, _, f in lines]
y = h * Y - sum(heights) / 2
for (line, key, f), height in zip(lines, heights):
space = f.getlength(" ")
x = (w - f.getlength(" ".join(word["text"] for word in line))) / 2
for word in line:
width = f.getlength(word["text"])
if t >= word["start"]:
grow = min(1.0, (t - word["start"]) / POP) if key else 1.0
g = f if grow >= 1 else font(f.size * (0.6 + 0.4 * (1 - (1 - grow) ** 2)))
draw.text((x + width / 2, y + height / 2), word["text"], font=g, anchor="mm", fill="white",
stroke_width=max(2, round(g.size * STROKE)), stroke_fill="black")
x += width + space
y += height
def overlay(t):
img = Image.new("RGBA", (w, h))
for group in groups:
if group[0]["start"] <= t < group[-1]["end"]:
draw_phrase(ImageDraw.Draw(img), group, t)
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]
groups = phrases(transcribe(src))
w, h, rate, fps, duration = probe(src)
render(src, out)





