Files
web-page-backend/docs/superpowers/specs/2026-05-07-music-youtube-pipeline-design.md
gahusb 2eeb98a723 docs(spec): Music YouTube 파이프라인 단계별 승인 자동화 설계
트랙 → 영상 → 발행까지 단계별 텔레그램 승인 워크플로 설계.
- 6단계 진행 바: 커버/영상/썸네/메타/AI검토/발행
- 자연어 의도 분류 (화이트리스트 + LLM 폴백)
- 반려 시 사용자 피드백 반영 재생성 (5회 한도)
- AI 최종 검토 4축 가중평균 (메타/정책/시청/트렌드)
- music-lab 5개 신규 테이블 + 12개 엔드포인트
- agent-office youtube_publisher 에이전트 + scheduler 폴링
- web-ui SetupTab + PipelineTab 신규 + Library 트리거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:13:29 +09:00

22 KiB
Raw Permalink Blame History

Music YouTube 파이프라인 — 단계별 승인 자동화 설계

작성일: 2026-05-07 상태: 설계 승인 대기 관련 후속 작업: STATUS.md 2-3, 2-4


1. 배경

현재 Music YouTube 탭에는 영상 제작 / 수익 추적 / 시장 트렌드 / 컴파일 4개 서브탭이 있고, music-lab 백엔드는 video_producer로 로컬 영상(MP4)까지 만들 수 있다. 그러나 YouTube 자동 업로드와 AI 커버·메타데이터 자동 생성, AI 검토는 없다. 트랙 생성부터 발행까지 한 편 완성하려면 매번 수동으로 영상 만들고 직접 YouTube Studio에 업로드해야 한다.

목표: 트랙을 골라 한 번 시작하면 단계별로 텔레그램 승인을 받으며 영상이 발행되는 파이프라인을 구축한다. 사용자는 각 단계 산출물을 텔레그램에서 승인/반려할 수 있고, 반려 시 자연어 피드백으로 같은 단계가 재생성된다.


2. 비목표 (Out of scope)

  • 가사 자막 영상 (synced lyrics → 영상) — 차후
  • YouTube Shorts 전용 워크플로 (1080×1920) — 비주얼 기본값에 옵션만 두고, 실제 Shorts 최적화(60초 클립 추출 등)는 차후
  • 멀티 채널 운영 — 단일 채널 OAuth 1행만 지원
  • 비디오 편집기 UI — 트림/페이드 등은 컴파일 탭에 있고 본 파이프라인은 단일 트랙 1개 영상 가정

3. 사용자 흐름

[사용자가 진행 시작]
  Library 트랙 카드 → "🎬 영상 파이프라인" 또는 진행 탭 → "+ 새 파이프라인"
  ↓
  step 2: AI 커버 아트 생성 → 텔레그램 알림 "커버 승인?"
  step 3: 영상 비주얼 생성 (커버 + 음원) → 텔레그램 알림
  step 4: 썸네일 생성 → 텔레그램 알림
  step 5: 메타데이터 생성 → 텔레그램 알림
  ↓
  AI 최종 검토 (자동, 4축 검사) → 텔레그램에 점수 + 발행 요청
  ↓
[사용자 발행 승인]
  step 6: YouTube 업로드 (private/public 정책에 따라)
  step 7: 발행 후 추적 시작 (수익 추적 탭에 표시)

각 단계 텔레그램 알림에 사용자가 자연어로 응답한다.

  • 승인: "승인" / "시작" / "진행" / "OK" / "Agree" / "네" / "예" / "좋아"
  • 반려: "반려" / "거절" / "취소" / "no" + 수정 방향 텍스트 (예: "썸네일 색 더 어둡게")

4. 아키텍처

┌──────────────────────────────────────────────────────────────┐
│  Frontend (web-ui)                                          │
│  /lab/music → MusicStudio → YouTube 탭                       │
│   ├─ 영상 제작 (기존)                                        │
│   ├─ 수익 추적 (기존)                                        │
│   ├─ 시장 트렌드 (기존)                                      │
│   ├─ 컴파일 (기존)                                          │
│   ├─ 진행 (NEW) ← 파이프라인 카드 보드                       │
│   └─ 구성 (NEW) ← 설정 허브                                  │
└──────────────────────────────────────────────────────────────┘
                       ↓ /api/music/pipeline/* (REST)
┌──────────────────────────────────────────────────────────────┐
│  music-lab (FastAPI, 18600)                                 │
│   • 파이프라인 CRUD + 상태 머신                              │
│   • AI 커버 (DALL·E 3) — 비동기 BackgroundTask               │
│   • 영상 비주얼 (FFmpeg, 기존 video_producer 확장)          │
│   • 썸네일 (FFmpeg + 텍스트 오버레이)                       │
│   • 메타데이터 생성 (Claude Haiku)                           │
│   • AI 최종 검토 (Claude Sonnet, 4축 가중)                   │
│   • YouTube 업로드 (google-api-python-client)               │
└──────────────────────────────────────────────────────────────┘
                       ↑ poll (30s) / push 결과
┌──────────────────────────────────────────────────────────────┐
│  agent-office (FastAPI + Telegram, 18900)                   │
│   • youtube_publisher 에이전트 (NEW) — 오케스트레이터          │
│   • 단계 *_pending 진입 감지 → 텔레그램 알림 발송            │
│   • 텔레그램 reply 자연어 의도 분류 (Claude or 화이트리스트)  │
│   • music-lab /feedback 호출 → 다음 단계 또는 재생성        │
└──────────────────────────────────────────────────────────────┘

책임 경계:

  • music-lab: 무엇을 만들지 안다. 산출물 생성·저장·상태 전이.
  • agent-office: 언제 다음으로 넘길지 결정. 텔레그램 단일 채널 인터페이스.
  • frontend: 진행 상태 조회 + 사용자 트리거(시작/취소/수동 발행).

5. 상태 머신

created
  → cover_pending           (자동 생성 후 진입)
  → cover_approved          (승인)
  → video_pending
  → video_approved
  → thumb_pending
  → thumb_approved
  → meta_pending
  → meta_approved
  → ai_review               (자동, 사용자 액션 X)
  → publish_pending         (검토 결과 + 발행 요청 텔레그램)
  → publishing              (업로드 중)
  → published               (완료)

어디서나:
  → cancelled               (사용자 취소)
  → failed                  (복구 불가 오류)
  → awaiting_manual         (재생성 5회 한도 초과)

*_pending 진입 시 → 텔레그램 알림. 각 *_approved 진입 시 → 다음 단계 BackgroundTask 시작.


6. 프론트엔드 상세

6-1. 새 탭 — 구성 (SetupTab.jsx)

세로 카드 형식, 카드별 저장 버튼:

카드 필드
YouTube 채널 연동 OAuth 시작 → Google 인증 → 채널명·아바타 표시. 재인증 / 연결 해제
Telegram 알림 채널 현재 chat_id (read-only, ENV 출처). 테스트 메시지 발송
메타데이터 템플릿 제목 패턴 ([{genre}] {title} | {bpm}BPM Lo-fi Mix 등), 설명 multiline, 태그 CSV, 카테고리
AI 커버 아트 prompt 장르별 prompt 템플릿 (lo-fi/phonk/ambient/pop/...) 추가/편집/삭제
AI 최종 검토 기준 4축 가중치 슬라이더 + pass score 임계값 (기본 60)
영상 비주얼 기본값 해상도 (1920×1080 / 1080×1920), 스타일 (visualizer/슬라이드쇼), 배경 (AI 커버/그라데이션)
발행 정책 즉시 / 예약 시간대 / privacy (private 우선)

6-2. 새 탭 — 진행 (PipelineTab.jsx)

상단: "+ 새 파이프라인 시작" 버튼 → Library 트랙 선택 모달.

카드 그리드 — 진행 중 + 완료/실패/취소 (필터 토글):

┌─ Track Title (genre · BPM) ───────────── [Cancel] ─┐
│  ●━━━━━●━━━━━●━━━━━○━━━━━○━━━━━○ (6단계 진행 바)    │
│  커버  영상  썸네  메타  검토  발행                  │
│                                                      │
│  현재: [메타데이터 승인 대기]                         │
│  텔레그램에 알림 보냄 — 12분 전                      │
│                                                      │
│  [최근 산출물 미리보기]                               │
│  • 메타: "[Lo-fi] Midnight Drive | 85BPM..."         │
│  • 썸네일: ▭                                         │
│                                                      │
│  📜 피드백 히스토리                                  │
│  • "썸네일 색이 너무 어두워" → 재생성 (5분 전)        │
└──────────────────────────────────────────────────────┘

상태 시각:

  • running — 스피너 + "처리 중..."
  • awaiting_approval — 점멸 도트 + "텔레그램 응답 대기"
  • regenerating — 회전 화살표 + "피드백 반영 중"
  • completed — 체크 + YouTube 링크
  • failed / awaiting_manual — 빨간 배지 + 사유

폴링: 카드 보일 때 5초 간격 GET /api/music/pipeline?status=active.

6-3. 영상 제작 탭 (기존)

그대로 유지. footer에 "💡 단계별 자동화는 진행 탭에서" 1줄 안내.

6-4. Library 카드 변경

기존 액션 옆에 "🎬 영상 파이프라인" 버튼 추가 → 클릭 시 신규 파이프라인 생성 후 진행 탭 이동.


7. 백엔드 상세

7-1. music-lab 신규 모듈

파일 역할
app/pipeline/state_machine.py 상태 전이 + 검증
app/pipeline/orchestrator.py start_step(pipeline_id, step) — BackgroundTask 등록
app/pipeline/cover.py DALL·E 3 호출 + 폴백
app/pipeline/metadata.py Claude Haiku 호출 + 템플릿 치환
app/pipeline/review.py Claude Sonnet 4축 검토 + 가중평균
app/pipeline/youtube.py OAuth + 업로드 (google-api-python-client)
app/pipeline/storage.py /data/videos/{id}/ 산출물 관리

기존 app/video_producer.pyapp/pipeline/video.py로 이동 + 슬라이드쇼 입력으로 AI 커버 사용 옵션 추가.

7-2. agent-office 신규/변경

파일 변경
app/agents/youtube_publisher.py NEW — 오케스트레이터
app/scheduler.py 30초 간격 _poll_pipelines 잡 추가
app/telegram/conversational.py reply 매칭 + youtube_publisher로 라우팅
app/service_proxy.py music-lab pipeline 호출 헬퍼 추가

youtube_publisher:

  • poll_state_changes() — music-lab /api/music/pipeline?status=active 폴링, *_pending 신규 진입 시 텔레그램 발송. 멱등 처리(메시지 ID 저장).
  • on_telegram_reply(message)reply_to_message_id로 pipeline 매칭, 자연어 분류 → /feedback 호출.

7-3. 자연어 의도 분류

APPROVE_WORDS = {"승인", "시작", "진행", "ok", "okay", "agree", "네", "예", "좋아", "go"}
REJECT_WORDS  = {"반려", "거절", "취소", "no", "nope"}

def classify_intent(text: str) -> tuple[str, str | None]:
    t = text.strip().lower()
    # 1. 명확한 단어만 — LLM 우회
    if t in APPROVE_WORDS:
        return ("approve", None)
    if t in REJECT_WORDS:
        return ("reject", None)
    # 2. 반려 단어 + 추가 텍스트 — 단순 분리
    for w in REJECT_WORDS:
        if t.startswith(w):
            return ("reject", text[len(w):].strip(" ,.-:"))
    # 3. 모호한 경우 — Claude Haiku 호출
    return _llm_classify(text)

LLM 분류 응답 (JSON):

{"intent": "approve|reject|unclear", "feedback": "..."}

unclear → 텔레그램에 "다시 입력해주세요. 예: '승인' 또는 '제목을 짧게'" 안내 + 같은 상태 유지.

7-4. AI 최종 검토 (4축)

meta_approved 직후 자동 진행. Claude Sonnet 1회 호출.

입력:

  • 트랙 정보 (title, genre, BPM, key, scale, moods, instruments)
  • 영상 정보 (length, resolution, style)
  • 메타데이터 (title, description, tags, category)
  • 썸네일 URL
  • 트렌드 데이터 (market_trends top 10)

출력 JSON:

{
  "metadata_quality": {"score": 0-100, "notes": "..."},
  "policy_compliance": {"score": 0-100, "issues": []},
  "viewer_experience": {"score": 0-100, "notes": "..."},
  "trend_alignment": {"score": 0-100, "matched_keywords": []},
  "weighted_total": 0-100,
  "verdict": "pass" | "fail",
  "summary": "..."
}

가중치 (기본, 구성 탭에서 조정 가능):

  • 메타데이터 품질 25
  • 콘텐츠 정책 30
  • 시청 경험 25
  • 트렌드 정렬 20

임계값 60 미만 → fail. 텔레그램 메시지에 "강제 발행" / "메타로 돌아가 재검토" 안내.

7-5. AI 커버 아트

  • 모델: OpenAI gpt-image-1 (DALL·E 3 후속)
  • 해상도: 1024×1024
  • 환경변수: OPENAI_API_KEY
  • 비용: 1024×1024 standard ≈ $0.04/장 (단계당 최대 5회 = $0.20)
  • 폴백: 그라데이션 (GENRE_COLORS) + 트랙 제목 텍스트 오버레이

prompt 빌더 (구성 탭의 장르별 템플릿 사용):

{genre_template}, {mood_descriptor}, no text, high quality

7-6. 메타데이터 자동 생성

  • 모델: Claude Haiku
  • 호출 시점: meta_pending 진입 시 (커버 승인 후 미리 생성하지 않음)
  • 입력: 트랙 정보 + 구성 탭 메타 템플릿 + 트렌드 키워드
  • 출력: title (60자 이내), description (3-5문단, 1000자 이내), tags (15개 이내), category_id

7-7. YouTube 업로드

  • 라이브러리: google-api-python-client + google-auth-oauthlib
  • OAuth flow: Authorization Code → refresh_token 저장 (youtube_oauth_tokens 테이블)
  • 업로드 시 access_token 갱신 → resumable upload
  • Privacy: 구성 탭 정책 (private/unlisted/public)
  • 카테고리: 메타데이터의 category_id (기본 10 = Music)

8. 데이터 모델

8-1. 신규 테이블 (music-lab db.py)

CREATE TABLE video_pipelines (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    track_id        INTEGER NOT NULL,
    state           TEXT NOT NULL,
    state_started_at TEXT NOT NULL,
    cover_url       TEXT,
    video_url       TEXT,
    thumbnail_url   TEXT,
    metadata_json   TEXT,
    review_json     TEXT,
    youtube_video_id TEXT,
    feedback_count_per_step TEXT NOT NULL DEFAULT '{}',
    last_telegram_msg_ids TEXT NOT NULL DEFAULT '{}',
    created_at      TEXT NOT NULL,
    updated_at      TEXT NOT NULL,
    cancelled_at    TEXT,
    failed_reason   TEXT,
    FOREIGN KEY (track_id) REFERENCES tracks(id)
);

CREATE TABLE pipeline_jobs (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    pipeline_id     INTEGER NOT NULL,
    step            TEXT NOT NULL,
    status          TEXT NOT NULL,
    error           TEXT,
    started_at      TEXT,
    finished_at     TEXT,
    duration_ms     INTEGER,
    FOREIGN KEY (pipeline_id) REFERENCES video_pipelines(id)
);

CREATE TABLE pipeline_feedback (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    pipeline_id     INTEGER NOT NULL,
    step            TEXT NOT NULL,
    feedback_text   TEXT NOT NULL,
    received_at     TEXT NOT NULL,
    FOREIGN KEY (pipeline_id) REFERENCES video_pipelines(id)
);

CREATE TABLE youtube_oauth_tokens (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    channel_id      TEXT NOT NULL,
    channel_title   TEXT,
    avatar_url      TEXT,
    refresh_token   TEXT NOT NULL,
    access_token    TEXT,
    expires_at      TEXT,
    created_at      TEXT NOT NULL
);

CREATE TABLE youtube_setup (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    metadata_template_json TEXT NOT NULL,
    cover_prompts_json     TEXT NOT NULL,
    review_weights_json    TEXT NOT NULL,
    review_threshold       INTEGER NOT NULL DEFAULT 60,
    visual_defaults_json   TEXT NOT NULL,
    publish_policy_json    TEXT NOT NULL,
    updated_at             TEXT NOT NULL
);

8-2. 산출물 저장 경로

/data/videos/{pipeline_id}/
  ├─ cover.jpg          (AI 또는 폴백)
  ├─ video.mp4          (FFmpeg 결과)
  ├─ thumbnail.jpg
  └─ logs/              (FFmpeg/upload 로그)

노출 URL: /media/videos/{pipeline_id}/<file> (nginx 정적 서빙).


9. API 엔드포인트

9-1. music-lab 신규

메서드 경로 용도
GET /api/music/pipeline 파이프라인 목록 (`?status=active
GET /api/music/pipeline/{id} 단건 + jobs + feedback
POST /api/music/pipeline 신규 (body: {track_id})
POST /api/music/pipeline/{id}/start 첫 단계 시작 → 202
POST /api/music/pipeline/{id}/feedback 승인/반려 (body: {step, intent, feedback_text?})
POST /api/music/pipeline/{id}/cancel 취소
POST /api/music/pipeline/{id}/publish 검토 후 업로드 트리거
GET /api/music/setup 구성 조회
PUT /api/music/setup 구성 저장
GET /api/music/youtube/auth-url OAuth 시작 URL
GET /api/music/youtube/callback OAuth callback
POST /api/music/youtube/disconnect 연결 해제
GET /api/music/youtube/status 연결 상태

모든 생성/처리 엔드포인트는 즉시 202 + job_id 반환, BackgroundTask로 처리. 프론트는 GET /api/music/pipeline/{id}로 폴링.

9-2. 멱등성

  • /feedback은 동일 (pipeline_id, step, intent) 중복 호출 시 무시 (이미 다음 상태로 넘어간 경우 텔레그램 reply 지연 방지)
  • 텔레그램 메시지 ID 저장으로 동일 메시지 중복 처리 방지

10. 비동기 처리 + 폴백

원칙: 모든 AI/생성 작업은 BackgroundTasks + DB job 상태로 처리. 호출 즉시 202, 폴링으로 결과 확인. 사용자 경험: 어떻게든 다음 단계로 보낸다, 단 폴백 사용 시 텔레그램에 명시.

작업 타임아웃 폴백
DALL·E 3 90초 그라데이션 + 텍스트 오버레이
Claude Haiku (메타) 30초 템플릿 변수 그대로 치환
Claude Sonnet (검토) 60초 휴리스틱만 (정책 단어 매치 + 길이 체크)
FFmpeg 5분 failed + 텔레그램 알림
YouTube upload 10분 재시도 3회 → failed

각 BackgroundTask는 pipeline_jobsrunning → succeeded/failed 기록. 진행 탭은 이 정보로 카드 진행도 표시.


11. 에러 처리 매트릭스

시나리오 동작
OAuth refresh 실패 발행 단계 failed + 텔레그램 "재인증 필요" + 구성 탭 빨간 배지
DALL·E timeout 폴백(그라데이션) + 텔레그램 "AI 폴백 사용됨"
Claude timeout 폴백(템플릿/휴리스틱) + 동일 표기
FFmpeg 실패 failed + 텔레그램 "수동 점검 필요" + task_id
YouTube quota 24시간 후 자동 재시도 1회 → 그래도 실패 시 failed
텔레그램 reply 의도 unclear 안내 메시지 + 같은 상태 유지
재생성 5회 초과 awaiting_manual + 텔레그램 안내
동일 트랙 파이프라인 중복 409 Conflict
트랙 삭제됨 파이프라인 보존, 재생성 불가, 진행 탭 "트랙 누락" 배지

12. 보안 / 비밀

  • OAuth refresh_token: SQLite에 평문(현재 패턴) — 향후 Fernet 암호화 또는 OS keystore 검토. 기본은 컨테이너 파일 권한 600 + DB 읽기 deny (이미 settings.json에 Read(**/*.db) 차단 추가됨)
  • OPENAI_API_KEY, ANTHROPIC_API_KEY, YOUTUBE_OAUTH_CLIENT_ID/SECRET: docker-compose env로 주입
  • 구성 탭은 인증 게이트 없음(개인 사이트 가정) — 향후 admin 게이트 필요시 personal 서비스의 /api/profile/auth 패턴 적용

13. 테스트 전략

13-1. 단위 테스트 (music-lab)

대상 테스트
state_machine 정상 전이 / 잘못된 전이 거부
feedback_handler approve → 다음 / reject → 동일 + feedback 저장 / 5회 초과 → awaiting_manual
cover.generate DALL·E mock 성공/timeout/오류 → 폴백
metadata.generate Claude mock + 템플릿 치환
review.run_4_axis 4축 점수 계산 + 가중평균 + verdict 임계값(60)
youtube_upload.upload google-api mock + 재시도 + quota 분기
OAuth code → refresh_token, refresh 만료 시 재인증 트리거

pytest + httpx_mock + freezegun. 기존 music-lab 테스트 컨벤션 준수.

13-2. 단위 테스트 (agent-office)

대상 테스트
classify_intent 화이트리스트 → LLM 미호출, 반려 단어 + 텍스트 → 분리, 모호 → LLM 호출 검증
_poll_pipelines state 변경 → 텔레그램 1회만(멱등)
reply 매칭 message_id로 정확한 pipeline_id 매칭

13-3. 통합 테스트

tests/test_pipeline_flow.py:

  • 전체 흐름 1회: track → pipeline → 모든 단계 mock 승인 → published
  • 반려 분기: cover에서 reject + feedback → 같은 단계 재생성 → 승인 → 다음 단계

13-4. 프론트엔드 테스트

  • SetupTab 폼 저장: 단순 단위 테스트 (API 인자 검증)
  • PipelineTab 카드 렌더링: 상태별 시각 — 빌드 + 수동 브라우저 확인
  • 폴링 로직: mock fetch + setInterval

기존 web-ui 패턴 (vitest 등 별도 러너 없음) 유지.

13-5. 수동 E2E 체크리스트 (출시 전)

  • OAuth 인증 → 구성 탭 채널명 표시
  • 트랙 → 파이프라인 시작 → 텔레그램 "커버 승인" 알림
  • "승인" 답장 → 다음 단계 진행
  • "썸네일 색 어둡게" 답장 → 재생성 → 알림 재도착
  • AI 최종 검토 4축 점수 표시
  • 발행 승인 → YouTube 업로드 (private) → URL 수신
  • 24시간 후 수익 추적 탭에 신규 영상 표시

14. 마이그레이션 / 환경

  • 신규 환경변수: OPENAI_API_KEY, YOUTUBE_OAUTH_CLIENT_ID, YOUTUBE_OAUTH_CLIENT_SECRET, YOUTUBE_OAUTH_REDIRECT_URI
  • music-lab Dockerfile: google-api-python-client, google-auth-oauthlib, openai 추가
  • 기존 music.db 마이그레이션: init_db()에 신규 테이블 5개 CREATE IF NOT EXISTS 추가
  • nginx 설정: /api/music/youtube/callback 외부 노출 필요 (OAuth redirect)

15. 산출물 / 후속

본 스펙은 다음 산출물을 가진다:

  • music-lab: pipeline 모듈, OAuth, 5개 테이블, 12개 엔드포인트
  • agent-office: youtube_publisher 에이전트, scheduler 폴링 잡, 자연어 분류기
  • web-ui: SetupTab, PipelineTab, Library 카드 트리거 버튼
  • 통합/단위 테스트, 수동 E2E 체크리스트

후속(이 스펙 외):

  • Shorts 전용 파이프라인 (60초 클립 추출 + 1080×1920)
  • 가사 자막 영상 (synced lyrics 영상화)
  • 멀티 채널 운영
  • 검토 임계값/가중치 학습 (실제 발행 후 성과 데이터 기반 자동 튜닝)