How to add an ASCII art with Claude

At one second the shot turns into rows of typed characters on white paper, the bottle built from heavy dark purple @ and % signs while the brighter backdrop thins to plus signs, dashes and dots, all still moving with the camera.

Get this effect

  1. Switch to Code in your Claude Desktop App

  2. 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/skills
  3. Ask 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.

SettingDefaultWhat it does
START1.0Second the picture turns into characters.
COLUMNS45Characters 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.
INK0.4How bright the characters are (0 = black, 1 = the shot's colors at full strength).
SATURATION2.5How 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.mp4
Show 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")

Find 3x winning ads

Recharm cuts your ad footage into tagged, searchable clips: hooks, testimonials, product shots. Find the shots that win, add the effect with Claude, and ship 3x more ad edits.

Start 14-day free trial