feat(music-lab): 다중 트랙 컴파일 백엔드 (FFmpeg concat+crossfade → MP4)

- db.py: compile_jobs 테이블 추가 + CRUD 5종 (create/get/list/update/delete)
- compiler.py: acrossfade 필터 체인 + 그라디언트 배경 + MP4 렌더링 워커
- main.py: /api/music/compile POST·GET·DELETE + /api/music/compiles GET + /api/music/compile/{id}/export GET

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 16:54:53 +09:00
parent 7c8d079f74
commit 096e291ed8
3 changed files with 268 additions and 0 deletions

View File

@@ -19,7 +19,10 @@ from .db import (
update_revenue_record, delete_revenue_record, get_revenue_dashboard,
get_market_trends as _get_market_trends,
get_latest_trend_report, get_trend_reports as _get_trend_reports,
create_compile_job, get_compile_jobs, get_compile_job,
update_compile_job, delete_compile_job,
)
from .compiler import run_compile
from .market import ingest_trends, get_suggestions
from .local_provider import run_local_generation
from .suno_provider import (
@@ -783,6 +786,66 @@ def delete_project(project_id: int):
return {"ok": True}
# ── Compile Jobs ──────────────────────────────────────────────────────────────
class CompileRequest(BaseModel):
title: str = ""
track_ids: list[int]
crossfade_sec: float = 3.0
@app.post("/api/music/compile")
def create_compile(req: CompileRequest, background_tasks: BackgroundTasks):
if not req.track_ids:
raise HTTPException(status_code=400, detail="track_ids 필수")
if not (0.5 <= req.crossfade_sec <= 15):
raise HTTPException(status_code=400, detail="crossfade_sec: 0.5~15")
job_id = create_compile_job(req.title, req.track_ids, req.crossfade_sec)
background_tasks.add_task(run_compile, job_id)
return {"id": job_id, "status": "rendering"}
@app.get("/api/music/compiles")
def list_compiles():
return {"jobs": get_compile_jobs()}
@app.get("/api/music/compile/{job_id}")
def get_compile(job_id: int):
job = get_compile_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Not found")
return job
@app.delete("/api/music/compile/{job_id}")
def delete_compile(job_id: int):
job = get_compile_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Not found")
if job.get("output_path"):
out_dir = os.path.dirname(job["output_path"])
if os.path.exists(out_dir):
shutil.rmtree(out_dir, ignore_errors=True)
delete_compile_job(job_id)
return {"ok": True}
@app.get("/api/music/compile/{job_id}/export")
def export_compile(job_id: int):
job = get_compile_job(job_id)
if not job or job["status"] != "done":
raise HTTPException(status_code=404, detail="Not ready")
out_dir = os.path.dirname(job["output_path"])
rel = os.path.relpath(job["output_path"], os.getenv("VIDEO_DATA_DIR", "/app/data/videos"))
mp4_url = f"/media/videos/{rel}"
return {
"mp4_url": mp4_url,
"duration_sec": job["duration_sec"],
"title": job["title"],
}
# ── 수익화 추적 API ───────────────────────────────────────────────────────────
@app.get("/api/music/revenue/dashboard")