---
title: "How to add a feed mockup with Claude (tested prompt and script)"
description: "The clip plays as a sponsored post that scrolls up into a white social feed, then gets a like: a heart flashes over the video and the like count ticks up. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/feed-mockup
---

# How to add a feed mockup with Claude

The clip plays as a sponsored post that scrolls up into a white social feed, then gets a like: a heart flashes over the video and the like count ticks up.

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 a feed mockup to input.mp4
   Handle, caption: glowlab, Your new glow-up in one drop.
   ```

## 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 feed mockup 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                                                     |
| ----------------- | ---------------------------------------- | ---------------------------------------------------------------- |
| `HANDLE, CAPTION` | `glowlab, Your new glow-up in one drop.` | Account name and caption on the post.                            |
| `LIKES`           | `12408`                                  | Like count before the like.                                      |
| `SCROLL`          | `0.9`                                    | Seconds the feed takes to scroll the post into place.            |
| `LIKE_AT`         | `2.0`                                    | Second the post gets liked.                                      |
| `LIKE_RED`        | `(237, 73, 86)`                          | Color of the liked heart.                                        |
| `BG, INK`         | `(255, 255, 255), (20, 20, 24)`          | Feed background and text color (swap them for a dark mode feed). |

## Without the skill

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

```
Turn the 540x960 vertical clip input.mp4 into a social feed mockup, 540x960 at 30fps for its full 6 seconds. Background white (255, 255, 255), text and icons near-black (20, 20, 24). A fixed app bar 64 pixels tall at the top reads "Feed" in 26-pixel bold on the left with heart and paper-plane outline icons on the right, and a fixed tab bar 60 pixels tall at the bottom has five icons (home, search, plus, play, a purple profile dot), each bar with a thin light gray line on its inner edge. Between them the feed scrolls: a post header 64 pixels tall (a round purple avatar with a white "G", "glowlab" in 16-pixel bold and "Sponsored" in gray under it, three dots on the right), then the clip center-cropped to 4:5 and scaled to 540x675, then a row of like, comment and share outline icons with a bookmark on the right, "12,408 likes" in 15-pixel bold and the caption "glowlab Your new glow-up in one drop." with the handle in bold. At rest the post header starts at y=64 and the video fills y=128 to 803. The feed starts 520 pixels lower, with the end of a previous post (a pastel blue-to-pink photo, "842 likes", "maya.k Sunday reset") above it, and scrolls up into place over the first 0.9 seconds with an ease-out. The video keeps playing the whole time. At 2.0 seconds the post is liked: the like icon turns into a filled heart in (237, 73, 86) with a small pop, the count changes to "12,409 likes", and a white heart 150 pixels across pops in over the center of the video and fades out between 2.5 and 2.8 seconds. Draw the feed with Python and Pillow (not ffmpeg's drawtext), keep the audio, and save it as output.mp4.
```

## Run it without Claude

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

```
python3 feed-mockup.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Feed mockup: the clip plays as a sponsored post in a social feed that scrolls up into place, then gets a like.

Usage: python3 feed-mockup.py input.mp4 output.mp4
Needs ffmpeg, numpy and Pillow (pip install numpy pillow).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont

HANDLE = "glowlab"                             # account name on the post
CAPTION = "Your new glow-up in one drop."      # caption under the post
LIKES = 12408                                  # like count before the like
SCROLL = 0.9                                   # seconds the feed takes to scroll the post into place
LIKE_AT = 2.0                                  # second the post gets liked
LIKE_RED = (237, 73, 86)                       # color of the liked heart
BG = (255, 255, 255)                           # feed background
INK = (20, 20, 24)                             # text and icon color
GRAY = (142, 142, 147)                         # secondary text color
FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf",     # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/arialbd.ttf"]                          # Windows
REGULAR_FONTS = ["/System/Library/Fonts/Supplemental/Arial.ttf",
                 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
                 "C:/Windows/Fonts/arial.ttf"]
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")
REGULAR_FILE = next((f for f in REGULAR_FONTS if Path(f).exists()), None) or sys.exit("No font found: add one to REGULAR_FONTS")

# Layout on a 540-pixel-wide phone screen, scaled to the clip's width. The post's video is cropped to 4:5.
TOP_BAR, NAV_BAR = 64, 60      # fixed app bar at the top and tab bar at the bottom
POST_HEAD = 64                 # avatar and account row above the video
VIDEO_H = 675                  # 540 x 675 is 4:5
START_OFFSET = 520             # how far below its resting place the post starts (the previous post shows above)

def font(size, bold=True):
    return ImageFont.truetype(FONT_FILE if bold else REGULAR_FILE, round(size * u))

def heart(draw, cx, cy, size, fill=None, outline=None, width=1):
    """A heart shape from the classic parametric curve, `size` pixels across."""
    pts = [(16 * math.sin(a) ** 3, 13 * math.cos(a) - 5 * math.cos(2 * a) - 2 * math.cos(3 * a) - math.cos(4 * a))
           for a in np.linspace(0, 2 * math.pi, 80)]
    k = size / 34
    poly = [(cx + x * k, cy - y * k - size * 0.05) for x, y in pts]
    draw.polygon(poly, fill=fill)
    if outline:
        draw.line(poly + [poly[0]], fill=outline, width=width, joint="curve")

def big_heart(size, alpha):
    """The white heart that flashes over the video, with a soft shadow, drawn at 2x and shrunk for smooth edges."""
    d = round(size * 3)
    img = Image.new("RGBA", (d, d))
    heart(ImageDraw.Draw(img), d / 2, d / 2 + size * 0.08, size * 2, fill=(0, 0, 0, round(70 * alpha)))
    img = img.filter(ImageFilter.GaussianBlur(size * 0.08))
    heart(ImageDraw.Draw(img), d / 2, d / 2, size * 2, fill=(255, 255, 255, round(240 * alpha)))
    return img.resize((d // 2, d // 2), Image.LANCZOS)

def post_header(draw, y, name, sub, ring):
    r = 18 * u
    cx, cy = 30 * u, y + POST_HEAD * u / 2
    draw.ellipse([cx - r - 3 * u, cy - r - 3 * u, cx + r + 3 * u, cy + r + 3 * u], fill=ring)
    draw.ellipse([cx - r - u, cy - r - u, cx + r + u, cy + r + u], fill=BG)
    draw.ellipse([cx - r + u, cy - r + u, cx + r - u, cy + r - u], fill=(120, 80, 170))
    draw.text((cx, cy), name[0].upper(), font=font(18), anchor="mm", fill="white")
    draw.text((60 * u, cy - 9 * u), name, font=font(16), anchor="lm", fill=INK)
    draw.text((60 * u, cy + 11 * u), sub, font=font(13, False), anchor="lm", fill=GRAY)
    for i in (-1, 0, 1):
        draw.ellipse([w - 28 * u + i * 7 * u - 2 * u, cy - 2 * u, w - 28 * u + i * 7 * u + 2 * u, cy + 2 * u], fill=INK)

def share(draw, cx, cy):
    """A paper-plane outline."""
    draw.line([(cx - 13 * u, cy - 10 * u), (cx + 13 * u, cy - 12 * u), (cx + 2 * u, cy + 13 * u),
               (cx - 2 * u, cy - 1 * u), (cx - 13 * u, cy - 10 * u)], fill=INK, width=max(2, round(2.2 * u)), joint="curve")

def action_icons(draw, y):
    """Comment, share and save icons (the like heart is drawn per frame)."""
    lw = max(2, round(2.2 * u))
    cx, cy = 68 * u, y
    draw.ellipse([cx - 12 * u, cy - 12 * u, cx + 12 * u, cy + 12 * u], outline=INK, width=lw)
    draw.polygon([(cx - 11 * u, cy + 6 * u), (cx - 14 * u, cy + 14 * u), (cx - 4 * u, cy + 11 * u)], fill=INK)
    share(draw, 110 * u, cy)
    cx = w - 28 * u
    draw.line([(cx - 10 * u, cy + 13 * u), (cx - 10 * u, cy - 12 * u), (cx + 10 * u, cy - 12 * u),
               (cx + 10 * u, cy + 13 * u), (cx, cy + 5 * u), (cx - 10 * u, cy + 13 * u)], fill=INK, width=lw, joint="curve")

def feed():
    """The scrolling part, drawn once: the end of the previous post, then the ad post with a hole for its video."""
    above = START_OFFSET * u
    img = Image.new("RGBA", (w, round(above + (POST_HEAD + VIDEO_H + 140) * u)), BG)
    draw = ImageDraw.Draw(img)
    top = above - 450 * u  # the previous post: a soft gradient photo, its likes and caption
    post_header(draw, top - POST_HEAD * u, "maya.k", "Lisbon, Portugal", (250, 160, 60))
    grad = np.linspace(0, 1, round(330 * u))[:, None, None]
    photo = (np.array([196, 214, 236]) * (1 - grad) + np.array([236, 206, 214]) * grad) * np.ones((1, w, 1))
    img.paste(Image.fromarray(photo.astype(np.uint8)), (0, round(top)))
    heart(draw, 26 * u, top + 360 * u, 26 * u, outline=INK, width=max(2, round(2.2 * u)))
    action_icons(draw, top + 360 * u)
    draw.text((16 * u, top + 395 * u), "842 likes", font=font(15), anchor="lm", fill=INK)
    draw.text((16 * u, top + 420 * u), "maya.k", font=font(15), anchor="lm", fill=INK)
    draw.text((16 * u + draw.textlength("maya.k ", font=font(15)), top + 420 * u), "Sunday reset",
              font=font(15, False), anchor="lm", fill=INK)
    post_header(draw, above, HANDLE, "Sponsored", (214, 41, 118))
    y = above + (POST_HEAD + VIDEO_H) * u
    draw.rectangle([0, above + POST_HEAD * u, w, y], fill=(0, 0, 0, 0))
    action_icons(draw, y + 24 * u)
    draw.text((16 * u, y + 82 * u), HANDLE, font=font(15), anchor="lm", fill=INK)
    draw.text((16 * u + draw.textlength(HANDLE + " ", font=font(15)), y + 82 * u), CAPTION,
              font=font(15, False), anchor="lm", fill=INK)
    return img

def bars():
    """The app bar and tab bar, which stay put while the feed scrolls."""
    img = Image.new("RGBA", (w, h))
    draw = ImageDraw.Draw(img)
    draw.rectangle([0, 0, w, TOP_BAR * u], fill=BG)
    draw.line([(0, TOP_BAR * u), (w, TOP_BAR * u)], fill=(219, 219, 222), width=1)
    draw.text((16 * u, TOP_BAR * u / 2), "Feed", font=font(26), anchor="lm", fill=INK)
    heart(draw, w - 74 * u, TOP_BAR * u / 2, 26 * u, outline=INK, width=max(2, round(2.2 * u)))
    share(draw, w - 28 * u, TOP_BAR * u / 2)
    y = h - NAV_BAR * u
    draw.rectangle([0, y, w, h], fill=BG)
    draw.line([(0, y), (w, y)], fill=(219, 219, 222), width=1)
    lw, cy = max(2, round(2.2 * u)), y + NAV_BAR * u / 2
    for i in range(5):
        cx = w * (i + 0.5) / 5
        if i == 0:    # home
            draw.polygon([(cx - 12 * u, cy - 1 * u), (cx, cy - 13 * u), (cx + 12 * u, cy - 1 * u),
                          (cx + 12 * u, cy + 12 * u), (cx - 12 * u, cy + 12 * u)], fill=INK)
        elif i == 1:  # search
            draw.ellipse([cx - 11 * u, cy - 11 * u, cx + 6 * u, cy + 6 * u], outline=INK, width=lw)
            draw.line([(cx + 4 * u, cy + 4 * u), (cx + 12 * u, cy + 12 * u)], fill=INK, width=lw)
        elif i == 2:  # new post
            draw.rounded_rectangle([cx - 12 * u, cy - 12 * u, cx + 12 * u, cy + 12 * u], 6 * u, outline=INK, width=lw)
            draw.line([(cx, cy - 6 * u), (cx, cy + 6 * u)], fill=INK, width=lw)
            draw.line([(cx - 6 * u, cy), (cx + 6 * u, cy)], fill=INK, width=lw)
        elif i == 3:  # reels
            draw.rounded_rectangle([cx - 12 * u, cy - 12 * u, cx + 12 * u, cy + 12 * u], 6 * u, outline=INK, width=lw)
            draw.polygon([(cx - 4 * u, cy - 6 * u), (cx + 6 * u, cy), (cx - 4 * u, cy + 6 * u)], fill=INK)
        else:         # profile
            draw.ellipse([cx - 12 * u, cy - 12 * u, cx + 12 * u, cy + 12 * u], fill=(120, 80, 170))
    return img

def ease_out(p):
    p = min(max(p, 0), 1)
    return 1 - (1 - p) ** 3

def pop(p):
    """0 -> 1 with a small overshoot, for the like."""
    p = min(max(p, 0), 1)
    return 1 + 2.70158 * (p - 1) ** 3 + 1.70158 * (p - 1) ** 2

def frame_at(t, video):
    offset = (1 - ease_out(t / SCROLL)) * START_OFFSET * u
    feed_top = TOP_BAR * u + offset - START_OFFSET * u  # where the feed image's top edge lands
    video_top = round(feed_top + (START_OFFSET + POST_HEAD) * u)
    img = Image.new("RGBA", (w, h), BG)
    img.paste(video, (0, video_top))
    img.alpha_composite(feed_img, (0, round(feed_top)))
    draw = ImageDraw.Draw(img)
    liked = t >= LIKE_AT
    action_y = video_top + vh + 24 * u
    lw = max(2, round(2.2 * u))
    if liked:
        heart(draw, 26 * u, action_y, 26 * u * pop((t - LIKE_AT) / 0.3), fill=LIKE_RED)
        big = 150 * u * pop((t - LIKE_AT) / 0.25)                # big heart over the video, fading out
        fade = min(max(1 - (t - LIKE_AT - 0.5) / 0.3, 0), 1)
        if fade > 0 and big > 1:
            b = big_heart(big, fade)
            img.alpha_composite(b, (round(w / 2 - b.width / 2), round(video_top + vh / 2 - b.height / 2)))
    else:
        heart(draw, 26 * u, action_y, 26 * u, outline=INK, width=lw)
    draw.text((16 * u, action_y + 32 * u), f"{LIKES + liked:,} likes", font=font(15), anchor="lm", fill=INK)
    img.alpha_composite(bars_img)
    return img.convert("RGB")

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):
    """Read src cropped to 4:5, put each frame into the feed and save to out, keeping the audio."""
    dec = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-vf", f"crop=iw:iw*5/4,scale={w}:{vh}",
                            "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
    enc = 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)
    size = w * vh * 3
    for i in range(math.ceil(duration * fps)):
        data = dec.stdout.read(size)
        if len(data) < size:
            break
        enc.stdin.write(frame_at(i / fps, Image.frombytes("RGB", (w, vh), data)).tobytes())
    enc.stdin.close()
    dec.stdout.close()
    if enc.wait() or dec.wait():
        sys.exit("ffmpeg failed")

src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
u = w / 540
vh = round(VIDEO_H * u)
feed_img = feed()
bars_img = bars()
render(src, out)
```

## More layouts and reframing

- [Background replacement **Background replacement**](https://www.recharm.com/effects/background-replacement)
- [Binocular view **Binocular view**](https://www.recharm.com/effects/binocular-view)
- [Blur region **Blur region**](https://www.recharm.com/effects/blur-region)
- [Cinematic bars **Cinematic bars**](https://www.recharm.com/effects/cinematic-bars)
- [Comic panel layout **Comic panel layout**](https://www.recharm.com/effects/comic-panel-layout)

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