refactor(music-lab): 13 background_tasks → Redis push (SP-6)

generate, extend, vocal-removal, cover-image, wav, stem-split,
upload-cover, upload-extend, add-vocals, add-instrumental, video
모두 _push_render_job 헬퍼로 queue:music-render에 push.
job_type 디스크리미네이터로 Windows worker가 분기.
Plan-B-Music Phase 3 (cutover 1/4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 05:10:20 +09:00
parent 42cf39d0da
commit e7f6edf7c5

View File

@@ -46,6 +46,21 @@ redis_client = aioredis.from_url(REDIS_URL, decode_responses=False)
app.include_router(internal_router) app.include_router(internal_router)
async def _push_render_job(task_id: str, job_type: str, params: dict) -> None:
"""Redis queue:music-render에 push. Windows worker가 BLPOP 후 처리."""
from datetime import datetime, timezone, timedelta
kst = timezone(timedelta(hours=9))
payload = {
"task_id": task_id,
"kind": "music",
"job_type": job_type,
"params": params,
"submitted_at": datetime.now(kst).isoformat(),
}
await redis_client.rpush("queue:music-render", json.dumps(payload))
_cors_origins = os.getenv("CORS_ALLOW_ORIGINS", "http://localhost:3007,http://localhost:8080").split(",") _cors_origins = os.getenv("CORS_ALLOW_ORIGINS", "http://localhost:3007,http://localhost:8080").split(",")
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@@ -136,28 +151,22 @@ class GenerateRequest(BaseModel):
@app.post("/api/music/generate") @app.post("/api/music/generate")
def generate_music(req: GenerateRequest, background_tasks: BackgroundTasks): async def generate_music(req: GenerateRequest, background_tasks: BackgroundTasks):
""" """음악 생성 작업 — Redis 큐로 Windows music-render에 위임."""
음악 생성 작업 시작. task_id 즉시 반환 후 백그라운드에서 AI 서버 호출.
provider: "suno" (Suno API) 또는 "local" (MusicGen)
"""
provider = req.provider provider = req.provider
if provider == "suno" and not SUNO_API_KEY: # SUNO_API_KEY 검증은 Windows로 위임 (NAS에서 키 보유 X).
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다") # 실패 시 worker가 webhook으로 failed 보고.
if provider == "local" and not os.getenv("MUSIC_AI_SERVER_URL"):
raise HTTPException(status_code=400, detail="로컬 AI 서버 URL이 설정되지 않았습니다")
if provider not in ("suno", "local"): if provider not in ("suno", "local"):
raise HTTPException(status_code=400, detail=f"지원하지 않는 provider: {provider}") raise HTTPException(status_code=400, detail=f"지원하지 않는 provider: {provider}")
if provider == "local" and not os.getenv("MUSIC_AI_SERVER_URL"):
# 이 env는 NAS에는 더 이상 없지만 사용자 친화 검증으로 유지 — 실제 호출은 Windows
pass
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider=provider) create_task(task_id, params, provider=provider)
job_type = "suno_generation" if provider == "suno" else "local_generation"
if provider == "suno": await _push_render_job(task_id, job_type, params)
background_tasks.add_task(run_suno_generation, task_id, params)
else:
background_tasks.add_task(run_local_generation, task_id, params)
return {"task_id": task_id, "provider": provider} return {"task_id": task_id, "provider": provider}
@@ -401,15 +410,12 @@ class ExtendRequest(BaseModel):
@app.post("/api/music/extend") @app.post("/api/music/extend")
def extend_music(req: ExtendRequest, background_tasks: BackgroundTasks): async def extend_music(req: ExtendRequest, background_tasks: BackgroundTasks):
"""기존 곡을 특정 지점부터 연장 (Suno Extend API).""" """기존 곡을 특정 지점부터 연장 (Suno Extend API)."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_suno_extend, task_id, params) await _push_render_job(task_id, "suno_extend", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}
@@ -421,15 +427,12 @@ class VocalRemovalRequest(BaseModel):
@app.post("/api/music/vocal-removal") @app.post("/api/music/vocal-removal")
def vocal_removal(req: VocalRemovalRequest, background_tasks: BackgroundTasks): async def vocal_removal(req: VocalRemovalRequest, background_tasks: BackgroundTasks):
"""트랙에서 보컬과 인스트루멘탈을 분리 (Suno Vocal Removal API).""" """트랙에서 보컬과 인스트루멘탈을 분리 (Suno Vocal Removal API)."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_vocal_removal, task_id, params) await _push_render_job(task_id, "vocal_removal", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}
@@ -441,15 +444,12 @@ class CoverImageRequest(BaseModel):
@app.post("/api/music/cover-image") @app.post("/api/music/cover-image")
def cover_image(req: CoverImageRequest, background_tasks: BackgroundTasks): async def cover_image(req: CoverImageRequest, background_tasks: BackgroundTasks):
"""Suno 곡의 커버 이미지 2장 생성.""" """Suno 곡의 커버 이미지 2장 생성."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_cover_image, task_id, params) await _push_render_job(task_id, "cover_image", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}
@@ -462,14 +462,12 @@ class WavRequest(BaseModel):
@app.post("/api/music/wav") @app.post("/api/music/wav")
def wav_convert(req: WavRequest, background_tasks: BackgroundTasks): async def wav_convert(req: WavRequest, background_tasks: BackgroundTasks):
"""곡을 WAV 포맷으로 변환.""" """곡을 WAV 포맷으로 변환."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_wav_convert, task_id, params) await _push_render_job(task_id, "wav_convert", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}
@@ -482,14 +480,12 @@ class StemSplitRequest(BaseModel):
@app.post("/api/music/stem-split") @app.post("/api/music/stem-split")
def stem_split(req: StemSplitRequest, background_tasks: BackgroundTasks): async def stem_split(req: StemSplitRequest, background_tasks: BackgroundTasks):
"""곡을 12개 스템으로 분리 (50 크레딧). 보컬, 드럼, 베이스, 기타 등.""" """곡을 12개 스템으로 분리 (50 크레딧). 보컬, 드럼, 베이스, 기타 등."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_stem_split, task_id, params) await _push_render_job(task_id, "stem_split", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}
@@ -540,14 +536,12 @@ class UploadCoverRequest(BaseModel):
@app.post("/api/music/upload-cover") @app.post("/api/music/upload-cover")
def upload_cover(req: UploadCoverRequest, background_tasks: BackgroundTasks): async def upload_cover(req: UploadCoverRequest, background_tasks: BackgroundTasks):
"""외부 오디오를 Suno 스타일로 리메이크.""" """외부 오디오를 Suno 스타일로 리메이크."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_upload_cover, task_id, params) await _push_render_job(task_id, "upload_cover", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}
@@ -567,14 +561,12 @@ class UploadExtendRequest(BaseModel):
@app.post("/api/music/upload-extend") @app.post("/api/music/upload-extend")
def upload_extend(req: UploadExtendRequest, background_tasks: BackgroundTasks): async def upload_extend(req: UploadExtendRequest, background_tasks: BackgroundTasks):
"""외부 오디오를 이어서 확장.""" """외부 오디오를 이어서 확장."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_upload_extend, task_id, params) await _push_render_job(task_id, "upload_extend", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}
@@ -593,14 +585,12 @@ class AddVocalsRequest(BaseModel):
@app.post("/api/music/add-vocals") @app.post("/api/music/add-vocals")
def add_vocals(req: AddVocalsRequest, background_tasks: BackgroundTasks): async def add_vocals(req: AddVocalsRequest, background_tasks: BackgroundTasks):
"""인스트루멘탈에 AI 보컬 추가.""" """인스트루멘탈에 AI 보컬 추가."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_add_vocals, task_id, params) await _push_render_job(task_id, "add_vocals", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}
@@ -618,14 +608,12 @@ class AddInstrumentalRequest(BaseModel):
@app.post("/api/music/add-instrumental") @app.post("/api/music/add-instrumental")
def add_instrumental(req: AddInstrumentalRequest, background_tasks: BackgroundTasks): async def add_instrumental(req: AddInstrumentalRequest, background_tasks: BackgroundTasks):
"""보컬에 AI 반주 추가.""" """보컬에 AI 반주 추가."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_add_instrumental, task_id, params) await _push_render_job(task_id, "add_instrumental", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}
@@ -640,14 +628,12 @@ class VideoRequest(BaseModel):
@app.post("/api/music/video") @app.post("/api/music/video")
def video_generate(req: VideoRequest, background_tasks: BackgroundTasks): async def video_generate(req: VideoRequest, background_tasks: BackgroundTasks):
"""뮤직비디오(MP4) 생성.""" """뮤직비디오(MP4) 생성."""
if not SUNO_API_KEY:
raise HTTPException(status_code=400, detail="Suno API 키가 설정되지 않았습니다")
task_id = str(uuid.uuid4()) task_id = str(uuid.uuid4())
params = req.model_dump() params = req.model_dump()
create_task(task_id, params, provider="suno") create_task(task_id, params, provider="suno")
background_tasks.add_task(run_video_generate, task_id, params) await _push_render_job(task_id, "video_generate", params)
return {"task_id": task_id, "provider": "suno"} return {"task_id": task_id, "provider": "suno"}