Files
drama-studio/audio-studio-api/app/routers/music.py
jungwoo choi cc547372c0 feat: Drama Studio 프로젝트 초기 구조 설정
- FastAPI 백엔드 (audio-studio-api)
- Next.js 프론트엔드 (audio-studio-ui)
- Qwen3-TTS 엔진 (audio-studio-tts)
- MusicGen 서비스 (audio-studio-musicgen)
- Docker Compose 개발/운영 환경

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:39:38 +09:00

279 lines
8.6 KiB
Python

"""배경음악 API 라우터
MusicGen 연동 및 외부 음악 소스
"""
import os
import uuid
from datetime import datetime
from typing import Optional, List
from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File, Form
from fastapi.responses import Response
from pydantic import BaseModel, Field
import httpx
from app.database import Database, get_db
router = APIRouter(prefix="/api/v1/music", tags=["music"])
MUSICGEN_URL = os.getenv("MUSICGEN_URL", "http://localhost:8002")
# ========================================
# Pydantic 모델
# ========================================
class MusicGenerateRequest(BaseModel):
"""음악 생성 요청"""
prompt: str = Field(..., min_length=5, max_length=500, description="음악 설명")
duration: int = Field(default=30, ge=5, le=30, description="생성 길이 (초)")
save_to_library: bool = Field(default=True, description="라이브러리에 저장")
class MusicTrackResponse(BaseModel):
"""음악 트랙 응답"""
id: str
name: str
description: Optional[str] = None
source: str # musicgen | pixabay | uploaded
generation_prompt: Optional[str] = None
duration_seconds: float
genre: Optional[str] = None
mood: List[str] = []
license: str = ""
created_at: datetime
class MusicListResponse(BaseModel):
"""음악 목록 응답"""
tracks: List[MusicTrackResponse]
total: int
page: int
page_size: int
# ========================================
# API 엔드포인트
# ========================================
@router.post("/generate")
async def generate_music(
request: MusicGenerateRequest,
db: Database = Depends(get_db),
):
"""AI로 배경음악 생성
MusicGen을 사용하여 텍스트 프롬프트 기반 음악 생성
"""
try:
# MusicGen 서비스 호출
async with httpx.AsyncClient(timeout=120.0) as client:
response = await client.post(
f"{MUSICGEN_URL}/generate",
json={
"prompt": request.prompt,
"duration": request.duration,
},
)
response.raise_for_status()
audio_bytes = response.content
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="Music generation timed out")
except httpx.HTTPStatusError as e:
raise HTTPException(status_code=502, detail=f"MusicGen error: {e.response.text}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Music generation failed: {str(e)}")
# 라이브러리에 저장
if request.save_to_library:
track_id = f"music_{uuid.uuid4().hex[:12]}"
now = datetime.utcnow()
# GridFS에 오디오 저장
audio_file_id = await db.save_audio(
audio_bytes,
f"{track_id}.wav",
metadata={
"type": "generated_music",
"prompt": request.prompt,
},
)
# DB에 트랙 정보 저장
track_doc = {
"track_id": track_id,
"name": f"Generated: {request.prompt[:30]}...",
"description": request.prompt,
"source": "musicgen",
"generation_prompt": request.prompt,
"audio_file_id": audio_file_id,
"duration_seconds": request.duration,
"format": "wav",
"genre": None,
"mood": [],
"license": "CC-BY-NC", # MusicGen 모델 라이센스
"created_at": now,
}
await db.music_tracks.insert_one(track_doc)
return Response(
content=audio_bytes,
media_type="audio/wav",
headers={
"X-Duration": str(request.duration),
"Content-Disposition": 'attachment; filename="generated_music.wav"',
},
)
@router.get("/library", response_model=MusicListResponse)
async def list_music_library(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
source: Optional[str] = Query(None, description="소스 필터 (musicgen, pixabay, uploaded)"),
genre: Optional[str] = Query(None, description="장르 필터"),
db: Database = Depends(get_db),
):
"""음악 라이브러리 목록 조회"""
query = {}
if source:
query["source"] = source
if genre:
query["genre"] = genre
total = await db.music_tracks.count_documents(query)
skip = (page - 1) * page_size
cursor = db.music_tracks.find(query).sort("created_at", -1).skip(skip).limit(page_size)
tracks = []
async for doc in cursor:
tracks.append(MusicTrackResponse(
id=doc.get("track_id", str(doc["_id"])),
name=doc["name"],
description=doc.get("description"),
source=doc.get("source", "unknown"),
generation_prompt=doc.get("generation_prompt"),
duration_seconds=doc.get("duration_seconds", 0),
genre=doc.get("genre"),
mood=doc.get("mood", []),
license=doc.get("license", ""),
created_at=doc.get("created_at", datetime.utcnow()),
))
return MusicListResponse(
tracks=tracks,
total=total,
page=page,
page_size=page_size,
)
@router.get("/{track_id}")
async def get_music_track(
track_id: str,
db: Database = Depends(get_db),
):
"""음악 트랙 상세 정보"""
doc = await db.music_tracks.find_one({"track_id": track_id})
if not doc:
raise HTTPException(status_code=404, detail="Track not found")
return MusicTrackResponse(
id=doc.get("track_id", str(doc["_id"])),
name=doc["name"],
description=doc.get("description"),
source=doc.get("source", "unknown"),
generation_prompt=doc.get("generation_prompt"),
duration_seconds=doc.get("duration_seconds", 0),
genre=doc.get("genre"),
mood=doc.get("mood", []),
license=doc.get("license", ""),
created_at=doc.get("created_at", datetime.utcnow()),
)
@router.get("/{track_id}/audio")
async def get_music_audio(
track_id: str,
db: Database = Depends(get_db),
):
"""음악 오디오 스트리밍"""
doc = await db.music_tracks.find_one({"track_id": track_id})
if not doc:
raise HTTPException(status_code=404, detail="Track not found")
audio_file_id = doc.get("audio_file_id")
if not audio_file_id:
raise HTTPException(status_code=404, detail="Audio file not found")
audio_bytes = await db.get_audio(audio_file_id)
return Response(
content=audio_bytes,
media_type="audio/wav",
headers={"Content-Disposition": f'inline; filename="{track_id}.wav"'},
)
@router.delete("/{track_id}")
async def delete_music_track(
track_id: str,
db: Database = Depends(get_db),
):
"""음악 트랙 삭제"""
doc = await db.music_tracks.find_one({"track_id": track_id})
if not doc:
raise HTTPException(status_code=404, detail="Track not found")
# 오디오 파일 삭제
if doc.get("audio_file_id"):
await db.delete_audio(doc["audio_file_id"])
# 문서 삭제
await db.music_tracks.delete_one({"track_id": track_id})
return {"status": "deleted", "track_id": track_id}
@router.get("/prompts/examples")
async def get_example_prompts():
"""예시 프롬프트 목록"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(f"{MUSICGEN_URL}/prompts")
response.raise_for_status()
return response.json()
except Exception:
# MusicGen 서비스 연결 실패 시 기본 프롬프트 반환
return {
"examples": [
{
"category": "Ambient",
"prompts": [
"calm piano music, peaceful, ambient",
"lo-fi hip hop beats, relaxing, study music",
"meditation music, calm, zen",
],
},
{
"category": "Electronic",
"prompts": [
"upbeat electronic dance music",
"retro synthwave 80s style",
"chill electronic ambient",
],
},
{
"category": "Cinematic",
"prompts": [
"epic orchestral cinematic music",
"tense suspenseful thriller music",
"cheerful happy video game background",
],
},
]
}