- suno_provider.py: Suno REST API 클라이언트 (곡 생성, 가사, 2변형 저장) - local_provider.py: 기존 MusicGen 로직 분리 - main.py: provider 라우팅, /providers·/lyrics 엔드포인트 추가 - db.py: provider, lyrics, image_url, suno_id 컬럼 마이그레이션 - docker-compose.yml: SUNO_API_KEY 환경변수 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
123 lines
4.3 KiB
Python
123 lines
4.3 KiB
Python
"""
|
|
Local MusicGen Provider — Windows AI 서버(MusicGen)를 통한 음악 생성
|
|
기존 _run_generation 로직을 그대로 분리.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import logging
|
|
import requests
|
|
|
|
from .db import update_task, add_track
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MUSIC_AI_SERVER_URL = os.getenv("MUSIC_AI_SERVER_URL", "")
|
|
MUSIC_DATA_DIR = "/app/data/music"
|
|
MUSIC_MEDIA_BASE = os.getenv("MUSIC_MEDIA_BASE", "/media/music")
|
|
|
|
|
|
def run_local_generation(task_id: str, params: dict) -> None:
|
|
"""BackgroundTask: Windows AI 서버(MusicGen)에 생성 요청 → 파일 저장 → 라이브러리 등록"""
|
|
try:
|
|
update_task(task_id, "processing", 10, "AI 서버에 연결 중...")
|
|
|
|
if not MUSIC_AI_SERVER_URL:
|
|
update_task(task_id, "failed", 0, "",
|
|
error="MUSIC_AI_SERVER_URL이 설정되지 않았습니다")
|
|
return
|
|
|
|
update_task(task_id, "processing", 30, "음악 생성 중... (수 분 소요될 수 있습니다)")
|
|
|
|
# 1단계: 생성 요청 → ai_task_id 반환
|
|
resp = requests.post(
|
|
f"{MUSIC_AI_SERVER_URL}/generate",
|
|
json=params,
|
|
timeout=30,
|
|
)
|
|
|
|
if resp.status_code != 200:
|
|
update_task(task_id, "failed", 0, "",
|
|
error=f"AI 서버 오류: {resp.status_code} {resp.text[:200]}")
|
|
return
|
|
|
|
ai_task_id = resp.json().get("task_id")
|
|
if not ai_task_id:
|
|
update_task(task_id, "failed", 0, "",
|
|
error="AI 서버 응답에 task_id가 없습니다")
|
|
return
|
|
|
|
# 2단계: 상태 폴링 (최대 10분, 5초 간격)
|
|
remote_url = None
|
|
for _ in range(120):
|
|
time.sleep(5)
|
|
status_resp = requests.get(
|
|
f"{MUSIC_AI_SERVER_URL}/status/{ai_task_id}", timeout=10,
|
|
)
|
|
status_data = status_resp.json()
|
|
ai_status = status_data.get("status")
|
|
|
|
ai_progress = status_data.get("progress", 0)
|
|
ai_message = status_data.get("message", "음악 생성 중...")
|
|
scaled = 30 + int(ai_progress * 0.49) # 30% ~ 79%
|
|
update_task(task_id, "processing", scaled, ai_message)
|
|
|
|
if ai_status == "succeeded":
|
|
remote_url = status_data.get("audio_url")
|
|
break
|
|
elif ai_status == "failed":
|
|
update_task(task_id, "failed", 0, "",
|
|
error=status_data.get("error", "AI 서버 생성 실패"))
|
|
return
|
|
|
|
if not remote_url:
|
|
update_task(task_id, "failed", 0, "",
|
|
error="AI 서버 타임아웃 (10분 초과)")
|
|
return
|
|
|
|
update_task(task_id, "processing", 80, "파일 저장 중...")
|
|
|
|
filename = f"{task_id}.mp3"
|
|
file_path = os.path.join(MUSIC_DATA_DIR, filename)
|
|
|
|
# 3단계: 오디오 파일 다운로드
|
|
dl = requests.get(remote_url, timeout=120, stream=True)
|
|
with open(file_path, "wb") as f:
|
|
for chunk in dl.iter_content(chunk_size=8192):
|
|
f.write(chunk)
|
|
|
|
audio_url = f"{MUSIC_MEDIA_BASE}/{filename}"
|
|
|
|
# 라이브러리 자동 등록
|
|
genre = params.get("genre", "")
|
|
moods = params.get("moods", [])
|
|
mood_str = moods[0] if moods else "Original"
|
|
title = params.get("title") or (
|
|
f"{genre} — {mood_str} Mix" if genre else f"{mood_str} Mix"
|
|
)
|
|
|
|
add_track({
|
|
"title": title,
|
|
"genre": genre,
|
|
"moods": params.get("moods", []),
|
|
"instruments": params.get("instruments", []),
|
|
"duration_sec": params.get("duration_sec"),
|
|
"bpm": params.get("bpm"),
|
|
"key": params.get("key", ""),
|
|
"scale": params.get("scale", ""),
|
|
"prompt": params.get("prompt", ""),
|
|
"audio_url": audio_url,
|
|
"file_path": file_path,
|
|
"task_id": task_id,
|
|
"provider": "local",
|
|
})
|
|
|
|
update_task(task_id, "succeeded", 100, "생성 완료", audio_url=audio_url)
|
|
|
|
except requests.Timeout:
|
|
update_task(task_id, "failed", 0, "",
|
|
error="AI 서버 타임아웃 (10분 초과)")
|
|
except Exception as e:
|
|
logger.exception("Local generation error for task %s", task_id)
|
|
update_task(task_id, "failed", 0, "", error=str(e))
|