---
title: "How to add an eye blink transition with Claude (tested prompt and script)"
description: "Black eyelids with curved edges sweep down and up over clip A until the screen goes dark, then part again to reveal clip B, like the viewer blinking. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/eye-blink-transition
---

# How to add an eye blink transition with Claude

Black eyelids with curved edges sweep down and up over clip A until the screen goes dark, then part again to reveal clip B, like the viewer blinking.

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

## 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

   ```
   Join clipA.mp4 and clipB.mp4 with an eye blink transition
   Seconds the eyelids take to shut over clip A: 0.3
   Seconds the screen stays black between the clips: 0.1
   ```

## 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.

Put a sale badge on **@clip1.mp4**, give **@clip2.mp4** a VHS look, then join them with an eye blink transition.

## 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                                                                 |
| ------- | ------- | ---------------------------------------------------------------------------- |
| `CLOSE` | `0.3`   | Seconds the eyelids take to shut over clip A.                                |
| `SHUT`  | `0.1`   | Seconds the screen stays black between the clips.                            |
| `OPEN`  | `0.4`   | Seconds the eyelids take to open onto clip B.                                |
| `CURVE` | `0.45`  | How much the lids dip toward the sides of the frame; 0 gives straight edges. |
| `SOFT`  | `10`    | Blur on the lid edges, in pixels.                                            |

## Without the skill

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

```
Join clipA.mp4 and clipB.mp4 with a 0.8 second eye blink transition starting 0.8 seconds before A ends (so the result is 11.2 seconds for two 6 second clips). Build the blink frames with Python, numpy and Pillow. The picture shows through a gap between two black eyelids: at column x, with dx = (x - (width-1)/2) / (width/2) running from -1 to 1, a pixel is visible when its distance from the frame's vertical center is less than o * G * (1 - 0.45*dx^2), where G = (height/2 + 20) / (1 - 0.45) so the frame is fully clear at o = 1. Blur that mask with a 10 pixel Gaussian blur so the lid edges are soft, and multiply the frame by it. Over the first 0.3 seconds the lids close over clip A (o = 1 - s(p), p from 0 to 1, with smoothstep s(p) = p*p*(3-2p)); for the next 0.1 seconds the screen is fully black; over the last 0.4 seconds they open onto clip B (o = s(p)), with B playing from its first frame at the start of the transition. Don't use ffmpeg's drawtext. Use ffmpeg for the rest: match B's size and frame rate to A (30fps), crossfade the audio over the same 0.8 seconds, keep the audio and save it as output.mp4 (H.264, yuv420p).
```

## Run it without Claude

This is the exact script we tested. Save it as `eye-blink-transition.py` and run:

```
python3 eye-blink-transition.py clipA.mp4 clipB.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Eye blink: black eyelids with curved edges close over clip A, stay shut a moment, then open onto clip B.

Usage: python3 eye-blink-transition.py clipA.mp4 clipB.mp4 output.mp4  (both clips need an audio track)
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import subprocess
import sys

import numpy as np
from PIL import Image, ImageFilter

CLOSE = 0.3       # seconds the lids take to shut over clip A
SHUT = 0.1        # seconds the screen stays black
OPEN = 0.4        # seconds the lids take to open onto clip B
CURVE = 0.45      # how much the lids dip toward the corners (0 = straight edges)
SOFT = 10         # blur on the lid edges, in pixels
FPS = 30

a_path, b_path, out_path = sys.argv[1:4]
info = json.loads(subprocess.run(
    ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height:format=duration",
     "-of", "json", a_path], capture_output=True, check=True).stdout)
w, h = info["streams"][0]["width"], info["streams"][0]["height"]
D = CLOSE + SHUT + OPEN
offset = float(info["format"]["duration"]) - D
n = round(D * FPS)

# the gap between the lids is widest in the middle and narrows toward the sides, like an eye;
# fully open, it is tall enough that even the corners (plus the blurred edge) are clear
dx = (np.arange(w) - (w - 1) / 2) / (w / 2)
shape = 1 - CURVE * dx ** 2
widest = (h / 2 + 2 * SOFT) / (1 - CURVE)
rows = np.abs(np.arange(h) - (h - 1) / 2)[:, None]

def frames(path, start):
    """The n frames from `start`, fitted to A's size, as RGB arrays."""
    raw = subprocess.run(
        ["ffmpeg", "-v", "error", "-ss", str(start), "-i", path, "-frames:v", str(n),
         "-vf", f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS}",
         "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], capture_output=True, check=True).stdout
    return np.frombuffer(raw, np.uint8).reshape(-1, h, w, 3)

def smoothstep(p):
    return p * p * (3 - 2 * p)

def openness(t):
    """1 = eye fully open, 0 = shut."""
    if t < CLOSE:
        return 1 - smoothstep(t / CLOSE)
    if t < CLOSE + SHUT:
        return 0.0
    return smoothstep((t - CLOSE - SHUT) / OPEN)

def blink_frame(picture, o):
    gap = Image.fromarray(np.where(rows < o * widest * shape, 255, 0).astype(np.uint8))
    gap = np.asarray(gap.filter(ImageFilter.GaussianBlur(SOFT)), np.float32)[:, :, None] / 255
    return (picture * gap).astype(np.uint8).tobytes()

tail, head = frames(a_path, offset), frames(b_path, 0)
middle = b"".join(blink_frame(tail[i] if i / FPS < CLOSE + SHUT else head[i], openness(i / FPS)) for i in range(n))

# A up to the blink, then the blink frames from stdin, then the rest of B; the sound crossfades underneath
subprocess.run(
    ["ffmpeg", "-y", "-v", "error", "-i", a_path, "-i", b_path,
     "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", str(FPS), "-i", "-",
     "-filter_complex",
     f"[0:v]fps={FPS},trim=end_frame={round(offset * FPS)},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[a];"
     f"[2:v]format=yuv420p,setsar=1[m];"
     f"[1:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS},"
     f"trim=start_frame={n},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[b];"
     f"[a][m][b]concat=n=3:v=1:a=0[v];[0:a][1:a]acrossfade=d={D}[au]",
     "-map", "[v]", "-map", "[au]", "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "aac", out_path],
    input=middle, check=True)
```

## More transitions

- [Barn door transition **Barn door transition**](https://www.recharm.com/effects/barn-door)
- [Blinds transition **Blinds transition**](https://www.recharm.com/effects/blinds-transition)
- [Block dissolve **Block dissolve**](https://www.recharm.com/effects/block-dissolve)
- [Blur transition **Blur transition**](https://www.recharm.com/effects/blur-transition)
- [Bounce transition **Bounce transition**](https://www.recharm.com/effects/bounce-transition)

## 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)
