FEAT: thêm nút bypass cho track strip để bypass không qua mastering panel
This commit is contained in:
+69
-73
@@ -8,12 +8,32 @@ from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
import json
|
||||
from app.config import settings
|
||||
from app.api.v1.auth import get_current_user
|
||||
from app.api.v1.auth import get_current_user, enforce_password_changed
|
||||
from app.api.v1.projects import get_optional_user
|
||||
from app.models.user import get_db_connection
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
MAX_AUDIO_UPLOAD_BYTES = 1024 * 1024 * 1024 # 1 GB
|
||||
|
||||
def _safe_file_id(file_id: str) -> str:
|
||||
"""Strip any path components from a client-supplied file id."""
|
||||
if not file_id:
|
||||
return ""
|
||||
return os.path.basename(file_id.replace("\\", "/"))
|
||||
|
||||
def _resolve_storage_path(file_id: str) -> str:
|
||||
"""Return the existing file path (processed first, then uploads) for a
|
||||
sanitized file id, or '' when not found."""
|
||||
fid = _safe_file_id(file_id)
|
||||
if not fid:
|
||||
return ""
|
||||
for d in (settings.PROCESSED_DIR, settings.UPLOADS_DIR):
|
||||
p = os.path.join(d, fid)
|
||||
if os.path.isfile(p):
|
||||
return p
|
||||
return ""
|
||||
|
||||
class EditRequest(BaseModel):
|
||||
file_id: str
|
||||
cut_start_ms: Optional[float] = None
|
||||
@@ -58,16 +78,32 @@ class PythonToolRequest(BaseModel):
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_audio(file: UploadFile = File(...), current_user: Optional[dict] = Depends(get_optional_user)):
|
||||
if current_user:
|
||||
enforce_password_changed(current_user)
|
||||
user_id = current_user["user_id"] if current_user else "anonymous"
|
||||
ext = os.path.splitext(file.filename)[1]
|
||||
ext = os.path.splitext(file.filename or "")[1]
|
||||
if not ext:
|
||||
ext = ".wav"
|
||||
file_id = f"user_{user_id}_{uuid.uuid4()}{ext}"
|
||||
file_path = os.path.join(settings.UPLOADS_DIR, file_id)
|
||||
|
||||
# Stream upload in chunks with a hard size cap (avoids loading a multi-GB
|
||||
# WAV into RAM and bounds disk usage).
|
||||
with open(file_path, "wb") as f:
|
||||
content = await file.read()
|
||||
f.write(content)
|
||||
size = 0
|
||||
while True:
|
||||
chunk = await file.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
size += len(chunk)
|
||||
if size > MAX_AUDIO_UPLOAD_BYTES:
|
||||
f.close()
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise HTTPException(status_code=413, detail="File âm thanh quá lớn (giới hạn 1GB)")
|
||||
f.write(chunk)
|
||||
|
||||
# Save original filename as sidecar metadata
|
||||
import json
|
||||
@@ -90,15 +126,12 @@ async def upload_audio(file: UploadFile = File(...), current_user: Optional[dict
|
||||
|
||||
@router.post("/edit")
|
||||
async def edit_audio(req: EditRequest):
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
|
||||
# Use uploaded file if it exists, or look in processed if it was already edited
|
||||
if not os.path.exists(upload_path) and not os.path.exists(processed_path):
|
||||
if not _resolve_storage_path(req.file_id):
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.tasks.worker import edit_audio_task
|
||||
task = edit_audio_task.delay(req.dict())
|
||||
task = edit_audio_task.delay(req.model_dump())
|
||||
|
||||
return {
|
||||
"task_id": task.id
|
||||
@@ -106,15 +139,10 @@ async def edit_audio(req: EditRequest):
|
||||
|
||||
@router.get("/download/{file_id}")
|
||||
async def download_audio(file_id: str):
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, file_id)
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
return FileResponse(processed_path, media_type="audio/wav", filename=file_id)
|
||||
elif os.path.exists(upload_path):
|
||||
return FileResponse(upload_path, media_type="audio/wav", filename=file_id)
|
||||
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
path = _resolve_storage_path(file_id)
|
||||
if not path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
return FileResponse(path, media_type="audio/wav", filename=os.path.basename(path))
|
||||
|
||||
@router.get("/waveform/{file_id}")
|
||||
async def get_waveform(file_id: str, num_peaks: int = Query(default=800, ge=50, le=4000)):
|
||||
@@ -122,14 +150,8 @@ async def get_waveform(file_id: str, num_peaks: int = Query(default=800, ge=50,
|
||||
API endpoint vẽ Peak Waveform đồng bộ (Week 2).
|
||||
Trả về dữ liệu peak waveform cho hiển thị đồ thị sóng âm trên Frontend.
|
||||
"""
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
else:
|
||||
file_path = _resolve_storage_path(file_id)
|
||||
if not file_path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.core.dsp_utils import generate_peak_waveform
|
||||
@@ -140,14 +162,8 @@ async def get_waveform_rms(file_id: str, num_points: int = Query(default=800, ge
|
||||
"""
|
||||
API endpoint vẽ RMS Waveform (mượt hơn peak).
|
||||
"""
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
else:
|
||||
file_path = _resolve_storage_path(file_id)
|
||||
if not file_path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.core.dsp_utils import generate_rms_waveform
|
||||
@@ -159,26 +175,20 @@ async def analyze_audio_with_ai(req: AIAnalysisRequest):
|
||||
API endpoint phân tích cấu trúc khuôn nhạc bằng AI (Week 4).
|
||||
Gọi OpenAI Compatible API (DeepSeek/Ollama) để phân đoạn bố cục.
|
||||
"""
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
else:
|
||||
file_path = _resolve_storage_path(req.file_id)
|
||||
if not file_path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.tasks.worker import analyze_ai_task
|
||||
task = analyze_ai_task.delay(
|
||||
file_id=req.file_id,
|
||||
file_id=_safe_file_id(req.file_id),
|
||||
api_base_url=req.api_base_url,
|
||||
model=req.model
|
||||
)
|
||||
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"file_id": req.file_id
|
||||
"file_id": _safe_file_id(req.file_id)
|
||||
}
|
||||
|
||||
@router.post("/export")
|
||||
@@ -186,19 +196,13 @@ async def export_audio(req: ExportRequest):
|
||||
"""
|
||||
API endpoint xuất tệp âm thanh sang nhiều định dạng (WAV/MP3/OGG).
|
||||
"""
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
|
||||
if os.path.exists(processed_path):
|
||||
source_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
source_path = upload_path
|
||||
else:
|
||||
source_path = _resolve_storage_path(req.file_id)
|
||||
if not source_path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
from app.tasks.worker import export_audio_task
|
||||
task = export_audio_task.delay(
|
||||
file_id=req.file_id,
|
||||
file_id=_safe_file_id(req.file_id),
|
||||
format=req.format,
|
||||
sample_rate=req.sample_rate,
|
||||
bit_depth=req.bit_depth
|
||||
@@ -206,29 +210,24 @@ async def export_audio(req: ExportRequest):
|
||||
|
||||
return {
|
||||
"task_id": task.id,
|
||||
"file_id": req.file_id
|
||||
"file_id": _safe_file_id(req.file_id)
|
||||
}
|
||||
|
||||
@router.post("/ai-scan")
|
||||
async def ai_scan_audio(req: AIScanRequest):
|
||||
async def ai_scan_audio(req: AIScanRequest, current_user: Optional[dict] = Depends(get_optional_user)):
|
||||
"""
|
||||
17_AI_SCAN.md Feature 1: AI Loop Scan & Automated Marker Labeling.
|
||||
Uses AIDSPEngine to find optimal recurring loop region with zero-crossing alignment.
|
||||
"""
|
||||
if current_user:
|
||||
enforce_password_changed(current_user)
|
||||
from app.core.ai_dsp_engine import AIDSPEngine
|
||||
import soundfile as sf
|
||||
import numpy as np
|
||||
|
||||
file_path = None
|
||||
if req.file_id:
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
file_path = _resolve_storage_path(req.file_id) if req.file_id else ""
|
||||
|
||||
if file_path and os.path.exists(file_path):
|
||||
if file_path:
|
||||
data, sr = sf.read(file_path)
|
||||
if data.ndim > 1:
|
||||
data = data.T
|
||||
@@ -252,6 +251,8 @@ async def ai_cut_audio(req: AICutRequest, current_user: Optional[dict] = Depends
|
||||
Executes raw binary sample slice at exact zero-crossing coordinates.
|
||||
"""
|
||||
user_id = current_user["user_id"] if current_user else "anonymous"
|
||||
if current_user:
|
||||
enforce_password_changed(current_user)
|
||||
from app.core.ai_dsp_engine import AIDSPEngine
|
||||
import soundfile as sf
|
||||
import numpy as np
|
||||
@@ -259,16 +260,9 @@ async def ai_cut_audio(req: AICutRequest, current_user: Optional[dict] = Depends
|
||||
output_file_id = f"user_{user_id}_ai_cut_{uuid.uuid4().hex[:8]}.wav"
|
||||
out_path = os.path.join(settings.PROCESSED_DIR, output_file_id)
|
||||
|
||||
file_path = None
|
||||
if req.file_id:
|
||||
upload_path = os.path.join(settings.UPLOADS_DIR, req.file_id)
|
||||
processed_path = os.path.join(settings.PROCESSED_DIR, req.file_id)
|
||||
if os.path.exists(processed_path):
|
||||
file_path = processed_path
|
||||
elif os.path.exists(upload_path):
|
||||
file_path = upload_path
|
||||
file_path = _resolve_storage_path(req.file_id) if req.file_id else ""
|
||||
|
||||
if file_path and os.path.exists(file_path):
|
||||
if file_path:
|
||||
data, sr = sf.read(file_path)
|
||||
if data.ndim > 1:
|
||||
data = data.T
|
||||
@@ -295,6 +289,8 @@ async def run_python_dsp_tool(req: PythonToolRequest, current_user: Optional[dic
|
||||
Handles normalize peak, invert phase, swap channels, zero-crossing align, and synth wave generation.
|
||||
"""
|
||||
user_id = current_user["user_id"] if current_user else "anonymous"
|
||||
if current_user:
|
||||
enforce_password_changed(current_user)
|
||||
from app.core.python_tools_engine import PythonToolsEngine
|
||||
from app.core.ai_dsp_engine import AIDSPEngine
|
||||
import soundfile as sf
|
||||
|
||||
Reference in New Issue
Block a user