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 scrolling lyrics to input.mp4 using the word timings in words.json Words on one lyric line: 2 Text height as a share of the frame width: 0.12
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 scrolling lyrics 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 |
|---|---|---|
LINE_WORDS | 2 | Words on one lyric line. |
SIZE | 0.12 | Text height as a share of the frame width. |
X | 0.08 | Left edge of the lines, as a share of the frame width. |
Y | 0.42 | Where the sung line's center sits, from 0 (top) to 1 (bottom). |
PITCH | 1.35 | Space between line centers, in text heights. |
SCROLL | 0.4 | Seconds the sheet takes to scroll up to the next line. |
LIT | 0.25 | Seconds a line takes to brighten or dim. |
DIM_TEXT | 0.4 | Opacity of the lines not being sung. |
HOLD | 0.8 | Seconds the last line stays bright after its last word. |
DIM | 0.6 | How much the footage darkens behind the lyrics. |
BLUR | 10 | Blur of the footage behind the lyrics (Gaussian sigma, pixels). |
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 scrolling lyrics view to input.mp4 (540x960), using the word timings in words.json (a list of {"word", "start", "end"} in seconds). Group the words into lines of 2 words; a line is sung from its first word's start to its last word's end. For the whole clip, blur the footage (Gaussian, sigma 10 pixels) and darken it with a 60% black layer. Show every line at once, left-aligned 43 pixels from the left edge, in white Arial Black (or another heavy bold font) 65 pixels tall, line centers 88 pixels apart. Lines not being sung are white at 40% opacity. The sheet starts with the first line's center 42% down the frame (y = 403); when each later line starts, the sheet scrolls up by one line (88 pixels) over 0.4 seconds with a smoothstep ease, so the sung line always ends up centered at y = 403. A line brightens to fully opaque white over 0.25 seconds from its start and dims back to 40% over the 0.25 seconds before the next line starts; the last line dims over the 0.25 seconds before 0.8 seconds after its last word, after which every line is dim. Draw the text with Python and Pillow (don't use ffmpeg's drawtext filter), put it on the video with ffmpeg, keep the audio and the 6 second length at 30fps, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as scrolling-lyrics.py and run:
python3 scrolling-lyrics.py input.mp4 words.json output.mp4Show the script
#!/usr/bin/env python3
"""Scrolling lyrics: the whole lyric sheet sits over blurred footage and scrolls up line by line, the sung line bright white and the rest dim.
Usage: python3 scrolling-lyrics.py input.mp4 words.json output.mp4
words.json is a list of {"word", "start", "end"} in seconds (from an LRC file, or faster-whisper with word_timestamps).
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
LINE_WORDS = 2 # words on one lyric line
SIZE = 0.12 # text height as a share of the frame width
X = 0.08 # left edge of the lines, as a share of the frame width
Y = 0.42 # where the sung line's center sits (0 = top, 1 = bottom)
PITCH = 1.35 # space between line centers, in text heights
SCROLL = 0.4 # seconds the sheet takes to scroll up to the next line
LIT = 0.25 # seconds a line takes to brighten or dim
DIM_TEXT = 0.4 # opacity of the lines not being sung
HOLD = 0.8 # seconds the last line stays bright after its last word
DIM = 0.6 # how much the footage darkens behind the lyrics
BLUR = 10 # blur of the footage behind the lyrics (Gaussian sigma, pixels)
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 lyric_lines(path):
"""(start, end, text) for each line of LINE_WORDS words."""
words = json.loads(Path(path).read_text())
groups = [words[i:i + LINE_WORDS] for i in range(0, len(words), LINE_WORDS)]
return [(g[0]["start"], g[-1]["end"], " ".join(x["word"] for x in g)) for g in groups]
def line_image(text):
font = ImageFont.truetype(FONT_FILE, round(w * SIZE))
img = Image.new("RGBA", (w, round(w * SIZE * 1.6)))
ImageDraw.Draw(img).text((round(w * X), img.height / 2), text, font=font, anchor="lm", fill="white")
return img
def ease(p):
p = min(max(p, 0), 1)
return p * p * (3 - 2 * p)
def overlay(t):
img = Image.new("RGBA", (w, h), (0, 0, 0, round(255 * DIM)))
pos = sum(ease((t - start) / SCROLL) for start, _, _ in lines[1:]) # how many lines have scrolled past
for i, (start, end, art) in enumerate(lines):
until = lines[i + 1][0] if i + 1 < len(lines) else end + HOLD
lit = min(ease((t - start) / LIT), ease((until - t) / LIT))
layer = art.copy()
layer.putalpha(art.getchannel("A").point(lambda a: a * (DIM_TEXT + (1 - DIM_TEXT) * lit)))
y = h * Y + (i - pos) * w * SIZE * PITCH
img.alpha_composite(layer, (0, round(y - art.height / 2)))
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):
"""Blur src, draw overlay(t) on every frame 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", f"[0:v]gblur=sigma={BLUR}[bg];[bg][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, words, out = sys.argv[1:4]
w, h, rate, fps, duration = probe(src)
lines = [(start, end, line_image(text)) for start, end, text in lyric_lines(words)]
render(src, out)





