기존 직접 run_suno_generation 호출 + asyncio.to_thread를 Redis push (queue:music-render, job_type=suno_generation) + task 상태 polling 패턴으로 변경. 결과는 task_id로 music_library 조회. Plan-B-Music Phase 3 (cutover 3/4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
5.1 KiB
Python
156 lines
5.1 KiB
Python
"""배치 음악 생성 + 자동 컴파일·영상 파이프라인."""
|
|
import asyncio
|
|
import logging
|
|
import uuid
|
|
|
|
from . import db
|
|
from .random_pools import randomize
|
|
|
|
logger = logging.getLogger("music-lab.batch")
|
|
|
|
POLL_INTERVAL_S = 5
|
|
TRACK_GEN_TIMEOUT_S = 240
|
|
|
|
|
|
async def run_batch(batch_id: int) -> None:
|
|
"""1) genre로 N트랙 순차 Suno 생성
|
|
2) 모두 완료 후 compile_job 자동 생성·실행
|
|
3) compile 완료 후 영상 파이프라인 시작 (cover step)
|
|
"""
|
|
job = db.get_batch_job(batch_id)
|
|
if not job:
|
|
return
|
|
genre = job["genre"]
|
|
count = job["count"]
|
|
duration = job["target_duration_sec"]
|
|
auto_pipe = bool(job["auto_pipeline"])
|
|
|
|
db.update_batch_job(batch_id, status="generating")
|
|
|
|
track_ids: list[int] = []
|
|
for i in range(1, count + 1):
|
|
title = f"{genre.title()} Mix Track {i}"
|
|
params = randomize(genre)
|
|
db.update_batch_job(batch_id,
|
|
current_track_index=i,
|
|
current_track_status="generating")
|
|
|
|
track_id = await _generate_one_track(
|
|
title=title, genre=genre,
|
|
duration_sec=duration, params=params,
|
|
)
|
|
if track_id:
|
|
track_ids.append(track_id)
|
|
db.append_batch_track(batch_id, track_id)
|
|
db.update_batch_job(batch_id, current_track_status="succeeded")
|
|
else:
|
|
db.update_batch_job(batch_id, current_track_status="failed")
|
|
logger.warning("배치 %d 트랙 %d 실패 — 계속 진행", batch_id, i)
|
|
|
|
if not track_ids:
|
|
db.update_batch_job(batch_id, status="failed",
|
|
error="모든 트랙 생성 실패")
|
|
return
|
|
|
|
db.update_batch_job(batch_id, status="generated")
|
|
|
|
if not auto_pipe:
|
|
return
|
|
|
|
# 자동 컴파일
|
|
db.update_batch_job(batch_id, status="compiling")
|
|
try:
|
|
compile_id = db.create_compile_job(
|
|
title=f"{genre.title()} Mix",
|
|
track_ids=track_ids,
|
|
crossfade_sec=3.0,
|
|
)
|
|
db.update_batch_job(batch_id, compile_job_id=compile_id)
|
|
except Exception as e:
|
|
logger.exception("compile create failed")
|
|
db.update_batch_job(batch_id, status="failed", error=f"compile create: {e}")
|
|
return
|
|
|
|
from . import compiler
|
|
try:
|
|
await asyncio.to_thread(compiler.run_compile, compile_id)
|
|
except Exception as e:
|
|
logger.exception("compile run failed")
|
|
db.update_batch_job(batch_id, status="failed", error=f"compile run: {e}")
|
|
return
|
|
|
|
job_after = db.get_compile_job(compile_id)
|
|
status_after = job_after.get("status") if job_after else None
|
|
if status_after not in ("done", "succeeded"):
|
|
db.update_batch_job(
|
|
batch_id, status="failed",
|
|
error=f"compile not done (status={status_after})"
|
|
)
|
|
return
|
|
|
|
# 자동 영상 파이프라인
|
|
try:
|
|
pipeline_id = db.create_pipeline(compile_job_id=compile_id)
|
|
db.update_batch_job(batch_id, pipeline_id=pipeline_id, status="piped")
|
|
|
|
from .pipeline import orchestrator
|
|
await orchestrator.run_step(pipeline_id, "cover")
|
|
except Exception as e:
|
|
logger.exception("pipeline launch failed")
|
|
db.update_batch_job(batch_id, status="failed", error=f"pipeline launch: {e}")
|
|
|
|
|
|
async def _generate_one_track(*, title: str, genre: str, duration_sec: int,
|
|
params: dict) -> int | None:
|
|
"""Redis 큐에 push + task 상태 polling. 성공 시 새 track id, 실패 시 None."""
|
|
import json
|
|
from datetime import datetime, timezone, timedelta
|
|
from .main import redis_client # 같은 컨테이너 — 동일 redis 클라이언트 공유
|
|
|
|
task_id = str(uuid.uuid4())
|
|
suno_params = {
|
|
"title": title,
|
|
"genre": genre,
|
|
"moods": params["moods"],
|
|
"instruments": params["instruments"],
|
|
"duration_sec": duration_sec,
|
|
"bpm": params["bpm"],
|
|
"key": params["key"],
|
|
"scale": params["scale"],
|
|
"prompt": params.get("prompt_modifier", ""),
|
|
"provider": "suno",
|
|
"model": "V4",
|
|
"instrumental": False,
|
|
"lyrics": "",
|
|
}
|
|
db.create_task(task_id, suno_params, provider="suno")
|
|
|
|
# Redis push (Windows music-render가 BLPOP 처리)
|
|
kst = timezone(timedelta(hours=9))
|
|
payload = {
|
|
"task_id": task_id,
|
|
"kind": "music",
|
|
"job_type": "suno_generation",
|
|
"params": suno_params,
|
|
"submitted_at": datetime.now(kst).isoformat(),
|
|
}
|
|
await redis_client.rpush("queue:music-render", json.dumps(payload))
|
|
|
|
waited = 0
|
|
while waited < TRACK_GEN_TIMEOUT_S:
|
|
await asyncio.sleep(POLL_INTERVAL_S)
|
|
waited += POLL_INTERVAL_S
|
|
task = db.get_task(task_id)
|
|
if not task:
|
|
continue
|
|
status = task.get("status")
|
|
if status == "succeeded":
|
|
# Windows webhook이 add_track 했으므로 task_id로 검색
|
|
track = db.get_track_by_task_id(task_id)
|
|
if track:
|
|
return track.get("id")
|
|
return None
|
|
if status == "failed":
|
|
return None
|
|
return None # timeout
|