# 영상 생성 성능 개선 (Video Performance Fix)

## 배경
카드뉴스 7장으로 21초 영상 생성 테스트 결과, **약 6분 소요** — 실서비스 불가능한 수준.
원인 분석 결과 3가지 개선점 확인.

## 프로젝트 정보
- 경로: `/home/jay/projects/ThreadAuto/`
- 영상 모듈: `video/` 디렉토리
- 기존 테스트: `video/tests/` (199 PASS)

---

## 작업 1: write_videofile에 pix_fmt yuv420p 강제

### 문제
`video/video_generator.py` line 174의 `write_videofile()` 호출에서 pix_fmt를 명시하지 않음.
MoviePy가 합성 클립(CompositeVideoClip)에 대해 rgba 포맷으로 ffmpeg에 전달 → ffmpeg이 yuva420p(알파 포함)로 중간 인코딩 → 불필요한 오버헤드.

### 수정

```python
# 현재
final_clip.write_videofile(
    output_path,
    fps=FPS,
    codec=OUTPUT_CODEC,
    logger=None,
)

# 변경
final_clip.write_videofile(
    output_path,
    fps=FPS,
    codec=OUTPUT_CODEC,
    logger=None,
    ffmpeg_params=["-pix_fmt", "yuv420p"],
)
```

---

## 작업 2: config.py에 FFMPEG_PRESET 추가 + 적용

### 문제
libx264 preset이 MoviePy 기본값(medium) 사용 중. 슬라이드쇼는 복잡한 모션이 없어 fast/ultrafast로 충분.

### 수정

**`video/config.py`** 추가:
```python
# FFmpeg 인코딩 프리셋 ("ultrafast", "fast", "medium", "slow")
# 슬라이드쇼는 모션이 적으므로 fast 추천 (medium 대비 ~50% 빠름)
FFMPEG_PRESET = "fast"
```

**`video/video_generator.py`** 수정:
```python
from video.config import ..., FFMPEG_PRESET

final_clip.write_videofile(
    output_path,
    fps=FPS,
    codec=OUTPUT_CODEC,
    logger=None,
    ffmpeg_params=["-pix_fmt", "yuv420p", "-preset", FFMPEG_PRESET],
)
```

---

## 작업 3: Ken Burns 효과 성능 최적화

### 문제
`video/effects.py` line 210의 ken_burns():
```python
result = clip.resized(lambda t: 1.0 + (zoom_ratio - 1.0) * (t / effective_duration))
```
- `resized(lambda t: ...)` 는 **매 프레임마다** 전체 이미지를 리사이즈
- 30fps × 3초 = 90프레임/슬라이드, 1080×1920 이미지를 90번 리사이즈
- 7장이면 630번 리사이즈 → 이것이 6분의 주범

### 수정 방안: 사전 스케일 + 크롭 방식

Ken Burns를 "매 프레임 resize" 대신 "미리 큰 이미지 → 매 프레임 crop"으로 변경:

```python
def ken_burns(clip, duration=None, zoom_ratio=1.2):
    """Ken Burns: 사전 확대 + 크롭 방식 (성능 최적화)"""
    if clip is None:
        raise TypeError("clip은 None이 될 수 없습니다.")
    if zoom_ratio <= 1.0:
        raise ValueError(f"zoom_ratio는 1보다 커야 합니다. 전달된 zoom_ratio: {zoom_ratio}")

    effective_duration = duration if duration is not None else clip.duration
    if effective_duration <= 0:
        raise ValueError(f"duration은 0보다 커야 합니다. 전달된 duration: {effective_duration}")
    if effective_duration > clip.duration:
        raise ValueError(f"duration({effective_duration}초)은 clip.duration({clip.duration}초)을 초과할 수 없습니다.")

    w, h = clip.w, clip.h

    # 1. 미리 zoom_ratio 배로 확대 (1회만 리사이즈)
    enlarged = clip.resized(zoom_ratio)

    # 2. 매 프레임에서 원래 크기만큼 크롭 (리사이즈 없이 좌표 이동만)
    def make_frame(get_frame, t):
        # t=0일 때 중앙, t=effective_duration일 때 약간 이동 (패닝 효과)
        progress = t / effective_duration if effective_duration > 0 else 0
        ew, eh = int(w * zoom_ratio), int(h * zoom_ratio)

        # 줌인 효과: progress에 따라 크롭 영역 축소
        crop_w = int(w + (ew - w) * (1 - progress))
        crop_h = int(h + (eh - h) * (1 - progress))

        x = (ew - crop_w) // 2
        y = (eh - crop_h) // 2

        frame = get_frame(t)
        cropped = frame[y:y+crop_h, x:x+crop_w]

        # 원래 크기로 리사이즈 (crop된 영역 → 출력 해상도)
        from PIL import Image
        import numpy as np
        img = Image.fromarray(cropped)
        img = img.resize((w, h), Image.LANCZOS)
        return np.array(img)

    from moviepy import VideoClip
    result = VideoClip(lambda t: make_frame(enlarged.get_frame, t), duration=effective_duration)
    result = result.with_fps(clip.fps if hasattr(clip, 'fps') and clip.fps else 30)
    return result
```

**핵심**:
- 기존: 매 프레임 `clip.resized(scale)` → MoviePy 내부에서 전체 이미지 리사이즈 x630회
- 개선: 1회 확대 후, numpy 배열 슬라이싱(크롭) + PIL 리사이즈 → 훨씬 빠름
- PIL.Image.resize(LANCZOS)가 MoviePy resized보다 단순 작업에서 2-3배 빠름

**대안 (더 간단)**: Ken Burns 효과를 아예 제거하고 fade 전환만 사용. 성능 극대화.
→ 이 대안은 선택적. 일단 최적화 먼저 적용.

---

## 테스트

### 성능 테스트 (핵심!)
1. 개선 전/후 비교를 위해 **동일 7장 이미지**로 테스트:
   ```bash
   # 이미지 목록
   ls /home/jay/projects/ThreadAuto/output/cardnews_20260306_012556_*.png
   ```
2. 테스트 스크립트 작성 (시간 측정 포함):
   ```python
   import time
   start = time.time()
   generate_slideshow(images, "output/videos/perf_test.mp4", transition_type="fade", use_ken_burns=True)
   elapsed = time.time() - start
   print(f"소요 시간: {elapsed:.1f}초")
   ```
3. **목표**: 6분(360초) → 2분(120초) 이내 (3배 이상 개선)

### 기존 테스트
- `video/tests/` 전체 199개 테스트 PASS 유지
- Ken Burns 관련 테스트가 내부 구현 변경에 영향받을 수 있음 → mock 패턴 확인 후 테스트 수정

### 품질 테스트
- 생성된 MP4 파일이 정상 재생되는지 ffmpeg으로 검증:
  ```bash
  /home/jay/.local/lib/python3.12/site-packages/imageio_ffmpeg/binaries/ffmpeg-linux-x86_64-v7.0.2 -i output/videos/perf_test.mp4 -f null - 2>&1 | tail -3
  ```
- yuv420p 픽셀 포맷 확인

## QC 기준
- 기존 199개 테스트 PASS (수정 필요시 테스트도 업데이트)
- pyright 0 errors
- 성능 목표 달성 (3배 이상 개선)
- 출력 영상 재생 정상