New: Earn 20% as an AgentAlley affiliate. Share your link and earn on every sale you refer. Join now New: Earn 20% as an affiliate. Join now

Python video composition with moviepy 2.x

moviepy for Video Production

by digitalsamba · MIT license · ★ 2,114 on GitHub · updated Sep 21

moviepy for Video Production cover

Python video composition with moviepy 2.x — overlaying deterministic text on AI-generated video (LTX-2, SadTalker), compositing clips, single-file build.py video projects. Use when adding labels/captions/lower-thirds to LTX-2 or…

---
name: moviepy
description: Python video composition with moviepy 2.x — overlaying deterministic text on AI-generated video (LTX-2, SadTalker), compositing clips, single-file build.py video projects. Use when adding labels/captions/lower-thirds to LTX-2 or SadTalker outputs, building short ad-style spots in pure Python without Remotion, or doing programmatic video composition. Triggers include text overlay on video, label LTX-2 clip, caption SadTalker output, lower third, build.py video, moviepy, Python video composition, sub-30s ad spot.
---

# moviepy for Video Production

moviepy is the toolkit's go-to library for **putting deterministic text on top of AI-generated video** and for building short, single-file Python video projects without a Remotion toolchain.

The deeper principle is **trustworthy text**: any genre where text *has to* be readable, accurate, and consistent (legally, editorially, or commercially) is a genre where AI-rendered in-frame text is unacceptable and a moviepy overlay step is the natural fix. Names must be spelled right. Prices must be exact. Source attributions must be pixel-perfect. AI generation models cannot guarantee any of that.

## When to use moviepy vs. Remotion

| Use moviepy when… | Use Remotion when… |
|-------------------|---------------------|
| Overlaying text/labels on an LTX-2 or SadTalker output | Building long-form sprint reviews or product demos |
| Building sub-30s ad-style spots in a single `build.py` | Multi-template, multi-brand, design-heavy work |
| Compositing data-driven visuals (matplotlib `FuncAnimation` → mp4) | Anything needing React components or design system reuse |

Overview

moviepy is the toolkit's go-to library for putting deterministic text on top of AI-generated video and for building short, single-file Python video projects without a Remotion toolchain.

Manual install (npx, ZIP)
Claude Codenpx skills add digitalsamba/claude-code-video-toolkit --skill moviepy -g -a claude-code
Claude Code (manual)mkdir -p ~/.claude/skills && curl -sL https://codeload.github.com/digitalsamba/claude-code-video-toolkit/tar.gz/225c80a1c3a5 \ | tar -xz -C ~/.claude/skills --strip-components=3 claude-code-video-toolkit-225c80a1c3a5/.claude/skills/moviepy
Windows (PowerShell)New-Item -ItemType Directory -Force "$HOME\.claude\skills" | Out-Null; Invoke-WebRequest "https://m.agentalley.io/zip/b4224f870cdba5cd4879/moviepy.zip" -OutFile "$env:TEMP\moviepy.zip"; Expand-Archive "$env:TEMP\moviepy.zip" -DestinationPath "$HOME\.claude\skills" -Force
Claude.ai / desktopDownload the ZIP and upload it under Settings → Capabilities → Skills.
Codex, Cursor, other agentsPoint them at SKILL.md. The other files are plain text.

Connects with Agent Alley MCP · agentalley.io/mcp

Files included

  • SKILL.md12.8 KB
Read the files here
digitalsamba/225c80a1 file
Shared references5SKILL.md reads these from elsewhere in the repo. Not in the ZIP.Get them on GitHub ↗
SKILL.md
Show the full text253 lines

moviepy for Video Production

moviepy is the toolkit's go-to library for putting deterministic text on top of AI-generated video and for building short, single-file Python video projects without a Remotion toolchain.

The deeper principle is trustworthy text: any genre where text has to be readable, accurate, and consistent (legally, editorially, or commercially) is a genre where AI-rendered in-frame text is unacceptable and a moviepy overlay step is the natural fix. Names must be spelled right. Prices must be exact. Source attributions must be pixel-perfect. AI generation models cannot guarantee any of that.

When to use moviepy vs. Remotion

Use moviepy when… Use Remotion when…
Overlaying text/labels on an LTX-2 or SadTalker output Building long-form sprint reviews or product demos
Building sub-30s ad-style spots in a single build.py Multi-template, multi-brand, design-heavy work
Compositing data-driven visuals (matplotlib FuncAnimation → mp4) Anything needing React components or design system reuse
One-off transformations on existing video files Anything where the project lifecycle (planning → render) matters
You want zero Node.js / no React mental overhead You want hot-reload preview in Remotion Studio

Two runnable references for everything in this skill live in examples/:

  • examples/quick-spot/build.py — 15-second ad-style spot. Audio-anchored timeline, text overlay, optional VO + ducked music. Renders silent out of the box with zero external assets.
  • examples/data-viz-chart/build.py — animated time-series chart with deterministic title and source attribution. Demonstrates the matplotlib (data) + moviepy (trustworthy text) split.

Both run with uv run build.py and produce a real out.mp4 immediately. Read them alongside this skill — every pattern below is shown working there.

Dependencies. moviepy, Pillow, and matplotlib are declared in the root pyproject.toml and installed with the toolkit's one-line Python setup: uv sync. If you hit Missing dependency when running an example, run that command from the repo root — the examples' build.py files will tell you the same thing in their error message and exit cleanly rather than printing a bare traceback.

The main use case: text on AI-generated video

Both LTX-2 and SadTalker output bare visuals:

  • LTX-2 cannot reliably render readable text (the model hallucinates letterforms — see the ltx2 skill's "Bad Prompts").
  • SadTalker outputs a talking head with no captions, labels, lower thirds, or context.

The fix is to generate the visual cleanly, then composite text over it deterministically with moviepy. This is the canonical pattern in this toolkit:

from moviepy import VideoFileClip, ImageClip, CompositeVideoClip

# 1. AI-generated visual (LTX-2 or SadTalker output)
bg = VideoFileClip("lugh_ltx.mp4").without_audio()

# 2. Text rendered via PIL → ImageClip (see "Text rendering" below)
title = (
    ImageClip("text_cache/intro_title.png")
    .with_duration(2.0)
    .with_start(0.5)
    .with_position(("center", 880))
)

# 3. Composite
final = CompositeVideoClip([bg, title], size=(1920, 1080))
final.write_videofile("lugh_with_caption.mp4", fps=30, codec="libx264")

Common shapes this takes:

Shape LTX-2 use SadTalker use
Title card over hero footage "INTRODUCING LONGARM" over a cinematic LTX-2 b-roll n/a
Lower third / name plate n/a "Lugh — Ancient Warrior God" under a talking head
Quote caption "I am going home." over an LTX-2 character cameo Same, over a SadTalker talking head
Brand attribution Logo + URL fade-in over the last second Same
Tinted overlay for contrast Dark navy semi-transparent layer behind text Same

Genres where this shines

The "AI-visual + deterministic text overlay" pattern is the natural production pipeline for several styles of video. If the request matches one of these, reach for moviepy by default:

Genre What you overlay Why moviepy is the right call
News / talking-head journalism Speaker name plates, location bars, breaking-news banners, source attribution, pull quotes Names must be spelled right (editorial / legal). The biggest category by volume.
Documentary segments Interviewee lower thirds, chapter titles, archival source credits, location stamps Same trust requirement as news.
Trailers / promo spots Title cards, credit overlays ("FROM THE DIRECTOR OF…"), date stings, quote cards, CTAs Tightly timed, text-heavy, every frame matters. The q2-townhall-longarm-ad example is exactly this.
Social short-form (Reels, TikTok, Shorts) Word-accurate captions for sound-off viewing, hashtag overlays Most social viewing is muted; captions are non-negotiable.
Product demos with annotations Pricing callouts, feature labels, "click here" pointers over screen recordings, before/after labels Prices and product names must be exact.
Tutorials / explainers Step number overlays, terminal-command captions, keyboard-shortcut callouts Step numbers must be sequential, commands must be copy-pasteable.

Lesser-but-real fits: music videos (lyric overlays), reaction videos (source attribution), sports recaps (score overlays), real-estate tours (price / sqft), conference talks (speaker + session plate).

For full SRT-driven subtitling (long-form, time-coded, multilingual) moviepy is workable but not ideal — reach for ffmpeg with subtitles filter or a dedicated subtitle tool. moviepy is best for hand-placed overlays, not bulk caption tracks.

Text rendering — use PIL, not TextClip

Critical gotcha: moviepy 2.x's TextClip(method='label') has a tight-bbox bug that clips letter ascenders and descenders (the tops of capitals, the tails of g/p/y). On Apple Silicon you'll see characters with sliced edges and not realise what's wrong for hours.

The workaround: render text to a transparent PNG via PIL, then load it as an ImageClip. Cache the result by content hash so re-builds are free.

import hashlib
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont

ARIAL_BOLD = "/System/Library/Fonts/Supplemental/Arial Bold.ttf"

def render_text_png(txt, size, hex_color, cache_dir="./text_cache"):
    cache = Path(cache_dir); cache.mkdir(parents=True, exist_ok=True)
    key = hashlib.sha1(f"{txt}|{size}|{hex_color}".encode()).hexdigest()[:16]
    path = cache / f"{key}.png"
    if path.exists():
        return str(path)

    font = ImageFont.truetype(ARIAL_BOLD, size)
    bbox = ImageDraw.Draw(Image.new("RGBA", (1, 1))).textbbox((0, 0), txt, font=font)
    tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
    pad = max(20, size // 4)

    img = Image.new("RGBA", (tw + pad * 2, th + pad * 2), (0, 0, 0, 0))
    rgb = tuple(int(hex_color.lstrip("#")[i:i+2], 16) for i in (0, 2, 4))
    ImageDraw.Draw(img).text((pad - bbox[0], pad - bbox[1]), txt, font=font, fill=(*rgb, 255))
    img.save(path)
    return str(path)

The full helper (with kwargs for bold, position, fades, and cleaner ergonomics) is in examples/quick-spot/build.py — copy it rather than re-implementing.

Audio-anchored timeline pattern

For ad-style edits where every frame matters, generate per-scene VO first and anchor every visual to known absolute timestamps. This eliminates timing drift entirely. See CLAUDE.md → Video Timing → Audio-Anchored Timelines for the full pattern. The short version:

# Audio-anchored timeline (25s):
#   Scene 1 tired      0.3 → 3.74  (audio 3.44s)
#   Scene 2 worries    4.0 → 8.88  (audio 4.88s)

text_clip("TIRED OF",     start=0.5,  duration=1.2)
text_clip("THIRD-PARTY",  start=1.0,  duration=1.8)
vo_clip("01_tired.mp3",   start=0.3)
vo_clip("02_worries.mp3", start=4.0)

Common recipes

Text on a single AI-generated clip
from moviepy import VideoFileClip, ImageClip, CompositeVideoClip

bg = VideoFileClip("ltx_hero.mp4").without_audio()
caption = (
    ImageClip(render_text_png("THE FUTURE OF AGENTS", 140, "#FFFFFF"))
    .with_duration(bg.duration)
    .with_position(("center", 880))
)
CompositeVideoClip([bg, caption], size=bg.size).write_videofile("captioned.mp4", fps=30)
Lower third over a SadTalker talking head
from moviepy import VideoFileClip, ImageClip, ColorClip, CompositeVideoClip

talking = VideoFileClip("narrator_sadtalker.mp4")
W, H = talking.size

# Semi-transparent bar across the bottom for contrast
bar = (
    ColorClip((W, 140), color=(20, 24, 38))
    .with_duration(talking.duration)
    .with_opacity(0.75)
    .with_position(("center", H - 160))
)
name = (
    ImageClip(render_text_png("LUGH", 72, "#F06859"))
    .with_duration(talking.duration)
    .with_position((80, H - 150))
)
title = (
    ImageClip(render_text_png("Ancient Warrior God", 36, "#FFFFFF"))
    .with_duration(talking.duration)
    .with_position((80, H - 80))
)
CompositeVideoClip([talking, bar, name, title]).write_videofile("with_lower_third.mp4", fps=30)
Tinted overlay for text contrast over busy footage

LTX-2 b-roll is often too visually busy for legible text. Drop a semi-transparent navy layer between the video and the text:

from moviepy import ColorClip

tint = (
    ColorClip((W, H), color=(20, 24, 38))
    .with_duration(duration)
    .with_opacity(0.55)
)
# Composite order: bg → tint → text
CompositeVideoClip([bg, tint, text_clip])
Side-by-side composite
from moviepy import VideoFileClip, CompositeVideoClip, ColorClip

left  = VideoFileClip("demo_a.mp4").resized(width=960).with_position((  0, "center"))
right = VideoFileClip("demo_b.mp4").resized(width=960).with_position((960, "center"))
bg    = ColorClip((1920, 1080), color=(0, 0, 0)).with_duration(max(left.duration, right.duration))
CompositeVideoClip([bg, left, right]).write_videofile("split.mp4", fps=30)
Mix per-scene VO with ducked music
from moviepy import AudioFileClip, CompositeAudioClip
from moviepy.audio.fx.MultiplyVolume import MultiplyVolume
from moviepy.audio.fx.AudioFadeIn import AudioFadeIn
from moviepy.audio.fx.AudioFadeOut import AudioFadeOut

music = AudioFileClip("music.mp3").with_effects([
    MultiplyVolume(0.22),  # duck under VO
    AudioFadeIn(0.5),
    AudioFadeOut(1.5),
])
vo = [
    AudioFileClip(f"scenes/0{i}.mp3").with_effects([MultiplyVolume(1.15)]).with_start(start)
    for i, start in [(1, 0.3), (2, 4.0), (3, 9.1)]
]
final_audio = CompositeAudioClip([music] + vo)

Gotchas

  • moviepy 2.x renamed methods. Use subclipped (not subclip), with_duration / with_start / with_position (not set_duration etc.), with_effects([...]) instead of .fadein()/.fadeout(). Many tutorials online still show 1.x syntax — be skeptical.
  • TextClip(method='label') clips ascenders/descenders. Always use the PIL workaround above.
  • OffthreadVideo is Remotion-only. moviepy uses VideoFileClip. Don't mix the two.
  • Resizing requires Pillow ≥ 10.0 for the LANCZOS resample. If you see ANTIALIAS errors, upgrade Pillow.
  • ColorClip takes RGB tuples, not hex strings. Use (20, 24, 38), not "#141826".
  • Audio in VideoFileClip is loaded by default. Call .without_audio() if you only want the visual — composing with audio you don't want will cause silent VO drops in CompositeAudioClip.
  • Always set size=(W, H) on CompositeVideoClip. Without it, output dimensions follow the first clip, which can be smaller than your target.

When to reach for what

Task Tool
Animate a still image tools/ltx2.py --input
Talking head from photoreal portrait tools/sadtalker.py
Talking head from stylized character tools/ltx2.py --input (see ltx2 skill)
Add a label/caption/lower third to either of the above moviepy + PIL (this skill)
Convert / compress / resize an existing file ffmpeg (see ffmpeg skill)
Long-form, design-system-driven video Remotion (see remotion skill)

References

  • Runnable example — short ad-style spot: examples/quick-spot/build.py
  • Runnable example — data-viz with text overlay: examples/data-viz-chart/build.py
  • Audio-anchored timelines: CLAUDE.md → Video Timing → Audio-Anchored Timelines
  • Related skills: ltx2, ffmpeg, remotion
1---
2name: moviepy
3description: Python video composition with moviepy 2.x — overlaying deterministic text on AI-generated video (LTX-2, SadTalker), compositing clips, single-file build.py video projects. Use when adding labels/captions/lower-thirds to LTX-2 or SadTalker outputs, building short ad-style spots in pure Python without Remotion, or doing programmatic video composition. Triggers include text overlay on video, label LTX-2 clip, caption SadTalker output, lower third, build.py video, moviepy, Python video composition, sub-30s ad spot.
4---
5 
6# moviepy for Video Production
7 
8moviepy is the toolkit's go-to library for **putting deterministic text on top of AI-generated video** and for building short, single-file Python video projects without a Remotion toolchain.
9 
10The deeper principle is **trustworthy text**: any genre where text *has to* be readable, accurate, and consistent (legally, editorially, or commercially) is a genre where AI-rendered in-frame text is unacceptable and a moviepy overlay step is the natural fix. Names must be spelled right. Prices must be exact. Source attributions must be pixel-perfect. AI generation models cannot guarantee any of that.
11 
12## When to use moviepy vs. Remotion
13 
14| Use moviepy when… | Use Remotion when… |
15|-------------------|---------------------|
16| Overlaying text/labels on an LTX-2 or SadTalker output | Building long-form sprint reviews or product demos |
17| Building sub-30s ad-style spots in a single `build.py` | Multi-template, multi-brand, design-heavy work |
18| Compositing data-driven visuals (matplotlib `FuncAnimation` → mp4) | Anything needing React components or design system reuse |
19| One-off transformations on existing video files | Anything where the project lifecycle (planning → render) matters |
20| You want zero Node.js / no React mental overhead | You want hot-reload preview in Remotion Studio |
21 
22Two runnable references for everything in this skill live in `examples/`:
23 
24- **`examples/quick-spot/build.py`** — 15-second ad-style spot. Audio-anchored timeline, text overlay, optional VO + ducked music. Renders silent out of the box with zero external assets.
25- **`examples/data-viz-chart/build.py`** — animated time-series chart with deterministic title and source attribution. Demonstrates the matplotlib (data) + moviepy (trustworthy text) split.
26 
27Both run with `uv run build.py` and produce a real `out.mp4` immediately. Read them alongside this skill — every pattern below is shown working there.
28 
29**Dependencies.** `moviepy`, `Pillow`, and `matplotlib` are declared in the root `pyproject.toml` and installed with the toolkit's one-line Python setup: `uv sync`. If you hit `Missing dependency` when running an example, run that command from the repo root — the examples' `build.py` files will tell you the same thing in their error message and exit cleanly rather than printing a bare traceback.
30 
31## The main use case: text on AI-generated video
32 
33Both LTX-2 and SadTalker output bare visuals:
34 
35- **LTX-2** cannot reliably render readable text (the model hallucinates letterforms — see the ltx2 skill's "Bad Prompts").
36- **SadTalker** outputs a talking head with no captions, labels, lower thirds, or context.
37 
38The fix is to generate the visual cleanly, then composite text over it deterministically with moviepy. This is the canonical pattern in this toolkit:
39 
40```python
41from moviepy import VideoFileClip, ImageClip, CompositeVideoClip
42 
43# 1. AI-generated visual (LTX-2 or SadTalker output)
44bg = VideoFileClip("lugh_ltx.mp4").without_audio()
45 
46# 2. Text rendered via PIL → ImageClip (see "Text rendering" below)
47title = (
48 ImageClip("text_cache/intro_title.png")
49 .with_duration(2.0)
50 .with_start(0.5)
51 .with_position(("center", 880))
52)
53 
54# 3. Composite
55final = CompositeVideoClip([bg, title], size=(1920, 1080))
56final.write_videofile("lugh_with_caption.mp4", fps=30, codec="libx264")
57```
58 
59Common shapes this takes:
60 
61| Shape | LTX-2 use | SadTalker use |
62|-------|-----------|---------------|
63| Title card over hero footage | "INTRODUCING LONGARM" over a cinematic LTX-2 b-roll | n/a |
64| Lower third / name plate | n/a | "Lugh — Ancient Warrior God" under a talking head |
65| Quote caption | "I am going home." over an LTX-2 character cameo | Same, over a SadTalker talking head |
66| Brand attribution | Logo + URL fade-in over the last second | Same |
67| Tinted overlay for contrast | Dark navy semi-transparent layer behind text | Same |
68 
69## Genres where this shines
70 
71The "AI-visual + deterministic text overlay" pattern is the natural production pipeline for several styles of video. If the request matches one of these, reach for moviepy by default:
72 
73| Genre | What you overlay | Why moviepy is the right call |
74|-------|------------------|-------------------------------|
75| **News / talking-head journalism** | Speaker name plates, location bars, breaking-news banners, source attribution, pull quotes | Names must be spelled right (editorial / legal). The biggest category by volume. |
76| **Documentary segments** | Interviewee lower thirds, chapter titles, archival source credits, location stamps | Same trust requirement as news. |
77| **Trailers / promo spots** | Title cards, credit overlays ("FROM THE DIRECTOR OF…"), date stings, quote cards, CTAs | Tightly timed, text-heavy, every frame matters. The `q2-townhall-longarm-ad` example is exactly this. |
78| **Social short-form (Reels, TikTok, Shorts)** | Word-accurate captions for sound-off viewing, hashtag overlays | Most social viewing is muted; captions are non-negotiable. |
79| **Product demos with annotations** | Pricing callouts, feature labels, "click here" pointers over screen recordings, before/after labels | Prices and product names must be exact. |
80| **Tutorials / explainers** | Step number overlays, terminal-command captions, keyboard-shortcut callouts | Step numbers must be sequential, commands must be copy-pasteable. |
81 
82Lesser-but-real fits: music videos (lyric overlays), reaction videos (source attribution), sports recaps (score overlays), real-estate tours (price / sqft), conference talks (speaker + session plate).
83 
84**For full SRT-driven subtitling** (long-form, time-coded, multilingual) moviepy is workable but not ideal — reach for `ffmpeg` with `subtitles` filter or a dedicated subtitle tool. moviepy is best for hand-placed overlays, not bulk caption tracks.
85 
86## Text rendering — use PIL, not `TextClip`
87 
88**Critical gotcha:** moviepy 2.x's `TextClip(method='label')` has a tight-bbox bug that **clips letter ascenders and descenders** (the tops of capitals, the tails of g/p/y). On Apple Silicon you'll see characters with sliced edges and not realise what's wrong for hours.
89 
90**The workaround:** render text to a transparent PNG via PIL, then load it as an `ImageClip`. Cache the result by content hash so re-builds are free.
91 
92```python
93import hashlib
94from pathlib import Path
95from PIL import Image, ImageDraw, ImageFont
96 
97ARIAL_BOLD = "/System/Library/Fonts/Supplemental/Arial Bold.ttf"
98 
99def render_text_png(txt, size, hex_color, cache_dir="./text_cache"):
100 cache = Path(cache_dir); cache.mkdir(parents=True, exist_ok=True)
101 key = hashlib.sha1(f"{txt}|{size}|{hex_color}".encode()).hexdigest()[:16]
102 path = cache / f"{key}.png"
103 if path.exists():
104 return str(path)
105 
106 font = ImageFont.truetype(ARIAL_BOLD, size)
107 bbox = ImageDraw.Draw(Image.new("RGBA", (1, 1))).textbbox((0, 0), txt, font=font)
108 tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
109 pad = max(20, size // 4)
110 
111 img = Image.new("RGBA", (tw + pad * 2, th + pad * 2), (0, 0, 0, 0))
112 rgb = tuple(int(hex_color.lstrip("#")[i:i+2], 16) for i in (0, 2, 4))
113 ImageDraw.Draw(img).text((pad - bbox[0], pad - bbox[1]), txt, font=font, fill=(*rgb, 255))
114 img.save(path)
115 return str(path)
116```
117 
118The full helper (with kwargs for bold, position, fades, and cleaner ergonomics) is in `examples/quick-spot/build.py` — copy it rather than re-implementing.
119 
120## Audio-anchored timeline pattern
121 
122For ad-style edits where every frame matters, generate per-scene VO first and anchor every visual to known absolute timestamps. This eliminates timing drift entirely. See **CLAUDE.md → Video Timing → Audio-Anchored Timelines** for the full pattern. The short version:
123 
124```python
125# Audio-anchored timeline (25s):
126# Scene 1 tired 0.3 → 3.74 (audio 3.44s)
127# Scene 2 worries 4.0 → 8.88 (audio 4.88s)
128 
129text_clip("TIRED OF", start=0.5, duration=1.2)
130text_clip("THIRD-PARTY", start=1.0, duration=1.8)
131vo_clip("01_tired.mp3", start=0.3)
132vo_clip("02_worries.mp3", start=4.0)
133```
134 
135## Common recipes
136 
137### Text on a single AI-generated clip
138 
139```python
140from moviepy import VideoFileClip, ImageClip, CompositeVideoClip
141 
142bg = VideoFileClip("ltx_hero.mp4").without_audio()
143caption = (
144 ImageClip(render_text_png("THE FUTURE OF AGENTS", 140, "#FFFFFF"))
145 .with_duration(bg.duration)
146 .with_position(("center", 880))
147)
148CompositeVideoClip([bg, caption], size=bg.size).write_videofile("captioned.mp4", fps=30)
149```
150 
151### Lower third over a SadTalker talking head
152 
153```python
154from moviepy import VideoFileClip, ImageClip, ColorClip, CompositeVideoClip
155 
156talking = VideoFileClip("narrator_sadtalker.mp4")
157W, H = talking.size
158 
159# Semi-transparent bar across the bottom for contrast
160bar = (
161 ColorClip((W, 140), color=(20, 24, 38))
162 .with_duration(talking.duration)
163 .with_opacity(0.75)
164 .with_position(("center", H - 160))
165)
166name = (
167 ImageClip(render_text_png("LUGH", 72, "#F06859"))
168 .with_duration(talking.duration)
169 .with_position((80, H - 150))
170)
171title = (
172 ImageClip(render_text_png("Ancient Warrior God", 36, "#FFFFFF"))
173 .with_duration(talking.duration)
174 .with_position((80, H - 80))
175)
176CompositeVideoClip([talking, bar, name, title]).write_videofile("with_lower_third.mp4", fps=30)
177```
178 
179### Tinted overlay for text contrast over busy footage
180 
181LTX-2 b-roll is often too visually busy for legible text. Drop a semi-transparent navy layer between the video and the text:
182 
183```python
184from moviepy import ColorClip
185 
186tint = (
187 ColorClip((W, H), color=(20, 24, 38))
188 .with_duration(duration)
189 .with_opacity(0.55)
190)
191# Composite order: bg → tint → text
192CompositeVideoClip([bg, tint, text_clip])
193```
194 
195### Side-by-side composite
196 
197```python
198from moviepy import VideoFileClip, CompositeVideoClip, ColorClip
199 
200left = VideoFileClip("demo_a.mp4").resized(width=960).with_position(( 0, "center"))
201right = VideoFileClip("demo_b.mp4").resized(width=960).with_position((960, "center"))
202bg = ColorClip((1920, 1080), color=(0, 0, 0)).with_duration(max(left.duration, right.duration))
203CompositeVideoClip([bg, left, right]).write_videofile("split.mp4", fps=30)
204```
205 
206### Mix per-scene VO with ducked music
207 
208```python
209from moviepy import AudioFileClip, CompositeAudioClip
210from moviepy.audio.fx.MultiplyVolume import MultiplyVolume
211from moviepy.audio.fx.AudioFadeIn import AudioFadeIn
212from moviepy.audio.fx.AudioFadeOut import AudioFadeOut
213 
214music = AudioFileClip("music.mp3").with_effects([
215 MultiplyVolume(0.22), # duck under VO
216 AudioFadeIn(0.5),
217 AudioFadeOut(1.5),
218])
219vo = [
220 AudioFileClip(f"scenes/0{i}.mp3").with_effects([MultiplyVolume(1.15)]).with_start(start)
221 for i, start in [(1, 0.3), (2, 4.0), (3, 9.1)]
222]
223final_audio = CompositeAudioClip([music] + vo)
224```
225 
226## Gotchas
227 
228- **moviepy 2.x renamed methods.** Use `subclipped` (not `subclip`), `with_duration` / `with_start` / `with_position` (not `set_duration` etc.), `with_effects([...])` instead of `.fadein()`/`.fadeout()`. Many tutorials online still show 1.x syntax — be skeptical.
229- **`TextClip(method='label')` clips ascenders/descenders.** Always use the PIL workaround above.
230- **`OffthreadVideo` is Remotion-only.** moviepy uses `VideoFileClip`. Don't mix the two.
231- **Resizing requires Pillow ≥ 10.0** for the LANCZOS resample. If you see `ANTIALIAS` errors, upgrade Pillow.
232- **`ColorClip` takes RGB tuples, not hex strings.** Use `(20, 24, 38)`, not `"#141826"`.
233- **Audio in `VideoFileClip` is loaded by default.** Call `.without_audio()` if you only want the visual — composing with audio you don't want will cause silent VO drops in `CompositeAudioClip`.
234- **Always set `size=(W, H)` on `CompositeVideoClip`.** Without it, output dimensions follow the first clip, which can be smaller than your target.
235 
236## When to reach for what
237 
238| Task | Tool |
239|------|------|
240| Animate a still image | `tools/ltx2.py --input` |
241| Talking head from photoreal portrait | `tools/sadtalker.py` |
242| Talking head from stylized character | `tools/ltx2.py --input` (see ltx2 skill) |
243| **Add a label/caption/lower third to either of the above** | **moviepy + PIL (this skill)** |
244| Convert / compress / resize an existing file | `ffmpeg` (see ffmpeg skill) |
245| Long-form, design-system-driven video | Remotion (see remotion skill) |
246 
247## References
248 
249- Runnable example — short ad-style spot: `examples/quick-spot/build.py`
250- Runnable example — data-viz with text overlay: `examples/data-viz-chart/build.py`
251- Audio-anchored timelines: `CLAUDE.md → Video Timing → Audio-Anchored Timelines`
252- Related skills: `ltx2`, `ffmpeg`, `remotion`
253 
View all files on GitHub →

Details

Updated
Sep 21, 2026
License
MIT
Stars on the repo
★ 2,114
Checked
Source
digitalsamba/claude-code-video-toolkit ↗
Creator
digitalsamba · 11 listings
View creator

Similar skills

See all →
SkillMake a polished video of an app or a featureMake a demo video with OpenScreengetopenscreen/openscreen · by getopenscreenMake a polished video of an app or a feature with OpenScreen's MCP tools, from storyboard to recording, editing and export, then either attach it to a pull request or draft a post on X. Use when the user asks for a demo, a demo video, a screen recording, a product video, a launch clip, a video for X/Twitter, LinkedIn or a README, or to show a change in a pull request ("add a video to the PR", "show the feature to reviewers"). Also use to set OpenScreen up for an agent, and to continue an OpenScreen demo in a new session.★ 4.5k GitHub starsSkillVideo and audio processing with FFmpegFFmpeg for Video Productiondigitalsamba/claude-code-video-toolkit · by digitalsambaVideo and audio processing with FFmpeg. Use for format conversion, resizing, compression, audio extraction, and preparing assets for Remotion. Triggers include converting GIF to MP4, resizing video, extracting audio, compressing files, or any media transformation task.★ 2.1k GitHub starsSkillTurn static SCREENSHOTS into a simulated screen recordingfake-screencast — screenshots → simulated screen recordinghassancs91/claude-youtube-editor · by hassancs91Turn static SCREENSHOTS into a simulated screen recording (TSX) — a fake screencast with an animated cursor that eases to targets and clicks, a browser URL bar that updates per page, hard-cut navigations, in-page filter crossfades, smooth scroll, and a ken-burns zoom onto the payoff. Use when a beat needs to show a walkthrough of a web app / dashboard / settings page and you'd rather build it from screenshots than get (or ask for) a real screen recording — "fake a screen recording", "turn these screenshots into a screencast", "simulate clicking through the dashboard", "animate this UI walkthrough", "cursor moving through the app". Built on remotion/src/lib/screencast.tsx. This is a technique WITHIN step 2 (make-tsx): defer timeline/render/bake orchestration to make-tsx and raw crash-free TSX rules to vidtsx-2d-generator. Not for a single static page clone (use WebBrowserFrame directly) and not when the point is genuine proof of real speed/output (use a real recording).★ 330 GitHub starsSkillA Khan Academy style explainer videoKhan explainerhaiderfarooq3/khan-explainer · by haiderfarooq3Use when the user wants a Khan Academy style explainer video, a blackboard or whiteboard lesson, a hand-drawn chalk-talk animation, a short narrated video that teaches a concept ("explain X in a video", "explainer video", "teach X in 30 seconds"), or a tutorial that draws hand-written arrows, circles and captions over product screenshots ("draw over the UI", "annotated walkthrough"), or clips cut from a YouTube video or podcast where the speaker's real audio plays under a blackboard drawn to it ("khanify this video", "clip this with the Khan visual"), especially when it must be free, local, and use no paid APIs or keys. macOS only.★ 113 GitHub starsSkillPick the output format that makes an explanation easiestUnderstanding ladderChangWenC/understanding-ladder · by ChangWenCPick the output format that makes an explanation easiest to understand, following Karpathy's understanding ladder — (1) plain controlled text, (2) diagram, (3) interactive HTML page, (4) 3Blue1Brown-style explainer video. Classify the content (definition/steps, process/causality, parameter→outcome, unfolding derivation), recommend the lowest rung that fully answers, then produce it. Works in English and Chinese. Use when the user says "explain this clearly", "help me understand", "draw a diagram", "make it interactive", "make an explainer video", "what's the best way to explain X", "understanding ladder", "Karpathy ladder", or 「讲清楚」「帮我理解」「画个图」「做个交互页」「做个讲解视频」「用哪种方式讲最好懂」「理解阶梯」; also when explaining a non-trivial concept, mechanism, algorithm, or an AI agent's output.★ 108 GitHub starsSkillAnimate first frames into A-roll talking head clipsAnimate (A-Roll)TheMattBerman/scrollclaw · by TheMattBermanAnimate first frames into A-roll talking head clips. Uses Sora 2 via fal.ai (primary) with Kling 3 auto-fallback when Sora is unavailable or sunset.★ 71 GitHub stars

More from digitalsamba

SkillAI music generation with ACE-Step 1.5ACE-Step 1.5 Music Generationdigitalsamba/claude-code-video-toolkit · by digitalsambaAI music generation with ACE-Step 1.5 — background music, vocal tracks, covers, stem extraction, audio repainting, and continuation for video production. Use when generating music, soundtracks, jingles, or working with audio stems. Triggers include background music, soundtrack, jingle, music generation, stem extraction, cover, style transfer, repaint, continuation, or musical composition tasks.★ 2.1k GitHub starsSkillGenerate AI voiceovers, sound effectsElevenLabs Audio Generationdigitalsamba/claude-code-video-toolkit · by digitalsambaGenerate AI voiceovers, sound effects, and music using ElevenLabs APIs. Use when creating audio content for videos, podcasts, or games. Triggers include generating voiceovers, narration, dialogue, sound effects from descriptions, background music, soundtrack generation, voice cloning, or any audio synthesis task.★ 2.1k GitHub starsSkillVideo and audio processing with FFmpegFFmpeg for Video Productiondigitalsamba/claude-code-video-toolkit · by digitalsambaVideo and audio processing with FFmpeg. Use for format conversion, resizing, compression, audio extraction, and preparing assets for Remotion. Triggers include converting GIF to MP4, resizing video, extracting audio, compressing files, or any media transformation task.★ 2.1k GitHub starsSkillPrompting patterns for Ideogram 4 text-to-imageIdeogram 4 Skilldigitalsamba/claude-code-video-toolkit · by digitalsambaPrompting patterns for Ideogram 4 text-to-image — best-in-class in-image text rendering and exact color/layout control via structured JSON captions. Use when generating images that need legible on-image text (title cards, thumbnails, logos, signage, CTAs), precise brand colors, or controlled spatial layout. Triggers include title slide image, thumbnail with text, on-image text, legible text in image, brand color palette image, bounding-box layout, Ideogram.★ 2.1k GitHub starsSkillAI video generation with LTX-2.3 22BLTX-2.3 Video Generationdigitalsamba/claude-code-video-toolkit · by digitalsambaAI video generation with LTX-2.3 22B — text-to-video, image-to-video clips for video production. Use when generating video clips, animating images, creating b-roll, animated backgrounds, or motion content. Triggers include video generation, animate image, b-roll, motion, video clip, text-to-video, image-to-video.★ 2.1k GitHub starsSkillRecord browser interactions as video using PlaywrightPlaywright Video Recordingdigitalsamba/claude-code-video-toolkit · by digitalsambaRecord browser interactions as video using Playwright. Use for capturing demo videos, app walkthroughs, and UI flows for Remotion videos. Triggers include recording a demo, capturing browser video, screen recording a website, or creating walkthrough footage.★ 2.1k GitHub stars