"""배경음악 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", ], }, ] }