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 an emoji rain to input.mp4 The emoji that rains down (heart eyes): "\U0001F60D" Second the first emojis drop in at the top edge: 0.5
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 an emoji rain 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 |
|---|---|---|
EMOJI | "\U0001F60D" | The emoji that rains down (heart eyes). |
START | 0.5 | Second the first emojis drop in at the top edge. |
COUNT | 36 | Emojis in the rain. |
SIZE | (0.10, 0.19) | Emoji width range, as a share of the frame width. |
SPEED | (0.32, 0.50) | Fall speed range, in frame heights per second. |
ROCK | 18 | How far each emoji tilts either way as it falls, in degrees. |
SEED | 4 | Change for a different rain. |
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 an emoji rain to input.mp4 (540x960). Render the heart-eyes emoji (U+1F60D) once with Pillow from a color emoji font (Apple Color Emoji at size 160 on macOS, with embedded_color=True), crop it to its edges and reuse it for every drop. Rain 36 copies down the frame starting at 0.5 seconds: each is scaled to 10% to 19% of the frame width (54 to 103 pixels; bigger ones fall faster, 0.32 to 0.50 frame heights per second, and are drawn in front), spread evenly from 6% to 94% across, and queued evenly from 8% to 138% of the frame height above the top edge so they enter one after another. Rock each emoji up to 18 degrees either way over a 1.2 to 2.2 second cycle as it falls. An emoji that falls 15% past the bottom comes back in at the top, so the rain keeps going to the end. Nothing shows before 0.5 seconds, the lower half stays clear until after 1 second, and by 3 seconds emojis cover the whole frame while the product stays visible between them. Use Python with numpy and Pillow to draw it and ffmpeg to put it on the video (don't use ffmpeg's drawtext filter; draw any text or emoji with Pillow), keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as emoji-rain.py and run:
python3 emoji-rain.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Emoji rain: dozens of heart-eyes emojis pour down from the top of the frame, rocking as they fall.
Usage: python3 emoji-rain.py input.mp4 output.mp4
Needs ffmpeg, numpy and Pillow (pip install numpy pillow), and a color emoji font (built into macOS and Windows).
"""
import json
import math
import subprocess
import sys
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw, ImageFont
EMOJI = "\U0001F60D" # the emoji that rains down (heart eyes)
START = 0.5 # second the first emojis drop in at the top edge
COUNT = 36 # emojis in the rain
SIZE = (0.10, 0.19) # emoji width range, as a share of the frame width
SPEED = (0.32, 0.50) # fall speed range, in frame heights per second (bigger emojis fall faster)
ROCK = 18 # how far each emoji tilts either way as it falls, in degrees
SEED = 4 # change for a different rain
FONTS = [("/System/Library/Fonts/Apple Color Emoji.ttc", 160), # macOS (color emoji fonts come in fixed sizes)
("/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf", 109), # Linux
("C:/Windows/Fonts/seguiemj.ttf", 160)] # Windows
FONT_FILE, FONT_SIZE = next((f for f in FONTS if Path(f[0]).exists()), None) or sys.exit("No emoji font: add one to FONTS")
def emoji():
"""The emoji drawn once at full size, cropped to its edges."""
img = Image.new("RGBA", (FONT_SIZE * 2, FONT_SIZE * 2))
ImageDraw.Draw(img).text((FONT_SIZE // 2, FONT_SIZE // 2), EMOJI, embedded_color=True,
font=ImageFont.truetype(FONT_FILE, FONT_SIZE))
return img.crop(img.getbbox())
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"])
src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
face = emoji()
rng = np.random.default_rng(SEED)
drops = []
for k in range(COUNT): # emojis queue up evenly above the frame
depth = rng.uniform(0, 1) # 0 = far (small, slow), 1 = near (big, fast)
size = round((SIZE[0] + depth * (SIZE[1] - SIZE[0])) * w)
across = (k * 0.618 + rng.uniform(0, 0.1)) % 1 # golden-ratio steps spread them evenly across
drops.append({"sprite": face.resize((size, size), Image.LANCZOS),
"x": (0.06 + 0.88 * across) * w, "y": -(0.08 + 1.3 * k / COUNT) * h,
"speed": (SPEED[0] + depth * (SPEED[1] - SPEED[0])) * h,
"rock": rng.uniform(1.2, 2.2), "phase": rng.uniform(0, 2 * math.pi), "depth": depth})
drops.sort(key=lambda d: d["depth"]) # draw far emojis first so near ones cover them
def draw_rain(img, t):
"""Paste every emoji at its place at time t onto the frame."""
age = t - START
span = h * 1.3 # an emoji that falls off the bottom comes back in at the top
for d in drops:
y = d["y"] + d["speed"] * age
if y > h * 1.15:
y = (y + 0.15 * h) % span - 0.15 * h
angle = ROCK * math.sin(2 * math.pi * age / d["rock"] + d["phase"])
sprite = d["sprite"].rotate(angle, expand=True, resample=Image.BICUBIC)
img.paste(sprite, (round(d["x"] - sprite.width / 2), round(y - sprite.height / 2)), sprite)
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)
i = 0
while raw := reader.stdout.read(w * h * 3):
t = i / fps
if t >= START:
img = Image.frombytes("RGB", (w, h), raw)
draw_rain(img, t)
raw = img.tobytes()
writer.stdin.write(raw)
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





