쇼츠 자동화를 처음 만들 때 가장 흔한 실수는 모든 과정을 스크립트 하나에 넣는 것입니다. 중간에 한 단계만 실패해도 처음부터 다시 돌려야 하고, 사람이 검수할 지점도 없어집니다. 실제로 운영해 보면 단계를 나누고, 단계마다 결과 파일을 남기는 구조가 훨씬 안정적입니다.
1. 전체 구조
| 단계 | 입력 → 출력 | 도구 예시 |
|---|---|---|
| 1. 주제·대본 | 주제 목록 → script.json (제목, 문장별 대본, 해시태그) | LLM API (Claude, GPT 등) |
| 2. 음성 | 대본 → narration.mp3 | TTS API 또는 직접 녹음 |
| 3. 자막 | 음성 → subs.srt | faster-whisper로 타이밍 추출 |
| 4. 이미지·영상 | 문장별 장면 설명 → images/*.png | 직접 촬영 소스, 스톡, 이미지 생성 API |
| 5. 합성 | 이미지 + 음성 + 자막 → short.mp4 (1080×1920) | ffmpeg |
| 6. 검수 | 사람이 확인 후 approved 표시 | 폴더 이동 또는 체크 파일 |
| 7. 업로드 | short.mp4 + 메타데이터 → 예약 공개 | YouTube Data API v3 |
작업 하나당 폴더 하나를 만들고 job.json에 현재 단계를 기록하면, 실패한 단계만 다시 돌릴 수 있습니다. PC 여러 대로 나눠 돌리기도 쉬워집니다.
jobs/
2026-11-09_grub-rescue/
job.json # {"stage": "compose", "topic": "...", "error": null}
script.json
narration.mp3
subs.srt
images/01.png 02.png 03.png
short.mp4
2. 합성 단계 코드 (ffmpeg)
가장 손이 많이 가는 합성 단계 코드입니다. 이미지 여러 장을 음성 길이에 맞게 나눠 세로 화면을 채우고, 천천히 확대되는 효과와 자막을 입혀 MP4로 만듭니다. ffmpeg가 설치되어 있어야 하고, 자막 폰트(예: Pretendard)는 시스템에 설치된 이름을 씁니다.
"""이미지 여러 장 + 내레이션 음성 + SRT 자막 → 1080x1920 쇼츠 MP4 (ffmpeg 필요)."""
import subprocess, json
from pathlib import Path
def duration(path: str) -> float:
out = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "json", path],
capture_output=True, text=True, check=True).stdout
return float(json.loads(out)["format"]["duration"])
def compose(images: list[str], audio: str, srt: str, out: str, fps: int = 30):
total = duration(audio)
per = total / len(images)
frames = int(per * fps)
inputs, filters = [], []
for i, img in enumerate(images):
inputs += ["-loop", "1", "-t", f"{per:.3f}", "-i", img]
# 세로 화면 채우기 + 천천히 확대(켄 번스 효과)
filters.append(
f"[{i}:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,"
f"zoompan=z='min(zoom+0.0008,1.08)':d={frames}:s=1080x1920:fps={fps},setsar=1[v{i}]")
concat = "".join(f"[v{i}]" for i in range(len(images)))
style = "FontName=Pretendard,FontSize=14,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,BorderStyle=1,Outline=2,Alignment=2,MarginV=120"
filters.append(f"{concat}concat=n={len(images)}:v=1:a=0[base]")
filters.append(f"[base]subtitles='{srt}':force_style='{style}'[outv]")
cmd = ["ffmpeg", "-y", *inputs, "-i", audio, "-filter_complex", ";".join(filters),
"-map", "[outv]", "-map", f"{len(images)}:a", "-c:v", "libx264", "-preset", "medium", "-crf", "20",
"-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k", "-shortest", "-movflags", "+faststart", out]
subprocess.run(cmd, check=True)
if __name__ == "__main__":
imgs = sorted(str(p) for p in Path("images").glob("*.png"))
compose(imgs, "narration.mp3", "subs.srt", "short.mp4")
- 가로 이미지를 넣어도
force_original_aspect_ratio=increase+crop으로 세로 화면을 꽉 채웁니다. zoompan의 확대 속도(0.0008)와 최대 배율(1.08)을 바꾸면 움직임 강도를 조절할 수 있습니다.- 윈도우에서는 자막 경로의 역슬래시와 드라이브 콜론(
C:) 때문에 subtitles 필터가 실패할 수 있으니, 작업 폴더 안에서 상대 경로로 실행하는 것이 편합니다.
3. 대본 단계: 품질을 좌우하는 곳
LLM에 “쇼츠 대본 써 줘”라고만 하면 비슷비슷한 결과가 나옵니다. 아래처럼 형식을 고정하고, 내가 가진 실제 경험이나 자료를 함께 넣어야 채널 고유의 내용이 됩니다.
- 출력은 JSON으로 고정: 제목, 첫 2초 훅 문장, 본문 문장 5~7개(문장별 장면 설명 포함), 마무리 문장, 해시태그
- 문장당 2~4초 분량, 전체 30~50초
- 사실 확인이 필요한 수치·고유명사는 따로 표시하게 해서 검수 단계에서 확인
4. 업로드 단계 주의점
- YouTube Data API v3의
videos.insert는 호출당 할당량 1,600 단위를 쓰고, 기본 할당량은 하루 10,000 단위라 하루 업로드 약 6개가 한도입니다. - 검증(감사)을 받지 않은 API 프로젝트로 올린 영상은 비공개로 고정됩니다. 공개 업로드를 하려면 Google의 API 감사 절차를 거쳐야 합니다.
status.privacyStatus를private로,status.publishAt에 공개 시각을 넣으면 예약 공개가 됩니다.
5. 정책: 자동화해도 사람 손이 들어가야 하는 이유
유튜브 파트너 프로그램은 대량 생산되거나 반복적인 콘텐츠를 수익 창출 대상에서 제외합니다. 템플릿만 바꾼 영상을 쏟아내면 채널 전체의 수익 창출이 막힐 수 있습니다. 자동화는 반복 작업(합성·자막·업로드)을 줄이는 데 쓰고, 주제 선정과 대본 검수, 직접 찍은 소스 같은 사람의 판단과 고유 자료는 남겨 두는 것이 장기적으로 안전합니다. 생성형 AI로 만든 사실적인 인물·장면을 쓴다면 업로드 시 합성 콘텐츠 공개 항목도 체크해야 합니다.
정리
- 단계 분리 + 작업 폴더 + job.json으로 재시도와 분산 처리
- 합성은 ffmpeg 한 번 호출로 이미지·음성·자막 처리
- 업로드는 하루 약 6개 한도, 미검증 프로젝트는 비공개 고정
- 검수 단계를 꼭 남겨서 품질과 수익 창출 정책을 함께 지키기