---
title: "How to add outlined captions with Claude (tested prompt and script)"
description: "Each spoken phrase appears in big white letters wrapped in a thick black outline, with no box behind them. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/outlined-captions
---

# How to add outlined captions with Claude

Each spoken phrase appears in big white letters wrapped in a thick black outline, with no box behind them.

Made by the script on this page, from our own test clip.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   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 outlined captions to input.mp4 using the word timings in words.json
   Fill: white
   Edge: black
   ```

## 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 outlined captions 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                                              |
| --------- | ------- | --------------------------------------------------------- |
| `SIZE`    | `0.085` | Text height as a share of the frame width.                |
| `OUTLINE` | `0.14`  | Outline thickness as a share of the text height.          |
| `Y`       | `0.72`  | Where the text's center sits, from top (0) to bottom (1). |
| `CHUNK`   | `4`     | Words per phrase.                                         |
| `FILL`    | `white` | Text color.                                               |
| `EDGE`    | `black` | Outline color.                                            |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Add captions to input.mp4 using the word timings in words.json. Show four words at a time in white bold text (Arial Black) with a thick black (0, 0, 0) outline, the text 8.5% of the frame width tall and the outline 14% of the text height thick, with no box behind it. Center each phrase horizontally, 72% of the way down the frame, shrinking the text if needed so the phrase fits in 90% of the width, and show it from the group's first word's start to its last word's end. Use Python with Pillow to draw the text (its stroke_width and stroke_fill options make the outline; don't use ffmpeg's drawtext filter) and ffmpeg to put it on the video, keep the audio, and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `outlined-captions.py` and run:

```
python3 outlined-captions.py input.mp4 words.json output.mp4
```

```
#!/usr/bin/env python3
"""Outlined captions: each phrase in white text with a thick black outline, on screen while it is spoken.

Usage: python3 outlined-captions.py input.mp4 words.json output.mp4
words.json holds word timings, for example from Whisper:
  [{"word": "This", "start": 0.30, "end": 0.69}, {"word": "serum", "start": 0.77, "end": 1.13}, ...]
Needs ffmpeg and Pillow (pip install pillow).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

SIZE = 0.085     # text height as a share of the frame width (shrinks to fit long phrases)
OUTLINE = 0.14   # outline thickness as a share of the text height
Y = 0.72         # where the text's center sits (0 = top, 1 = bottom)
CHUNK = 4        # words per phrase
FILL = "white"   # text color
EDGE = "black"   # outline color
FONTS = ["/System/Library/Fonts/Supplemental/Arial Black.ttf",    # macOS
         "/System/Library/Fonts/Supplemental/Arial Bold.ttf",     # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/ariblk.ttf"]                           # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")

fonts = {}

def fitted_font(text):
    """The largest font up to SIZE that fits the phrase, outline included, in 90% of the frame width."""
    size = round(w * SIZE)
    while size > 12:
        font = fonts.get(size) or fonts.setdefault(size, ImageFont.truetype(FONT_FILE, size))
        if font.getlength(text) + 2 * size * OUTLINE <= w * 0.9:
            return font
        size -= 2
    return font

def overlay(t):
    img = Image.new("RGBA", (w, h))
    for group in (words[i:i + CHUNK] for i in range(0, len(words), CHUNK)):
        if group[0]["start"] <= t < group[-1]["end"]:
            text = " ".join(word["word"].strip() for word in group)
            font = fitted_font(text)
            ImageDraw.Draw(img).text((w / 2, h * Y), text, font=font, anchor="mm", fill=FILL,
                                     stroke_width=round(font.size * OUTLINE), stroke_fill=EDGE)
    return img

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate:format=duration", "-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), float(info["format"]["duration"])

def render(src, out):
    """Draw overlay(t) on every frame of src and save to out, keeping the audio."""
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
                           "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-filter_complex", "[0:v][1:v]overlay=shortest=1,format=yuv420p[v]",
                           "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
                          stdin=subprocess.PIPE)
    for i in range(math.ceil(duration * fps)):
        ff.stdin.write(overlay(i / fps).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")

src, words_file, out = sys.argv[1:4]
words = json.loads(Path(words_file).read_text())
w, h, rate, fps, duration = probe(src)
render(src, out)
```

## More captions and text

- [AI chat reply animation **AI chat reply animation**](https://www.recharm.com/effects/ai-chat-typing)
- [Animated bar chart **Animated bar chart**](https://www.recharm.com/effects/animated-bar-chart)
- [Animated checklist **Animated checklist**](https://www.recharm.com/effects/animated-checklist)
- [Animated line graph **Animated line graph**](https://www.recharm.com/effects/animated-line-graph)
- [Animated pie chart **Animated pie chart**](https://www.recharm.com/effects/animated-pie-chart)

## 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](https://www.recharm.com/get-started)
