"""썸네일 생성 — 영상 5초 프레임 추출 + 텍스트 오버레이.""" import os import subprocess import logging from PIL import Image, ImageDraw, ImageFont from . import storage logger = logging.getLogger("music-lab.thumb") THUMB_TIMEOUT_S = 60 class ThumbGenerationError(Exception): pass def generate(*, pipeline_id: int, video_path: str, track_title: str = "", overlay_text: bool = True) -> dict: out_path = os.path.join(storage.pipeline_dir(pipeline_id), "thumbnail.jpg") cmd = ["ffmpeg", "-y", "-i", video_path, "-ss", "00:00:05", "-vframes", "1", "-q:v", "2", out_path] result = subprocess.run(cmd, capture_output=True, text=True, timeout=THUMB_TIMEOUT_S) if result.returncode != 0: raise ThumbGenerationError(f"ffmpeg 썸네일 실패: {result.stderr[-500:]}") if overlay_text and track_title: _overlay_title(out_path, track_title) return {"url": storage.media_url(pipeline_id, "thumbnail.jpg"), "used_fallback": False} def _overlay_title(path: str, title: str) -> None: try: with Image.open(path) as src: img = src.convert("RGB") try: font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 80) except OSError: font = ImageFont.load_default() draw = ImageDraw.Draw(img) # 하단 30% 영역에 검정 반투명 박스 + 흰 글씨 w, h = img.size box_h = int(h * 0.3) with Image.new("RGBA", (w, box_h), (0, 0, 0, 160)) as overlay: img.paste(overlay, (0, h - box_h), overlay) bbox = draw.textbbox((0, 0), title, font=font) tw = bbox[2] - bbox[0] draw.text(((w - tw) // 2, h - box_h + 30), title, fill=(255, 255, 255), font=font) img.save(path, "JPEG", quality=92) except Exception as e: logger.warning("썸네일 오버레이 실패: %s", e)