Automating Video Creation with Python (MoviePy + Pillow)

Learn how to create an automatic 30-second video generator using Python. Mix random images, take the first 30 seconds of each song, overlay text, and export MP4 clips with MoviePy and Pillow.

In this post, we’ll build a fully automated video generator bot in Python. It shuffles random images, takes the first 30 seconds of your music tracks, overlays the song title and username, and outputs 30-second MP4 videos.

With MoviePy and Pillow, you can automate short-form content for YouTube Shorts, Instagram Reels, or TikTok in a creative way. It’s a perfect example of how simple code can turn into an automated content pipeline.

Setup Steps

  1. Install Python and FFmpeg.
  2. Install MoviePy, Pillow, and Numpy libraries.
  3. Put your music files in audio/ and images in images/.
  4. Run the make_videos.py script.

Result

Each run creates random 30-second vertical videos with your song name and username overlay. A creative and practical way to automate video production with Python!


import PIL
from PIL import Image
if not hasattr(Image, “ANTIALIAS”):
Image.ANTIALIAS = Image.Resampling.LANCZOS
# —————————————————

import os
import random
import re
from pathlib import Path
from typing import List, Tuple

from PIL import Image, ImageDraw, ImageFont
import numpy as np
from moviepy.editor import (
AudioFileClip,
ImageClip,
CompositeVideoClip,
concatenate_videoclips,
vfx,
)

# ==== KULLANICI AYARLARI ====
USERNAME = “@arabeskradyo6” # Videoda görünecek kullanıcı adı
OUTPUT_SIZE = (1080, 1920) # (genişlik, yükseklik) 9:16 Reels/TikTok tarzı. İstersen (1920,1080) yap.
FPS = 30 # Çıktı FPS
DURATION = 30 # Video süresi (sabit 30 sn)
IMAGES_PER_VIDEO = 6 # 6 görsel -> her biri ~5sn. images/ azsa otomatik düşer.
FONT_PATH = “fonts/Rubik-SemiBold.ttf” # Türkçe karakter destekli bir TTF. Yoksa default yedek kullanır.
TITLE_FONT_SIZE = 64
USER_FONT_SIZE = 42
TEXT_MARGIN = 40 # Kenarlardan iç boşluk (px)
TEXT_BG_ALPHA = 140 # Metin arkaplan saydamlığı (0-255)

# ==== KAYNAK KLASÖRLER ====
AUDIO_DIR = Path(“audio”)
IMAGES_DIR = Path(“images”)
OUTPUT_DIR = Path(“output”)
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

# ==== YARDIMCI FONKSİYONLAR ====
def list_media_files(folder: Path, exts: Tuple[str, …]) -> List[Path]:
return sorted([p for p in folder.glob(“*”) if p.suffix.lower() in exts])

def infer_title_from_filename(path: Path) -> str:
# “Artist – Song Name (feat.) .mp3” -> “Artist – Song Name (feat.)”
title = path.stem
# Kaba temizlik: underscore -> boşluk, çift boşlukları düzelt
title = re.sub(r”[_]+”, ” “, title).strip()
title = re.sub(r”\s{2,}”, ” “, title)
return title

def load_font(size: int) -> ImageFont.FreeTypeFont:
try:
if Path(FONT_PATH).exists():
return ImageFont.truetype(FONT_PATH, size=size)
# Sistem fontlarına düşmek için isim vermeden dener (platforma göre değişir)
return ImageFont.truetype(“DejaVuSans.ttf”, size=size)
except Exception:
# Son çare: PIL default bitmap font (Türkçe destek sınırlı olabilir)
return ImageFont.load_default()

def make_text_overlay(
title: str,
username: str,
size: Tuple[int, int],
margin: int = 40,
title_size: int = 64,
user_size: int = 42,
bg_alpha: int = 140,
) -> str:
“””
Metinleri (şarkı adı + kullanıcı adı) şeffaf PNG’ye yazar ve dosya yolunu döner.
Pillow 10/11 uyumlu: textbbox() varsa onu, yoksa font.getsize() kullanır.
“””
W, H = size
title_font = load_font(title_size)
user_font = load_font(user_size)

# Ölçüm yardımcıları
from PIL import ImageDraw as _ImageDraw
def measure(draw_obj, text, font):
# Pillow 8+ : textbbox mevcut, en doğru ölçüm
if hasattr(draw_obj, “textbbox”):
l, t, r, b = draw_obj.textbbox((0, 0), text, font=font)
return (r – l, b – t)
# Eski sürümler için yedek
return font.getsize(text)

# Satır kaydırma
draw_tmp = ImageDraw.Draw(Image.new(“RGBA”, (W, H)))
max_text_width = W – 2 * margin

def wrap_text(text, font, max_width):
words = text.split()
lines, line = [], “”
for w in words:
test = (line + ” ” + w).strip()
tw, _ = measure(draw_tmp, test, font)
if tw <= max_text_width or not line:
line = test
else:
lines.append(line)
line = w
if line:
lines.append(line)
return lines

title_lines = wrap_text(title, title_font, max_text_width)
user_lines = wrap_text(f”@{username}”, user_font, max_text_width)

# Yükseklik hesapla
line_spacing = 8
def block_height(lines, font):
if not lines:
return 0
heights = [measure(draw_tmp, ln, font)[1] for ln in lines]
return sum(heights) + line_spacing * (len(lines) – 1)

title_h = block_height(title_lines, title_font)
user_h = block_height(user_lines, user_font)

block_pad_v = 16
total_h = title_h + user_h + block_pad_v * 2 + 10

overlay = Image.new(“RGBA”, (W, total_h), (0, 0, 0, 0))
bg = Image.new(“RGBA”, (W, total_h), (0, 0, 0, bg_alpha))
overlay.paste(bg, (0, 0))

draw = ImageDraw.Draw(overlay)

# Başlığı merkezle
y = block_pad_v
for ln in title_lines:
tw, th = measure(draw, ln, title_font)
draw.text(((W – tw) // 2, y), ln, font=title_font, fill=(255, 255, 255, 255))
y += th + line_spacing

y += 2 # küçük boşluk

# Kullanıcı adını merkezle
for ln in user_lines:
tw, th = measure(draw, ln, user_font)
draw.text(((W – tw) // 2, y), ln, font=user_font, fill=(220, 220, 220, 255))
y += th + line_spacing

tmp_path = OUTPUT_DIR / f”overlay_{abs(hash(title))}.png”
overlay.save(tmp_path)
return str(tmp_path)

def make_image_clip(img_path: Path, duration: float, target_size: Tuple[int, int]) -> ImageClip:
“””
Görseli ekranı dolduracak şekilde letterbox olmadan kırp/zoom et (cover),
hafif bir yavaş zoom efekti ekle.
“””
W, H = target_size
clip = ImageClip(str(img_path)).set_duration(duration)
# Önce ortalama bir cover yapalım:
clip = clip.resize(height=H) if clip.h < clip.w else clip.resize(width=W)

# Cover: gerekiyorsa kırp
# Eni boyu aşan kenarları merkezden kırp
if clip.w < W:
clip = clip.resize(width=W)
if clip.h < H:
clip = clip.resize(height=H)
x_center = (clip.w – W) / 2
y_center = (clip.h – H) / 2
clip = clip.crop(x1=x_center, y1=y_center, x2=x_center + W, y2=y_center + H)

# Yumuşak zoom (Ken Burns hissi)
# 30 sn’de %6 büyüme için ~0.002/ sn
zoom_rate = 0.002 # istersen arttır/azalt
clip = clip.resize(lambda t: 1 + zoom_rate * t)

return clip

def build_video_for_audio(audio_path: Path, images: List[Path]) -> Path:
# Başlık + overlay PNG
title = infer_title_from_filename(audio_path)
overlay_png = make_text_overlay(
title=title,
username=USERNAME,
size=OUTPUT_SIZE,
margin=TEXT_MARGIN,
title_size=TITLE_FONT_SIZE,
user_size=USER_FONT_SIZE,
bg_alpha=TEXT_BG_ALPHA,
)

# 30 sn ses
audio = AudioFileClip(str(audio_path)).subclip(0, DURATION)

# Görsel süre/adet
n_images = min(IMAGES_PER_VIDEO, len(images)) if len(images) > 0 else 1
if n_images <= 0:
raise RuntimeError(“images/ klasöründe görsel bulunamadı.”)
segment = DURATION / n_images

chosen = random.sample(images, n_images) if len(images) >= n_images else images[:]
while len(chosen) < n_images:
chosen += random.sample(images, min(len(images), n_images – len(chosen)))

# Arka plan klipleri (Ken Burns hafif zoom içerir)
clips = [make_image_clip(p, segment, OUTPUT_SIZE) for p in chosen]
bg = concatenate_videoclips(clips, method=”chain”).set_duration(DURATION)

# Overlay bant
overlay_clip = (
ImageClip(overlay_png)
.set_duration(DURATION)
.set_position((“center”, “top”))
)

# Final kompozit
final = CompositeVideoClip([bg, overlay_clip], size=OUTPUT_SIZE).set_audio(audio)

# Çıktı adı
safe_title = re.sub(r”[^\w\-]+”, “_”, title)[:80]
out_path = OUTPUT_DIR / f”{safe_title or ‘video’}.mp4″

# Yaz ve temizle
try:
final.write_videofile(
str(out_path),
fps=FPS,
codec=”libx264″,
audio_codec=”aac”,
threads=os.cpu_count() or 4,
preset=”medium”,
ffmpeg_params=[“-movflags”, “+faststart”, “-pix_fmt”, “yuv420p”],
temp_audiofile=str(OUTPUT_DIR / “temp-audio.m4a”),
remove_temp=True,
)
finally:
# Geçici overlay PNG’sini sil (yazım başarısız olsa bile)
try:
os.remove(overlay_png)
except Exception:
pass

return out_path

 

def main():
audio_files = list_media_files(AUDIO_DIR, (“.mp3”, “.wav”, “.m4a”, “.flac”, “.aac”, “.ogg”))
image_files = list_media_files(IMAGES_DIR, (“.jpg”, “.jpeg”, “.png”, “.webp”))

if not audio_files:
raise SystemExit(“audio/ klasöründe ses dosyası bulunamadı.”)
if not image_files:
raise SystemExit(“images/ klasöründe görsel bulunamadı.”)

random.shuffle(image_files)

print(f”{len(audio_files)} şarkı bulundu, {len(image_files)} görsel bulundu.”)
for i, a in enumerate(audio_files, 1):
print(f”[{i}/{len(audio_files)}] {a.name} -> video oluşturuluyor…”)
try:
out = build_video_for_audio(a, image_files)
print(f” ✔ {out}”)
except Exception as e:
print(f” ✖ Hata: {a.name}: {e}”)

if __name__ == “__main__”:
main()

 



cd “C:\Users\Win10\Desktop\video maker”
venv\Scripts\activate
python make_videos.py

 

 

Leave a Comment