import os from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import List, Optional from app.config import settings from app.api.v1.auth import enforce_password_changed from app.api.v1.projects import get_optional_user from app.api.v1.audio import _safe_file_id router = APIRouter() class ClipConfig(BaseModel): clip_id: str start_time_seconds: float end_time_seconds: float loop_count: int = 1 apply_zero_crossing: bool = True fade_in_ms: int = 150 fade_out_ms: int = 150 class TrackConfig(BaseModel): track_id: str file_id: str volume: float = 1.0 muted: bool = False clips: List[ClipConfig] = [] class ExportSettings(BaseModel): sample_rate: int = 44100 bit_depth: int = 16 format: str = "wav" class MultitrackSessionRequest(BaseModel): session_id: str export_settings: ExportSettings tracks: List[TrackConfig] @router.post("/mix") async def mix_multitrack_session(req: MultitrackSessionRequest, current_user: Optional[dict] = Depends(get_optional_user)): """ API endpoint để xử lý hòa âm đa kênh (Multitrack Mixdown). Nhận cấu hình JSON từ Client và gửi task xuống Celery Worker. """ if current_user: enforce_password_changed(current_user) # Bug #9: guard auth-optional + sanitize file_id (chống traversal vào worker) for track in req.tracks: track.file_id = _safe_file_id(track.file_id) if not track.file_id: raise HTTPException(status_code=400, detail=f"file_id không hợp lệ cho track {track.track_id}") # Kiểm tra xem các file nguồn có tồn tại không if track.muted: continue upload_path = os.path.join(settings.UPLOADS_DIR, track.file_id) processed_path = os.path.join(settings.PROCESSED_DIR, track.file_id) if not os.path.exists(upload_path) and not os.path.exists(processed_path): raise HTTPException( status_code=404, detail=f"File not found for track {track.track_id}: {track.file_id}" ) # Gửi task xuống Celery Worker from app.tasks.worker import mix_multitrack_task task = mix_multitrack_task.delay(req.model_dump()) return { "task_id": task.id, "session_id": req.session_id, "status": "processing" } @router.post("/process-session") async def process_session(req: MultitrackSessionRequest, current_user: Optional[dict] = Depends(get_optional_user)): """ API endpoint để xử lý toàn bộ session với nhiều tracks và clips. Xử lý từng clip, sau đó hòa âm tất cả tracks lại với nhau. """ if current_user: enforce_password_changed(current_user) for track in req.tracks: track.file_id = _safe_file_id(track.file_id) from app.tasks.worker import process_multitrack_session_task task = process_multitrack_session_task.delay(req.model_dump()) return { "task_id": task.id, "session_id": req.session_id, "status": "processing" }