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 ASCII art to input.mp4 Second the picture turns into characters: 1.0 Characters across the frame: 45
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 ascii art 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 characters. |
COLUMNS | 45 | Characters across the frame. Fewer gives bigger, chunkier characters. |
RAMP | "@%#*+=-:. " | Characters for the darkest to the brightest parts of the shot. |
PAPER | (255, 255, 255) | Background color. |
INK | 0.4 | How bright the characters are (0 = black, 1 = the shot's colors at full strength). |
SATURATION | 2.5 | How much more colorful the characters are than the shot (1 = same). |
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 moving ASCII art from 1.0 seconds to the end; before 1.0 seconds the video must be untouched. Split each frame into a grid of cells 12 pixels wide and 19 pixels tall (45 across, 50 down, leaving a 10 pixel white strip at the bottom) and average each cell's color. Rank the cells by brightness (0.299 R + 0.587 G + 0.114 B) within the frame and split the ranks into 10 equal groups, so every character gets used: the darkest tenth gets "@", then "%", "#", "*", "+", "=", "-", ":", ".", and the brightest tenth a space. Draw each character in Menlo Bold 16 pixels, centered in its cell, on a pure white (255, 255, 255) background that fills the whole frame. Color each character with its cell's color made 2.5 times as saturated (gray + 2.5 x (color - gray), where gray is the cell's brightness) and then scaled so its strongest channel is 102 (40% of 255). Redraw every frame so the characters follow the moving shot. Use Python with Pillow to draw the characters and numpy to build the frames, with ffmpeg reading and writing the video (don't rely on ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as ascii-art.py and run:
python3 ascii-art.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""ASCII art: at one second the shot is redrawn as colored text characters on white, heavier characters where it's darker.
Usage: python3 ascii-art.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import subprocess
import sys
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw, ImageFont
START = 1.0 # second the picture turns into characters
COLUMNS = 45 # characters across the frame
RAMP = "@%#*+=-:. " # characters for the darkest to the brightest parts of the shot
PAPER = (255, 255, 255) # background color
INK = 0.4 # how bright the characters are (0 = black, 1 = the shot's colors at full strength)
SATURATION = 2.5 # how much more colorful the characters are than the shot (1 = same)
FONTS = [("/System/Library/Fonts/Menlo.ttc", 1), # macOS (index 1 is Menlo Bold)
("/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf", 0), # Linux
("C:/Windows/Fonts/consolab.ttf", 0)] # Windows
FONT_FILE, FONT_INDEX = next((f for f in FONTS if Path(f[0]).exists()), None) or sys.exit("No monospace font found: add one to FONTS")
def glyphs(cw, ch):
"""Every character of RAMP as a grayscale cell (0-1), shape (len(RAMP), ch, cw)."""
font = ImageFont.truetype(FONT_FILE, round(ch * 0.85), index=FONT_INDEX)
cells = []
for c in RAMP:
img = Image.new("L", (cw, ch))
ImageDraw.Draw(img).text((cw / 2, ch / 2), c, font=font, anchor="mm", fill=255)
cells.append(np.asarray(img, np.float32) / 255)
return np.stack(cells)
def ascii_frame(img):
"""Redraw one frame (h, w, 3) as characters: each cell's brightness picks the character, its color inks it."""
cells = img[:rows * ch, :cols * cw].reshape(rows, ch, cols, cw, 3).mean(axis=(1, 3))
luma = cells @ np.array([0.299, 0.587, 0.114], np.float32)
rank = luma.argsort(axis=None).argsort().reshape(luma.shape) / luma.size # 0-1, so every character gets used
shape = atlas[(rank * len(RAMP)).astype(int)] # (rows, cols, ch, cw)
gray = luma[..., None]
color = np.maximum(gray + (cells - gray) * SATURATION, 0)
color *= 255 * INK / np.maximum(color.max(axis=2, keepdims=True), 1)
alpha = shape[..., None]
pic = paper * (1 - alpha) + color[:, :, None, None, :] * alpha
out = np.broadcast_to(paper, img.shape).copy()
out[:rows * ch, :cols * cw] = pic.transpose(0, 2, 1, 3, 4).reshape(rows * ch, cols * cw, 3)
return out
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)
cw = w // COLUMNS
ch = round(cw * 1.6)
cols, rows = w // cw, h // ch
atlas = glyphs(cw, ch)
paper = np.array(PAPER, np.float32)
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).astype(np.float32)
if i / fps >= START:
img = ascii_frame(img)
writer.stdin.write(np.clip(img, 0, 255).astype(np.uint8).tobytes())
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





