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:
165
audio-studio-api/app/services/freesound_client.py
Normal file
165
audio-studio-api/app/services/freesound_client.py
Normal file
@ -0,0 +1,165 @@
|
||||
"""Freesound API 클라이언트
|
||||
|
||||
효과음 검색 및 다운로드
|
||||
https://freesound.org/docs/api/
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FreesoundClient:
|
||||
"""Freesound API 클라이언트"""
|
||||
|
||||
BASE_URL = "https://freesound.org/apiv2"
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv("FREESOUND_API_KEY", "")
|
||||
self.timeout = httpx.Timeout(30.0, connect=10.0)
|
||||
|
||||
def _get_headers(self) -> dict:
|
||||
"""인증 헤더 반환"""
|
||||
return {"Authorization": f"Token {self.api_key}"}
|
||||
|
||||
async def search(
|
||||
self,
|
||||
query: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
filter_fields: Optional[str] = None,
|
||||
sort: str = "score",
|
||||
min_duration: Optional[float] = None,
|
||||
max_duration: Optional[float] = None,
|
||||
) -> Dict:
|
||||
"""효과음 검색
|
||||
|
||||
Args:
|
||||
query: 검색어
|
||||
page: 페이지 번호
|
||||
page_size: 페이지당 결과 수
|
||||
filter_fields: 필터 (예: "duration:[1 TO 5]")
|
||||
sort: 정렬 (score, duration_asc, duration_desc, created_desc 등)
|
||||
min_duration: 최소 길이 (초)
|
||||
max_duration: 최대 길이 (초)
|
||||
|
||||
Returns:
|
||||
검색 결과 딕셔너리
|
||||
"""
|
||||
if not self.api_key:
|
||||
logger.warning("Freesound API 키가 설정되지 않음")
|
||||
return {"count": 0, "results": []}
|
||||
|
||||
# 필터 구성
|
||||
filters = []
|
||||
if min_duration is not None or max_duration is not None:
|
||||
min_d = min_duration if min_duration is not None else 0
|
||||
max_d = max_duration if max_duration is not None else "*"
|
||||
filters.append(f"duration:[{min_d} TO {max_d}]")
|
||||
|
||||
if filter_fields:
|
||||
filters.append(filter_fields)
|
||||
|
||||
params = {
|
||||
"query": query,
|
||||
"page": page,
|
||||
"page_size": min(page_size, 150), # Freesound 최대 150
|
||||
"sort": sort,
|
||||
"fields": "id,name,description,duration,tags,previews,license,username",
|
||||
}
|
||||
|
||||
if filters:
|
||||
params["filter"] = " ".join(filters)
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.BASE_URL}/search/text/",
|
||||
params=params,
|
||||
headers=self._get_headers(),
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# 결과 정리
|
||||
results = []
|
||||
for sound in data.get("results", []):
|
||||
results.append({
|
||||
"freesound_id": sound["id"],
|
||||
"name": sound.get("name", ""),
|
||||
"description": sound.get("description", ""),
|
||||
"duration": sound.get("duration", 0),
|
||||
"tags": sound.get("tags", []),
|
||||
"preview_url": sound.get("previews", {}).get("preview-hq-mp3", ""),
|
||||
"license": sound.get("license", ""),
|
||||
"username": sound.get("username", ""),
|
||||
})
|
||||
|
||||
return {
|
||||
"count": data.get("count", 0),
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"results": results,
|
||||
}
|
||||
|
||||
async def get_sound(self, sound_id: int) -> Dict:
|
||||
"""사운드 상세 정보 조회"""
|
||||
if not self.api_key:
|
||||
raise ValueError("Freesound API 키 필요")
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.BASE_URL}/sounds/{sound_id}/",
|
||||
headers=self._get_headers(),
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
async def download_preview(self, preview_url: str) -> bytes:
|
||||
"""프리뷰 오디오 다운로드 (인증 불필요)"""
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(preview_url)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
|
||||
async def get_similar_sounds(
|
||||
self,
|
||||
sound_id: int,
|
||||
page_size: int = 10,
|
||||
) -> List[Dict]:
|
||||
"""유사한 사운드 검색"""
|
||||
if not self.api_key:
|
||||
return []
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.get(
|
||||
f"{self.BASE_URL}/sounds/{sound_id}/similar/",
|
||||
params={
|
||||
"page_size": page_size,
|
||||
"fields": "id,name,description,duration,tags,previews,license",
|
||||
},
|
||||
headers=self._get_headers(),
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
results = []
|
||||
for sound in data.get("results", []):
|
||||
results.append({
|
||||
"freesound_id": sound["id"],
|
||||
"name": sound.get("name", ""),
|
||||
"description": sound.get("description", ""),
|
||||
"duration": sound.get("duration", 0),
|
||||
"tags": sound.get("tags", []),
|
||||
"preview_url": sound.get("previews", {}).get("preview-hq-mp3", ""),
|
||||
"license": sound.get("license", ""),
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# 싱글톤 인스턴스
|
||||
freesound_client = FreesoundClient()
|
||||
Reference in New Issue
Block a user