feat: bổ sung MIDI

This commit is contained in:
2026-07-23 08:32:23 +07:00
parent 6545e1746e
commit 0e44e44ecb
15 changed files with 3304 additions and 12680 deletions
+146 -6
View File
@@ -1,14 +1,119 @@
import time
import json
import uuid
import os
from fastapi import APIRouter, HTTPException, Depends, Header
from pydantic import BaseModel
from typing import Optional, Any, Dict
from jsonschema import validate, ValidationError
from app.models.user import get_db_connection
from app.api.v1.auth import get_current_user, decode_token
router = APIRouter()
SCHEMA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "models", "project_schema.json")
def upgrade_project_json_if_needed(project_data: dict) -> dict:
if "main_session" in project_data:
return project_data
tracks = project_data.get("tracks", [])
upgraded_tracks = []
for t in tracks:
track_id = str(t.get("id", ""))
track_name = t.get("name", "Track")
vol = t.get("volumeDb", 0.0)
pan = t.get("pan", 0.0)
muted = t.get("muted", False)
solo = t.get("solo", False)
items = []
for c in t.get("clips", []):
items.append({
"id": c.get("id"),
"name": c.get("name", "Audio Clip"),
"type": "AUDIO_ITEM",
"start_bar": c.get("startTime", 0.0) / 4.0,
"duration_bars": 4.0,
"clip_start_offset_bars": 0.0,
"source_data": {
"audio_file_url": f"/static/audio/uploads/{t.get('serverFileId')}" if t.get("serverFileId") else "",
"sample_rate": 44100,
"channels": 2,
"gain": 1.0
}
})
for m in t.get("midiItems", []):
items.append({
"id": m.get("id"),
"name": m.get("name", "MIDI Item"),
"type": "MIDI_ITEM",
"start_bar": m.get("startTime", 0.0) / 4.0,
"duration_bars": m.get("duration", 4.0),
"clip_start_offset_bars": 0.0,
"source_data": {
"total_buffer_bars": m.get("duration", 8.0),
"notes": m.get("notes", [])
}
})
upgraded_tracks.append({
"id": track_id,
"name": track_name,
"type": "MIDI" if t.get("midiItems") else "AUDIO",
"volume_db": vol,
"pan": pan,
"mute": muted,
"solo": solo,
"fx_chain": [],
"synth_engine": {
"plugin_id": "synth",
"preset_id": "default",
"parameters": {}
},
"items": items
})
return {
"project_id": project_data.get("id", "temp_project"),
"metadata": {
"title": project_data.get("name", "Dự án mới"),
"bpm": 120.0,
"time_signature_numerator": 4,
"time_signature_denominator": 4,
"sample_rate": 44100
},
"main_session": {
"id": "main",
"name": "MAIN SESSION",
"is_root": True,
"length_bars": 16.0,
"auto_compute_length": True,
"tracks": upgraded_tracks
},
"section_store": {}
}
def validate_project_data(data_json: str) -> str:
try:
data = json.loads(data_json)
if "main_session" not in data:
data = upgrade_project_json_if_needed(data)
data_json = json.dumps(data)
if os.path.exists(SCHEMA_PATH):
with open(SCHEMA_PATH, "r") as f:
schema = json.load(f)
validate(instance=data, schema=schema)
return data_json
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"Cấu trúc JSON không hợp lệ: {str(e)}")
except ValidationError as e:
path = " -> ".join(str(p) for p in e.path)
raise HTTPException(status_code=400, detail=f"Lỗi xác thực project schema tại [{path}]: {e.message}")
except Exception as e:
raise HTTPException(status_code=400, detail=f"Lỗi xác thực dự án: {str(e)}")
class SaveProjectRequest(BaseModel):
name: str
data_json: str
@@ -24,11 +129,12 @@ def get_optional_user(authorization: Optional[str] = Header(None)) -> Optional[d
@router.post("/temp")
async def save_temp_project(req: SaveTempProjectRequest, current_user: Optional[dict] = Depends(get_optional_user)):
validated_data_json = validate_project_data(req.data_json)
user_id = current_user["user_id"] if current_user else "anonymous"
conn = get_db_connection()
cursor = conn.cursor()
size_bytes = len(req.data_json.encode("utf-8"))
size_bytes = len(validated_data_json.encode("utf-8"))
now = time.time()
temp_id = f"temp_{user_id}"
@@ -36,7 +142,7 @@ async def save_temp_project(req: SaveTempProjectRequest, current_user: Optional[
INSERT INTO projects (id, user_id, name, data_json, is_temp, size_bytes, updated_at)
VALUES (?, ?, 'Dự án tạm chưa lưu', ?, 1, ?, ?)
ON CONFLICT(id) DO UPDATE SET data_json = excluded.data_json, size_bytes = excluded.size_bytes, updated_at = excluded.updated_at
""", (temp_id, user_id, req.data_json, size_bytes, now))
""", (temp_id, user_id, validated_data_json, size_bytes, now))
conn.commit()
conn.close()
@@ -64,6 +170,7 @@ async def get_temp_project(current_user: Optional[dict] = Depends(get_optional_u
@router.post("/cloud")
async def save_cloud_project(req: SaveProjectRequest, current_user: dict = Depends(get_current_user)):
validated_data_json = validate_project_data(req.data_json)
user_id = current_user["user_id"]
conn = get_db_connection()
cursor = conn.cursor()
@@ -76,7 +183,7 @@ async def save_cloud_project(req: SaveProjectRequest, current_user: dict = Depen
used_row = cursor.fetchone()
used_bytes = used_row["total_used"] if used_row and used_row["total_used"] else 0
new_size_bytes = len(req.data_json.encode("utf-8"))
new_size_bytes = len(validated_data_json.encode("utf-8"))
max_bytes = storage_limit_mb * 1024 * 1024
if used_bytes + new_size_bytes > max_bytes:
@@ -92,7 +199,7 @@ async def save_cloud_project(req: SaveProjectRequest, current_user: dict = Depen
cursor.execute("""
INSERT INTO projects (id, user_id, name, data_json, is_temp, size_bytes, updated_at)
VALUES (?, ?, ?, ?, 0, ?, ?)
""", (project_id, user_id, req.name, req.data_json, new_size_bytes, now))
""", (project_id, user_id, req.name, validated_data_json, new_size_bytes, now))
temp_id = f"temp_{user_id}"
cursor.execute("DELETE FROM projects WHERE id = ? AND is_temp = 1", (temp_id,))
@@ -157,6 +264,7 @@ async def delete_cloud_project(project_id: str, current_user: dict = Depends(get
@router.put("/cloud/{project_id}")
async def update_cloud_project(project_id: str, req: SaveProjectRequest, current_user: dict = Depends(get_current_user)):
validated_data_json = validate_project_data(req.data_json)
user_id = current_user["user_id"]
conn = get_db_connection()
cursor = conn.cursor()
@@ -167,15 +275,47 @@ async def update_cloud_project(project_id: str, req: SaveProjectRequest, current
conn.close()
raise HTTPException(status_code=404, detail="Không tìm thấy dự án để cập nhật")
new_size_bytes = len(req.data_json.encode("utf-8"))
new_size_bytes = len(validated_data_json.encode("utf-8"))
now = time.time()
cursor.execute("""
UPDATE projects
SET name = ?, data_json = ?, size_bytes = ?, updated_at = ?
WHERE id = ? AND user_id = ?
""", (req.name, req.data_json, new_size_bytes, now, project_id, user_id))
""", (req.name, validated_data_json, new_size_bytes, now, project_id, user_id))
conn.commit()
conn.close()
return {"success": True, "message": "Đã cập nhật dự án thành công"}
class RenderProjectRequest(BaseModel):
sample_rate: Optional[int] = 44100
@router.post("/cloud/{project_id}/render")
async def render_project_endpoint(project_id: str, req: RenderProjectRequest, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT name, data_json FROM projects WHERE id = ? AND user_id = ? AND is_temp = 0", (project_id, user_id))
row = cursor.fetchone()
conn.close()
if not row:
raise HTTPException(status_code=404, detail="Không tìm thấy dự án để kết xuất")
# Validate schema
validate_project_data(row["data_json"])
# Trigger Celery task
from app.tasks.worker import render_project_task
task = render_project_task.delay(
project_id=project_id,
project_name=row["name"],
project_json_str=row["data_json"],
sample_rate=req.sample_rate or 44100
)
return {
"task_id": task.id,
"status": "processing"
}