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 space crawl text to input.mp4 Lines: [("EPISODE C", 0.065), ("THE GLOW AWAKENS", 0.085), ...]
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 space crawl 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 | [("EPISODE C", 0.065), ("THE GLOW AWAKENS", 0.085), ...] | The crawl's lines with their heights as a share of the frame width, top to bottom. |
FADE_START | 0.4 | Second the clip starts fading into space. |
FADE | 0.6 | Seconds the fade takes. |
CRAWL_START | 1.0 | Second the first line enters at the bottom. |
SPEED | 0.22 | How far the text slides up the plane each second, in frame widths. |
HORIZON | 0.20 | Where the plane vanishes, as a share of the frame height. |
TOP_WIDTH, BOTTOM_WIDTH | 0.24, 1.40 | Widths of the plane's far and near edges, in frame widths. |
DEPTH | 2.6 | Length of the visible plane, in frame widths. |
COLOR | (255, 205, 50) | Text color. |
STARS | 220 | Number of stars. |
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 space opening crawl to input.mp4 (540x960). Play the clip untouched until 0.4 seconds, then cross-fade it into a starfield between 0.4 and 1.0 seconds: a black frame with 220 small round stars at fixed random spots, radius 0.6 to 1.4 pixels, gray levels 120 to 255 with a slight blue tint. From 1.0 seconds, yellow (255, 205, 50) text in Arial Bold crawls up the screen on a tilted plane. Draw the text flat with Pillow on a transparent strip 540 pixels wide: 1404 pixels of empty space, then these lines centered, top-anchored, each line's slot 1.35 times its font size tall: "EPISODE C" (35 px), "THE GLOW AWAKENS" (46 px), an empty 35 px line, then "It is a period of dull,", "tired skin. A bright", "new serum, made in", "a hidden lab, has", "won its first battle", "against dark spots." (38 px each), then another 1404 pixels of empty space. Each frame, take a 540x1404 window of the strip starting 118.8 pixels further down for every second after 1.0 seconds (starting at the top), and warp it with OpenCV's perspective transform so its top edge lands from (205.2, 192) to (334.8, 192) and its bottom edge from (-108, 960) to (648, 960): text enters at the bottom wider than the frame and shrinks toward a vanishing line 20% down. Fade the text's opacity from 0 at y = 192 to full at y = 403 so it dissolves near the horizon. Composite it over the starfield. Use Python with Pillow to draw the text (don't rely on ffmpeg's drawtext filter), OpenCV and numpy for the warp, ffmpeg to encode, 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 space-crawl-text.py and run:
python3 space-crawl-text.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Space crawl text: the clip fades into a starfield and yellow text scrolls up and away into the distance on a tilted plane.
Usage: python3 space-crawl-text.py input.mp4 output.mp4
Needs ffmpeg, Pillow, OpenCV and numpy (pip install pillow opencv-python-headless numpy).
"""
import json
import math
import random
import subprocess
import sys
from pathlib import Path
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont
LINES = [("EPISODE C", 0.065), # (text, height as a share of the frame width), top to bottom
("THE GLOW AWAKENS", 0.085),
("", 0.065),
("It is a period of dull,", 0.070),
("tired skin. A bright", 0.070),
("new serum, made in", 0.070),
("a hidden lab, has", 0.070),
("won its first battle", 0.070),
("against dark spots.", 0.070)]
FADE_START = 0.4 # second the clip starts fading into space
FADE = 0.6 # seconds the fade takes
CRAWL_START = 1.0 # second the first line enters at the bottom
SPEED = 0.22 # how far the text slides up the plane each second, in frame widths
HORIZON = 0.20 # where the plane vanishes, as a share of the frame height
TOP_WIDTH = 0.24 # width of the plane's far edge, as a share of the frame width
BOTTOM_WIDTH = 1.40 # width of the plane's near edge (wider than the frame, so lines run off the sides)
DEPTH = 2.6 # length of the visible plane, in frame widths
COLOR = (255, 205, 50) # crawl yellow
STARS = 220 # number of stars
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 crawl_strip():
"""The text drawn flat on a tall transparent strip as wide as the frame, with a plane's length of empty space above
and below so it can enter at the near edge and leave at the far one."""
depth = round(w * DEPTH)
heights = [round(w * size * 1.35) for _, size in LINES]
strip = Image.new("RGBA", (w, depth * 2 + sum(heights)))
draw = ImageDraw.Draw(strip)
y = depth
for (line, size), line_h in zip(LINES, heights):
draw.text((w / 2, y), line, font=ImageFont.truetype(FONT_FILE, round(w * size)), anchor="mt", fill=COLOR)
y += line_h
return np.array(strip)
def starfield():
"""Black sky with small white and pale blue stars at fixed random spots."""
sky = Image.new("RGB", (w, h))
draw = ImageDraw.Draw(sky)
rng = random.Random(7)
for _ in range(STARS):
x, y, r = rng.uniform(0, w), rng.uniform(0, h), rng.choice([0.6, 0.8, 1.0, 1.4])
level = rng.randint(120, 255)
draw.ellipse([x - r, y - r, x + r, y + r], fill=(level, level, min(255, level + 20)))
return np.array(sky).astype(np.float32)
def plane():
"""Perspective warp from the strip's visible window onto a trapezoid narrowing toward the horizon, and a mask
that fades the text out as it nears the horizon."""
depth = round(w * DEPTH)
window = np.float32([[0, 0], [w, 0], [w, depth], [0, depth]])
top, bottom = w * TOP_WIDTH / 2, w * BOTTOM_WIDTH / 2
trapezoid = np.float32([[w / 2 - top, h * HORIZON], [w / 2 + top, h * HORIZON],
[w / 2 + bottom, h], [w / 2 - bottom, h]])
rows = np.arange(h, dtype=np.float32)[:, None, None]
fade = np.clip((rows - h * HORIZON) / (h * 0.22), 0, 1)
return cv2.getPerspectiveTransform(window, trapezoid), fade
def frame_at(t, video):
"""The clip faded into the starfield, with the strip's window slid SPEED frame widths further each second."""
fade = min(max((t - FADE_START) / FADE, 0), 1)
img = video * (1 - fade) + sky * fade
if t >= CRAWL_START:
top = round((t - CRAWL_START) * SPEED * w)
window = strip[top:top + round(w * DEPTH)]
text = cv2.warpPerspective(window, warp, (w, h), flags=cv2.INTER_AREA).astype(np.float32)
alpha = text[:, :, 3:] / 255 * horizon_fade
img = img * (1 - alpha) + text[:, :, :3] * alpha
return img.astype(np.uint8)
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 src frame by frame, draw frame_at(t) and save to out, keeping 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
video = np.frombuffer(raw, np.uint8).reshape(h, w, 3).astype(np.float32)
writer.stdin.write(frame_at(i / fps, video).tobytes())
writer.stdin.close()
reader.stdout.close()
if writer.wait() or reader.wait():
sys.exit("ffmpeg failed")
src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
strip, sky = crawl_strip(), starfield()
warp, horizon_fade = plane()
render(src, out)





