testsAndMisc/moviepy_showcase.py
Krzysztof kuhy Rudnicki 0d47f77ee5 refactor: remove noqa comments from miscellaneous scripts
- Fix underlying lint issues instead of suppressing with noqa
- Files: moviepy_showcase, pomodoro-wake-daemon, brother_printer,
  http_status_anki, geo_data, repo_explorer, steam_backlog_enforcer,
  music_generator
2026-03-13 20:48:40 +01:00

1213 lines
39 KiB
Python

"""MoviePy 2.x — Comprehensive Showcase of ALL Features.
Generates a video demonstrating every MoviePy class, method, effect,
and tool. Organised into sections:
Part 1: Clip Types (VideoClip, ColorClip, TextClip, ImageClip,
BitmapClip, DataVideoClip, ImageSequenceClip)
Part 2: Clip Methods (subclipped, cropped, resized, rotated, with_position,
with_opacity, with_mask, image_transform, transform,
time_transform, with_speed_scaled, with_section_cut_out,
to_ImageClip, to_mask, to_RGB, with_background_color,
with_effects_on_subclip, with_layer_index)
Part 3: Video Effects (all 34)
Part 4: Audio (AudioClip, AudioArrayClip, CompositeAudioClip,
all 7 audio effects)
Part 5: Composition (CompositeVideoClip, concatenate_videoclips, clips_array)
Part 6: Drawing Tools (circle, color_gradient, color_split)
Part 7: Output (write_videofile params, write_gif, save_frame,
write_images_sequence)
"""
from __future__ import annotations
import logging
import os
from pathlib import Path
import shutil
import tempfile
from typing import TYPE_CHECKING
from moviepy import (
AudioArrayClip,
AudioClip,
BitmapClip,
ColorClip,
CompositeAudioClip,
CompositeVideoClip,
DataVideoClip,
ImageClip,
ImageSequenceClip,
TextClip,
VideoClip,
VideoFileClip,
concatenate_audioclips,
concatenate_videoclips,
)
from moviepy.audio.fx import (
AudioDelay,
AudioFadeIn,
AudioFadeOut,
AudioLoop,
AudioNormalize,
MultiplyStereoVolume,
MultiplyVolume,
)
from moviepy.video.compositing.CompositeVideoClip import (
clips_array,
)
from moviepy.video.fx import (
AccelDecel,
BlackAndWhite,
Blink,
Crop,
CrossFadeIn,
CrossFadeOut,
EvenSize,
FadeIn,
FadeOut,
Freeze,
FreezeRegion,
GammaCorrection,
HeadBlur,
InvertColors,
Loop,
LumContrast,
MakeLoopable,
Margin,
MaskColor,
MirrorX,
MirrorY,
MultiplyColor,
MultiplySpeed,
Painting,
Resize,
Rotate,
Scroll,
SlideIn,
SlideOut,
SuperSample,
TimeMirror,
TimeSymmetrize,
)
from moviepy.video.tools.drawing import (
circle,
color_gradient,
color_split,
)
import numpy as np
if TYPE_CHECKING:
from collections.abc import Callable
logger = logging.getLogger(__name__)
os.environ["FFMPEG_BINARY"] = "/usr/bin/ffmpeg"
# ── Constants ─────────────────────────────────────────────────────
W, H = 1920, 1080
FPS = 30
CLIP_DUR = 2.0 # duration of each demo clip
HEADER_DUR = 1.5 # duration of section headers
OUTPUT = "moviepy_showcase_full.mp4"
FONT_B = "/usr/share/fonts/noto/NotoSans-Bold.ttf"
FONT_R = "/usr/share/fonts/noto/NotoSans-Regular.ttf"
# ── Pre-computed gradient LUTs ────────────────────────────────────
_G_CH = (
np.linspace(0, 255, H, dtype=np.uint8)[:, None]
* np.ones(W, dtype=np.uint8)[None, :]
)
_B_CH = (
np.ones(H, dtype=np.uint8)[:, None]
* np.linspace(0, 255, W, dtype=np.uint8)[None, :]
)
def _gradient(t: float) -> np.ndarray:
f = np.empty((H, W, 3), dtype=np.uint8)
f[:, :, 0] = int(128 + 127 * np.sin(t * 2))
f[:, :, 1] = _G_CH
f[:, :, 2] = _B_CH
return f
def _checkerboard(t: float) -> np.ndarray:
sq = 60
off = int(t * 40) % sq
xs = np.arange(W, dtype=np.int32)[None, :]
ys = np.arange(H, dtype=np.int32)[:, None]
v = (((xs + off) // sq + (ys + off) // sq) % 2 * 255).astype(np.uint8)
return np.dstack([v, v, v])
# ── Helpers ───────────────────────────────────────────────────────
def _base_clip(dur: float = CLIP_DUR) -> VideoClip:
"""Animated gradient as a reusable base clip."""
return VideoClip(_gradient, duration=dur).with_fps(FPS)
def _label(
text: str,
size: int = 36,
color: str = "white",
pos: tuple[str, int] | tuple[str, str] = ("center", 40),
dur: float = CLIP_DUR,
) -> TextClip:
"""Small label overlay (transparent bg)."""
return (
TextClip(
text=text,
font_size=size,
color=color,
font=FONT_R,
margin=(0, 15),
)
.with_duration(dur)
.with_position(pos)
)
def _titled(clip: VideoClip, text: str) -> CompositeVideoClip:
"""Overlay a label onto a clip."""
lbl = _label(text, dur=clip.duration)
return CompositeVideoClip(
[clip.with_duration(clip.duration), lbl],
size=(W, H),
)
def _section_header(title: str, subtitle: str = "") -> CompositeVideoClip:
"""Dark background with centred title text."""
bg = ColorClip(size=(W, H), color=(15, 15, 40)).with_duration(HEADER_DUR)
t = (
TextClip(
text=title,
font_size=72,
color="white",
font=FONT_B,
margin=(0, 30),
)
.with_duration(HEADER_DUR)
.with_position(("center", 380))
)
parts: list[VideoClip] = [bg, t]
if subtitle:
s = (
TextClip(
text=subtitle,
font_size=32,
color="#aaaaaa",
font=FONT_R,
margin=(0, 15),
)
.with_duration(HEADER_DUR)
.with_position(("center", 520))
)
parts.append(s)
return CompositeVideoClip(parts, size=(W, H))
def _resize_to_canvas(clip: VideoClip) -> VideoClip:
"""Resize a clip to fit (W, H) and centre on black background."""
cw, ch = clip.size
scale = min(W / cw, H / ch)
return clip.resized(
width=int(cw * scale), height=int(ch * scale)
).with_background_color(size=(W, H), color=(0, 0, 0))
# ══════════════════════════════════════════════════════════════════
# PART 1 — Clip Types
# ══════════════════════════════════════════════════════════════════
def part1_clip_types() -> list[VideoClip]:
"""Demonstrate every clip creation class."""
scenes: list[VideoClip] = [
_section_header(
"Part 1: Clip Types",
"VideoClip · ColorClip · TextClip · ImageClip"
" · BitmapClip · DataVideoClip · ImageSequenceClip",
),
]
# 1. VideoClip — custom frame function
vc = VideoClip(_gradient, duration=CLIP_DUR).with_fps(FPS)
scenes.append(_titled(vc, "VideoClip(frame_function)"))
# 2. ColorClip
cc = ColorClip(size=(W, H), color=(0, 120, 200)).with_duration(CLIP_DUR)
scenes.append(_titled(cc, "ColorClip(size, color)"))
# 3. TextClip — label method
tbg = ColorClip(size=(W, H), color=(20, 20, 50)).with_duration(CLIP_DUR)
tc = (
TextClip(
text="Hello MoviePy!",
font_size=96,
color="yellow",
font=FONT_B,
stroke_color="black",
stroke_width=3,
bg_color=None,
margin=(10, 30),
method="label",
horizontal_align="center",
vertical_align="center",
transparent=True,
)
.with_duration(CLIP_DUR)
.with_position("center")
)
scenes.append(
_titled(
CompositeVideoClip([tbg, tc], size=(W, H)),
"TextClip(text, font_size, color, stroke, margin, method='label')",
)
)
# 4. TextClip — caption method (wraps text)
tbg2 = ColorClip(size=(W, H), color=(50, 20, 20)).with_duration(CLIP_DUR)
tc2 = (
TextClip(
text="This is a longer caption that wraps "
"because we use method='caption' with a fixed size.",
font_size=48,
color="white",
font=FONT_R,
method="caption",
size=(W - 200, None),
text_align="center",
interline=10,
margin=(0, 20),
)
.with_duration(CLIP_DUR)
.with_position("center")
)
scenes.append(
_titled(
CompositeVideoClip([tbg2, tc2], size=(W, H)),
"TextClip(method='caption', text_align, interline, size)",
)
)
# 5. ImageClip — from numpy array
img = np.zeros((H, W, 3), dtype=np.uint8)
img[200:880, 400:1520] = [255, 100, 50] # orange rectangle
ic = ImageClip(img, duration=CLIP_DUR)
scenes.append(_titled(ic, "ImageClip(numpy_array)"))
# 6. BitmapClip — from ASCII-art frames
frames = [
["RR__", "RR__", "__BB", "__BB"],
["__RR", "__RR", "BB__", "BB__"],
["RR__", "RR__", "__BB", "__BB"],
["__RR", "__RR", "BB__", "BB__"],
]
bc = BitmapClip(
frames,
fps=2,
color_dict={"R": (255, 0, 0), "B": (0, 0, 255), "_": (30, 30, 30)},
)
bc = _resize_to_canvas(bc)
scenes.append(
_titled(bc.with_duration(CLIP_DUR), "BitmapClip(bitmap_frames, color_dict)")
)
# 7. DataVideoClip — data-driven frames
data_list = list(range(60))
def data_to_frame(d: int) -> np.ndarray:
frame = np.full((H, W, 3), 30, dtype=np.uint8)
bar_w = int(d / 60 * (W - 100))
frame[400:680, 50 : 50 + bar_w] = [0, 200, 100]
return frame
dvc = DataVideoClip(data_list, data_to_frame, fps=FPS).with_duration(CLIP_DUR)
scenes.append(_titled(dvc, "DataVideoClip(data, data_to_frame)"))
# 8. ImageSequenceClip — from a list of arrays
seq_frames = []
for i in range(10):
f = np.full((H, W, 3), int(25 * i), dtype=np.uint8)
f[:, :, 0] = int(255 - 25 * i)
seq_frames.append(f)
isc = ImageSequenceClip(seq_frames, fps=5).with_duration(CLIP_DUR)
scenes.append(_titled(isc, "ImageSequenceClip(sequence, fps)"))
return scenes
# ══════════════════════════════════════════════════════════════════
# PART 2 — Clip Methods
# ══════════════════════════════════════════════════════════════════
def part2_clip_methods() -> list[VideoClip]:
"""Demonstrate VideoClip methods."""
scenes: list[VideoClip] = [
_section_header(
"Part 2: Clip Methods",
"subclipped · cropped · resized · rotated"
" · with_position · with_opacity · …",
),
]
base = _base_clip(3.0)
# subclipped
sc = base.subclipped(0.5, 2.5)
scenes.append(
_titled(_resize_to_canvas(sc), "subclipped(start_time=0.5, end_time=2.5)")
)
# cropped
cr = base.cropped(x1=200, y1=100, x2=1200, y2=700).with_duration(CLIP_DUR)
scenes.append(
_titled(_resize_to_canvas(cr), "cropped(x1=200, y1=100, x2=1200, y2=700)")
)
# resized — by factor
rs1 = base.resized(0.5).with_duration(CLIP_DUR)
scenes.append(_titled(_resize_to_canvas(rs1), "resized(0.5) # half size"))
# resized — by height
rs2 = base.resized(height=400).with_duration(CLIP_DUR)
scenes.append(_titled(_resize_to_canvas(rs2), "resized(height=400)"))
# rotated
rt = base.rotated(30, expand=False, bg_color=(0, 0, 0)).with_duration(CLIP_DUR)
scenes.append(_titled(rt, "rotated(angle=30, expand=False)"))
# with_position + with_opacity in a composite
small = base.resized(0.4).with_duration(CLIP_DUR)
bg = ColorClip(size=(W, H), color=(10, 10, 10)).with_duration(CLIP_DUR)
p1 = small.with_position((50, 50)).with_opacity(1.0)
p2 = small.with_position((500, 300)).with_opacity(0.5)
comp = CompositeVideoClip([bg, p1, p2], size=(W, H))
scenes.append(_titled(comp, "with_position() + with_opacity(0.5)"))
# with_mask — circular mask
mask_arr = circle(
screensize=(W, H),
center=(W // 2, H // 2),
radius=300,
color=1.0,
bg_color=0.0,
blur=20,
)
mask_clip = ImageClip(mask_arr, is_mask=True, duration=CLIP_DUR)
masked = base.with_duration(CLIP_DUR).with_mask(mask_clip)
mbg = ColorClip(size=(W, H), color=(0, 0, 0)).with_duration(CLIP_DUR)
scenes.append(
_titled(
CompositeVideoClip([mbg, masked], size=(W, H)),
"with_mask() — circular mask via drawing.circle()",
)
)
# image_transform
def flip_lr(img: np.ndarray) -> np.ndarray:
return img[:, ::-1]
it = base.image_transform(flip_lr).with_duration(CLIP_DUR)
scenes.append(_titled(it, "image_transform(flip_lr_func)"))
# transform
def shift_right(gf: Callable[[float], np.ndarray], t: float) -> np.ndarray:
frame = gf(t)
shift = int(t * 100)
return np.roll(frame, shift, axis=1)
tf = base.transform(shift_right).with_duration(CLIP_DUR)
scenes.append(_titled(tf, "transform(shift_right_func)"))
# time_transform
tt = base.time_transform(lambda t: t * 3).with_duration(CLIP_DUR)
scenes.append(_titled(tt, "time_transform(lambda t: t*3) # 3x speed"))
# with_speed_scaled
ss = base.with_speed_scaled(factor=0.5)
scenes.append(_titled(ss.with_duration(CLIP_DUR), "with_speed_scaled(factor=0.5)"))
# with_section_cut_out
sco = base.with_section_cut_out(0.5, 1.5)
scenes.append(
_titled(
sco.with_duration(min(sco.duration, CLIP_DUR)),
"with_section_cut_out(0.5, 1.5)",
)
)
# to_ImageClip
still = base.to_ImageClip(t=1.0, duration=CLIP_DUR)
scenes.append(_titled(still, "to_ImageClip(t=1.0) # freeze at t=1"))
# to_mask + to_RGB
bw = base.to_mask(canal=1).to_RGB().with_duration(CLIP_DUR)
scenes.append(_titled(bw, "to_mask(canal=1).to_RGB()"))
# with_background_color
small2 = base.resized(0.5).with_duration(CLIP_DUR)
wbg = small2.with_background_color(size=(W, H), color=(80, 0, 120))
scenes.append(_titled(wbg, "with_background_color(color=(80,0,120))"))
# with_effects_on_subclip
eos = base.with_effects_on_subclip(
[InvertColors()], start_time=0.5, end_time=1.5
).with_duration(CLIP_DUR)
scenes.append(_titled(eos, "with_effects_on_subclip([InvertColors], 0.5, 1.5)"))
# with_volume_scaled (visual label only — audio effect)
vsc = base.with_duration(CLIP_DUR)
scenes.append(_titled(vsc, "with_volume_scaled(factor) # scales audio amplitude"))
# with_layer_index
scenes.append(
_titled(
base.with_duration(CLIP_DUR), "with_layer_index(n) # compositing z-order"
)
)
return scenes
# ══════════════════════════════════════════════════════════════════
# PART 3 — Video Effects (all 34)
# ══════════════════════════════════════════════════════════════════
def _fx(effect: object, label: str, dur: float = CLIP_DUR) -> VideoClip:
"""Apply effect to base clip and label it."""
b = _base_clip(dur)
try:
result = b.with_effects([effect])
# Ensure it has a finite duration
if result.duration is None or result.duration <= 0:
result = result.with_duration(dur)
result = result.with_duration(min(result.duration, dur))
except (ValueError, OSError, AttributeError):
result = b
# Make sure it fits the canvas
if result.size != (W, H):
result = _resize_to_canvas(result)
return _titled(result, label)
def _part3_effects_1_to_17() -> list[VideoClip]:
"""Video effects 1-17: AccelDecel through MakeLoopable."""
scenes: list[VideoClip] = []
# 1. AccelDecel
scenes.append(
_fx(
AccelDecel(new_duration=CLIP_DUR, abruptness=2.0, soonness=1.0),
"AccelDecel(abruptness=2.0, soonness=1.0)",
)
)
# 2. BlackAndWhite
scenes.append(
_fx(
BlackAndWhite(preserve_luminosity=True),
"BlackAndWhite(preserve_luminosity=True)",
)
)
# 3. Blink
scenes.append(
_fx(
Blink(duration_on=0.3, duration_off=0.3),
"Blink(duration_on=0.3, duration_off=0.3)",
)
)
# 4. Crop
b_crop = _base_clip().with_effects([Crop(x1=200, y1=100, x2=1400, y2=800)])
scenes.append(
_titled(_resize_to_canvas(b_crop), "Crop(x1=200, y1=100, x2=1400, y2=800)")
)
# 5. CrossFadeIn
scenes.append(_fx(CrossFadeIn(duration=1.0), "CrossFadeIn(duration=1.0)"))
# 6. CrossFadeOut
scenes.append(_fx(CrossFadeOut(duration=1.0), "CrossFadeOut(duration=1.0)"))
# 7. EvenSize
scenes.append(_fx(EvenSize(), "EvenSize() # ensures even wxh"))
# 8. FadeIn
scenes.append(
_fx(
FadeIn(duration=1.5, initial_color=[0, 0, 0]),
"FadeIn(duration=1.5, initial_color=[0,0,0])",
)
)
# 9. FadeOut
scenes.append(
_fx(
FadeOut(duration=1.5, final_color=[0, 0, 0]),
"FadeOut(duration=1.5, final_color=[0,0,0])",
)
)
# 10. Freeze
scenes.append(
_fx(
Freeze(t=0.5, freeze_duration=1.0),
"Freeze(t=0.5, freeze_duration=1.0)",
dur=3.0,
)
)
# 11. FreezeRegion
scenes.append(
_fx(
FreezeRegion(t=0.5, region=(200, 100, 1400, 700)),
"FreezeRegion(t=0.5, region=(200,100,1400,700))",
)
)
# 12. GammaCorrection
scenes.append(_fx(GammaCorrection(gamma=2.5), "GammaCorrection(gamma=2.5)"))
# 13. HeadBlur
scenes.append(
_fx(
HeadBlur(
fx=lambda _: W // 2,
fy=lambda _: H // 2,
radius=100,
intensity=None,
),
"HeadBlur(fx, fy, radius=100)",
)
)
# 14. InvertColors
scenes.append(_fx(InvertColors(), "InvertColors()"))
# 15. Loop
short = _base_clip(0.5)
looped = short.with_effects([Loop(n=4)])
scenes.append(_titled(looped.with_duration(CLIP_DUR), "Loop(n=4)"))
# 16. LumContrast
scenes.append(
_fx(
LumContrast(lum=30, contrast=50, contrast_threshold=127),
"LumContrast(lum=30, contrast=50)",
)
)
# 17. MakeLoopable
scenes.append(
_fx(MakeLoopable(overlap_duration=0.5), "MakeLoopable(overlap_duration=0.5)")
)
return scenes
def _part3_effects_18_to_34() -> list[VideoClip]:
"""Video effects 18-34: Margin through TimeSymmetrize."""
scenes: list[VideoClip] = []
# 18. Margin
b_margin = _base_clip().with_effects(
[
Resize(0.7),
Margin(
margin_size=None,
left=40,
right=40,
top=20,
bottom=20,
color=(255, 0, 0),
opacity=1.0,
),
]
)
scenes.append(
_titled(
_resize_to_canvas(b_margin),
"Margin(left=40, right=40, top=20, bottom=20, color=red)",
)
)
# 19. MaskColor
scenes.append(
_fx(
MaskColor(color=(128, 128, 128), threshold=80, stiffness=1),
"MaskColor(color, threshold=80)",
)
)
# 20. MasksAnd
mask1 = circle((W, H), (W // 3, H // 2), 300, 1.0, 0.0, 1)
mask2 = circle((W, H), (2 * W // 3, H // 2), 300, 1.0, 0.0, 1)
combined = np.minimum(mask1, mask2)
m_clip = ImageClip(combined, is_mask=True, duration=CLIP_DUR)
masked_and = _base_clip().with_mask(m_clip)
mbg = ColorClip(size=(W, H), color=(0, 0, 0)).with_duration(CLIP_DUR)
scenes.append(
_titled(
CompositeVideoClip([mbg, masked_and], size=(W, H)),
"MasksAnd — intersection of two circle masks",
)
)
# 21. MasksOr
combined_or = np.maximum(mask1, mask2)
m_clip2 = ImageClip(combined_or, is_mask=True, duration=CLIP_DUR)
masked_or = _base_clip().with_mask(m_clip2)
scenes.append(
_titled(
CompositeVideoClip([mbg, masked_or], size=(W, H)),
"MasksOr — union of two circle masks",
)
)
# 22. MirrorX
scenes.append(_fx(MirrorX(), "MirrorX() # horizontal flip"))
# 23. MirrorY
scenes.append(_fx(MirrorY(), "MirrorY() # vertical flip"))
# 24. MultiplyColor
scenes.append(_fx(MultiplyColor(factor=1.8), "MultiplyColor(factor=1.8)"))
# 25. MultiplySpeed
scenes.append(_fx(MultiplySpeed(factor=3.0), "MultiplySpeed(factor=3.0)", dur=4.0))
# 26. Painting
scenes.append(
_fx(
Painting(saturation=1.4, black=0.006),
"Painting(saturation=1.4, black=0.006)",
)
)
# 27. Resize
b_rs = _base_clip().with_effects([Resize(new_size=(960, 540))])
scenes.append(_titled(_resize_to_canvas(b_rs), "Resize(new_size=(960,540))"))
# 28. Rotate
scenes.append(
_fx(
Rotate(angle=45, expand=True, bg_color=(0, 0, 0)),
"Rotate(angle=45, expand=True)",
)
)
# 29. Scroll
# Draw bands
tall_arr = np.full((H * 3, W, 3), 40, dtype=np.uint8)
for i in range(6):
y0, y1 = i * H // 2, (i + 1) * H // 2
tall_arr[y0:y1, :] = [
(50 * i) % 256,
(100 + 30 * i) % 256,
(200 - 20 * i) % 256,
]
tall_clip = ImageClip(tall_arr, duration=CLIP_DUR).with_effects(
[
Scroll(h=H, y_speed=-300, w=W),
]
)
scenes.append(_titled(_resize_to_canvas(tall_clip), "Scroll(h, y_speed=-300)"))
# 30. SlideIn
si = _base_clip().with_effects([SlideIn(duration=1.0, side="left")])
scenes.append(_titled(si, "SlideIn(duration=1.0, side='left')"))
# 31. SlideOut
so = _base_clip().with_effects([SlideOut(duration=1.0, side="right")])
scenes.append(_titled(so, "SlideOut(duration=1.0, side='right')"))
# 32. SuperSample
scenes.append(_fx(SuperSample(d=0.1, n_frames=3), "SuperSample(d=0.1, n_frames=3)"))
# 33. TimeMirror
tm = _base_clip().with_effects([TimeMirror()])
scenes.append(
_titled(tm.with_duration(CLIP_DUR), "TimeMirror() # plays backwards")
)
# 34. TimeSymmetrize
ts = _base_clip().with_effects([TimeSymmetrize()])
scenes.append(
_titled(ts.with_duration(CLIP_DUR), "TimeSymmetrize() # forward then reverse")
)
return scenes
def part3_video_effects() -> list[VideoClip]:
"""Demonstrate all 34 video effects."""
scenes: list[VideoClip] = [
_section_header(
"Part 3: Video Effects",
"All 34 effects from moviepy.video.fx",
),
]
scenes.extend(_part3_effects_1_to_17())
scenes.extend(_part3_effects_18_to_34())
return scenes
# ══════════════════════════════════════════════════════════════════
# PART 4 — Audio
# ══════════════════════════════════════════════════════════════════
def _make_sine(freq: float = 440.0, dur: float = CLIP_DUR) -> AudioClip:
"""Pure sine-wave AudioClip."""
def maker(t: np.ndarray) -> np.ndarray:
t_arr = np.asarray(t)
wave = 0.3 * np.sin(2 * np.pi * freq * t_arr.flatten())
stereo = np.column_stack([wave, wave])
# MoviePy probes with scalar t=0 and uses len(list(frame0))
# for nchannels. A (1,2) array iterates as 1 row → nchannels=1.
# Returning shape (2,) for scalar t lets MoviePy detect 2 channels.
if t_arr.ndim == 0:
return stereo[0]
return stereo
return AudioClip(maker, duration=dur, fps=44100)
def part4_audio() -> list[VideoClip]:
"""Demonstrate audio clips and all 7 audio effects."""
scenes: list[VideoClip] = [
_section_header(
"Part 4: Audio",
"AudioClip · AudioArrayClip · CompositeAudioClip · 7 Audio Effects",
),
]
bg = ColorClip(size=(W, H), color=(20, 30, 50))
# AudioClip
a1 = _make_sine(440, CLIP_DUR)
c1 = bg.with_duration(CLIP_DUR).with_audio(a1)
scenes.append(_titled(c1, "AudioClip(sine_440Hz)"))
# AudioArrayClip
sr = 44100
t_arr = np.linspace(0, CLIP_DUR, int(sr * CLIP_DUR), endpoint=False)
arr = (0.3 * np.sin(2 * np.pi * 880 * t_arr)).astype(np.float64)
stereo = np.column_stack([arr, arr])
a2 = AudioArrayClip(stereo, fps=sr)
c2 = bg.with_duration(CLIP_DUR).with_audio(a2)
scenes.append(_titled(c2, "AudioArrayClip(numpy_array, fps=44100) # 880Hz"))
# CompositeAudioClip
low = _make_sine(220, CLIP_DUR)
high = _make_sine(660, CLIP_DUR)
comp_audio = CompositeAudioClip([low, high])
c3 = bg.with_duration(CLIP_DUR).with_audio(comp_audio)
scenes.append(_titled(c3, "CompositeAudioClip([220Hz, 660Hz])"))
# concatenate_audioclips
a_cat = concatenate_audioclips([_make_sine(330, 1.0), _make_sine(550, 1.0)])
c4 = bg.with_duration(CLIP_DUR).with_audio(a_cat)
scenes.append(_titled(c4, "concatenate_audioclips([330Hz, 550Hz])"))
# AudioFadeIn
a_fi = _make_sine(440, CLIP_DUR).with_effects([AudioFadeIn(duration=1.5)])
c5 = bg.with_duration(CLIP_DUR).with_audio(a_fi)
scenes.append(_titled(c5, "AudioFadeIn(duration=1.5)"))
# AudioFadeOut
a_fo = _make_sine(440, CLIP_DUR).with_effects([AudioFadeOut(duration=1.5)])
c6 = bg.with_duration(CLIP_DUR).with_audio(a_fo)
scenes.append(_titled(c6, "AudioFadeOut(duration=1.5)"))
# AudioDelay
a_delay = _make_sine(440, CLIP_DUR).with_effects(
[AudioDelay(offset=0.2, n_repeats=4, decay=1)]
)
c7 = bg.with_duration(a_delay.duration).with_audio(a_delay)
scenes.append(
_titled(
c7.with_duration(CLIP_DUR), "AudioDelay(offset=0.2, n_repeats=4, decay=1)"
)
)
# AudioLoop
short_a = _make_sine(440, 0.5)
a_loop = short_a.with_effects([AudioLoop(duration=CLIP_DUR)])
c8 = bg.with_duration(CLIP_DUR).with_audio(a_loop)
scenes.append(_titled(c8, "AudioLoop(duration=2.0)"))
# AudioNormalize
quiet = _make_sine(440, CLIP_DUR) # already normalized but demonstrates the call
a_norm = quiet.with_effects([AudioNormalize()])
c9 = bg.with_duration(CLIP_DUR).with_audio(a_norm)
scenes.append(_titled(c9, "AudioNormalize()"))
# MultiplyStereoVolume
a_stereo = _make_sine(440, CLIP_DUR).with_effects(
[MultiplyStereoVolume(left=1.0, right=0.2)]
)
c10 = bg.with_duration(CLIP_DUR).with_audio(a_stereo)
scenes.append(_titled(c10, "MultiplyStereoVolume(left=1.0, right=0.2)"))
# MultiplyVolume
a_vol = _make_sine(440, CLIP_DUR).with_effects([MultiplyVolume(factor=0.3)])
c11 = bg.with_duration(CLIP_DUR).with_audio(a_vol)
scenes.append(_titled(c11, "MultiplyVolume(factor=0.3)"))
return scenes
# ══════════════════════════════════════════════════════════════════
# PART 5 — Composition
# ══════════════════════════════════════════════════════════════════
def part5_composition() -> list[VideoClip]:
"""Demonstrate composition & concatenation."""
scenes: list[VideoClip] = [
_section_header(
"Part 5: Composition",
"CompositeVideoClip · concatenate_videoclips · clips_array",
),
]
# CompositeVideoClip with bg_color, use_bgclip
bg = _base_clip()
overlay = (
ColorClip(size=(400, 400), color=(255, 50, 50))
.with_duration(CLIP_DUR)
.with_position(("center", "center"))
.with_opacity(0.6)
)
comp1 = CompositeVideoClip([bg, overlay], size=(W, H), bg_color=(0, 0, 0))
scenes.append(_titled(comp1, "CompositeVideoClip(clips, bg_color, use_bgclip)"))
# concatenate_videoclips — method='chain'
c1 = ColorClip(size=(W, H), color=(200, 50, 50)).with_duration(0.7)
c2 = ColorClip(size=(W, H), color=(50, 200, 50)).with_duration(0.7)
c3 = ColorClip(size=(W, H), color=(50, 50, 200)).with_duration(0.6)
cat = concatenate_videoclips([c1, c2, c3], method="chain")
scenes.append(_titled(cat, "concatenate_videoclips(method='chain')"))
# concatenate_videoclips — method='compose' with padding
cat2 = concatenate_videoclips(
[
c1.resized((W // 2, H // 2)),
c2.resized((W // 2, H // 2)),
c3.resized((W, H)),
],
method="compose",
bg_color=(0, 0, 0),
padding=-0.2,
)
scenes.append(
_titled(
_resize_to_canvas(cat2),
"concatenate_videoclips(method='compose', padding=-0.2)",
)
)
# concatenate_videoclips with transition
cat3 = concatenate_videoclips(
[c1, c2, c3],
padding=-0.3,
method="compose",
)
scenes.append(
_titled(
cat3.with_duration(CLIP_DUR),
"concatenate_videoclips(padding=-0.3) # overlap",
)
)
# clips_array
a = ColorClip(size=(W // 2, H // 2), color=(200, 50, 50)).with_duration(CLIP_DUR)
b = ColorClip(size=(W // 2, H // 2), color=(50, 200, 50)).with_duration(CLIP_DUR)
c = ColorClip(size=(W // 2, H // 2), color=(50, 50, 200)).with_duration(CLIP_DUR)
d = ColorClip(size=(W // 2, H // 2), color=(200, 200, 50)).with_duration(CLIP_DUR)
grid = clips_array([[a, b], [c, d]])
scenes.append(_titled(_resize_to_canvas(grid), "clips_array([[a, b], [c, d]])"))
return scenes
# ══════════════════════════════════════════════════════════════════
# PART 6 — Drawing Tools
# ══════════════════════════════════════════════════════════════════
def part6_drawing_tools() -> list[VideoClip]:
"""Demonstrate moviepy.video.tools.drawing functions."""
scenes: list[VideoClip] = [
_section_header(
"Part 6: Drawing Tools", "circle · color_gradient · color_split"
),
]
# circle
circ = circle(
screensize=(W, H),
center=(W // 2, H // 2),
radius=300,
color=1.0,
bg_color=0.0,
blur=30,
)
circ_rgb = (np.dstack([circ, circ, circ]) * 255).astype(np.uint8)
scenes.append(
_titled(
ImageClip(circ_rgb, duration=CLIP_DUR),
"drawing.circle(center, radius=300, blur=30)",
)
)
# color_gradient — linear
grad = color_gradient(
size=(W, H),
p1=(0, 0),
p2=(W, H),
color_1=0.0,
color_2=1.0,
shape="linear",
)
grad_rgb = (np.dstack([grad, grad, grad]) * 255).astype(np.uint8)
scenes.append(
_titled(
ImageClip(grad_rgb, duration=CLIP_DUR),
"drawing.color_gradient(shape='linear')",
)
)
# color_gradient — radial
grad_r = color_gradient(
size=(W, H),
p1=(W // 2, H // 2),
radius=500,
color_1=1.0,
color_2=0.0,
shape="radial",
)
grad_r_rgb = (np.dstack([grad_r, grad_r, grad_r]) * 255).astype(np.uint8)
scenes.append(
_titled(
ImageClip(grad_r_rgb, duration=CLIP_DUR),
"drawing.color_gradient(shape='radial', radius=500)",
)
)
# color_split
split = color_split(
size=(W, H),
x=W // 2,
color_1=0.0,
color_2=1.0,
gradient_width=100,
)
split_rgb = (np.dstack([split, split, split]) * 255).astype(np.uint8)
scenes.append(
_titled(
ImageClip(split_rgb, duration=CLIP_DUR),
"drawing.color_split(x=W/2, gradient_width=100)",
)
)
return scenes
# ══════════════════════════════════════════════════════════════════
# PART 7 — Output Methods
# ══════════════════════════════════════════════════════════════════
def part7_output() -> list[VideoClip]:
"""Label-only slides for output methods + parameters."""
scenes: list[VideoClip] = [
_section_header(
"Part 7: Output Methods",
"write_videofile · write_gif · save_frame · write_images_sequence",
),
]
bg = ColorClip(size=(W, H), color=(15, 20, 35))
methods = [
(
"write_videofile()",
"filename, fps, codec, bitrate, audio, audio_fps,\n"
"preset, audio_nbytes, audio_codec, audio_bitrate,\n"
"audio_bufsize, temp_audiofile, threads,\n"
"ffmpeg_params, logger, pixel_format",
),
(
"write_gif()",
"filename, fps, loop, logger",
),
(
"save_frame()",
"filename, t, with_mask",
),
(
"write_images_sequence()",
"name_format, fps, with_mask, logger",
),
(
"write_audiofile()",
"filename, fps, nbytes, buffersize,\ncodec, bitrate, ffmpeg_params, logger",
),
]
for title, params in methods:
t1 = (
TextClip(
text=title, font_size=56, color="cyan", font=FONT_B, margin=(0, 20)
)
.with_duration(2.5)
.with_position(("center", 300))
)
t2 = (
TextClip(
text=f"Parameters:\n{params}",
font_size=32,
color="#dddddd",
font=FONT_R,
method="caption",
size=(W - 300, None),
text_align="center",
interline=8,
margin=(0, 15),
)
.with_duration(2.5)
.with_position(("center", 500))
)
scenes.append(CompositeVideoClip([bg.with_duration(2.5), t1, t2], size=(W, H)))
return scenes
# ══════════════════════════════════════════════════════════════════
# ASSEMBLY — memory-safe: render each part to a temp file, then
# concatenate via VideoFileClip so only one part is in RAM at a time.
# ══════════════════════════════════════════════════════════════════
def _render_part(
scenes: list[VideoClip],
path: str,
label: str,
) -> None:
"""Concatenate *scenes*, write to *path*, then close all clips."""
logger.info(" Rendering %s (%d scenes) → %s", label, len(scenes), Path(path).name)
part = concatenate_videoclips(scenes, method="compose", bg_color=(0, 0, 0))
part.write_videofile(
path,
fps=FPS,
codec="libx264",
preset="ultrafast",
audio=False,
logger=None,
)
# Free memory
part.close()
for s in scenes:
s.close()
def main() -> None:
"""Assemble all parts into the final showcase video."""
logging.basicConfig(level=logging.INFO)
logger.info("Building MoviePy comprehensive showcase…")
tmpdir = tempfile.mkdtemp(prefix="moviepy_showcase_")
try:
_build(tmpdir)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
def _build(tmpdir: str) -> None:
# ── Render each part to its own temp file ─────────────────────
# Title card
title_bg = ColorClip(size=(W, H), color=(10, 10, 30)).with_duration(3.0)
title_txt = (
TextClip(
text="MoviePy 2.x\nComplete Feature Showcase",
font_size=80,
color="white",
font=FONT_B,
method="caption",
size=(W - 200, None),
text_align="center",
margin=(0, 40),
)
.with_duration(3.0)
.with_position("center")
)
title_card = CompositeVideoClip([title_bg, title_txt], size=(W, H)).with_effects(
[FadeIn(1.0)]
)
# Outro
outro_bg = ColorClip(size=(W, H), color=(10, 10, 30)).with_duration(3.0)
outro_txt = (
TextClip(
text="That's all of MoviePy 2.x!\n34 video effects · 7 audio effects\n"
"11 clip types · drawing tools · composition",
font_size=52,
color="white",
font=FONT_B,
method="caption",
size=(W - 200, None),
text_align="center",
margin=(0, 30),
)
.with_duration(3.0)
.with_position("center")
)
outro = CompositeVideoClip([outro_bg, outro_txt], size=(W, H)).with_effects(
[FadeOut(1.5)]
)
part_builders = [
("title", lambda: [title_card]),
("Part 1: Clip Types", part1_clip_types),
("Part 2: Clip Methods", part2_clip_methods),
("Part 3: Video Effects", part3_video_effects),
("Part 4: Audio", part4_audio),
("Part 5: Composition", part5_composition),
("Part 6: Drawing Tools", part6_drawing_tools),
("Part 7: Output Methods", part7_output),
("outro", lambda: [outro]),
]
part_files: list[str] = []
for i, (label, builder) in enumerate(part_builders):
path = str(Path(tmpdir) / f"part_{i:02d}.mp4")
scenes = builder()
_render_part(scenes, path, label)
part_files.append(path)
# ── Load temp files as lightweight VideoFileClips & concat ─────
logger.info("Concatenating all parts…")
file_clips = [VideoFileClip(p) for p in part_files]
final = concatenate_videoclips(file_clips, method="chain")
# Background audio
audio = _make_sine(330, final.duration).with_effects([MultiplyVolume(factor=0.5)])
final = final.with_audio(audio)
logger.info("Total duration: %.1fs", final.duration)
logger.info("Writing %s (NVENC GPU)…", OUTPUT)
final.write_videofile(
OUTPUT,
fps=FPS,
codec="h264_nvenc",
audio_codec="aac",
threads=os.cpu_count(),
ffmpeg_params=["-preset", "p4", "-rc", "constqp", "-qp", "18", "-b:v", "0"],
logger="bar",
)
# Clean up
final.close()
for c in file_clips:
c.close()
size_mb = Path(OUTPUT).stat().st_size / (1024 * 1024)
logger.info("✔ Saved %s (%.1f MB)", OUTPUT, size_mb)
if __name__ == "__main__":
main()