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:
@@ -169,6 +169,21 @@ def init_db() -> None:
|
||||
)
|
||||
""")
|
||||
|
||||
# ── compile_jobs 테이블 ───────────────────────────────────────────
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS compile_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
track_ids TEXT NOT NULL DEFAULT '[]',
|
||||
crossfade_sec REAL NOT NULL DEFAULT 3.0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
output_path TEXT,
|
||||
duration_sec REAL,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
# ── music_tasks CRUD ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -707,3 +722,72 @@ def get_trend_reports(limit: int = 10) -> list:
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
# ── Compile Jobs ─────────────────────────────────────────
|
||||
|
||||
def create_compile_job(title: str, track_ids: list, crossfade_sec: float) -> int:
|
||||
with _conn() as conn:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO compile_jobs (title, track_ids, crossfade_sec) VALUES (?,?,?)",
|
||||
(title, json.dumps(track_ids), crossfade_sec),
|
||||
)
|
||||
return cur.lastrowid
|
||||
|
||||
|
||||
def get_compile_jobs() -> list:
|
||||
with _conn() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT id, title, track_ids, crossfade_sec, status, duration_sec, created_at "
|
||||
"FROM compile_jobs ORDER BY created_at DESC LIMIT 50"
|
||||
).fetchall()
|
||||
return [
|
||||
{
|
||||
"id": r["id"],
|
||||
"title": r["title"],
|
||||
"track_ids": json.loads(r["track_ids"]),
|
||||
"crossfade_sec": r["crossfade_sec"],
|
||||
"status": r["status"],
|
||||
"duration_sec": r["duration_sec"],
|
||||
"created_at": r["created_at"],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def get_compile_job(job_id: int) -> Optional[Dict[str, Any]]:
|
||||
with _conn() as conn:
|
||||
r = conn.execute(
|
||||
"SELECT * FROM compile_jobs WHERE id = ?", (job_id,)
|
||||
).fetchone()
|
||||
if not r:
|
||||
return None
|
||||
return {
|
||||
"id": r["id"],
|
||||
"title": r["title"],
|
||||
"track_ids": json.loads(r["track_ids"]),
|
||||
"crossfade_sec": r["crossfade_sec"],
|
||||
"status": r["status"],
|
||||
"output_path": r["output_path"],
|
||||
"duration_sec": r["duration_sec"],
|
||||
"error": r["error"],
|
||||
"created_at": r["created_at"],
|
||||
}
|
||||
|
||||
|
||||
def update_compile_job(job_id: int, **kwargs) -> None:
|
||||
allowed = {"status", "output_path", "duration_sec", "error"}
|
||||
fields = {k: v for k, v in kwargs.items() if k in allowed}
|
||||
if not fields:
|
||||
return
|
||||
set_clause = ", ".join(f"{k} = ?" for k in fields)
|
||||
with _conn() as conn:
|
||||
conn.execute(
|
||||
f"UPDATE compile_jobs SET {set_clause} WHERE id = ?",
|
||||
(*fields.values(), job_id),
|
||||
)
|
||||
|
||||
|
||||
def delete_compile_job(job_id: int) -> None:
|
||||
with _conn() as conn:
|
||||
conn.execute("DELETE FROM compile_jobs WHERE id = ?", (job_id,))
|
||||
|
||||
Reference in New Issue
Block a user