# task: FFmpeg 파이프 스트리밍 + 영상 피드백 반영 + 재업로드

## ★ 핵심 수정 1: FFmpeg 파이프 스트리밍 (RAM 문제 해결)

### 현재 문제
- `render_all_scenes()`가 모든 프레임(~1200개)을 list에 누적 → 7.1GB RAM 사용
- 서버 RAM 7.7GB → 영상 렌더링 시 100% 도달 → 서버 다운
- 1 프레임 = 1080×1920×3 bytes = 5.9MB, 1200 프레임 = 7.1GB

### 수정 방향: 프레임을 메모리에 쌓지 않고, 생성 즉시 FFmpeg stdin으로 파이프

#### A. `evan_dynamic.py` 수정

1. `render_scene()` (line 245~312): **generator로 변환**
   - `frames: list[np.ndarray] = []` + `frames.append()` + `return frames` → 삭제
   - `yield np.array(img, dtype=np.uint8)` 로 교체
   - 반환 타입: `list[np.ndarray]` → `Iterator[np.ndarray]`

2. `render_all_scenes()` (line 314~320): **generator로 변환**
   - `all_frames` 리스트 누적 → `yield from self.render_scene(scene)`
   - 반환 타입: `list[np.ndarray]` → `Iterator[np.ndarray]`

3. `generate_evan_dynamic_video()` (line 905~943): **FFmpeg pipe 방식으로 교체**
   - `ImageSequenceClip(all_frames)` 삭제
   - subprocess로 FFmpeg 실행:
   ```python
   import subprocess
   cmd = [
       "ffmpeg", "-y",
       "-f", "rawvideo",
       "-vcodec", "rawvideo",
       "-pix_fmt", "rgb24",
       "-s", f"{renderer.width}x{renderer.height}",
       "-r", str(renderer.fps),
       "-i", "-",  # stdin에서 읽기
       "-c:v", "libx264",
       "-preset", "medium",
       "-pix_fmt", "yuv420p",
       "-an",  # 오디오 없음
       output_path,
   ]
   proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
   for frame in renderer.render_all_scenes(scenes):
       proc.stdin.write(frame.tobytes())
   proc.stdin.close()
   proc.wait()
   ```
   - 메모리: 7.1GB → 프레임 1장(5.9MB) + FFmpeg 버퍼 → 수십MB

#### B. `pipeline_orchestrator.py` 수정

`generate_shortform_video()` (line 124~209):
- **6~7단계 변경** (line 179~194):
  - `all_frames = renderer.render_all_scenes(scenes)` → generator 유지
  - `ImageSequenceClip(all_frames)` + `write_videofile()` → FFmpeg pipe로 교체
  - 같은 subprocess 패턴 적용:
  ```python
  # 6-7. FFmpeg 파이프로 무음 MP4 생성 (RAM 절약)
  silent_video_path = str(tmp / "silent.mp4")
  cmd = [
      "ffmpeg", "-y",
      "-f", "rawvideo", "-vcodec", "rawvideo",
      "-pix_fmt", "rgb24",
      "-s", f"{renderer.width}x{renderer.height}",
      "-r", str(renderer.fps),
      "-i", "-",
      "-c:v", "libx264", "-preset", "ultrafast",
      "-pix_fmt", "yuv420p", "-an",
      silent_video_path,
  ]
  proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
  for frame in renderer.render_all_scenes(scenes):
      proc.stdin.write(frame.tobytes())
  proc.stdin.close()
  proc.wait()
  ```
  - `from moviepy import ImageSequenceClip` import 제거 (더 이상 불필요)

### ⚠️ 주의사항
- `render_scene()`이 generator가 되므로, 호출하는 곳에서 list로 변환이 필요하면 `list(renderer.render_scene(scene))` 사용
- `generate_evan_dynamic_video()`의 썸네일 생성(line 937~941)은 첫 장면만 list로 받아서 처리:
  ```python
  if thumb_path:
      first_scene_frames = list(renderer.render_scene(scenes[0]))
      thumb_frame = first_scene_frames[-1]
      Image.fromarray(thumb_frame).save(thumb_path)
  ```
  - 단, 이 함수가 직접 호출되는 경우가 아니면 수정 불필요 (pipeline_orchestrator가 메인 경로)

---

## 수정 2: 나레이션-화면텍스트 분리

현재 문제: 영상의 텍스트를 나레이션이 그냥 읽는 형태. 화면 텍스트 = TTS 텍스트 동일.

### 올바른 구조:
- **나레이션(TTS)**: 자연스러운 설명 스크립트 (구어체, 상세 설명)
- **화면 텍스트**: 나레이션 내용을 **요약**해서 보여주는 핵심 키워드/문장
- **Sync 필수**: 나레이션이 해당 내용을 말할 때 해당 화면이 표시

### 구현:
1. **프롬프트 수정** — `/home/jay/projects/ThreadAuto/prompts/pipeline/03_writing.md`에 규칙 추가:
   - "영상용 슬라이드는 narration 필드에 TTS 스크립트를, title/description에 화면 요약을 작성한다"
   - "narration은 구어체로 자연스럽게, 화면 텍스트는 핵심만 간결하게"
2. **extract_narration_texts()** — `pipeline_orchestrator.py`에서 slides의 `narration` 필드를 우선 사용 (이미 구현됨, line 74~76 확인)
3. **화면 렌더링** — `evan_dynamic.py`의 `generate_scenes_from_slides()`는 기존대로 title/description 사용

### 슬라이드 JSON 예시:
```json
{
    "title": "보험료 새는 3가지 패턴",
    "description": "특약 중복 / 갱신형 / 미청구",
    "narration": "매달 나가는 보험료, 혹시 새고 있진 않으신가요? 보험료가 새는 패턴은 크게 세 가지입니다."
}
```

---

## 수정 3: 텍스트 크기 1.3배

- `/home/jay/projects/ThreadAuto/video/evan_dynamic.py`의 `generate_scenes_from_slides()` 함수에서 font_size 값을 1.3배:
  - cover title: 94 → 122
  - cover hook: 68 → 88
  - card_list title: 62 → 81
  - card_list item: 49 → 64
  - detail title: 62 → 81
  - detail label: 42 → 55
  - detail value: 62 → 81
  - cta title: 73 → 95
  - cta main: 104 → 135
  - cta badge: 47 → 61
  - cta sub: 36 → 47
  - body title: 73 → 95
  - body text: 52 → 68
- `generate_insurance_scenes()`도 같은 비율로 조정

---

## 수정 4: 콘텐츠 정체성 규칙 (프롬프트 반영)

`/home/jay/projects/ThreadAuto/prompts/pipeline/03_writing.md`에 아래 규칙 추가:

### 조직 구조 정확성 규칙
- 인카다이렉트 = 상위 조직 전체. 서울대보험쌤그룹 = 하위 그룹.
- AI 활용, DB 수집 자동화 = 서울대보험쌤그룹이 잘 하는 것
- ❌ "AI 시스템 덕분에 인카다이렉트가 성장" — 금지
- ✅ "서울대보험쌤그룹에서 AI를 활용해..." — 올바른 표현
- 상세: `/home/jay/workspace/memory/specs/content-identity-rules.md` 참조

---

## Threads 업로드
- 수정 완료 후 영상 1건 생성 → Threads에 업로드
- 토픽: 자유 선택 (보험/연금 도메인)
- 업로드 모듈: `/home/jay/projects/ThreadAuto/publisher/threads_publisher.py`

## 주의사항
- 에반 6원칙 유지 (검정배경, 테마색상, 레이아웃변주, 타이핑효과, 시간차등장, 다이나믹)
- narration 필드가 없는 legacy 슬라이드에서도 정상 동작하도록 fallback 필수
- Remotion 관련 코드/디렉토리 삭제하지 말 것
- `moviepy` import는 pipeline_orchestrator.py에서 제거 (더 이상 불필요)
- 수정 후 간단한 테스트 렌더링으로 RAM 사용량 감소 확인
