First commit

This commit is contained in:
2026-07-18 15:09:05 +07:00
parent 0e8693882b
commit a34b01a035
29 changed files with 7400 additions and 0 deletions
View File
+174
View File
@@ -0,0 +1,174 @@
import os
import uuid
import asyncio
from fastapi import APIRouter, UploadFile, File, HTTPException, Query
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional
from app.config import settings
router = APIRouter()
class EditRequest(BaseModel):
file_id: str
cut_start_ms: Optional[float] = None
cut_end_ms: Optional[float] = None
zero_crossing_align: bool = True
loop_count: int = 1
fade_in_ms: float = 0.0
fade_out_ms: float = 0.0
volume_change_db: float = 0.0
class ExportRequest(BaseModel):
file_id: str
format: str = "wav"
sample_rate: int = 44100
bit_depth: int = 16
class AIAnalysisRequest(BaseModel):
file_id: str
api_base_url: Optional[str] = None
model: str = "deepseek-chat"
@router.post("/upload")
async def upload_audio(file: UploadFile = File(...)):
ext = os.path.splitext(file.filename)[1]
if not ext:
ext = ".wav"
file_id = f"{uuid.uuid4()}{ext}"
file_path = os.path.join(settings.UPLOADS_DIR, file_id)
with open(file_path, "wb") as f:
content = await file.read()
f.write(content)
# Trigger celery task
from app.tasks.worker import analyze_audio_task
task = analyze_audio_task.delay(file_id)
return {
"file_id": file_id,
"filename": file.filename,
"analysis_task_id": task.id
}
@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):
raise HTTPException(status_code=404, detail="File not found")
from app.tasks.worker import edit_audio_task
task = edit_audio_task.delay(req.dict())
return {
"task_id": task.id
}
@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")
@router.get("/waveform/{file_id}")
async def get_waveform(file_id: str, num_peaks: int = Query(default=800, ge=50, le=4000)):
"""
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:
raise HTTPException(status_code=404, detail="File not found")
from app.core.dsp_utils import generate_peak_waveform
return await asyncio.to_thread(generate_peak_waveform, file_path, num_peaks)
@router.get("/waveform-rms/{file_id}")
async def get_waveform_rms(file_id: str, num_points: int = Query(default=800, ge=50, le=4000)):
"""
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:
raise HTTPException(status_code=404, detail="File not found")
from app.core.dsp_utils import generate_rms_waveform
return await asyncio.to_thread(generate_rms_waveform, file_path, num_points)
@router.post("/analyze-ai")
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:
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,
api_base_url=req.api_base_url,
model=req.model
)
return {
"task_id": task.id,
"file_id": req.file_id
}
@router.post("/export")
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:
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,
format=req.format,
sample_rate=req.sample_rate,
bit_depth=req.bit_depth
)
return {
"task_id": task.id,
"file_id": req.file_id
}
+78
View File
@@ -0,0 +1,78 @@
import os
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from app.config import settings
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):
"""
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.
"""
# Kiểm tra xem các file nguồn có tồn tại không
for track in req.tracks:
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.dict())
return {
"task_id": task.id,
"session_id": req.session_id,
"status": "processing"
}
@router.post("/process-session")
async def process_session(req: MultitrackSessionRequest):
"""
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.
"""
from app.tasks.worker import process_multitrack_session_task
task = process_multitrack_session_task.delay(req.dict())
return {
"task_id": task.id,
"session_id": req.session_id,
"status": "processing"
}
+19
View File
@@ -0,0 +1,19 @@
from fastapi import APIRouter
from celery.result import AsyncResult
from app.tasks.worker import celery_app
router = APIRouter()
@router.get("/tasks/{task_id}")
async def get_task_status(task_id: str):
res = AsyncResult(task_id, app=celery_app)
response_data = {
"task_id": task_id,
"status": res.status,
}
if res.ready():
if res.successful():
response_data["result"] = res.result
else:
response_data["error"] = str(res.result)
return response_data