Compare commits
2 Commits
657ffdc55f
...
e03d074222
| Author | SHA1 | Date | |
|---|---|---|---|
| e03d074222 | |||
| 2eeb98a723 |
3325
docs/superpowers/plans/2026-05-07-music-youtube-pipeline.md
Normal file
3325
docs/superpowers/plans/2026-05-07-music-youtube-pipeline.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,519 @@
|
|||||||
|
# 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 영상화)
|
||||||
|
- 멀티 채널 운영
|
||||||
|
- 검토 임계값/가중치 학습 (실제 발행 후 성과 데이터 기반 자동 튜닝)
|
||||||
Reference in New Issue
Block a user