How to add a CRT TV with Claude

The whole clip bulges onto a rounded glass tube inside a black bezel, striped with thin dark scanlines and softened by a hazy glow and slight color fringing, like footage playing on an old television.

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 a CRT TV to input.mp4
    How much the screen bulges; 0 is flat glass, 0.25 is a fishbowl: 0.12
    Radius of the screen's rounded corners, in pixels: 60

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 crt tv 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
CURVE0.12How much the screen bulges; 0 is flat glass, 0.25 is a fishbowl.
CORNER60Radius of the screen's rounded corners, in pixels.
EDGE6Pixels over which the picture fades into the black bezel.
SCANLINE4Rows per scanline: the last row of each is darkened.
SCANLINE_DARK0.4Brightness left in the dark row (1 hides the scanlines).
BLEED2Pixels red is pushed right and blue left, like a soft analog signal.
GLOW0.35How strongly a blurred copy is screened back over the picture (0-1).
VIGNETTE0.45How much the screen darkens toward its corners (0-1).
FLICKER0.04Frame-to-frame brightness wobble (0.04 = up to 4%).

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
Make input.mp4 look like it's playing on an old CRT television, from the first frame to the last, using Python with numpy and Pillow and ffmpeg for reading and writing frames. For each 540x960 frame: (1) Color bleed: shift the red channel 2 pixels right and the blue channel 2 pixels left. (2) Soften with a Gaussian blur of radius 1, then add a glow by screen-blending a radius-10 Gaussian-blurred copy at 0.35 strength. (3) Bulge it onto curved glass: with u and v running from -1 to 1 across and down the output, take each output pixel from the source at (u, v) * (1 + 0.12 * (u^2 + v^2)). Anywhere that lands outside the source frame is black bezel; give the screen 60-pixel rounded corners (measured in source pixels) and fade the edge to black over 6 pixels. This leaves the left and right edges black about 26 pixels in at mid-height and the top and bottom about 46 pixels in, with the bezel thicker toward the corners. (4) Darken toward the corners: multiply by 1 - 0.45 * (su^2 + sv^2) / 2, where su, sv are the warped source coordinates. (5) Scanlines: every 4th row of the output (rows 3, 7, 11...) is multiplied by 0.4, straight across the frame. (6) Flicker: multiply frame i's brightness by 1 + 0.04 * sin(2.3i) * sin(0.7i). Keep the 540x960 size, 30fps, 6-second length and the audio. Save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as crt-tv.py and run:

python3 crt-tv.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""CRT TV: the picture bulges onto a curved glass screen with dark scanlines, soft color bleed and a glow.

Usage: python3 crt-tv.py input.mp4 output.mp4
Needs ffmpeg, numpy and Pillow (pip install numpy pillow).
"""
import json
import subprocess
import sys

import numpy as np
from PIL import Image, ImageFilter

CURVE = 0.12         # how much the screen bulges; 0 is flat glass, 0.25 is a fishbowl
CORNER = 60          # radius of the screen's rounded corners, in pixels
EDGE = 6             # pixels over which the picture fades into the black bezel
SCANLINE = 4         # rows per scanline: the last row of each is darkened
SCANLINE_DARK = 0.4  # brightness left in the dark row (1 hides the scanlines)
BLEED = 2            # pixels red is pushed right and blue left, like a soft analog signal
GLOW = 0.35          # how strongly a blurred copy is screened back over the picture (0-1)
VIGNETTE = 0.45      # how much the screen darkens toward its corners (0-1)
FLICKER = 0.04       # frame-to-frame brightness wobble (0.04 = up to 4%)


def screen_map():
    """For every output pixel, the source pixel it shows through the curved glass, and how much of it is lit."""
    v, u = np.mgrid[-1:1:h * 1j, -1:1:w * 1j].astype(np.float32)
    bulge = 1 + CURVE * (u * u + v * v)
    su, sv = u * bulge, v * bulge
    xs = np.clip(((su + 1) / 2 * (w - 1)).round().astype(int), 0, w - 1)
    ys = np.clip(((sv + 1) / 2 * (h - 1)).round().astype(int), 0, h - 1)
    # distance inside the screen's rounded edge, in pixels: negative means outside, on the bezel
    qx = np.abs(su) * w / 2 - (w / 2 - CORNER)
    qy = np.abs(sv) * h / 2 - (h / 2 - CORNER)
    inside = CORNER - np.hypot(np.maximum(qx, 0), np.maximum(qy, 0)) - np.minimum(np.maximum(qx, qy), 0)
    lit = np.clip(inside / EDGE, 0, 1)
    lit *= 1 - VIGNETTE * (su * su + sv * sv) / 2
    lit *= np.where(np.arange(h)[:, None] % SCANLINE == SCANLINE - 1, SCANLINE_DARK, 1.0)
    return ys, xs, lit[..., None].astype(np.float32)


def crt(img, i):
    bled = img.copy()
    bled[:, BLEED:, 0] = img[:, :-BLEED, 0]
    bled[:, :-BLEED, 2] = img[:, BLEED:, 2]
    soft = np.asarray(Image.fromarray(bled).filter(ImageFilter.GaussianBlur(1)), np.float32)
    halo = np.asarray(Image.fromarray(bled).filter(ImageFilter.GaussianBlur(10)), np.float32)
    glowing = 255 - (255 - soft) * (255 - GLOW * halo) / 255
    flicker = 1 + FLICKER * np.sin(i * 2.3) * np.sin(i * 0.7)
    return np.clip(glowing[ys, xs] * lit * flicker, 0, 255).astype(np.uint8)


src, out = sys.argv[1:3]
info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                           "stream=width,height", "-of", "json", src]))["streams"][0]
w, h = info["width"], info["height"]
ys, xs, lit = screen_map()

reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-r", "30", "-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", "30", "-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 True:
    raw = reader.stdout.read(w * h * 3)
    if len(raw) < w * h * 3:
        break
    writer.stdin.write(crt(np.frombuffer(raw, np.uint8).reshape(h, w, 3), i).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