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>
This commit is contained in:
278
audio-studio-api/app/routers/music.py
Normal file
278
audio-studio-api/app/routers/music.py
Normal file
@ -0,0 +1,278 @@
|
||||
"""배경음악 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",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user