diff --git a/docs/superpowers/specs/2026-04-08-music-lab-suno-enhancement-design.md b/docs/superpowers/specs/2026-04-08-music-lab-suno-enhancement-design.md new file mode 100644 index 0000000..02cd712 --- /dev/null +++ b/docs/superpowers/specs/2026-04-08-music-lab-suno-enhancement-design.md @@ -0,0 +1,398 @@ +# Music Lab Suno API 전체 기능 확장 설계 + +> 작성일: 2026-04-08 +> 범위: 백엔드 (web-backend/music-lab) + 프론트엔드 (web-ui/src/pages/music) + +--- + +## 1. 목표 + +Suno API의 미사용 기능을 전부 활용하여 music-lab을 완전한 AI 음악 프로덕션 스튜디오로 업그레이드한다. 실용도 기준 3단계로 점진 확장. + +## 2. 단계별 기능 목록 + +### Phase 1: 핵심 생성 강화 + +| # | 기능 | 설명 | +|---|------|------| +| 1-1 | 크레딧 잔액 상시 표시 | 헤더에 실시간 크레딧 배지, 10 이하 경고 | +| 1-2 | 보컬 성별 선택 | Male / Female / Auto 3버튼 | +| 1-3 | negativeTags | 제외 스타일 텍스트 + 프리셋 칩 | +| 1-4 | V5_5 모델 추가 | SUNO_MODELS 딕셔너리에 추가 | +| 1-5 | styleWeight / audioWeight | 0~1.0 슬라이더 2개 | +| 1-6 | 커버 이미지 생성 | 라이브러리 카드에서 앨범아트 2장 생성 | + +### Phase 2: 후처리 파워업 + +| # | 기능 | 설명 | +|---|------|------| +| 2-1 | WAV 고음질 변환 | MP3→WAV 변환 다운로드 | +| 2-2 | 12스템 분리 | 드럼/베이스/기타 등 12개 개별 추출 | +| 2-3 | 타임스탬프 가사 | 가라오케 스타일 싱크 재생 | +| 2-4 | 스타일 부스트 | AI로 최적 스타일 프롬프트 자동 생성 | + +### Phase 3: 고급 크리에이티브 + +| # | 기능 | 설명 | +|---|------|------| +| 3-1 | 오디오 업로드 + 커버 | 외부 음원을 Suno 스타일로 리메이크 | +| 3-2 | 오디오 업로드 + 확장 | 외부 음원 이어서 만들기 | +| 3-3 | 보컬 추가 | 인스트루멘탈에 AI 보컬 입히기 | +| 3-4 | 인스트루멘탈 추가 | 보컬에 AI 반주 입히기 | +| 3-5 | 뮤직비디오 생성 | MP4 자동 생성 | + +--- + +## 3. 백엔드 API 설계 + +### 3.1 기존 엔드포인트 수정 + +#### GenerateRequest 스키마 확장 (main.py) + +```python +class GenerateRequest(BaseModel): + # 기존 필드 유지 + provider: str = "suno" + model: str = "V4" + title: str = "" + genre: str = "" + moods: list[str] = [] + instruments: list[str] = [] + duration_sec: int | None = None + bpm: int | None = None + key: str = "" + scale: str = "" + prompt: str = "" + lyrics: str = "" + instrumental: bool = False + + # Phase 1 추가 + vocal_gender: str | None = None # "m" | "f" | None(auto) + negative_tags: str | None = None # 제외 스타일 + style_weight: float | None = None # 0.0~1.0 + audio_weight: float | None = None # 0.0~1.0 +``` + +#### SUNO_MODELS 확장 (suno_provider.py) + +```python +SUNO_MODELS = { + "V4": {"name": "V4", "max_duration": 240}, + "V4_5": {"name": "V4.5", "max_duration": 480}, + "V4_5PLUS": {"name": "V4.5+", "max_duration": 480}, + "V4_5ALL": {"name": "V4.5 All","max_duration": 480}, + "V5": {"name": "V5", "max_duration": 480}, + "V5_5": {"name": "V5.5", "max_duration": 480}, # 추가 +} +``` + +#### _build_suno_payload 확장 + +새 파라미터를 Suno API 페이로드에 매핑: +- `vocal_gender` → `vocalGender` +- `negative_tags` → `negativeTags` +- `style_weight` → `styleWeight` +- `audio_weight` → `audioWeight` + +None이 아닌 경우에만 페이로드에 포함. + +### 3.2 신규 엔드포인트 + +#### Phase 1 + +``` +POST /api/music/cover-image +Request: { "task_id": str, "suno_id": str } +Response: { "task_id": str } → 폴링 → { "images": [url1, url2] } +``` + +#### Phase 2 + +``` +POST /api/music/wav +Request: { "task_id": str, "suno_id": str } +Response: { "task_id": str } → 폴링 → { "wav_url": str } + +POST /api/music/stem-split +Request: { "task_id": str, "suno_id": str } +Response: { "task_id": str } → 폴링 → { "stems": { vocal: url, drums: url, ... } } + +GET /api/music/timestamped-lyrics?task_id=...&suno_id=... +Response: { "aligned_words": [...], "waveform_data": [...] } + +POST /api/music/style-boost +Request: { "content": str } +Response: { "result": str, "credits_consumed": float } +``` + +#### Phase 3 + +``` +POST /api/music/upload-cover +Request: { "upload_url": str, "model": str, "custom_mode": bool, + "instrumental": bool, "prompt"?: str, "style"?: str, "title"?: str, + "vocal_gender"?: str, "negative_tags"?: str, + "style_weight"?: float, "audio_weight"?: float } +Response: { "task_id": str } + +POST /api/music/upload-extend +Request: { "upload_url": str, "model": str, "continue_at"?: float, + "default_param_flag": bool, "prompt"?: str, "style"?: str, "title"?: str, + "vocal_gender"?: str, "negative_tags"?: str } +Response: { "task_id": str } + +POST /api/music/add-vocals +Request: { "upload_url": str, "prompt": str, "title": str, "style": str, + "negative_tags": str, "vocal_gender"?: str, "model"?: str, + "style_weight"?: float, "audio_weight"?: float } +Response: { "task_id": str } + +POST /api/music/add-instrumental +Request: { "upload_url": str, "title": str, "tags": str, "negative_tags": str, + "vocal_gender"?: str, "model"?: str, + "style_weight"?: float, "audio_weight"?: float } +Response: { "task_id": str } + +POST /api/music/video +Request: { "task_id": str, "suno_id": str, "author"?: str, "domain_name"?: str } +Response: { "task_id": str } → 폴링 → { "video_url": str } +``` + +### 3.3 suno_provider.py 리팩토링 + +**공통 폴링 헬퍼 추출:** + +```python +def _poll_suno_task( + record_info_url: str, + task_id: str, + max_attempts: int = 40, + interval: int = 8, + success_extractor: Callable[[dict], Any] = None +) -> dict: + """ + 범용 Suno 작업 폴링. + record_info_url: 예) "/api/v1/generate/record-info" + success_extractor: SUCCESS 상태일 때 결과 추출 함수 + """ +``` + +기존 `run_suno_generation`, `run_suno_extend`, `run_vocal_removal`도 이 헬퍼를 사용하도록 리팩토링. + +**신규 함수 목록:** + +| 함수 | Phase | Suno 엔드포인트 | 비동기 | +|------|-------|----------------|--------| +| `run_cover_image` | 1 | `POST /api/v1/suno/cover/generate` | 폴링 | +| `run_wav_convert` | 2 | `POST /api/v1/wav/generate` | 폴링 | +| `run_stem_split` | 2 | `POST /api/v1/vocal-removal/generate` (type=split_stem) | 폴링 | +| `get_timestamped_lyrics` | 2 | `POST /api/v1/generate/get-timestamped-lyrics` | 동기 | +| `generate_style_boost` | 2 | `POST /api/v1/style/generate` | 동기 | +| `run_upload_cover` | 3 | `POST /api/v1/generate/upload-cover` | 폴링 | +| `run_upload_extend` | 3 | `POST /api/v1/generate/upload-extend` | 폴링 | +| `run_add_vocals` | 3 | `POST /api/v1/generate/add-vocals` | 폴링 | +| `run_add_instrumental` | 3 | `POST /api/v1/generate/add-instrumental` | 폴링 | +| `run_video_generate` | 3 | `POST /api/v1/mp4/generate` | 폴링 | + +### 3.4 DB 스키마 변경 + +**music_library 테이블 컬럼 추가 (ALTER TABLE 마이그레이션):** + +```sql +ALTER TABLE music_library ADD COLUMN cover_images TEXT NOT NULL DEFAULT '[]'; +ALTER TABLE music_library ADD COLUMN wav_url TEXT NOT NULL DEFAULT ''; +ALTER TABLE music_library ADD COLUMN video_url TEXT NOT NULL DEFAULT ''; +ALTER TABLE music_library ADD COLUMN stem_urls TEXT NOT NULL DEFAULT '{}'; +``` + +**db.py 함수 추가:** + +```python +def update_track_cover_images(track_id: int, images: list[str]) +def update_track_wav_url(track_id: int, wav_url: str) +def update_track_video_url(track_id: int, video_url: str) +def update_track_stem_urls(track_id: int, stems: dict) +``` + +--- + +## 4. 프론트엔드 UI/UX 설계 + +### 4.1 파일 구조 (컴포넌트 분할) + +``` +web-ui/src/pages/music/ +├── MusicStudio.jsx -- 메인 (탭 라우팅 + 공유 상태) +├── MusicStudio.css -- 전체 스타일 +├── components/ +│ ├── CreateTab.jsx -- 생성 폼 (6단계 + Phase 1 확장) +│ ├── LyricsTab.jsx -- 가사 관리 +│ ├── LibraryTab.jsx -- 라이브러리 + 카드 +│ ├── RemixTab.jsx -- Phase 3: 업로드/리믹스 +│ ├── AudioPlayer.jsx -- 오디오 플레이어 +│ ├── LibraryCard.jsx -- 트랙 카드 + 액션 메뉴 +│ ├── StemModal.jsx -- 12스템 결과 모달 +│ ├── CoverArtModal.jsx -- 커버 이미지 선택 모달 +│ ├── SyncedLyricsPlayer.jsx -- 타임스탬프 가사 오버레이 +│ └── CreditsBadge.jsx -- 크레딧 잔액 배지 +``` + +### 4.2 Phase 1 UI 변경 + +#### 크레딧 배지 (CreditsBadge) +- 위치: 헤더 우측 상단, 탭 옆 +- 표시: `⚡ 127 credits` +- 10 이하: 빨간색 + pulse 애니메이션 +- 갱신 시점: 페이지 로드, 생성 완료 후, 30초 자동 갱신 + +#### Create 탭 Step 4 확장 + +**Vocal Gender (Suno 전용):** +- 3버튼 토글: `♂ Male` | `♀ Female` | `Auto` +- 기본값: Auto +- 스타일: `.ms-gender-toggle` (기존 duration-rail과 유사) + +**Negative Tags:** +- 텍스트 입력 필드 + 프리셋 칩 +- 프리셋: screaming, autotune, distortion, whisper, falsetto, rap +- 칩 클릭 시 텍스트에 추가/제거 +- 스타일: `.ms-negative-tags` (기존 mood-rack과 유사) + +**Style Weight / Audio Weight:** +- range 슬라이더 2개 (기존 BPM 슬라이더와 동일 스타일) +- 레이블: "Prompt ↔ Style Balance" / "Original ↔ AI Balance" +- 0~100 표시, API 전송 시 0.0~1.0 변환 +- 기본값: 미설정 (슬라이더 중앙, 값 전송 안 함) + +#### Library 카드 액션 메뉴 확장 + +기존 5개 버튼 → 6개 (Cover Art 추가) +4개 초과 시 `•••` 더보기 드롭다운 메뉴로 분기: +- 기본 노출: Play, Download, Delete +- 더보기: Extend, Vocal Split, Cover Art (+ Phase 2/3 추가분) + +#### CoverArtModal +- 2장 이미지 좌우 비교 표시 +- 각 이미지 아래 "이 이미지 사용" 버튼 +- 선택 시 라이브러리 카드 썸네일 업데이트 + +### 4.3 Phase 2 UI 변경 + +#### Library 카드 더보기 메뉴 추가 +- WAV 다운로드 +- Stem Split (12스템) +- Synced Lyrics +- Style Boost (Create 탭 프롬프트로 전달) + +#### StemModal +- 3×4 그리드 카드 레이아웃 +- 각 스템: 이름 아이콘 + 미니 재생 버튼 + 다운로드 버튼 +- 12개 스템: vocal, backing_vocals, drums, bass, guitar, keyboard, strings, brass, woodwinds, percussion, synth, fx +- 스타일: 기존 라이브러리 카드의 축소 버전 + +#### SyncedLyricsPlayer +- AudioPlayer 교체/오버레이 모드 +- 재생 중 현재 단어를 accent 컬러로 하이라이트 +- 하단에 waveformData 기반 파형 바 +- 닫기 버튼으로 일반 플레이어 복귀 + +#### Style Boost 버튼 +- Create 탭 장르 선택 영역에 `✨ Style Boost` 버튼 +- 클릭 시: 현재 genre + moods 조합 → API 호출 → 결과를 프롬프트에 삽입 +- 로딩 중 버튼 스피너 + +### 4.4 Phase 3 UI 변경 + +#### Remix 탭 (신규 4번째 탭) +- 탭 레이블: `REMIX` +- 상단: 오디오 URL 입력 필드 (또는 라이브러리에서 선택) +- 4개 액션 카드 그리드 (2×2): + - **AI Cover**: 아이콘 + 설명 + 파라미터 폼 (펼침) + - **Extend**: 아이콘 + 설명 + continue_at 입력 + - **Add Vocals**: 아이콘 + 설명 + prompt/style 입력 + - **Add Instrumental**: 아이콘 + 설명 + tags 입력 +- 선택한 카드만 펼쳐서 세부 옵션 표시 +- 하단: 뮤직비디오 생성 버튼 (라이브러리에서 선택한 곡 대상) + +### 4.5 디자인 토큰 추가 + +```css +/* Phase 1 추가 토큰 */ +--ms-danger: #e74c3c; /* 크레딧 경고 빨간색 */ +--ms-male: #4a9eff; /* 남성 보컬 파란색 */ +--ms-female: #ff6b9d; /* 여성 보컬 분홍색 */ +``` + +--- + +## 5. api.js 추가 함수 + +```javascript +// Phase 1 +export const generateCoverImage = (payload) => apiPost('/api/music/cover-image', payload); + +// Phase 2 +export const convertToWav = (payload) => apiPost('/api/music/wav', payload); +export const splitStems = (payload) => apiPost('/api/music/stem-split', payload); +export const getTimestampedLyrics = (taskId, sunoId) => + apiGet(`/api/music/timestamped-lyrics?task_id=${taskId}&suno_id=${sunoId}`); +export const generateStyleBoost = (content) => apiPost('/api/music/style-boost', { content }); + +// Phase 3 +export const uploadAndCover = (payload) => apiPost('/api/music/upload-cover', payload); +export const uploadAndExtend = (payload) => apiPost('/api/music/upload-extend', payload); +export const addVocals = (payload) => apiPost('/api/music/add-vocals', payload); +export const addInstrumental = (payload) => apiPost('/api/music/add-instrumental', payload); +export const generateVideo = (payload) => apiPost('/api/music/video', payload); +``` + +--- + +## 6. 폴링 패턴 + +모든 비동기 작업은 기존 음악 생성과 동일한 폴링 패턴: + +1. POST 요청 → `{ task_id }` 반환 +2. 프론트: 3초 간격 `GET /api/music/status/{task_id}` 폴링 +3. 백엔드: BackgroundTask에서 Suno 폴링 → music_tasks 상태 업데이트 +4. `status: succeeded` → 결과 반환 + 라이브러리 자동 갱신 + +동기 API (타임스탬프 가사, 스타일 부스트)는 즉시 응답. + +--- + +## 7. 구현 순서 + +### Phase 1 (핵심 생성 강화) +1. 백엔드: SUNO_MODELS에 V5_5 추가 + 공통 폴링 헬퍼 추출 +2. 백엔드: GenerateRequest 스키마 확장 + _build_suno_payload 매핑 +3. 백엔드: 커버 이미지 생성 엔드포인트 + suno_provider 함수 +4. 백엔드: DB 마이그레이션 (cover_images, wav_url, video_url, stem_urls) +5. 프론트: 컴포넌트 분할 (MusicStudio → CreateTab, LyricsTab, LibraryTab, etc.) +6. 프론트: CreditsBadge 구현 +7. 프론트: Create 탭 Step 4 확장 (vocal gender, negative tags, weight 슬라이더) +8. 프론트: LibraryCard 더보기 메뉴 + CoverArtModal +9. 프론트: api.js 함수 추가 + +### Phase 2 (후처리 파워업) +10. 백엔드: WAV/스템/타임스탬프/스타일부스트 엔드포인트 +11. 프론트: StemModal + SyncedLyricsPlayer + Style Boost 버튼 +12. 프론트: Library 카드 Phase 2 액션 추가 + +### Phase 3 (고급 크리에이티브) +13. 백엔드: upload-cover/upload-extend/add-vocals/add-instrumental/video 엔드포인트 +14. 프론트: RemixTab 구현 +15. 프론트: Library 카드 Phase 3 액션 (Video) + +--- + +## 8. 제약사항 및 주의점 + +- **Suno 파일 보관**: 14~15일 후 자동 삭제 → 로컬 다운로드 필수 (기존 패턴 유지) +- **동시 요청 제한**: 10초당 20건 → 배치 작업 시 rate limiting 고려 +- **12스템 분리 비용**: 50크레딧 → UI에 비용 경고 표시 +- **WAV 중복 변환**: 409 에러 → 이미 변환된 경우 기존 URL 반환 +- **뮤직비디오**: taskId + audioId 필요 → 라이브러리에 task_id 저장 필수 +- **V5_5 모델**: 커스텀 모델 → 문서상 제한사항 추가 확인 필요 +- **크레딧 조회 엔드포인트**: 기존 `/api/v1/get-credits` vs 문서 `/api/v1/generate/credit` → 두 엔드포인트 폴백 시도 +- **upload 계열 API**: upload_url은 외부 접근 가능한 URL이어야 함 → 로컬 파일은 NAS nginx URL로 변환