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
Make a color match from input.mp4 and reference.mp4 Reference (second argument): reference.mp4
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 color match 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 |
|---|---|---|
reference (second argument) | reference.mp4 | The shot whose look you want. A clip or a still frame (.jpg or .png) both work. |
SAMPLES | 12 | Frames measured from each clip, spread evenly through it. More gives a steadier average on shots that change a lot. |
STRENGTH | 1.0 | How far to go toward the reference. 1 matches it fully, 0.5 goes halfway, 0 leaves the clip as it is. |
LUT_SIZE | 33 | Grid points per axis in the .cube file the correction is baked into. The .cube is saved next to the output, so you can reuse it in any editor. |
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
Color match input.mp4 to reference.mp4 so the two shots cut together. Use Reinhard color transfer in Lab: take 12 frames spread evenly through each clip, convert them to Lab with OpenCV (float RGB 0-1 into cv2.COLOR_RGB2LAB) and work out the mean and standard deviation of L, a and b over all those pixels. For every color, the matched Lab value is (value - input mean) / input std * reference std + reference mean, per channel. Use one correction for the whole clip, not one per frame, so it can't flicker: bake it into a 33x33x33 .cube LUT (LUT_3D_SIZE 33, red changing fastest, then green, then blue, values clipped to 0-1) and apply it with ffmpeg's lut3d filter (interp=tetrahedral) to every frame from first to last. The dull gray backdrop, around RGB (150, 156, 158), should come out the reference's pale icy blue, around RGB (196, 204, 216), the whole frame about 1.3 times as bright (around 190 out of 255, like the reference), and the purple glass should keep its color as it brightens, from around RGB (80, 73, 87) to around RGB (121, 112, 143), rather than going gray. Don't crop, zoom or add anything else, and don't add any text (if you ever do, draw it with Pillow, not ffmpeg's drawtext). Use Python with opencv-python-headless and numpy for the LUT and ffmpeg for the video. Keep the 540x960 size, 30fps, the 6 second length and the audio, encode H.264 yuv420p and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as color-match.py and run:
python3 color-match.py input.mp4 reference.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Color match: regrade a clip so its brightness, contrast and color cast match a reference shot.
Usage: python3 color-match.py input.mp4 reference.mp4 output.mp4 (the reference can also be a .jpg or .png)
Needs ffmpeg and Python 3 with OpenCV and numpy (pip install opencv-python-headless numpy).
It measures both shots in Lab color (Reinhard color transfer), bakes the correction into a .cube LUT
next to the output and applies that to every frame with ffmpeg, so the grade can't flicker.
"""
import subprocess
import sys
from pathlib import Path
import cv2
import numpy as np
SAMPLES = 12 # frames measured from each clip (spread evenly through it)
STRENGTH = 1.0 # 1 matches the reference fully, 0.5 goes halfway, 0 leaves the clip as it is
LUT_SIZE = 33 # grid points per axis in the .cube file
def lab_stats(path):
"""Mean and spread of L, a and b over frames sampled evenly through a clip (or one image)."""
count = subprocess.run(["ffprobe", "-v", "error", "-count_packets", "-select_streams", "v:0", "-show_entries",
"stream=nb_read_packets", "-of", "csv=p=0", path], capture_output=True, text=True).stdout
step = max(1, int(count.strip() or 1) // SAMPLES)
raw = subprocess.check_output(["ffmpeg", "-v", "error", "-i", path, "-vf", f"select='not(mod(n,{step}))',scale=270:-2",
"-fps_mode", "passthrough", "-frames:v", str(SAMPLES), "-f", "rawvideo", "-pix_fmt", "rgb24", "-"])
pixels = np.frombuffer(raw, np.uint8).reshape(1, -1, 3).astype(np.float32) / 255
lab = cv2.cvtColor(pixels, cv2.COLOR_RGB2LAB).reshape(-1, 3)
return lab.mean(axis=0), lab.std(axis=0)
def write_lut(path, src, ref):
"""A .cube file that moves each Lab channel from the source's mean and spread to the reference's."""
(src_mean, src_std), (ref_mean, ref_std) = src, ref
steps = np.linspace(0, 1, LUT_SIZE, dtype=np.float32)
b, g, r = np.meshgrid(steps, steps, steps, indexing="ij") # red changes fastest in a .cube file
rgb = np.stack([r, g, b], axis=-1).reshape(1, -1, 3)
lab = cv2.cvtColor(rgb, cv2.COLOR_RGB2LAB)
matched = (lab - src_mean) * (ref_std / np.maximum(src_std, 1e-3)) + ref_mean
lab = lab + STRENGTH * (matched - lab)
out = np.clip(cv2.cvtColor(lab.astype(np.float32), cv2.COLOR_LAB2RGB), 0, 1).reshape(-1, 3)
lines = ['TITLE "color match"', f"LUT_3D_SIZE {LUT_SIZE}"] + [f"{r:.6f} {g:.6f} {b:.6f}" for r, g, b in out]
Path(path).write_text("\n".join(lines) + "\n")
src, ref, out = sys.argv[1:4]
lut = str(Path(out).with_name("color-match.cube"))
write_lut(lut, lab_stats(src), lab_stats(ref))
subprocess.run(["ffmpeg", "-y", "-v", "error", "-i", src, "-vf", f"lut3d=file='{lut}':interp=tetrahedral",
"-r", "30", "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "copy", out], check=True)





