music-lab: 서비스 고도화 — duration 수정 + 모델/크레딧/연장/분리 API 추가
Phase 1A: - mutagen으로 MP3 실제 재생시간 추출 (sync + startup backfill) - update_track_duration() DB 헬퍼 추가 Phase 2: - GET /api/music/models — Suno 모델 목록 (V4~V5) - GET /api/music/credits — 잔여 크레딧 조회 - POST /api/music/extend — 곡 연장 (continueAt 지점부터) - POST /api/music/vocal-removal — 보컬/인스트루멘탈 분리 - GenerateRequest에 model 필드 추가 (하드코딩 V4 제거) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,14 @@ from .db import (
|
||||
init_db,
|
||||
create_task, get_task,
|
||||
get_all_tracks, add_track, delete_track, get_track_file_path, get_track_by_task_id,
|
||||
update_track_duration,
|
||||
)
|
||||
from .local_provider import run_local_generation
|
||||
from .suno_provider import run_suno_generation, generate_lyrics, SUNO_API_KEY
|
||||
from .suno_provider import (
|
||||
run_suno_generation, run_suno_extend, run_vocal_removal,
|
||||
generate_lyrics, get_credits,
|
||||
SUNO_API_KEY, SUNO_MODELS,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -27,10 +32,30 @@ app.add_middleware(
|
||||
MUSIC_DATA_DIR = "/app/data"
|
||||
|
||||
|
||||
def _get_mp3_duration(file_path: str) -> Optional[int]:
|
||||
"""MP3 파일에서 실제 재생 시간(초) 추출."""
|
||||
try:
|
||||
from mutagen.mp3 import MP3
|
||||
audio = MP3(file_path)
|
||||
return int(audio.info.length)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _backfill_durations():
|
||||
"""duration_sec이 없는 기존 트랙에 MP3 메타데이터에서 길이 채우기."""
|
||||
for t in get_all_tracks():
|
||||
if t["duration_sec"] is None and t.get("file_path"):
|
||||
dur = _get_mp3_duration(t["file_path"])
|
||||
if dur:
|
||||
update_track_duration(t["id"], dur)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
init_db()
|
||||
os.makedirs(MUSIC_DATA_DIR, exist_ok=True)
|
||||
_backfill_durations()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
@@ -63,6 +88,7 @@ def get_providers():
|
||||
|
||||
class GenerateRequest(BaseModel):
|
||||
provider: str = "suno" # "suno" | "local"
|
||||
model: str = "V4" # Suno 모델 (V4, V4_5, V5 등)
|
||||
title: str = ""
|
||||
genre: str = ""
|
||||
moods: List[str] = []
|
||||
@@ -205,15 +231,17 @@ def _sync_library_with_disk():
|
||||
if fname not in disk_files:
|
||||
delete_track(t["id"])
|
||||
|
||||
# 디스크에는 있지만 DB에 없는 → 추가
|
||||
# 디스크에는 있지만 DB에 없는 → 추가 (duration 자동 추출)
|
||||
for f in disk_files:
|
||||
if f not in db_filenames:
|
||||
file_path = os.path.join(MUSIC_DATA_DIR, f)
|
||||
title = os.path.splitext(f)[0].replace("-", " ").replace("_", " ")
|
||||
add_track({
|
||||
"title": title,
|
||||
"audio_url": f"{media_base}/{f}",
|
||||
"file_path": os.path.join(MUSIC_DATA_DIR, f),
|
||||
"file_path": file_path,
|
||||
"provider": "suno",
|
||||
"duration_sec": _get_mp3_duration(file_path),
|
||||
})
|
||||
|
||||
|
||||
@@ -239,3 +267,68 @@ def remove_from_library(track_id: int):
|
||||
pass
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── 모델 목록 API ────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/music/models")
|
||||
def get_models():
|
||||
"""사용 가능한 Suno AI 모델 목록."""
|
||||
return {"models": SUNO_MODELS}
|
||||
|
||||
|
||||
# ── 크레딧 조회 API ──────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/music/credits")
|
||||
def check_credits():
|
||||
"""Suno 잔여 크레딧 조회."""
|
||||
if not SUNO_API_KEY:
|
||||
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
|
||||
result = get_credits()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=502, detail="크레딧 조회 실패")
|
||||
return result
|
||||
|
||||
|
||||
# ── 곡 연장 API ──────────────────────────────────────────────────────────────
|
||||
|
||||
class ExtendRequest(BaseModel):
|
||||
suno_id: str # 원본 Suno 곡 ID
|
||||
continue_at: int = 0 # 연장 시작 지점 (초)
|
||||
prompt: str = "" # 추가 가사/프롬프트
|
||||
style: str = "" # 스타일 오버라이드
|
||||
title: str = ""
|
||||
model: str = "V4"
|
||||
|
||||
|
||||
@app.post("/api/music/extend")
|
||||
def extend_music(req: ExtendRequest, background_tasks: BackgroundTasks):
|
||||
"""기존 곡을 특정 지점부터 연장 (Suno Extend API)."""
|
||||
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_suno_extend, task_id, params)
|
||||
return {"task_id": task_id, "provider": "suno"}
|
||||
|
||||
|
||||
# ── 보컬 분리 API ────────────────────────────────────────────────────────────
|
||||
|
||||
class VocalRemovalRequest(BaseModel):
|
||||
suno_id: str # Suno 곡 ID
|
||||
title: str = "" # 원본 트랙 제목
|
||||
|
||||
|
||||
@app.post("/api/music/vocal-removal")
|
||||
def vocal_removal(req: VocalRemovalRequest, background_tasks: BackgroundTasks):
|
||||
"""트랙에서 보컬과 인스트루멘탈을 분리 (Suno Vocal Removal API)."""
|
||||
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_vocal_removal, task_id, params)
|
||||
return {"task_id": task_id, "provider": "suno"}
|
||||
|
||||
Reference in New Issue
Block a user