내일의 영상 스크립트 편향을 조정하는 YouTube 성과 분류기를 구축한 방법
요약
자동화된 YouTube 채널 운영을 위해 영상 성과를 분석하고 스크립트 생성기에 피드백을 제공하는 폐쇄 루프(closed-loop) 시스템 구축 방법을 설명합니다. YouTube Data API를 활용해 성과를 분류하고 이를 지식 저장소에 반영하여 콘텐츠 품질을 개선합니다.
핵심 포인트
- YouTube Data API를 활용한 자동화된 성과 분석 파이프라인 구축
- 채널 핸들 기반의 유연한 채널 식별 및 검색 폴백 전략
- 중앙값(Median) 기반의 단순하고 효과적인 영상 성과 분류 로직
- 데이터 노이즈 방지를 위한 72시간 유예 기간 설정
저는 4월부터 세 개의 프로그래매틱 디렉토리 사이트와 함께 자동화된 YouTube 채널을 운영해 왔습니다. 영상 측면에서는 매일 스크립트를 생성하고 밤사이에 렌더링하는 두 명의 호스트 VTuber 파이프라인을 사용합니다. 지난주까지 저에게 부족했던 것은 피드백 메커니즘이었습니다. 스크립트 생성기는 어떤 영상이 실제로 반응을 얻고 있는지 전혀 모른 채, 진공 상태에서 콘텐츠를 생산할 뿐이었습니다.
해결책은 scripts/yt-analytics/run.py입니다. 이는 매일 실행되는 330줄의 Python 스크립트로, YouTube Data API v3를 통해 지난 30개의 영상을 읽어오고, 이를 성과가 높은 영상(high performer) 또는 낮은 영상(low performer)으로 분류한 뒤, 스크립트 생성기가 매 세션 전에 읽는 파일인 docs/yt-knowhow-bank-en.md에 편향 힌트(bias hints)를 다시 기록합니다.
이것은 마법이 아니라 폐쇄 루프(closed loop)입니다. 하지만 루프를 닫는 것(closing the loop)이 바로 핵심입니다.
안정적인 채널 ID 없이 채널 가져오기
첫 번째 문제는 채널 식별(channel resolution)이었습니다. YouTube의 v3 API는 대부분의 엔드포인트에서 채널 ID를 요구하지만, 저는 채널이 재생성될 경우 깨질 수 있는 ID를 하드코딩하고 싶지 않았습니다. 스크립트는 다음 네 가지 전략을 순서대로 시도합니다:
YT_CHANNEL_HANDLE환경 변수 값을 사용한forHandleforHandle=claudeautomateforHandle=claude_automateforHandle=claude-automate- 모두 실패할 경우: 반환된 채널 ID들을 루프 돌며 "claude automate"에 대해 검색 API(search API) 호출
for handle in handles:
body = http_get(
f"...channels?part=contentDetails,statistics,snippet&forHandle={handle}&key={api_key}"
...
검색 폴백(fallback) 방식은 더 느리고 더 많은 할당량(quota)을 소모하지만, 모든 직접적인 핸들(handle) 시도가 실패했을 때만 실행됩니다. 실제로 claudeautomate는 첫 번째 시도에서 일치합니다.
채널이 식별되면, relatedPlaylists.uploads를 통해 업로드 플레이리스트 ID를 얻을 수 있습니다. 거기서부터 playlistItems는 ID가 포함된 최근 영상 최대 30개를 반환하며, 이는 통계 정보를 위한 두 번째 videos.list 요청으로 이어집니다.
영상을 High 또는 Low로 분류하기
이 분류기는 의도적으로 단순하게 설계되었습니다. 머신러닝 (Machine Learning)을 사용하지 않고 중앙값 (Median) 기반의 임계값을 사용합니다.
views = [int(v["statistics"].get("viewCount", 0)) for v in videos]
median = statistics.median(views)
...
중앙값 조회수(median views)의 1.5배를 초과하는 영상은 HIGH로 분류합니다. 중앙값의 0.6배 미만인 영상은 LOW로 분류하되, 게시된 지 72시간이 지난 경우에만 해당합니다. 이 72시간의 유예 기간은 매우 중요합니다. 어제 게시된 영상이 중앙값 조회수의 40%를 기록했다면, 이는 단지 영상이 아직 '어리기' 때문일 수 있습니다. 이를 즉시 실패작(dud)으로 표시하는 것은 노이즈 (Noise)가 될 것입니다.
0.6배에서 1.5배 사이의 모든 영상은 어느 쪽도 아닙니다. 이는 실행 가능한 신호 (Actionable signal)가 아니므로 무시합니다.
평균 (Mean) 대신 중앙값 (Median)을 선택한 것은 의도적인 결정입니다. 만약 영상 하나가 바이럴 (Viral)된다면, 평균 조회수는 다른 모든 영상의 분류를 왜곡합니다. 중앙값은 이상치 (Outliers)에 강건합니다. 이는 디렉토리 측면에서 학습한 3단계 콘텐츠 품질 접근 방식 (three-tier content quality approach)에서 얻은 교훈입니다. 단순한 버킷팅 (Bucketing)이 단일 수치를 최적화하려는 시도보다 낫습니다.
제목 중첩을 통한 아키타입 (Archetype) 매칭
스크립트 생성기는 생성된 각 영상에 "tutorial", "recap", "comparison", "technical"과 같은 아키타입 레이블을 할당하고, 이를 content/yt-queue/uploaded/ 경로의 업로드 대기열에 저장합니다. 하지만 YouTube의 분석 API (Analytics API)는 이러한 레이블을 노출하지 않습니다. 따라서 아키타입을 성과 통계와 다시 연결해야 합니다.
이 재연결은 제목 중첩 (Title overlap)을 통해 이루어집니다.
def title_overlap(a: str, b: str) -> int:
aw = {w.lower().strip(",.!?:;\"'") for w in a.split() if len(w) > 2}
bw = {w.lower().strip(",.!?:;\"'") for w in b.split() if len(w) > 2}
...
API 응답의 각 영상에 대해, 해당 제목을 업로드된 모든 대기열 파일과 비교하여 가장 잘 일치하는 것을 찾습니다. 단, 단어 중첩이 4개 이상인 경우에만 수행합니다. 의미 있는 단어의 일치 횟수가 4개 미만인 제목은 "unknown"으로 레이블이 지정됩니다.
이것은 완벽하지 않습니다. 제목은 게시 과정에서 변할 수 있습니다. 하지만 4단어 이상의 일치는 오탐(false positive)이 드물 정도로 충분히 엄격합니다. 25개의 비디오 세트를 대상으로 테스트했을 때, 21개는 정확하게 일치했고 4개는 "unknown"으로 반환되었습니다. 아주 훌륭하지도 않지만, 사용할 수 없는 수준도 아닙니다. 집계된 패턴 분석 (aggregate pattern analysis)을 수행하기에는 충분합니다.
첫 단어로부터 후크 패턴 (Hook Patterns) 추론하기
전형적인 유형 (archetype)을 넘어, 저는 비디오 스크립트의 특정 도입 패턴이 성과와 상관관계가 있는지 알고 싶었습니다. 후크 패턴 추론은 스크립트 첫 문장의 첫 단어에 대한 단일 함수 조회 (single-function lookup)입니다:
def hook_pattern(text: str) -> str:
first_word = text.strip().lower().split()[0]
if first_word in {"why", "how", "what", "when", "who"}:
...
이는 투박한 휴리스틱 (heuristic)입니다. 첫 단어로 "How"나 "Why"가 나온다고 해서 비디오가 자동으로 좋아지는 것은 아닙니다. 하지만 규모가 커지면 — 한 번의 실행당 30개의 비디오를 분류할 경우 — HIGH 및 LOW 버킷 간의 분포가 의미 있는 신호 (signal)를 생성합니다. 만약 "질문형 (question)" 후크가 지속적으로 LOW에 모이고 "숫자형 (numeric)" 후크가 HIGH에 모인다면, 이는 스크립트 생성기 (script generator)의 프롬프트 컨텍스트 (prompt context)로 다시 피드백할 가치가 있습니다.
이 부분은 만약 제가 이 시스템을 50개 이상의 비디오 규모로 확장한다면 가장 먼저 교체할 부분이기도 합니다. 첫 단어 분류는 오프닝 이후의 모든 내용을 놓칩니다. "I"로 시작하는 제목은 "I ditched X after 3 months and here's why (3개월 만에 X를 버렸고 그 이유는 다음과 같습니다)"가 될 수도 있고, 지루한 "I made another video today (오늘 또 다른 영상을 만들었습니다)"가 될 수도 있습니다. 저는 결국 카테고리 분류를 위해 전체 첫 문장을 작은 LLM 호출을 통해 전달할 것입니다.
지식 뱅크 (Knowledge Bank)에 편향 힌트 쓰기
분류기의 출력물은 대시보드가 아닙니다. 그것은 스크립트 생성기가 각 세션 시작 시 읽는 docs/yt-knowhow-bank-en.md 파일의 한 섹션입니다. update_kb 함수는 ## Routine Auto-Tuner Notes 헤더를 찾아 다음 ##가 나오기 전까지의 모든 내용을 교체합니다:
marker = "## Routine Auto-Tuner Notes"
idx = text.find(marker)
if idx == -1:
...
작성된 섹션에는 잘 작동하는 요소(고성과 아키타입(archetypes) 및 훅(hook) 패턴), 작동하지 않는 요소(저성과 패턴), 그리고 다음 날을 위해 선호되는 아키타입과 훅 스타일을 명시하는 "내일의 편향(Tomorrow's bias)" 단락이 포함됩니다. 스크립트 생성기(script generator)는 각 영상을 작성하기 전, 시스템 프롬프트(system prompt) 컨텍스트에서 이 내용을 읽습니다.
이것은 편향을 맹목적으로 따르는 것이 아니라, 더 정보에 기반한 선택을 하기 위해 해당 정보를 활용하는 것입니다. 이는 콘텐츠 ETL 레벨에서 프롬프트 캐싱(prompt caching)이 제공하는 이점과 유사합니다. 즉, 매번 처음부터 결정을 다시 내리는 대신 세션 시작 시점에 적절한 컨텍스트를 주입하는 것입니다.
GitHub Actions 통합
전체 프로세스는 두 개의 YouTube 채널과 세 개의 디렉토리 사이트를 구동하는 단일 CI 워크플로(workflow) 내부에서 일일 크론 잡(cron job)으로 실행됩니다. 필요한 환경 변수(env vars)는 YT_API_KEY (YouTube Data API v3 키 — 무료 티어는 하루 10,000 유닛을 제공하며, 이는 충분하고도 남는 양입니다)와 일일 요약 푸시를 위한 선택 사항인 DISCORD_WEBHOOK_URL입니다.
스크립트는 API 키가 누락된 경우를 유연하게 처리합니다. YT_API_KEY가 설정되어 있지 않으면 경고를 출력하고 종료 코드 0으로 종료합니다. CI 잡(job)이 실패하지 않도록 하는 것입니다. 이는 제가 Bluesky 포스트 큐(post queue)와 CI 내부에서 YouTube 썸네일을 생성하는 작업에 사용했던 것과 동일한 패턴입니다. 즉, 선택적인 도구는 해당 자격 증명(credentials)이 설정되지 않은 환경에서 인증 정보가 없다고 해서 빌드를 중단시켜서는 안 됩니다.
내가 다르게 했을 점
조회수보다 시청 시간(Watch time)을 우선시하기. 단순 조회수(Raw views)는 참여도(engagement)를 나타내는 노이즈가 많은 대리 지표(proxy)입니다. 평균 시청 지속 시간(average view duration)이 90%인 조회수 200회의 영상이, 유지율(retention)이 20%인 조회수 500회의 영상보다 더 낫습니다. YouTube Analytics API(Data API와는 별개이며 OAuth가 필요함)는 averageViewDuration을 제공합니다. GitHub Actions를 통한 OAuth 구현은 번거롭기 때문에(리프레시 토큰(refresh tokens)을 비밀값(secrets)으로 저장하고 로테이션(rotation)을 처리하는 것은 상당한 유지보수 부담을 가중시킵니다) 이를 연결하지는 않았습니다. 하지만 이것이 올바른 지표입니다.
LLM 훅(hook) 카테고리화. 첫 단어 휴리스틱(heuristic)은 너무 거칠게 분류됩니다. 각 제목을 단일 Claude Haiku 호출을 통해 전달하여 훅의 유형과 주제를 분류한다면, 한 달에 몇 센트의 비용만으로 훨씬 더 나은 신호(signal)를 얻을 수 있을 것입니다.
낮은 영상 수에 대한 베이지안 평활화(Bayesian smoothing). 영상이 20개 미만일 때는 중앙값 임계값(median threshold)이 불안정합니다. 참조 모집단(reference population)으로부터 사전 확률(prior)을 가져온다면 초기 단계에서 더 신뢰할 수 있는 신호를 얻을 수 있을 것입니다.
현재 버전은 이전에는 존재하지 않았던 피드백 루프(feedback loop)를 완성했습니다. 저는 어디를 개선해야 할지 정확히 알고 있으며, 채널에 개선 작업을 할 만큼 충분한 데이터가 쌓이면 그렇게 할 것입니다.
FAQ
유료 YouTube API 할당량(quota)이 필요한가요?
아니요. YouTube Data API v3 무료 티어는 하루 10,000 유닛을 제공합니다. 각 playlistItems 요청은 1 유닛이 소모되며, 각 videos.list 호출은 50개 단위 배치당 1 유닛이 소모됩니다. 전체 일일 실행 비용은 대략 3~5 유닛입니다.
영상이 업로드된 큐(queue) 파일과 일치하지 않으면 어떻게 되나요?
이것이 스크립트 생성기 (script generator)의 프롬프트를 자동으로 수정하게 될까요?
자동으로는 아닙니다. 지식 뱅크 (knowledge bank)는 컨텍스트 (context)로 읽히지만, 아키타입 (archetype)을 변경할지에 대한 결정은 이 분류기 (classifier)가 아닌 스크립트 생성기의 판단에 달려 있습니다.
72시간의 유예 기간이 HIGH 분류에도 영향을 미치나요?
아니요 — 오직 LOW 분류에만 연령 제한 (age gate)이 적용됩니다. 업로드 후 첫 24시간 이내에 바이럴 (viral)이 되는 영상은 즉시 HIGH로 분류되어야 합니다. 유예 기간은 어린 영상들이 잘못된 LOW 분류를 받는 것을 방지하기 위해서만 존재합니다.
이 글은 세 개의 AI 큐레이션 디렉토리 사이트를 운영하는 6개월간의 지속적인 실험의 일부입니다. 여기에 언급된 기술적 주장들은 사실이며, 이 기사는 AI의 도움을 받아 작성되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기