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

520 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.py``app/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. 자연어 의도 분류
```python
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):
```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:
```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`)
```sql
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|all`) |
| 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_jobs``running → 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 영상화)
- 멀티 채널 운영
- 검토 임계값/가중치 학습 (실제 발행 후 성과 데이터 기반 자동 튜닝)