
뉴스 영상을 전자동으로 생성하는 파이프라인을 만든 이야기 ― TTS와 LLM에서 빠졌던 함정
요약
뉴스 수집부터 YouTube 게시까지 전 과정을 자동화하는 멀티 채널 영상 생성 파이프라인 구축 경험을 다룹니다. Python과 Claude Code를 활용하여 구축한 시스템의 구조와 운영 과정에서 겪은 API 시행착오 및 기술적 해결책을 공유합니다.
핵심 포인트
- 뉴스 수집, 대본 생성, TTS, 영상 편집, YouTube 게시의 전 과정을 자동화
- Claude Code를 활용한 8.5개월간의 지속적인 개발 및 유지보수
- 각 단계를 별도 프로세스로 실행하고 JSON으로 데이터를 전달하는 구조 설계
- 실제 운영 과정에서 발생한 API 에러 및 기술적 함정 해결 방법 제시
서론
계기는 이 기사였습니다.
「AI로 뉴스 영상을 전자동으로 생성하여 YouTube에 게시한다」라는 내용을 읽고, **'이건 나도 만들 수 있겠는데'**라는 생각이 들었습니다. 그로부터 약 8.5개월 동안 Claude Code로 코드를 써 내려가며 키워왔고, 지금은 뉴스 수집부터 YouTube 게시까지 전자동으로 돌아가는 멀티 채널 파이프라인이 되었습니다 (지금도 세세한 개수는 계속되고 있습니다).
완성된 영상은 실제로 여러 채널에서 공개하고 있습니다. 우선 결과물을 보여드리는 것이 가장 빠를 것 같아, 우선 일부 채널을 먼저 소개하겠습니다.

↑ 뉴스 선정부터 대본·음성·자막·썸네일·메타데이터까지, 이 영상 한 편을 통째로 자동 생성 (AI 생성물임을 명시하여 게시)
【다독 라디오】 해외 뉴스 × 일본 경제 (메인. 해외 뉴스가 일본 경제에 어떤 영향을 미치는지라는 관점에서 해설)
주간 메탈 리포트 (TTS를 VOICEVOX로 한 사례. 금속 시장의 주간 요약)
이 기사는 기술자 관점에서 작성합니다. 시스템 구성과 처리 흐름을 간략히 설명한 뒤, 가장 쓰고 싶었던 **당시 API와의 시행착오(빠졌던 함정과 회피책)**를 실제 에러 문자열과 코드와 함께 남기겠습니다.
만든 것
한마디로 말하면, 뉴스 수집 → AI 분석 → 대본 생성 → TTS 음성 합성 → 자막 → 영상 편집 → 썸네일 생성 → YouTube 게시까지 사람의 손을 거치지 않고 돌아가는 시스템입니다. 전부 Python으로 작성되었으며, macOS의 launchd를 통해 매일 정해진 시간에 자동으로 실행됩니다.
- 언어: Python (100%). Lint는 ruff, 타입 체크는 basedpyright
- 개발: 약 8.5개월 동안 약 530 커밋. 거의 매일 Claude Code로 수정을 쌓아 올림
- 규모: 여러 채널을 병행 운용 (일본 경제 뉴스, 주간 메탈 리포트 외)
- 실행:
launchd로 하루에 여러 번 배치(batch) 실행. 실행 결과는 Discord로 통지
의외일 수도 있겠지만, 만들기 시작해서 실제로 영상을 생성하기 시작할 때까지 2주도 걸리지 않았습니다. 첫 번째 영상이 돌아간 것은 착수 후 며칠 뒤였고, YouTube 자동 게시와 정기 실행까지 갖춰진 것도 그 주 안이었습니다. 돌아가는 단계까지는 매우 빨랐습니다. 문제는 그 이후로, 운영을 시작한 뒤로는 끊임없는 수정, 수정의 연속이었습니다. 이 기사의 실패담 대부분은 그 '계속 가동하면서 밟았던 지뢰'에서 가져왔습니다.
계기는 서두의 참고 기사이지만, 만드는 방식을 그대로 모방한 것은 아니며 운영하면서 시행착오를 거쳐 최종적으로 이 형태로 안착했습니다. 구조적으로는 「공통 처리 엔진」 + 「채널별 파이프라인」으로 되어 있어, 새로운 채널을 추가할 때는 스텝을 재구성하는 것만으로 끝낼 수 있도록 설계했습니다.
시스템 구성
전체는 3개 층으로 구성됩니다.
┌──────────────────────────────────────────────┐
│ 채널 층 │
│ daily_news / metal_report / 기타 │
...
파이프라인 실행 방식
핵심은 pipeline_base.py로, **각 스텝을 별도의 프로세스(subprocess)로 기동하고, 결과를 디스크 상의 JSON으로 전달(bucket relay)**해 나가는 설계입니다.
# stepNN_*.py 를 subprocess.Popen 으로 기동하고,
# start_new_session=True 로 프로세스 그룹 전체를 타임아웃 kill 할 수 있도록 함.
# 스텝 간의 전달은 output/<channel>/<run_id>/*.json 을 경유.
왜 모놀리식(monolithic)한 하나의 프로세스로 만들지 않았느냐 하면,
- 스텝 단위로 타임아웃·재시도(retry)·강제 kill을 적용하고 싶음 (TTS가 멈춰도 다른 스텝에 영향을 주지 않음)
- 스텝 단독을
--run-id지정으로 개별 재실행하여 디버깅하고 싶음 - 메모리 누수나 라이브러리 간의 호환성 사고를 한 스텝 안에 가두고 싶음
실제 운용 시에는 API가 아무렇지 않게 무응답 상태가 되거나 폭주하는 경우가 있기 때문에, 이 「프로세스 분리 + JSON 전달」 방식이 매우 효과적이었습니다.
기술 스택
| 역할 | 채택 기술 |
|---|---|
| LLM (분석·대본·메타데이터) | Anthropic Claude (현재 claude-sonnet-4-6로 통일) |
| ... | |
| 이 스택은 처음부터 이랬던 것이 아니라, 수차례 교체한 결과 지금의 모습이 되었습니다. |
처리 흐름 (크게 12단계)
메인 콘텐츠인 【다독 라디오】(해외 뉴스 × 일본 경제)를 예로 들면, 영상 한 편은 다음과 같은 흐름으로 제작됩니다.
뉴스 수집— GNews API에서 기사를 수집하고, Claude로 중요도를 판정하여 선정 -
관련 뉴스 수집— 선정된 테마의 관련 기사 및 본문 URL을 추가로 취득 -
시장 영향 분석— Claude로 "이 뉴스가 일본 시장에 미치는 영향"을 구조화 -
대본 생성— Claude로 2인의 대화 형식(전문가 + 진행자) 대본을 생성 -
TTS 음성 합성— 화자별로 Gemini TTS를 통해 음성화 → 인트로/간격을 포함하여 결합 -
자막 생성— 생성된 음성을 ElevenLabs STT로 받아쓰기하고, 대본과 대조하여 타임스탬프 자막을 작성 -
영상 합성— FFmpeg로 배경·자막 PNG·BGM을 합성 -
메타데이터 생성— Claude로 제목·설명·태그를 생성 -
댓글 생성— Claude로 고정 댓글을 생성 -
썸네일 생성— Pillow로 썸네일 이미지를 그리기 -
업로드— YouTube Data API로 게시 -
완료 처리— 비용 집계·임시 파일 삭제·Discord 알림
의존 관계가 없는 단계는 병렬로 실행합니다. 우선 5·8·9(음성·메타·댓글)를 병렬로 생성하고, 음성이 완성되면 그것을 사용하는 6(자막)과 10(썸네일)을 병렬로 만드는 2단계 구조입니다. 대본을 대화 형식으로 만든 이유는, 진행자 역할의 질문이 시청자의 의문을 대신해 주어 이해하기 쉬워지고, 목소리가 바뀌면서 리듬감이 생기도록 하기 위함입니다.
여기까지가 영상 생성의 뼈대입니다. 이제부터가 본론, 계속 가동하면서 겪은 시행착오 이야기입니다.
시행착오의 나날들
LLM 프로바이더 교체 이력
처음에는 뉴스 분석을 Gemini로 수행했습니다. 하지만 2025년 10월 시점의 Gemini 무료 티어가 매우 불안정해서, 어느 날 다음과 같은 상황이 발생했습니다.
500 Internal error encountered.
google.api_core.exceptions.InternalServerError: 500 Internal error encountered.
하루 동안 500 / 429 / 503 에러가 빈번하게 발생하여 파이프라인이 멈췄습니다. 무료 티어는 서버 측 에러(500/503)가 발생하기 쉬워 불안정할 뿐만 아니라, 레이트 리밋/쿼터 제한(429)에 걸리면 거기서 생성이 중단됩니다. "다 쓰고 나면 불안정해진다"기보다 "애초에 안정적으로 양산할 수 있는 상태가 아니다"라는 느낌이었습니다. 그래서 분석은 Claude로 옮기고, 지수 백오프(Exponential Backoff, 2s→4s→8s) 재시도 로직을 도입했습니다.
그 후에도 LLM 관련 상황은 계속 변했습니다.
- Claude 측 부하가 높을 경우
529 overloaded를 반환하므로, GPT로의 폴백(Fallback) 경로를 마련 - 모델 폐지에 따른 추격전:
gemini-2.0-flash계열이 2026-03-31에 폐지 예고되어 2.5로 변경.gpt-4o-mini역시 은퇴 및 리네임 대응 - 그 리네임 과정에서
gpt-5.1-mini라고 적었다가 그런 모델은 존재하지 않는다며 오류가 발생하여, 즉시gpt-5-mini로 수정 (커밋 한 번 치기 민망한 실수)
한때는 대본 생성을 포함한 대부분의 작업을 claude-haiku-4-5에 맡겼습니다. 대본 생성은 1만 자가 넘는 출력량 때문에 전체 비용의 대부분을 차지하는데, 이 부분을 Haiku로 바꾸는 것만으로 API 비용을 약 66% 절감할 수 있었기 때문입니다. 품질 또한 대화형 대본이라면 충분했습니다. 이후 구형 모델의 폐지 리스크를 회피하고 채널 간 품질을 통일하기 위해, 최종적으로는 claude-sonnet-4-6으로 일원화했습니다.
교훈을 얻자면, 모델명과 엔드포인트는 코드에 직접 쓰지 말고 설정 파일로 분리하라는 것입니다. 이 영역은 모델이 분기마다 바뀌기 때문에, settings.yaml에서 교체할 수 있도록 해두지 않으면 폐지될 때마다 코드를 수정해야 하는 상황에 직면하게 됩니다.
Gemini TTS의 undocumented한 동작 (최대의 늪)
가장 많은 시간을 허비한 것이 Gemini TTS입니다. 프리뷰 버전 특유의, **문서에 명시되지 않은 동작(undocumented behavior)**을 하나하나 다 겪었습니다.
① 1일 100요청(RPD)의 벽
무료 티어의 TTS는 하루 요청 수 상한이 엄격하여, 처음에는 "100자마다 청크(chunk)"로 나누었더니 10분 분량의 영상에서 약 101요청이 발생해 영상 1개당 상한에 도달했습니다. 그래서 글자 수 기반 방식을 버리고 **행 수 기반(동일 화자의 연속 10행을 1개 블록으로 구성)**으로 변경했더니, 요청 수가 약 1/10로 줄어들었습니다.
이전: 100자마다 → 101요청
변경: 10행마다 → 약 10요청
절감률: 90%
화자가 바뀌는 지점에서는 반드시 블록을 나누도록 제약을 걸었습니다. (그 후 다시 수정하여, 현재는 ④번의 음질 저하와의 균형을 고려해 500자 전후를 기준으로 대사의 끊기는 지점에서 나누는 방식을 사용하고 있습니다.)
② systemInstruction 미지원으로 인해, 말투 지시문이 그대로 읽히는 문제
"TV 뉴스 캐스터로서 차분하게 말해줘"와 같은 스타일 프롬프트(style prompt)를 본문에 삽입(prepend)했더니, 그 지시문 자체가 음성으로 읽히는 사고가 비결정론적(non-deterministic)으로 발생했습니다.
원인은 명확했습니다. TTS 모델이 systemInstruction을 지원하지 않았기 때문입니다. 본문에 섞는 것 외에는 전달할 방법이 없는데, 본문에 섞으면 읽힐 위험이 남습니다. 결국 스타일 프롬프트 사용 자체를 중단했습니다.
나아가 보험으로서, 생성된 음성의 초반 5초를 STT로 전사(transcribe)하여 지시문 혼입을 검출하는 메커니즘도 추가했습니다.
# TTS 음성 초반을 검증하기 위한 검출 키워드
_TTS_CONTAMINATION_KEYWORDS = [
"뉴스 캐스터", "캐스터", "전문가",
...
③ 특정 본문에서 400 에러 발생, 심지어 재전송해도 해결되지 않음
구두점이 과도하게 연속되거나 물음표가 밀집된 본문을 보내면, 400 에러와 함께 다음과 같은 메시지가 돌아올 때가 있었습니다.
should only be used for TTS ...
("tried to generate text" 계열의 메시지)
Gemini가 입력을 "음성화가 아닌 텍스트 생성"으로 오판하는 듯하며, 동일한 본문을 재전송해도 결정론적(deterministic)으로 똑같은 400 에러가 발생합니다. 즉, 재시도(retry)는 무의미하며, 이를 해결하려면 대본 자체를 다시 작성할 수밖에 없습니다. 그래서 전용 예외(exception)를 발생시켜 상위 단계로 넘기고, Step 4(대본 생성)부터 다시 시작하는 플로우로 구성했습니다(최대 2회).
# 본문 기인 400 에러(음성화 거부)는 동일 본문 재전송으로 해결되지 않으므로,
# 재시도하지 않고 예외를 발생시켜 상위(대본 재생성) 단계에 위임함
if response.status_code == 400 and _is_tts_content_rejection(response.text):
...
④ 긴 문장을 한 번에 전달하면 음질이 심하게 열화됨
글자 수 과금과 요청 수 제한이 있으므로, 비용을 낮추기 위해 한 번의 요청에 긴 문장을 담고 싶어집니다. 하지만 긴 문장을 전달하면 생성된 음성이 깨지는(음질 열화) 현상이 발생했는데, 이 또한 문서에 없던(undocumented) 내용이었습니다. 현재는 화자와 상관없이 500자 전후를 기준으로 대사의 끊기는 지점에서 청크 분할(중간에는 자르지 않음)을 하여 안정화했습니다. "긴 문장으로 묶고 싶다(비용 절감)"와 "긴 문장은 깨진다(음질 유지)" 사이의 균형을 고려하여 기준 글자 수를 정한 것입니다.

TTS 엔진 자체의 선정
TTS는 "품질·비용·언어 지원"의 트레이드오프(trade-off) 관계에 있으며, 채널마다 구분하여 사용하고 있습니다.
메인인 다독 라디오(多読ラジオ)는 비교적 빠른 단계에서 Gemini 2.5의 TTS로 결정했습니다. 자연스럽고 표현력이 풍부한 일본어 내레이션을 출력할 수 있어, 대화 형식의 뉴스 해설에 적합했기 때문입니다.
TTS 선정 과정에서는 성격이 다른 두 가지 문제를 별도로 해결했습니다.
① 무료 티어의 속도 제한(Rate Limit) → 유료 티어로 전환
가장 먼저 부딪힌 문제는 무료 티어의 속도 제한이었습니다. Gemini 무료 티어는 3 RPM으로 매우 엄격하여, 10분 분량의 대본(13개 세그먼트)을 제대로 처리할 수 없었습니다. 초기 테스트에서는 **13개 세그먼트 중 단 1개만 완성(약 8%)**되었고, 완료까지 30분 이상 걸릴 것으로 예상되었습니다. 메인인 다독 라디오는 이 문제를 유료 티어로 전환하여 속도 제한을 해결했습니다.
② 글자 수 과금으로 인한 비용 누적 → VOICEVOX 병용
또 다른 하나는 비용입니다. Gemini TTS는 글자 수에 따른 과금 방식이기 때문에, 매일 여러 편을 제작하는 채널에서 계속 사용하면 비용이 은근히 쌓입니다. 그래서 비용을 절감하고 싶은 채널에는 VOICEVOX(로컬 Docker로 무료 사용)를 사용하는 구성을 늘렸습니다. 주간 메탈 리포트(Weekly Metal Report)는 이 방식을 사용하며, Rate Limit(속도 제한)이나 API 과금 없이 안정적으로 운영됩니다.
참고로, 초기에 무료 티어인 Gemini와 로컬의 VOICEVOX를 비교했을 때의 수치는 다음과 같습니다.
| 지표 | Gemini TTS (무료 티어) | VOICEVOX |
|---|---|---|
| 성공률 | 8% (1/13) | 100% (13/13) |
| ... | ... | ... |
단, VOICEVOX에도 약점이 있습니다. 무료로 고품질을 사용할 수 있다는 점은 좋지만, 기본적으로 일본어 전용입니다. 한국어, 번체자 같은 다국어 채널에서는 애초에 사용할 수 없으므로, 그 부분은 Gemini/ElevenLabs 측에 의존하게 됩니다.
게다가 일본어 채널에서도 영어 고유명사, 종목명, 전문 용어를 그대로 정확하게 읽어주지 않습니다. 외래어가 가득한 금속/가상자산 채널에서는 특히 이 현상이 두드러집니다. 그래서 읽기 변환 사전(config/pronunciation_ja.yaml)을 준비하여 읽기(pronunciation)를 보정하고 있습니다.
# 고유명사나 특수한 읽기 방식을 가진 단어의 읽기 정의
孫正義: そんまさよし
ChatGPT: チャットジーピーティー
...
이 사전은 운영하면서 "또 이 단어를 잘못 읽었네"라고 깨달을 때마다 추가하고 있으며, 은근히 유지보수의 효과가 좋습니다.
목소리 톤의 통일 (결국 포기한 이야기)
대화 형식의 뉴스 해설은 A(청취자)와 B(전문가)가 번갈아 가며 말합니다. 그런데 Gemini TTS는 요청(request)마다 목소리가 미묘하게 흔들리기 때문에, 대사를 하나씩 던지면 같은 화자임에도 목소리 톤, 어조, 말하기 속도가 어긋나서, 듣기에 "매번 조금씩 다른 사람 같다"는 이질감이 생깁니다.
당초에는 두 화자를 한 번의 요청으로 말하게 하는 멀티 스피커 방식(multiSpeakerVoiceConfig)을 사용했으나, 대본이 길어서 청크(chunk)를 넘어가게 되면 역시 같은 화자의 목소리가 어긋났습니다.
현재 채택하고 있는 방식은 화자 통일 방식입니다. 생각은 간단합니다:
- 화자별로 대사를 모아서 생성한다 — A의 대사는 가능한 한 한 번의 요청에, B도 마찬가지로 모읍니다. 같은 요청으로 생성한 부분은 목소리 톤이 일치하기 쉽습니다. 다만 긴 화자의 경우 ④에서 설명한 것처럼 대사 끊김 지점에서 500자 내외로 분할하기 때문에, 해당 청크마다 별도의 요청을 보내게 되어 생성 타이밍이 달라지면 결국 차이가 발생합니다 (어디까지나 "나아지는" 정도입니다).
- 하지만 이렇게 하면 "A의 음성 블록", "B의 음성 블록"이 따로 만들어질 뿐, 대화 순서대로 나열되어 있지는 않습니다.
- 그래서 각 화자의 음성을 STT(Speech-to-Text)로 받아쓰기하여 타임스탬프(timestamp)를 추출하고, 대사 사이의 무음 구간에서 원래의 대사 단위로 잘라낸 뒤, 대화 순서에 맞춰 재구축합니다.
이 "긴 단일 화자 음성을 원래의 대사 단위로 정확하게 되돌리는" 작업이 가장 어려운 부분이며, 다음 단계인 자막 타이밍(무음 구간 검출 + DP 최적 분할)과 완전히 동일한 메커니즘을 사용하고 있습니다.
다만 솔직히 말씀드리면, 이 방법으로 목소리 톤의 흔들림이 완전히 사라지는 것은 아닙. 화자별로 모아서 생성함으로써 나아지기는 하지만, 최종적으로 어떤 목소리와 톤으로 돌아올지는 그때그때 Gemini의 "기분"에 달려 있으며, 우리가 완전히 컨트롤할 수 있는 수단이 없습니다. 따라서 완벽한 통일은 일단 포기하고, TTS 모델 측이 진화하여 안정화되기를 기다리고 있는 것이 실정입니다.
목소리 흔들림에 대해 시청자로부터 이런 지적이 있었습니다... 죄송합니다.

자막과 대사의 어긋남
자막은 "TTS로 만든 음성을 STT로 다시 받아쓰기한 뒤, 대본 텍스트와 대조하여 타임스탬프를 추출하는" 방식을 사용합니다. 처음에 ElevenLabs STT를 도입했을 때의 효과는 극적이었으며, 음성과 자막의 어긋남이 124초에서 2초까지 줄어들었습니다.
다만 STT의 텍스트 매칭에도 한계가 있어(오인식으로 인해 대응이 깨짐), 최종적으로는 무음 구간 검출을 통한 동적 계획법(DP)의 최적 분할을 주축으로 삼고, STT는 분할이 범위를 벗어났을 때의 폴백(fallback) 용도로 격하시켰습니다. 현재는 "무음 DP 분할 + STT 폴백 + 타이밍 정합성 체크"의 3단계 방어 체제를 갖추고 있습니다.
환경 및 빌드 관련 함정
API뿐만 아니라 로컬 환경과 빌드 구성에서도 고전했습니다.
No such filter: 'drawtext'
엔딩의 CTA 텍스트를 ffmpeg의 drawtext로 그리고 있었는데, 어떤 단말에서 갑자기 다음과 같은 메시지가 나타났습니다.
No such filter: 'drawtext'
원인은 해당 단말의 ffmpeg(8.1.1)가 libfreetype 없이 빌드되었기 때문입니다. drawtext는 freetype에 의존하므로, 빌드 플래그에 따라 존재하지 않을 수 있습니다. 그래서 텍스트는 Pillow를 사용하여 투명 PNG로 그린 뒤 overlay로 합성하는 방식으로 통일하여, ffmpeg의 빌드 구성에 의존하지 않도록 했습니다 (자막도 이 방식을 사용합니다).
그 외에도 다음과 같은 문제들을 해결해 나갔습니다.
- 슬립(Sleep) 모드로 인해 API 연결이 끊김 →
caffeinate -i를 사용하여 실행 중 슬립 방지 - Apple Silicon에서 x86_64 패키지가 섞여
incompatible architecture발생 → venv를 사용하여 클린하게 관리 - 파이프라인이 7시간 동안 계속 실행되는 사고 → 스텝 단위의 타임아웃(기본 10~20분)을 설정하고, 일부 채널은 전체 실행 시간을 2시간으로 제한(Cap)
이러한 문제들은 "실제로 계속 돌려봐야 비로소 나타나는" 유형들이었습니다.
운영 및 플랫폼 측면의 함정
YouTube Data API의 quota
다국어 전개를 목표로 했을 때, 영상 1개를 업로드하는 데 1,600 quota가 소모됩니다. 8개 언어라면 1,600 × 8 = 12,800이 되어 기본 제공량인 일일 10,000을 초과하게 됩니다. quota 증설 신청은 심사에 수 주가 소요되므로, 언어(채널)별로 독립된 GCP 프로젝트를 만들어 각각 10,000 quota씩 확보하는 구성으로 변경했습니다.
뉴스 수집 소스의 변천
- NewsAPI → 무료 플랜에서는 일본어 기사가 취약하며,
/v2/top-headlines의country=jp결과가 0건임 - Google News RSS → 무료이며 무제한이고 일본어 콘텐츠가 풍부하여 일단 이쪽으로 이전. 하지만 RSS가 반환하는 것은 Google News를 경유하는 리다이렉트 URL이라 원문 기사의 본문을 읽을 수 없었고, 본문 없이는 핵심인 뉴스 분석이 불가능하여 포기
- GNews API (유료) → 본문까지 가져올 수 있으므로 최종적으로 이 소스를 선택
정책 위반으로 인한 Ban
다국어 버전 중 한 채널이 YouTube의 스팸, 기만, 사기 정책 위반으로 삭제되었습니다. 까다로운 점은 정책 명칭만 제시될 뿐 구체적으로 어느 부분이 해당되는지 알 수 없다는 것입니다. 썸네일에 단정적인 예측을 적은 것이 클릭베이트(Clickbait)로 간주되었을 가능성이 있지만 확증은 없으므로, 예방 차원에서 메타 생성 프롬프트에 "미래 예측을 썸네일/제목에 단정적으로 작성하지 말 것"을 추가했습니다. 다국어화 메커니즘 자체는 유지하되, 해당 채널의 코드와 인증 정보는 제거했습니다.
견고화 및 자동 운영 시스템 (간략히)
매일 전자동으로 돌리는 이상, "고장 나더라도 스스로 복구하거나 알아챌 수 있는" 장치가 필요했습니다.
- 비용 집계: 스텝별로 Claude의 토큰 수, Gemini TTS의 글자 수, ElevenLabs의 초 단위 사용량을 집계하여 JPY로 환산 (
cost_calculator.py) - 자동 클린업: 7일이 지난
.mp4/.mp3파일을 삭제 (대본, 메타데이터, 썸네일은 유지). 디스크 용량을 크게 절약 - 외부에서의 재실행: 이전에는 GitHub Issue를 폴링(Polling)하는 자체 제작 watcher로 실패한 채널을 재실행했으나, 현재는 Claude의 디스패치(Dispatch) 기능을 통해 외부에서 직접 지시합니다 (자체 제작 장치가 불필요해짐)
- Discord 알림: 채널별로 성공/실패를 알림
- 평가 서브시스템: 생성된 대본, 메타데이터, 뉴스 선정 내용을 LLM으로 채점하여 주간 리포트 생성
실제 알림은 다음과 같은 형태로 전달됩니다. 실패 시에는 에러 상세 내용이, 성공 시에는 실행 시간과 AI 비용이 포함됩니다.

↑ 에러와 성공 모두 이곳으로 전달됩니다 (실행 시간 및 AI 비용 포함)
자기 문서화(Self-documentation) 문화
이 프로젝트는 Claude Code를 이용한 이른바 '바이브 코딩(Vibe Coding)'으로 키워왔지만, 중간부터 의사 결정과 실패 사례를 반드시 로그로 남기도록 했습니다.
DECISION_LOG.md— "왜 Gemini 대신 Claude를 선택했는가"와 같은 기술적 판단과 실제 에러 사례 기록IMPROVEMENT_LOG.md— "가설 → 관찰 지표 → 체크포인트 → 결과"의 폐쇄 루프(Closed-loop)를 통해 대책을 검증
이 기사의 실패담 중 상당수는 이 로그와 커밋 메시지에서 다시 찾아낸 것들입니다. 참고로 커밋의 Co-Authored-By를 따라가 보면 Claude
→ Opus 4.5
→ 4.6
→ 4.7
→ Opus 4.8
순으로 나열되어 있어, 사용한 모델의 세대교체 기록이기도 했습니다. 자동화를 구축할 때야말로 판단 근거를 기록해 두어야 나중에의 자신에게 엄청난 도움이 됩니다.
요약
"AI로 영상을 완전 자동 양산한다"라고 하면 쉬워 보이지만, 실체는 당시의 날카로운(尖った) API들과 끊임없이 격투하는 엔지니어링이었습니다. 마지막으로 빠졌던 함정들을 목록으로 정리해 둡니다. 참고로 여기에 언급한 것은 대표적인 것들일 뿐이며, 실제로는 이 외에도 셀 수 없이 많은 함정에 빠졌습니다. 다 나열하자니 끝이 없으므로 이쯤에서 마무리하겠습니다.
| 빠졌던 함정 | 증상 | 회피책 |
|---|---|---|
| Gemini 무료 티어가 양산에 부적합 | 500/503 빈번 발생 + 429로 중단 | 분석을 Claude로 전환 + 지수 백오프 (Exponential Backoff) |
| ... | Pillow 오버레이로 | |
| YouTube quota | 8개 언어에서 상한 초과 | 언어별 GCP 프로젝트 생성 |
여기까지가 "만드는 법"과 "빠졌던 함정"에 대한 기술적인 리캡 (Recap)입니다. 마지막으로, 운영해 본 솔직한 소감을 적으며 마치겠습니다.
그래서, 이게 수지타산이 맞는가? (운용의 현실)
...솔직히 말하면, 미묘합니다.
지금까지 "완전 자동으로 영상을 양산할 수 있다"라고 설명해 왔지만, 실상은 그렇게 만만하지 않습니다.
- 매일 십수 개를 사람의 손길 없이 생성 및 게시할 수 있다는 점은 확실히 기분이 좋습니다. 매일 아침 일어나면 저절로 영상이 올라와 있습니다.
- 하지만, 거의 매일 어딘가를 수정하고 있습니다. API의 사양 변경, 모델의 폐지, 우연히 마주친 본문 패턴의 오류(400)... 이 기사의 실패담은 "과거형"으로 쓰고 있지만, 새로운 함정은 지금도 계속 늘어나고 있으며, 완전히 방치한 채로 돌아가는 것이 실상입니다.
- 결정타는 YouTube의 Ban 축제 (앞서 언급한 "운용·플랫폼 측면의 함정")입니다. 정책 명칭만 제시될 뿐 이유에 대한 상세한 내용은 알 수 없고, 대책도 더듬더듬 찾아야 합니다. 꾸준히 쌓아 올린 채널이 한순간에 삭제될 리스크는 항상 곁에 있습니다.
종합적으로 보면, 솔직히 "수지타산이 맞는다"고는 생각되지 않습니다 (웃음). 그럼에도 불구하고, "매일 아침 저절로 영상이 올라와 있는" 상태 그 자체가 흥미로워서 계속하고 있습니다.
참고로, 수익·시청 지표·채널 운영 관련 이야기는 기술과는 별개의 축이므로, 별도의 기사에 쓸 예정입니다. 이 기사는 어디까지나 "만드는 법과 빠졌던 함정"에 집중했습니다.
마찬가지로 "AI로 영상 자동 생성을 해보고 싶다"는 분들에게 지도가 될 수 있다면 좋겠습니다.
Discussion

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