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 charcoal drawing to input.mp4 Second the picture turns into a charcoal drawing: 1.0 Paper color: (236, 232, 222)
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 charcoal drawing 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 |
|---|---|---|
START | 1.0 | Second the picture turns into a charcoal drawing. |
PAPER | (236, 232, 222) | Paper color. |
CHARCOAL | (24, 22, 22) | Color of the densest charcoal. |
OUTLINE | 5 | Outline thickness in pixels. |
SHADE | 0.7 | Power on the shading. Higher leaves more of the mid-tones as bare paper; lower fills them in. |
SMUDGE | 21 | Length in pixels of the diagonal smear on the shading. Longer looks more rubbed in. |
GRAIN | 0.45 | How much the paper's tooth breaks up the charcoal, 0 (solid) to 1 (very patchy). |
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
Turn input.mp4 (540x960) into a charcoal drawing from 1.0 seconds to the end; before 1.0 seconds leave it untouched. For each frame from 1.0 s, using Python with numpy and OpenCV: convert to grayscale and Gaussian-blur it (sigma 2). Shading: stretch the brightness so the 2nd percentile is 0 and the 90th percentile is 1, take darkness = (1 - brightness) ** 0.7, and smear it along a 21 pixel diagonal line running bottom-left to top-right (average along that line). Multiply the shading by a fixed stroke texture: random noise (seed it so it doesn't change between frames), Gaussian-blurred with sigma 2, smeared along the same 21 pixel diagonal, normalized to 0-1 and mapped to 0.55-1.3. Outlines: Canny edges with thresholds 30 and 90, dilated with a 5 pixel round brush, then the lines blurred with sigma 1.5 plus half of the lines blurred with sigma 6 as a soft smudge around them. Charcoal amount = the larger of outlines and textured shading, times (1 - 0.45 x paper grain), where the paper grain is fixed random noise blurred with sigma 1 and normalized to 0-1; clip to 0-1. Mix the paper color (236, 232, 222) toward charcoal (24, 22, 22) by that amount. No color from the original should be left. Use ffmpeg to read and write the video (don't use ffmpeg's drawtext if you add any text), keep the size, 30fps, the 6 second length and the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as charcoal-drawing.py and run:
python3 charcoal-drawing.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Charcoal drawing: at one second the shot is redrawn as thick black outlines and smudged diagonal shading on grainy off-white paper.
Usage: python3 charcoal-drawing.py input.mp4 output.mp4
Needs ffmpeg, numpy and OpenCV for the edges, smudging and paper grain (pip install numpy opencv-python-headless).
"""
import json
import subprocess
import sys
import cv2
import numpy as np
START = 1.0 # second the picture turns into a charcoal drawing
PAPER = (236, 232, 222) # off-white paper color
CHARCOAL = (24, 22, 22) # color of the densest charcoal
OUTLINE = 5 # outline thickness in pixels
SHADE = 0.7 # power on the shading; higher leaves more of the mid-tones as bare paper
SMUDGE = 21 # length in pixels of the diagonal smear on the shading
GRAIN = 0.45 # how much the paper's tooth breaks up the charcoal (0 = solid, 1 = very patchy)
def charcoal(img):
"""Redraw one frame (h, w, 3, RGB) in charcoal: dark outlines plus smudged shading, broken up by paper grain."""
gray = cv2.GaussianBlur(cv2.cvtColor(img, cv2.COLOR_RGB2GRAY), (0, 0), 2)
lo, hi = np.percentile(gray, (2, 90))
tone = np.clip((gray.astype(np.float32) - lo) / max(hi - lo, 1), 0, 1) # 0 = darkest, 1 = brightest
shading = cv2.filter2D((1 - tone) ** SHADE, -1, smear) # dark areas, smeared diagonally
edges = cv2.Canny(gray, 30, 90)
lines = cv2.dilate(edges, pen).astype(np.float32) / 255
outlines = cv2.GaussianBlur(lines, (0, 0), 1.5) + 0.5 * cv2.GaussianBlur(lines, (0, 0), 6) # stroke plus soft smudge
ink = np.maximum(outlines, shading * strokes) * (1 - GRAIN * tooth) # charcoal skips the paper's tooth
ink = np.clip(ink, 0, 1)[..., None]
return (paper * (1 - ink) + coal * ink).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", "-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)
src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
paper, coal = np.array(PAPER, np.float32), np.array(CHARCOAL, np.float32)
pen = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (OUTLINE, OUTLINE))
smear = np.eye(SMUDGE, dtype=np.float32)[::-1] / SMUDGE # a "/" line kernel
noise = np.random.default_rng(1).random((h, w), np.float32)
streaks = cv2.filter2D(cv2.GaussianBlur(noise, (0, 0), 2), -1, smear)
strokes = 0.55 + 0.75 * cv2.normalize(streaks, None, 0, 1, cv2.NORM_MINMAX) # diagonal stroke texture in the shading
tooth = cv2.normalize(cv2.GaussianBlur(noise, (0, 0), 1.0), None, 0, 1, cv2.NORM_MINMAX)
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):
img = np.frombuffer(raw, np.uint8).reshape(h, w, 3)
if i / fps >= START:
img = charcoal(img)
writer.stdin.write(img.tobytes())
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





