
「아저씨가 잘난 척하며 떠드는 영상」이 도저히 못 봐줄 정도라 AI에게 전부 맡겨버렸다 (자동 영상 생성)
요약
JSON 파일 하나를 소스로 사용하여 음성 합성, 이미지 생성, 자막, 영상 합성 및 YouTube 게시까지 자동화하는 영상 생성 파이프라인 구축 사례를 소개합니다. ffmpeg와 Pillow를 활용해 Remotion 없이도 캐릭터 립싱크와 연출이 포함된 고품질 영상을 자동 생성합니다.
핵심 포인트
- 에피소드 JSON을 단일 진실 공급원(SSOT)으로 활용한 자동화 파이프라인
- Aivis Cloud API와 gpt-image-2를 이용한 음성 및 이미지 생성
- ffmpeg와 OpenCV를 조합한 캐릭터 립싱크 및 영상 합성 구현
- 명령어 하나로 영상 제작부터 YouTube 게시까지 전 과정 자동화
갑작스럽지만, 여러분. 자신이 말하고 있는 영상을 다시 본 적이 있나요?
저는 있습니다. 그리고... 도저히 못 봐주겠더라고요...

차마 눈 뜨고 볼 수가 없었습니다 (아저씨가 뭔가 잘난 척하며 떠들고 있네...)
그래서 제가 방송하고 있는 해설 영상 시리즈는 저 대신 여자 캐릭터인 「레무(Remu)」가 말해줍니다. 제가 하는 일은 대본에 해당하는 에피소드 JSON 파일을 1개 작성하는 것뿐입니다. 나머지는 명령어 하나만 입력하면 음성, 패널 이미지, 자막, 영상 합성, YouTube 게시까지 전부 알아서 돌아갑니다. 이 영상 생성에 이르기까지의 개발 에피소드를 기록해보고자 합니다.
영상 양산을 목적으로 한 시도는 아닙니다. 「전자동」이라는 울림에서 상상하는 것처럼 효율화에 눈이 벌개져 있는 것도 아니니, 그 부분은 느긋하게 읽어주시면 감사하겠습니다

제가 만들고 있는 것은 「네코로비 AWS 입문(ねころびAWS入門)」이라는 하나의 AWS 서비스를 심도 있게 해설하는 YouTube 영상 시리즈입니다. 화면에 나오는 것은 「레무」라는 여자 캐릭터. 차분한 말투로 때때로 탈선하면서 AWS의 개념을 하나씩 해설해 나가는 스타일입니다.
가장 큰 특징은, 인간이 하는 일은 대본(에피소드 JSON)을 쓰는 것뿐이라는 점입니다. 나머지는 명령어 하나만 입력하면 영상이 완성됩니다. 음성 합성도, 해설 패널의 이미지 생성도, 자막 삽입도, 장(Chapter) 나누기도, BGM 믹싱도, YouTube 게시도 전부 자동입니다. 에피소드 JSON이 「단일 진실 공급원(Single Source of Truth)」이 되어, 그 파일 하나로부터 파이프라인 전체가 구동됩니다.
# 이 명령어 한 방으로, 음성 → 패널 이미지 → 자막 → 영상 합성까지 전부 실행
uv run scripts/generate_episode.py episodes/L055_dynamodb.json
명령어를 입력하고 차를 한 잔 타러 다녀오면 영상이 완성되어 있는, 그런 세계입니다.
완성되는 영상에는 대략 다음과 같은 요소들이 들어있습니다. 이 모든 것이 에피소드 JSON으로부터 자동으로 구성됩니다.
| 요소 | 내용 | 생성 수단 |
|---|---|---|
| 말하는 캐릭터 (입 모양 포함) | 음성에 맞춰 입이 움직이는 레무의 영상 | 캐릭터 PNG 시퀀스 + 자체 립싱크 |
| ... |
음성은 Aivis Cloud API, 패널 이미지는 gpt-image-2, 입 모양은 ffmpeg의 음량 분석과 OpenCV를 조합한 자체 구현입니다. 영상 합성에 Remotion 같은 프레임워크는 사용하지 않고, ffmpeg와 Pillow만으로 완결 지었습니다.

① 장(Chapter)
② 프로그레스 바 (진행 상황)
③ 설명 슬라이드
④ 용어 해설
⑤ 캐릭터
⑥ 자막
캐릭터가 말하며 입이 움직이고, 장이 바뀔 때마다 컷 인(Cut-in)이 들어가며, 진행 바를 캐릭터가 달리는... 이러한 작은 연출의 축적으로 「제대로 10분 동안 계속 볼 수 있는 영상」을 목표로 하고 있습니다. 수수한 노력이 가장 효과적입니다.
저는 엔지니어이지만 기술적인 필연성이라기보다는 꽤 솔직한 「해보고 싶었다」가 출발점입니다.
기술 기사는 지금도 매우 좋아하며, 검색성이나 정보 밀도 면에서는 텍스트를 능가하는 것이 없다고 생각합니다. 다만, 기사를 읽으려면 마음을 가다듬고 화면 앞에 앉아야 하죠. 반면 음성 중심의 해설은 진입 장벽이 낮아서, 출퇴근 중이나 집안일을 하면서 흘려듣는 것만으로도 어느 정도 머릿속에 들어옵니다. 학습 콘텐츠로서의 영상에는 텍스트와는 다른 접근성이 있거든요. 그것을 직접 만들어보고 싶었던 것이 첫 번째 동기였습니다.
저는 디자이너도 영상 크리에이터도 아닙니다. 조금 전이었다면 해설 영상에 필요한 스킬셋(일러스트, 음성 녹음, 편집)을 생각하는 것만으로도 「아, 무리다」라며 조용히 에디터를 닫았을 것입니다. 그 부분이 생성 AI의 진화로 완전히 바뀌었습니다.
| 요소 | 기존 방식 | 이번에 채택한 수단 |
|---|---|---|
| 내레이션 음성 | 직접 녹음 / 성우에게 의뢰 | Aivis Cloud API로 합성 |
| ... |
코드와 API의 조합으로 「일단 형태가 갖춰지는」 단계까지 가져갈 수 있습니다. 요컨대, 영상 제작을 엔지니어의 영역(즉, 코드로 해결하기)으로 끌어들일 수 있다면 나도 할 수 있겠다는 생각이 든 것입니다. 익숙한 무기로 싸울 수 있다는 것만으로도 의욕이 샘솟으니까요.
...그런데, 결국 이것이 가장 큰 이유일지도 모릅니다. 순수하게 해설 영상을 만들어보고 싶었습니다.
다만 문제가 하나 있습니다. 자신이 얼굴을 내밀고 말하는 영상, 다시 보면 괴롭습니다. 이는 서두에 쓴 대로입니다. 그래서 내비게이터를 여자 캐릭터 「레무」에게 맡기기로 했습니다. 말하는 것은 레무, 뒤에서 묵묵히 구조를 짜는 것은 저, 라는 역할 분담입니다.
이 파이프라인의 설계 사상은 한마디로 말하면 이것입니다.
단 하나의 JSON 파일만이 상태(State)를 가지며, 그 외의 모든 것은 거기서부터 유도된다.
음성도 이미지도 자막도 영상도 모두 에피소드 JSON으로부터 결정론적(Deterministic)으로 생성됩니다. JSON이 왕이고 나머지는 모두 그 가신입니다.
이 일련의 스크립트 군(음성·이미지·자막·합성·게시)은 Claude Code의 Skill로서 패키지화했습니다. 스킬의 CLAUDE.md에 워크플로우와 엄수 규칙(대본 검증은 필수, 이미지 생성은 quality=low 고정 등)을 적어두면, Claude Code가 이를 읽고 절차에 따라 파이프라인을 구동해 줍니다.
제가 하는 일은 "이 대본으로 영상을 만들어줘"와 같이 자연어(Natural Language)로 대충 부탁하는 것뿐입니다. 나머지는 스킬의 절차에 따라 각 스크립트가 순차적으로 실행됩니다. 인간이 만지는 것은 JSON과 지시뿐이라는 운용 방식이 여기서 성립되었습니다.
은근히 효과적인 부분은 규칙을 스킬 측에 심어둘 수 있다는 점입니다. "검증 페이즈를 건너뛰는" 식의 워크플로우 이탈을 방지할 수 있습니다. AI에게 맡기면 인간이 "뭐 이번에는 괜찮겠지"라며 게을리하기 쉬운 공정도 제대로 밟아주기 때문에 오히려 저보다 성실합니다.
1 에피소드에 대해 하나의 JSON 파일을 준비합니다. 이 파일에는 해당 회차를 재생성하기 위해 필요한 모든 정보가 들어 있습니다.
{
"episode_id": "L058_iam_hard", // L + 3자리. 장편 버전은 L로 시작하는 규약
"title": "IAM 완전 입문",
...
대본·이미지 프롬프트(Prompt)·음성 파라미터(Parameter)·자막용 키워드·용어 해설·YouTube 게시 메타데이터가 한 파일에 공존하고 있는 것이 핵심입니다. "이번 회차의 모든 것"을 이곳만 보면 알 수 있으므로, 차분 관리(Diff management)도 리뷰도 JSON 한 장으로 끝납니다. 커밋 로그를 보면 "아, 여기서 대사를 수정했구나"라는 것을 한눈에 알 수 있습니다.
JSON을 입력으로 하여 각 공정은 다음과 같은 흐름으로 움직입니다.
episodes/L058_iam_hard.json ← 단일 진실의 원천 (Single Source of Truth)
│
▼
...
공정 간에는 꽤 명확한 의존 관계(Dependency)가 있습니다.
- 음성(Aivis)과 이미지(gpt-image-2)는 서로 독립적 → 병렬 처리가 가능함
- 자막은 음성의 실제 길이에 의존 → 음성 생성 이후에만 제작 가능
- 합성은 음성·자막·이미지 모두에 의존 → 마지막 단계
음성 생성은 2030분, 이미지 생성은 1015분이 걸립니다(이게 꽤 길거든요). 그래서 두 가지를 모두 할 때는 2개의 스레드(Thread)로 동시 기동하여 전체 시간을 단축합니다.
if run_audio and run_illustrations:
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
future_audio = executor.submit(_run_audio)
...
둘 다 외부 API를 기다리는 방식이라 병렬 처리의 이점이 그대로 나타납니다. 게다가 한쪽에서 예외(Exception)가 발생하더라도 양쪽의 완료를 기다린 후 집약하도록 설계했기 때문에, "이미지 생성만 실패했는데 음성 생성까지 도맡아 중단되는" 식의 슬픈 사고를 피하고 있습니다.
이 파이프라인은 "동일한 JSON → 동일한 출력"을 만족하도록 만들어졌습니다. 기분에 따라 결과가 바뀌는 영상 생성기만큼 신뢰할 수 없는 것도 없으니까요.
- 결정론적인 선택: 아웃로(Outro) 패턴이나 립싱크(Lip-sync)의 루프 연결은 에피소드 ID나 세그먼트(Segment) ID로부터 결정됩니다. 재실행해도 결과가 어긋나지 않습니다.
- 출력 경로 고정: 출력은
output/<title>/하위에 집약되며, 재실행 시 동일한 위치에 덮어씁니다. - 캐시(Cache): 음성 MP3·패널 PNG·립싱크 중간 단계의 mp4는 파일 단위로 캐시되며,
--segments 2,4와 같이 대상을 좁힌 부분 재생성도 가능합니다.
덕분에 "JSON을 git으로 관리하고, 필요하다면 output/을 통째로 버리고 재생성한다"라는 거친 운용이 가능해집니다. 진실의 원천은 JSON에 남아 있으므로, 출력물은 최악의 경우 망가져도 괜찮습니다. (재생성 비용은 들겠지만요.)
이 부분은 꽤 중요합니다. 영상은 몇 개월에서 몇 년간 남는 자산이므로, AWS에 대한 사실 오인을 그대로 음성 합성해 버리면 돌이킬 수 없습니다. 나중에 "죄송합니다, 아까 설명이 틀렸습니다"라며 음성만 교체하는 것은 현실적으로 불가능하기 때문입니다.
그래서 음성 생성(Voice Generation)을 하기 전에 대본을 검증하여 사실 오인(Fact Error)을 검사합니다. 경고든 에러든 발생하면 생성을 중단합니다. 중단되었을 때는 리포트가 남기 때문에 어느 세그먼트(Segment)의 어떤 부분이 걸렸는지 확인할 수 있습니다.
자신의 지식에 의한 체크와 AI에 의한 팩트 체크(Fact Check)의 이중 체크로 오류를 잡아내는 방식입니다. 자동 검사는 놓치기 쉬운 부분을 찾아내는 데 능숙하지만, 뉘앙스의 타당성은 사람이 직접 보는 것이 더 빠르다는 것이 솔직한 체감입니다.
그렇다고 매번 LLM으로 검증하면 시간과 비용이 들기 때문에, **해시 기반의 자동 스킵(Hash-based Auto-skip)**을 도입했습니다. 모든 세그먼트의 텍스트에서 SHA256을 추출하여, 지난번 통과(PASS)했을 때의 해시와 비교하는 방식입니다.
def _compute_verify_hash(flat_episode: dict) -> str:
parts = []
for seg in flat_episode.get("segments", []):
...
검증(verify)이 통과(PASS)하면, 이 해시를 verify_state.passed_hash로 JSON에 다시 기록합니다. 다음에 해시가 일치하면 "텍스트가 한 글자도 바뀌지 않았다"라고 판단하여 검증을 스킵합니다. 반대로 대본을 한 글자라도 수정하면 해시가 바뀌어 자동으로 재검증이 실행됩니다. 검증 비용을 지불하는 것은 "대본이 실제로 바뀌었을 때만"이라는 설계입니다. 게으를 수 있는 부분은 당당하게 게으름을 피웁니다.
플랫(Flat)하게 전개된 세그먼트를 음성, 이미지, 자막의 3개 계통으로 흘려보냅니다. 음성(Aivis)과 이미지(gpt-image-2)는 병렬로 실행하며, 자막에서 합성으로 이어지는 의존 순서만 엄격히 지키는 구성입니다.
모든 기점은 세그먼트입니다. 1세그먼트는 "한 덩어리의 나레이션 + 그 사이에 표시할 그림 1장"이라는 단위입니다.
{
"segment_id": "L001_s01",
"text": "{EC2:이시투}는 가상 서버를 구동하기 위한 것입니다.",
...
text: 나레이션과 자막의 원본 텍스트.{표기:읽기}형태의 인라인 읽기 방식(Inline Reading Notation)을 가집니다. 음성 계통은 '읽기' 쪽을, 자막 계통은 '표기' 쪽을 사용합니다.visual.type: 패널을 생성·표시하는illustration또는 캐릭터만 있는character_only.glossary_ids: 화면 오른쪽에 띄울 용어 패널의 필터링.
이 구조 덕분에 3개 계통이 동일한 세그먼트 ID를 축으로 제각각 움직여도, 출력은 제대로 결정론적(Deterministic)으로 일치하게 됩니다.
음성은 Aivis Cloud API를 통해 세그먼트별로 합성한 뒤, 마지막에 하나의 MP3 파일로 연결합니다.
payload = {
"model_uuid": model_uuid,
"text": text,
...
text에는 읽기 방식(Reading side)으로 변환된 것을 전달합니다 (표기와 읽기의 분리). speaking_rate는 장편 버전의 표준을 1.15로 통일했습니다. 너무 빠르면 알아듣기 힘들고, 너무 느리면 템포가 떨어져 졸음이 오는데, 그 절묘한 중간 지점입니다.
이미지 생성은 병렬화되어 있음에도 불구하고, 음성 합성만은 **일부러 직렬(Serial)**로 처리합니다. 이유는 Aivis 측의 속도 제한(Rate Limit, 1분당 10개 요청)과 일시적 장애(503)에 대비하기 위해서입니다.
최소 6초의 간격이 필요하므로, 안전 마진을 더해 **세그먼트 사이에 7초 슬립(Sleep)**을 넣습니다. 또한, 429 에러라면 백오프(Backoff) 후 재시도하고, 503 에러라면 더 길게 기다렸다가 재시도하는 등 상황에 따라 리트라이(Retry) 방침을 나누어 두었습니다. '급할수록 돌아가라'를 그대로 실천하는 설계입니다. 속도 제한에 걸려 전부 다시 하는 것보다, 처음부터 얌전하게 기다리는 것이 결국 더 빠릅니다.
해설 패널은 OpenAI gpt-image-2로 생성합니다. 장편 버전은 16:9이므로 size="1536x1024"를 직접 지정하여, 크롭(Crop) 없이 그대로 JPEG로 저장합니다.
payload = {
"model": "gpt-image-2",
"prompt": prompt,
...
프롬프트는 세그먼트의 prompt_main(장면의 내용만 작성)에 스타일 지정, 패널 설계 규칙, 가로형 구도 규칙을 자동으로 연결하여 구성합니다. 대본 작성자(즉, 나)는 "무엇을 그릴 것인가"만 작성하면 되며, "요소를 좌우로 배치한다", "AWS 공식 로고 금지"와 같은 제약 사항은 모든 에피소드에 공통으로 사후 적용됩니다. 매번 똑같은 주의사항을 손으로 추가하다가는 반드시 까먹기 때문입니다.
quality는 low로 고정해 두었습니다.
high는 low
약 28배의 토큰을 소모하는 데 비해, 해설용 도표나 키워드 정도라면 low로도 충분한 퀄리티가 나오거든요. 28배나 더 지불할 가치는 없었습니다. 이 파이프라인의 심장부는 compose_video.py입니다.
생성된 음성, 패널 이미지, 자막을 받아서 1920×1080 / 60fps의 MP4로 조립합니다. 약 2,000줄 정도 됩니다. …하지만 하는 일은 요컨대 "Pillow로 동적인 이미지 레이어를 구워내고, ffmpeg로 전부 겹치는 것"뿐입니다. 줄 수에 비하면 하는 일은 심플합니다.
처음에는 Remotion(React로 영상을 작성하는 프레임워크)도 검토했지만, 이번 용도에는 제외했습니다. 이유는 다음과 같습니다.
의존성이 무거움: Node + 헤드리스 브라우저(Headless Browser)로 프레임을 렌더링하기 때문에 환경 구축 비용이 꽤 큽니다. "JSON 하나로 10분짜리 영상 1개"라는 소박한 사용법에 브라우저를 띄울 정도의 동적 기능은 필요하지 않거든요. -
레이어가 거의 정지화면 + α로 충분함: 배경, 패널, 자막은 구간마다 바뀌는 정지화면입니다. 움직이는 것은 캐릭터의 루프와 상단을 지나가는 캐릭터 정도입니다. PNG 시퀀스(PNG Sequence) + ffmpeg의 overlay로 충분합니다. -
ffmpeg가 빠르고 정직함: 60fps의 장편 영상도 1패스(1-pass)로 통과하며, 서버리스(Serverless) 환경에도 바이너리 하나로 가볍게 올리기 쉽습니다.
최종 프레임은 다음 레이어를 아래에서부터 쌓습니다.
z=8 상단 프로그레스 바(Progress Bar) + 달리는 캐릭터
z=7 장간 컷인(2.5초 · 장 경계에서만 · SE 포함)
z=6 장 타이틀 자막 「제N장: …」(장 시작 시 3초)
...
파이프라인 전체를 구축하는 과정에서 대본 검증, 음성, YouTube 게시 각각의 단계에서 함정을 밟았습니다. 제가 실제로 빠졌던 부분과 해결책을 공정 순서대로 숨김없이 공유하겠습니다. 같은 실수를 반복하는 분들이 줄어들기를 바랍니다.
처음에는 검증 과정에서 warning(경고)이 나와도 슬쩍 그냥 넘어가곤 했습니다. "경고 정도라면 괜찮겠지"라며 말이죠.
하지만 영상은 몇 개월에서 몇 년간 남는 자산입니다. 사실 오인 상태로 음성까지 합성해 버리면, 나중에 교체하는 것은 현실적으로 불가능합니다. 그래서 warning이든 error든 일단 반드시 멈추고 대본을 수정하는 운영 방식으로 바꿨습니다. "나중에 고치면 돼"가 통하지 않는 영역에서는 번거롭더라도 전 단계에서 확실히 막는 것이 결국 가장 안전했습니다. 급할수록 돌아가라는 말이 정말 맞습니다.
합성 음성에서 가장 은근히 곤란했던 점은 전문 용어의 오독입니다. 예를 들어 "EC2"를 "이-씨-니"라고 읽어버리거나, 영문 약어나 어려운 한자가 예상과 다르게 읽히는 경우입니다. AWS 해설은 고유 명사의 집합체이기 때문에, 이 부분이 무너지면 단번에 아마추어 느낌이 납니다. 레무(Rem)가 "이-씨-니"라고 말하는 순간 신뢰도는 제로가 됩니다.
해결책은 심플합니다. 대본에 미리 루비(후리가나/읽기 표기)를 달아두고, 음성 생성 시에만 읽기 쪽으로 변환하여 합성하도록 했습니다. 대본에는 {EC2:イーシーツー}와 같이 "표기"와 "읽기"를 병기해 둡니다.
- 음성 생성 … 읽기 쪽(イーシーツー)을 Aivis에 전달
- 자막 묘사 … 표기 쪽(EC2)을 그대로 화면에 출력
이렇게 하면 "정확하게 읽어주는 음성"과 "올바른 표기의 자막"을 동일한 JSON에서 동시에 가져올 수 있습니다. 읽기 오류가 발생한 용어에만 나중에 루비를 추가하면 되므로 운영도 매우 편해졌습니다. 키워를 키울수록 똑똑해지는 방식이죠.
마지막은 게시 관련입니다. YouTube 설명란에 장별 타임스탬프(챕터)를 기재하고 있는데, JSON에 수기로 작성한 예상 시각을 그대로 사용하면 오차가 발생합니다.
이유는 장의 경계에 2.5초의 장간 컷인을 삽입하기 때문입니다. 본편의 길이는 컷인만큼 뒤로 밀려나게 됩니다. 컷인 × 장의 개수만큼 조금씩 밀려나게 되는 것이죠.
처음부터 전부를 만든 것은 아닙니다. 짧은 영상부터 시작해서 장편화, 장 나누기, 장 타이틀, 진행 바, 그리고 입 모양 동기화(Lip-sync)까지 기능을 하나씩 추가해 왔습니다. 갑자기 완성형을 만들려고 했다면 아마 첫 일주일 만에 마음이 꺾였을 겁니다.
이 부분은 조금 진지하게 써보겠습니다.
생성형 AI를 영상 제작에 도입하면 쉽게 양산형 콘텐츠가 됩니다. 대본조차 AI로 작성하는 완전 자동화도 가능합니다. 템플릿을 따르는 대본, 어디선가 본 듯한 구도의 썸네일, 균질한 내레이션. 효율만 추구한다면 그것으로 충분할지도 모릅니다.
하지만 제가 만들고 싶었던 것은 그것이 아닙니다. 레무라는 캐릭터성. 작성자의 경험, 실패담——그러한 경험과 세부적인 고민이야말로 독창성(Originality)이 깃드는 지점이라고 생각합니다.
AI는 「인간을 대신해 대량 생산하는 기계」가 아니라, 인간의 수작업을 줄여서, 공을 들이고 싶은 부분에 시간을 쏟기 위한 도구라고 생각합니다.
…솔직히 말하자면, 지금까지 해온 내용들은 거의 자기만족을 위한 영상 제작입니다. 만들고 싶었던 것을 만들었다는 만족감은 분명히 있습니다만...
영상을 세상에 내놓고 있는 이상, 「시청자에게 도움이 되는 것을 정말로 만들고 있는가?」라는 관점에도 앞으로는 제대로 마주하고 싶습니다. 시스템을 만드는 것이 너무 즐거워서 수단이 목적이 되어버린 면은, 뭐, 부정할 수 없네요 (웃음). 이제부터는 「보고 도움이 되었다」라고 느낄 수 있는 콘텐츠 그 자체를 갈고닦는 것이 과제입니다.
만약을 위해 적어두자면, 이 채널에 수익화(Monetization) 계획은 없습니다.
…라기보다, 애초에 수익화 조건(총 재생 시간)을 전혀 충족하지 못하고 있어서 계획을 세우기 이전의 단계입니다. 조회수가… 조회수가 말이죠…
뭐, 「만들고 싶으니까 만든다」, 그것만으로 충분하다는 마음으로 느긋하게 계속하고 있습니다. 허세 부리는 거 아니에요

여기까지 읽어주셔서 감사합니다!
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기