feat(music-lab): Phase 1 DB 마이그레이션 + GenerateRequest 확장 + 커버이미지 엔드포인트
This commit is contained in:
@@ -83,6 +83,18 @@ def init_db() -> None:
|
|||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass
|
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 ──────────────────────────────────────────────────────────
|
# ── 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 "",
|
"image_url": r["image_url"] if "image_url" in keys else "",
|
||||||
"suno_id": r["suno_id"] if "suno_id" 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 "",
|
"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"],
|
"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
|
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:
|
def delete_lyrics(lyrics_id: int) -> bool:
|
||||||
with _conn() as conn:
|
with _conn() as conn:
|
||||||
row = conn.execute("SELECT id FROM saved_lyrics WHERE id = ?", (lyrics_id,)).fetchone()
|
row = conn.execute("SELECT id FROM saved_lyrics WHERE id = ?", (lyrics_id,)).fetchone()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .db import (
|
|||||||
from .local_provider import run_local_generation
|
from .local_provider import run_local_generation
|
||||||
from .suno_provider import (
|
from .suno_provider import (
|
||||||
run_suno_generation, run_suno_extend, run_vocal_removal,
|
run_suno_generation, run_suno_extend, run_vocal_removal,
|
||||||
|
run_cover_image,
|
||||||
generate_lyrics, get_credits,
|
generate_lyrics, get_credits,
|
||||||
SUNO_API_KEY, SUNO_MODELS,
|
SUNO_API_KEY, SUNO_MODELS,
|
||||||
)
|
)
|
||||||
@@ -102,6 +103,11 @@ class GenerateRequest(BaseModel):
|
|||||||
# Suno 전용
|
# Suno 전용
|
||||||
lyrics: str = "" # 커스텀 가사 ([Verse], [Chorus] 등)
|
lyrics: str = "" # 커스텀 가사 ([Verse], [Chorus] 등)
|
||||||
instrumental: bool = False # True면 보컬 없이 인스트루멘탈만
|
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")
|
@app.post("/api/music/generate")
|
||||||
@@ -402,6 +408,26 @@ def vocal_removal(req: VocalRemovalRequest, background_tasks: BackgroundTasks):
|
|||||||
return {"task_id": task_id, "provider": "suno"}
|
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 ────────────────────────────────────────────────────
|
# ── 저장된 가사 CRUD API ────────────────────────────────────────────────────
|
||||||
|
|
||||||
class LyricsSave(BaseModel):
|
class LyricsSave(BaseModel):
|
||||||
|
|||||||
Reference in New Issue
Block a user