diff --git a/music-lab/app/db.py b/music-lab/app/db.py index 3023872..c8504db 100644 --- a/music-lab/app/db.py +++ b/music-lab/app/db.py @@ -83,6 +83,18 @@ def init_db() -> None: except sqlite3.OperationalError: pass + # Phase 1~3 신규 컬럼 마이그레이션 + for col, default in [ + ("cover_images", "'[]'"), + ("wav_url", "''"), + ("video_url", "''"), + ("stem_urls", "'{}'"), + ]: + try: + conn.execute(f"ALTER TABLE music_library ADD COLUMN {col} TEXT NOT NULL DEFAULT {default}") + except sqlite3.OperationalError: + pass + # ── music_tasks CRUD ────────────────────────────────────────────────────────── @@ -161,6 +173,10 @@ def _track_row_to_dict(r) -> Dict[str, Any]: "image_url": r["image_url"] if "image_url" in keys else "", "suno_id": r["suno_id"] if "suno_id" in keys else "", "file_hash": r["file_hash"] if "file_hash" in keys else "", + "cover_images": json.loads(r["cover_images"]) if "cover_images" in keys and r["cover_images"] else [], + "wav_url": r["wav_url"] if "wav_url" in keys else "", + "video_url": r["video_url"] if "video_url" in keys else "", + "stem_urls": json.loads(r["stem_urls"]) if "stem_urls" in keys and r["stem_urls"] else {}, "created_at": r["created_at"], } @@ -300,6 +316,26 @@ def update_lyrics(lyrics_id: int, data: Dict[str, Any]) -> Optional[Dict[str, An return _lyrics_row_to_dict(row) if row else None +def update_track_cover_images(track_id: int, images: list) -> None: + with _conn() as conn: + conn.execute("UPDATE music_library SET cover_images=? WHERE id=?", (json.dumps(images), track_id)) + + +def update_track_wav_url(track_id: int, wav_url: str) -> None: + with _conn() as conn: + conn.execute("UPDATE music_library SET wav_url=? WHERE id=?", (wav_url, track_id)) + + +def update_track_video_url(track_id: int, video_url: str) -> None: + with _conn() as conn: + conn.execute("UPDATE music_library SET video_url=? WHERE id=?", (video_url, track_id)) + + +def update_track_stem_urls(track_id: int, stems: dict) -> None: + with _conn() as conn: + conn.execute("UPDATE music_library SET stem_urls=? WHERE id=?", (json.dumps(stems), track_id)) + + def delete_lyrics(lyrics_id: int) -> bool: with _conn() as conn: row = conn.execute("SELECT id FROM saved_lyrics WHERE id = ?", (lyrics_id,)).fetchone() diff --git a/music-lab/app/main.py b/music-lab/app/main.py index d5287aa..47de044 100644 --- a/music-lab/app/main.py +++ b/music-lab/app/main.py @@ -15,6 +15,7 @@ from .db import ( from .local_provider import run_local_generation from .suno_provider import ( run_suno_generation, run_suno_extend, run_vocal_removal, + run_cover_image, generate_lyrics, get_credits, SUNO_API_KEY, SUNO_MODELS, ) @@ -102,6 +103,11 @@ class GenerateRequest(BaseModel): # Suno 전용 lyrics: str = "" # 커스텀 가사 ([Verse], [Chorus] 등) instrumental: bool = False # True면 보컬 없이 인스트루멘탈만 + # Phase 1 신규 + vocal_gender: Optional[str] = None # "m" | "f" + negative_tags: Optional[str] = None # 제외 스타일 + style_weight: Optional[float] = None # 0.0~1.0 + audio_weight: Optional[float] = None # 0.0~1.0 @app.post("/api/music/generate") @@ -402,6 +408,26 @@ def vocal_removal(req: VocalRemovalRequest, background_tasks: BackgroundTasks): return {"task_id": task_id, "provider": "suno"} +# ── 커버 이미지 생성 API ──────────────────────────────────────────────────── + +class CoverImageRequest(BaseModel): + suno_task_id: str # Suno 생성 task ID + track_id: Optional[int] = None # 라이브러리 트랙 ID (결과 저장용) + + +@app.post("/api/music/cover-image") +def cover_image(req: CoverImageRequest, background_tasks: BackgroundTasks): + """Suno 곡의 커버 이미지 2장 생성.""" + if not SUNO_API_KEY: + raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다") + + task_id = str(uuid.uuid4()) + params = req.model_dump() + create_task(task_id, params, provider="suno") + background_tasks.add_task(run_cover_image, task_id, params) + return {"task_id": task_id, "provider": "suno"} + + # ── 저장된 가사 CRUD API ──────────────────────────────────────────────────── class LyricsSave(BaseModel):