숏폼 자동화 파이프라인 구축하기 2

소개

지난 주에, 당초 제출했던 프로젝트 플랜을 '롱폼 콘텐츠로 구독을 유도하는 쇼츠'를 완성하는 것으로 수정하고 시도함. 뭔가 부족한 부분이 많이 느껴져 고도화하고자 함. 이번 기수는 오프라인 모임을 참여하지 못하고 수업도 다시보기로 봐야하는 난관. 스터디장이 제공한 교안을 열어보며 어디서부터 어떻게 진행해야 대략 난감한 상황에서...

진행 방법

일단 교안 전체를 클로드코드에 몽땅 긁어 넣은 뒤, 플랜모드로 작업 계획을 먼저 정리함.

## 프로젝트 개요
- **프로젝트명**: 낭독산책 숏폼 자동화 파이프라인
- **목적**: 낭독산책 유튜브 채널의 롱폼 본편 유입을 위한 45초~65초 숏폼 자동 생성
- **장르**: 독서 리뷰 숏폼
- **제작 편수**: 3편
- **작업 기간**: 2026-03-27 ~ 2026-03-28

이 전에 작성된 에이전트와 스킬을 역할 분담을 좀 더 섬세하게 하도록 수정한 뒤.

.claude/
├── agents/
│   ├── script-writer.md      ← 롱폼→숏폼 추출 + 스크립트
│   ├── image-creator.md      ← 장면 이미지 생성
│   ├── audio-creator.md      ← TTS 나레이션 생성
│   └── video-editor.md       ← 자막 + 영상 합성
├── skills/
│   ├── gemini-script/SKILL.md
│   ├── gemini-image/SKILL.md
│   ├── typecast-tts/SKILL.md
│   └── ffmpeg-render/SKILL.md
└── commands/
    ├── review.md             ← /review <책이름> (기획)
    └── review-produce.md     ← /review-produce <책이름> (제작)


## 아키텍처: 에이전트 / 스킬 / 커맨드 구조

허세임AI "프롬프트에서 파이프라인으로" 강의(Ch2~Ch10)의 과제로, 기존 Python 파이프라인을 Claude Code의 에이전트/스킬/커맨드 체계로 재설계했다.

### 에이전트 (판단하는 역할) — `.claude/agents/`

| 에이전트 | 역할 | 영화 비유 |
|----------|------|----------|
| script-writer | 롱폼→숏폼 요약 추출 + 스크립트 생성 | 작가 |
| image-creator | 장면 이미지 생성 판단 | 촬영감독 |
| audio-creator | 음성/감정/속도 판단 | 음향감독 |
| video-editor | 자막 + 영상 합성 판단 | 편집감독 |

### 스킬 (실행하는 도구) — `.claude/skills/`

| 스킬 | 실행 내용 | 사용하는 에이전트 |
|------|----------|-----------------|
| gemini-script | Gemini API로 script.json 생성 | script-writer |
| gemini-image | Imagen 4.0으로 장면 일러스트 생성 | image-creator |
| typecast-tts | Typecast API(ssfm-v30)로 TTS 음성 생성 | audio-creator |
| ffmpeg-render | FFmpeg로 영상 합성 | video-editor |

### 커맨드 (리모컨) — `.claude/commands/`

| 커맨드 | 용도 |
|--------|------|
| `/review <책이름>` | 기획: 롱폼에서 input.md 추출 → script.json 생성 → 사용자 확인 |
| `/review-produce <책이름>` | 제작: 이미지 + TTS + 영상 합성 |

### 파이프라인 흐름

```
[기획 모드] /review <책이름>
  Phase 1: script-writer → input.md (롱폼 낭독 스크립트에서 추출)
  Phase 2: script-writer → script.json + narration.txt (Gemini)
  ⏸️ 사용자 확인

[제작 모드] /review-produce <책이름>
  Phase 3: image-creator → images/*.png (Imagen 4.0)
  Phase 4: audio-creator → audio/scene_*.wav (Typecast TTS, 장면별 개별 생성)
  Phase 5: video-editor → 장면별 클립 생성 → 합쳐서 output/short.mp4
```
---

## 기술 스택

| 도구 | 용도 | 비고 |
|------|------|------|
| Gemini 2.5 Flash | 스크립트(script.json) 생성 | 무료 티어 |
| Imagen 4.0 | 장면 일러스트 생성 (9:16 세로) | 무료 티어 |
| Typecast TTS | 한국어 나레이션 (ssfm-v30, X-API-KEY) | 체험판 |
| FFmpeg | 영상 합성 (H.264/AAC, 1080x1920) | 무료 |
| Whisper | (초기에 사용, 최종 제거됨) | - |

실행에 옮기면서 진행됐던 디버깅의 대강의 티키타카는 아래에 정리.

## 설계 변경 히스토리

### 1. 에이전트 수: 교안 7개 → 실제 4개

교안(shortform-drama)은 드라마용으로 7개 에이전트를 사용했지만, 독서 리뷰 숏폼은 캐릭터 설정이나 씬 연출이 불필요하여 4개로 축소했다.

- **빠진 것**: character-designer, scene-director, producer
- **교훈**: 프로젝트 복잡도에 맞게 에이전트 수를 조정하는 것이 올바른 설계

### 2. Typecast API: 레거시 → 새 API

기존 계획은 레거시 API(Bearer token, `typecast.ai/api/`)였으나 403 에러 발생.
새 API(`api.typecast.ai`, X-API-KEY 헤더, ssfm-v30 모델)로 전환하여 해결.

- **레거시**: `Authorization: Bearer <token>`, actor_id 사용
- **새 API**: `X-API-KEY: <key>`, voice_id 사용, 동기식 TTS (폴링 불필요)

### 3. 자막 품질: Whisper STT → script.json 직접 사용

샤덴프로이데(기존 제작물)에서 Whisper 한국어 인식 오류가 심각했다:
- "뇌" → "내", "불행" → "프랭", "선량" → "설령" 등

**해결**: Whisper는 타이밍 추출에만 사용하고, 자막 텍스트는 script.json에서 직접 가져오도록 변경.
최종적으로는 Whisper 자체를 제거하고 장면별 클립 방식으로 전환.

### 4. 싱크 문제: 3번의 근본 수정

**1차 시도**: 단일 TTS → Whisper 세그먼트 단위 매칭 → 싱크 부정확
**2차 시도**: 장면별 개별 TTS → wav 합산 타이밍 → mp3 변환 시 오차 누적
**3차 시도 (최종)**: 장면별 개별 TTS → **장면별 개별 영상 클립**(이미지+오디오+자막) 생성 → 클립 합치기

```
최종 방식:
  장면1(이미지+wav+ASS) → clip_01.mp4
  장면2(이미지+wav+ASS) → clip_02.mp4
  ...
  CTA(표지+wav+ASS) → clip_cta.mp4
  → FFmpeg concat → short.mp4
```

이 방식은 각 클립 안에서 이미지-오디오-자막이 100% 일치하므로, 절대 싱크가 밀리지 않는다.

**교훈**: "타이밍을 계산하는" 접근은 항상 오차가 쌓인다. "물리적으로 묶어버리는" 접근이 확실하다.

### 5. 이미지 생성 에러 핸들링

2편 제작 시 Imagen API가 3번째 이미지에서 None을 반환 (안전 필터 작동).
images.py에 재시도 로직 + 기존 이미지 스킵 로직 추가.

### 6. CTA 한글 깨짐

FFmpeg drawtext 필터로 한글 CTA를 넣었더니 글자가 네모(□)로 표시.
원인: 기본 폰트에 한글 없음.
해결: `font='Malgun Gothic'` 명시.

### 7. 영상 코덱 호환성

최초 렌더링이 `yuv444p` (High 4:4:4 프로파일)로 생성되어 Windows 기본 플레이어에서 재생 불가.
`-pix_fmt yuv420p` 추가로 해결.

### 8. TTS 무음 패딩

Typecast TTS가 각 wav 앞뒤에 무음을 삽입.
ffmpeg silenceremove 필터로 앞뒤 무음 제거 적용.

### 9. drawtext → ASS 자막 전환

drawtext 필터로 한글 자막을 넣으면 이스케이프 문제(특수문자, 따옴표, 콜론)로 깨짐/누락 발생.
ASS 자막 파일로 전환하여 한글 문제 완전 해결.

### 10. Windows cp949 인코딩 에러

Windows 터미널의 cp949 인코딩과 한글/특수문자(—, ✅ 등) 충돌로 print 에러 발생.
subprocess 출력을 DEVNULL로 보내고, print에서 특수문자 제거.

---

최종 파일 구조는 아래와 같이 정리.

## 최종 파일 구조

```
shortform-generator/
├── CLAUDE.md                          # 프로젝트 기획서 (에이전트/스킬/커맨드 아키텍처)
├── generate.py                        # CLI 파이프라인 (script→images→audio→render)
├── config.py                          # 설정 (API키, 음성매핑, 자막스타일)
├── .env                               # API 키 (GEMINI_API_KEY, TYPECAST_API_KEY)
├── steps/
│   ├── script.py                      # Gemini → script.json
│   ├── images.py                      # Imagen → 장면 일러스트 (에러 핸들링)
│   ├── audio.py                       # Typecast TTS → 장면별 wav (무음 제거)
│   ├── render.py                      # 장면별 클립 + ASS 자막 → 합본 MP4
│   └── subtitles.py                   # (레거시, 더 이상 사용 안 함)
├── .claude/
│   ├── agents/                        # 에이전트 4개
│   │   ├── script-writer.md
│   │   ├── image-creator.md
│   │   ├── audio-creator.md
│   │   └── video-editor.md
│   ├── skills/                        # 스킬 4개
│   │   ├── gemini-script/SKILL.md
│   │   ├── gemini-image/SKILL.md
│   │   ├── typecast-tts/SKILL.md
│   │   └── ffmpeg-render/SKILL.md
│   └── commands/                      # 커맨드 2개
│       ├── review.md
│       └── review-produce.md
├── reviews/
│   ├── 당뇨의종말/                     # 1편
│   │   ├── input.md
│   │   ├── script.json
│   │   ├── cover.jpg
│   │   ├── images/01~08.png
│   │   ├── audio/narration.mp3
│   │   └── output/short.mp4
│   ├── 현명한부모가꼭알아야할대화법/    # 2편
│   │   ├── input.md
│   │   ├── script.json
│   │   ├── cover.jpg
│   │   ├── images/01~11.png
│   │   ├── audio/scene_*.wav + narration.wav
│   │   └── output/short.mp4
│   └── 혼자는외롭고함께는괴로운당신에게/ # 3편
│       ├── input.md
│       ├── script.json
│       ├── cover.jpg
│       ├── images/01~09.png
│       ├── audio/scene_*.wav + narration.wav
│       └── output/short.mp4
└── docs/
    ├── project-report.md              # 이 문서
    └── design-change-history.md       # 설계 변경 상세
```

결과와 배운 점

## 잘 된 점

1. **에이전트/스킬 분리 설계**: 교안의 "판단=에이전트, 실행=스킬" 원칙이 실제로 유효했다. script-writer가 어떤 내용을 뽑을지 판단하고, gemini-script 스킬이 실행하는 구조가 깔끔하게 작동했다.

2. **기획/제작 커맨드 분리**: `/review`로 스크립트를 확인한 후 `/review-produce`로 제작하는 2단계 구조가 수정 비용을 크게 줄였다.

3. **장면별 클립 방식**: 싱크 문제의 근본 해결책. 타이밍 계산이 아닌 물리적 결합으로 100% 싱크를 보장한다.

## 아쉬운 점

1. **싱크 문제 해결에 많은 반복**: 단일 TTS → Whisper 매칭 → wav 합산 → 클립 방식까지 3번의 시행착오. 처음부터 장면별 클립 방식으로 갔으면 시간을 아꼈을 것이다.

2. **Windows 환경 이슈**: cp949 인코딩, FFmpeg 경로 이스케이프, drawtext 한글 깨짐 등 Windows 특유의 문제에 시간을 많이 썼다.

3. **이미지 품질 제어**: Imagen이 생성하는 이미지의 스타일 일관성이 아쉽다. 레퍼런스 이미지를 활용하면 개선할 수 있을 것이다.

## 배운 것 (3줄)

> 1. "프롬프트 한 줄"과 "파이프라인"의 차이는 **설계**다. 에이전트/스킬/커맨드로 역할을 나누고, 의존성과 순서를 정하면, 복잡한 작업도 커맨드 한 줄로 실행된다.
> 2. 싱크 같은 정밀한 문제는 "계산"이 아니라 "물리적 결합"으로 풀어야 한다. 타이밍을 추정하지 말고, 클립 단위로 묶어버리면 오차가 0이다.
> 3. 처음 설계는 반드시 바뀐다. 중요한 건 변경 이유를 기록하고, 각 실패에서 배우는 것이다.

제작 결과 요약
#	제목	길이	크기	이미지	음성
1편	당뇨의종말	48.8초	3.7MB	8장	TTS (Kangil)
2편	현명한부모가꼭알아야할대화법	61.5초	5.1MB	11장	TTS (Kangil)
3편	혼자는외롭고함께는괴로운당신에게	65.6초	5.2MB	9장	TTS (Kangil)
영상 파일 위치:

reviews/당뇨의종말/output/short.mp4
reviews/현명한부모가꼭알아야할대화법/output/short.mp4
reviews/혼자는외롭고함께는괴로운당신에게/output/short.mp4

## 결과물

| # | 제목 | 길이 | 장면 수 | 음성 |
|---|------|------|---------|------|
| 1편 | 당뇨의 종말 | 52초 | 8장면 + CTA | TTS (Kangil) |
| 2편 | 현명한 부모가 꼭 알아야 할 대화법 | 63초 | 11장면 + CTA | TTS (Kangil) |
| 3편 | 혼자는 외롭고 함께는 괴로운 당신에게 | 66초 | 9장면 + CTA | TTS (Kangil) |

일단 그림 톤이 일관성이 있고 전편에 비해 내용을 잘 이해하고 반영하고 있음(초기 중요 요청 사항으로 집어 넣음)

추후 개선 방향

  • 좀 더 고도화하고 동영상으로 확장하는 것도 고려중

  • 이 콘텐츠를 유튜브는 물론 운용중인 몇 개의 SNS 채널에 자동업로드로 연결해보기

3

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요