Get this effect
Switch to Code in your Claude Desktop App
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/skillsAsk for the effect
Add a video call frame to input.mp4 Name: Maya
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 video call frame 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 |
|---|---|---|
NAME | Maya | Name on the tag in the tile's corner. |
CALL_START | 83 | Call timer reading at the first frame, in seconds (83 = 01:23). |
BACKGROUND | (32, 33, 36) | App background around the tile. |
BUTTON | (60, 64, 67) | Mic and camera button color. |
RED | (234, 67, 53) | Hang-up button and mute badge color. |
TILE | (0.04, 0.10, 0.96, 0.86) | Video tile's left, top, right and bottom as shares of the frame. |
RADIUS | 0.045 | Tile corner radius as a share of the frame width. |
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 a live video call. Keep the 540x960 frame and fill it with a dark (32, 33, 36) app background. Put the video in a rounded tile from 4% to 96% across and 10% to 86% down (corner radius 4.5% of the frame width), scaled to cover the tile and cropped from the middle. In the tile's bottom-left corner, inset 3% of the frame width, add a name tag: a black pill at 60% opacity, 6.8% of the frame width tall, holding a red (234, 67, 53) circle with a white crossed-out microphone and the name "Maya" in white bold text 3.4% of the frame width tall. Centered above the tile at 5% down, show a call timer in white bold text 4.2% of the frame width tall with a small green (52, 168, 83) dot to its left, reading 01:23 at the start and counting up one second per second. Centered in the band below the tile (at 93% down), draw three controls centered at 33%, 50% and 69% across: a (60, 64, 67) circle 12% of the frame width across with a white crossed-out microphone, the same circle with a white camera icon, and a red (234, 67, 53) pill 16% of the frame width wide and 12% tall with a white hang-up handset. Show it all from the first frame to the end. Use Python with Pillow to draw everything (draw any text with Pillow, not ffmpeg's drawtext) and ffmpeg to read and write 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 video-call-frame.py and run:
python3 video-call-frame.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Video call frame: the clip plays as the live tile of a video call, with a name tag, mute icons and a call timer.
Usage: python3 video-call-frame.py input.mp4 output.mp4
Needs ffmpeg and Pillow (pip install pillow).
"""
import json
import subprocess
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
NAME = "Maya" # name on the tag in the tile's bottom-left corner
CALL_START = 83 # call timer reading at the first frame, in seconds (83 = 01:23)
BACKGROUND = (32, 33, 36) # app background around the tile
BUTTON = (60, 64, 67) # mic and camera button color
RED = (234, 67, 53) # hang-up button and mute badge color
TILE = (0.04, 0.10, 0.96, 0.86) # video tile: left, top, right, bottom as shares of the frame
RADIUS = 0.045 # tile corner radius as a share of the frame width
FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf", # macOS
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", # Linux
"C:/Windows/Fonts/arialbd.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")
SS = 3 # shapes are drawn at 3x and scaled down, for smooth edges
def mic(draw, cx, cy, s, color, behind):
"""A microphone icon of height ~s with a slash through it; `behind` is the color it sits on."""
draw.rounded_rectangle([cx - 0.2 * s, cy - 0.5 * s, cx + 0.2 * s, cy + 0.12 * s], 0.2 * s, fill=color)
draw.arc([cx - 0.36 * s, cy - 0.3 * s, cx + 0.36 * s, cy + 0.3 * s], 0, 180, fill=color, width=round(0.09 * s))
draw.line([cx, cy + 0.3 * s, cx, cy + 0.48 * s], fill=color, width=round(0.09 * s))
draw.line([cx - 0.2 * s, cy + 0.48 * s, cx + 0.2 * s, cy + 0.48 * s], fill=color, width=round(0.09 * s))
draw.line([cx - 0.42 * s, cy - 0.5 * s, cx + 0.42 * s, cy + 0.5 * s], fill=behind, width=round(0.22 * s))
draw.line([cx - 0.42 * s, cy - 0.5 * s, cx + 0.42 * s, cy + 0.5 * s], fill=color, width=round(0.09 * s))
def camera(draw, cx, cy, s, color):
draw.rounded_rectangle([cx - 0.5 * s, cy - 0.3 * s, cx + 0.15 * s, cy + 0.3 * s], 0.1 * s, fill=color)
draw.polygon([(cx + 0.2 * s, cy), (cx + 0.5 * s, cy - 0.25 * s), (cx + 0.5 * s, cy + 0.25 * s)], fill=color)
def hang_up(draw, cx, cy, s, color):
"""A phone handset lying face down: a thick arc with an ear and a mouth piece."""
r = 0.5 * s
cy += 0.2 * s
draw.arc([cx - r, cy - r, cx + r, cy + r], 200, 340, fill=color, width=round(r * 0.38))
for ex in (cx - r * 0.81, cx + r * 0.81):
ey = cy - r * 0.3
draw.ellipse([ex - r * 0.3, ey - r * 0.3, ex + r * 0.3, ey + r * 0.3], fill=color)
def call_ui():
"""Everything that doesn't move, drawn once: the background with a hole for the tile, name tag and buttons."""
W, H = w * SS, h * SS
x0, y0, x1, y1 = tile_box(SS)
ui = Image.new("RGBA", (W, H), BACKGROUND)
hole = Image.new("L", (W, H), 255)
ImageDraw.Draw(hole).rounded_rectangle([x0, y0, x1, y1], RADIUS * W, fill=0)
ui.putalpha(hole)
draw = ImageDraw.Draw(ui)
# name tag: a dark pill with a red mute badge and the name
font = ImageFont.truetype(FONT_FILE, round(0.034 * W))
pad, tag_h = 0.018 * W, 0.068 * W
badge = tag_h - 2 * 0.012 * W
tx, ty = x0 + 0.03 * W, y1 - 0.03 * W - tag_h
tag_w = 0.012 * W + badge + pad * 0.8 + draw.textlength(NAME, font=font) + pad
tag = Image.new("RGBA", (W, H))
ImageDraw.Draw(tag).rounded_rectangle([tx, ty, tx + tag_w, ty + tag_h], tag_h / 2, fill=(0, 0, 0, 150))
ui.alpha_composite(tag)
bx, by = tx + 0.012 * W + badge / 2, ty + tag_h / 2
draw.ellipse([bx - badge / 2, by - badge / 2, bx + badge / 2, by + badge / 2], fill=RED)
mic(draw, bx, by, badge * 0.55, "white", RED)
draw.text((bx + badge / 2 + pad * 0.8, by), NAME, font=font, fill="white", anchor="lm")
# control bar: muted mic, camera, red hang-up
cy, r, gap = (y1 + H) / 2, 0.06 * W, 0.17 * W
for cx in (W / 2 - gap, W / 2):
draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=BUTTON)
mic(draw, W / 2 - gap, cy, r * 0.9, "white", BUTTON)
camera(draw, W / 2, cy, r * 0.9, "white")
cx = W / 2 + gap + r * 0.35
draw.rounded_rectangle([cx - r * 1.35, cy - r, cx + r * 1.35, cy + r], r, fill=RED)
hang_up(draw, cx, cy, r * 1.1, "white")
return ui.resize((w, h), Image.LANCZOS)
def tile_box(scale=1):
return [round(TILE[0] * w * scale), round(TILE[1] * h * scale), round(TILE[2] * w * scale), round(TILE[3] * h * scale)]
def fill_tile(video):
"""Scale the video to cover the tile and crop the middle, as call apps do."""
x0, y0, x1, y1 = tile_box()
tw, th = x1 - x0, y1 - y0
k = max(tw / w, th / h)
big = video.resize((round(w * k), round(h * k)), Image.LANCZOS)
left, top = (big.width - tw) // 2, (big.height - th) // 2
frame = Image.new("RGB", (w, h))
frame.paste(big.crop((left, top, left + tw, top + th)), (x0, y0))
return frame
def call_frame(video, t):
img = fill_tile(video).convert("RGBA")
img.alpha_composite(ui)
draw = ImageDraw.Draw(img)
seconds = CALL_START + int(t)
timer = f"{seconds // 60:02d}:{seconds % 60:02d}"
cy = TILE[1] * h / 2
tw = draw.textlength(timer, font=timer_font)
dot = 0.012 * w
draw.ellipse([w / 2 - tw / 2 - 3 * dot, cy - dot, w / 2 - tw / 2 - dot, cy + dot], fill=(52, 168, 83))
draw.text((w / 2 + dot, cy), timer, font=timer_font, fill="white", anchor="mm")
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", "-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)
ui = call_ui()
timer_font = ImageFont.truetype(FONT_FILE, round(0.042 * w))
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 chunk := reader.stdout.read(w * h * 3):
writer.stdin.write(call_frame(Image.frombytes("RGB", (w, h), chunk), i / fps).tobytes())
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





