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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user