Nedir:

audio klasöründeki müzikleri sırayla alır, images klasöründeki resimlerden her 5 saniyeye 1 tane düşecek şekilde rasgele görsel seçer,
fontklasöründen ttf çağırır ve şarkının ismini, sizin sosyal medya kullanıcı adını vb videoya ekler,
en son output klasörüne .mp4 formatında kaydeder.

Örneğin : https://www.tiktok.com/@arabeskradyo6

Bunları youtube, tiktok, instagram hesaplarınıza yükleyin (bunu da otomatik yapacak script üzerinde çalışıyorum)
Python kullanarak görselleri rastgele karıştıran, müziğin ilk 30 saniyesini alan ve üzerine şarkı adı ile kullanıcı adını ekleyerek 30 saniyelik videolar üreten bir bot hazırladık. MoviePy ve Pillow kütüphaneleriyle kendi video otomasyon sistemini nasıl kurabileceğini adım adım anlatıyoruz.
Bu yazıda Python kullanarak tamamen otomatik çalışan bir video oluşturma botu geliştiriyoruz. Elimizdeki görselleri rastgele karıştırıyor, seçtiğimiz müzik dosyalarının ilk 30 saniyesini alıyor, şarkı adını ve kullanıcı adımızı ekrana basıyor. Tüm bunları MoviePy ve Pillow kütüphaneleri ile 30 saniyelik MP4 formatında çıktı olarak alıyoruz.
Böyle bir sistem, YouTube Shorts veya Instagram Reels gibi platformlara hızlı içerik üretmek isteyenler için harika bir otomasyon fikri. Kendi klasöründeki müzikleri ve görselleri kullanarak her çalıştırmada farklı sonuçlar üreten bir mini video üretim motoru aslında.

Kurulum Adımları
  1. Python ve FFmpeg kur.
  2. MoviePy, Pillow, Numpy kütüphanelerini yükle.
  3. Görselleri images/, müzikleri audio/ klasörüne yerleştir.
  4. make_videos.py betiğini çalıştır.

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


Kaynaklar bana aittir, alıntı değildir:
Github
TR : Python ile Otomatik Video Oluşturma (MoviePy + Pillow)
EN : Automating Video Creation with Python (MoviePy + Pillow)


# --- Pillow 10+ uyumluluk yaması (MoviePy için) ---
import PIL
from PIL import Image
if not hasattr(Image, "ANTIALIAS"):
    # Pillow 10+ sürümlerinde kaldırıldı -> LANCZOS ile eşleştir
    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()