feat: thêm tính năng lưu dự án bằng modal, Lưu dưới tên khác (Save As) và quản trị dự án/tệp tin trong Hồ sơ cá nhân

This commit is contained in:
2026-07-22 18:42:14 +07:00
parent 6f0fac9f2d
commit 9bf9f38864
5 changed files with 1140 additions and 181 deletions
+88 -4
View File
@@ -236,16 +236,17 @@ async def ai_scan_audio(req: AIScanRequest):
}
@router.post("/ai-cut")
async def ai_cut_audio(req: AICutRequest):
async def ai_cut_audio(req: AICutRequest, current_user: Optional[dict] = Depends(get_optional_user)):
"""
17_AI_SCAN.md Feature 2: Fade-Free AI Cut (Zero-Crossing Aligned Slicing).
Executes raw binary sample slice at exact zero-crossing coordinates.
"""
user_id = current_user["user_id"] if current_user else "anonymous"
from app.core.ai_dsp_engine import AIDSPEngine
import soundfile as sf
import numpy as np
output_file_id = f"ai_cut_{uuid.uuid4().hex[:8]}.wav"
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
@@ -278,11 +279,12 @@ async def ai_cut_audio(req: AICutRequest):
}
@router.post("/python-tool")
async def run_python_dsp_tool(req: PythonToolRequest):
async def run_python_dsp_tool(req: PythonToolRequest, current_user: Optional[dict] = Depends(get_optional_user)):
"""
Non-AI Python DSP Tools endpoint.
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"
from app.core.python_tools_engine import PythonToolsEngine
from app.core.ai_dsp_engine import AIDSPEngine
import soundfile as sf
@@ -290,7 +292,7 @@ async def run_python_dsp_tool(req: PythonToolRequest):
if req.tool_type == "synth_wave":
wave = PythonToolsEngine.generate_synth_wave(req.wave_type or "sine", req.freq or 440.0, req.duration or 2.0)
output_file_id = f"synth_{req.wave_type}_{uuid.uuid4().hex[:6]}.wav"
output_file_id = f"user_{user_id}_synth_{req.wave_type}_{uuid.uuid4().hex[:6]}.wav"
out_path = os.path.join(settings.PROCESSED_DIR, output_file_id)
sf.write(out_path, wave, 44100)
return {
@@ -311,3 +313,85 @@ async def run_python_dsp_tool(req: PythonToolRequest):
"success": True,
"message": f"Python Tool '{req.tool_type}' executed successfully for track {req.track_id}"
}
class MyFilesRequest(BaseModel):
active_file_ids: List[str] = []
@router.post("/my-files")
async def list_user_files(req: MyFilesRequest, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
prefix = f"user_{user_id}_"
# Scan all user's projects to find referenced files
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT data_json FROM projects WHERE user_id = ?", (user_id,))
rows = cursor.fetchall()
conn.close()
referenced_in_db = set()
for row in rows:
try:
proj = json.loads(row["data_json"])
for track in proj.get("tracks", []):
fid = track.get("serverFileId")
if fid:
referenced_in_db.add(fid)
except Exception:
pass
active_set = set(req.active_file_ids) | referenced_in_db
files_map = {}
def scan_dir(directory, type_label):
if not os.path.exists(directory):
return
for filename in os.listdir(directory):
if filename.startswith(prefix):
filepath = os.path.join(directory, filename)
if os.path.isfile(filepath):
stat = os.stat(filepath)
is_in_use = filename in active_set
if filename in files_map:
files_map[filename]["size_mb"] = round(files_map[filename]["size_mb"] + stat.st_size / (1024 * 1024), 2)
else:
files_map[filename] = {
"file_id": filename,
"size_mb": round(stat.st_size / (1024 * 1024), 2),
"created_at": stat.st_mtime,
"type": type_label,
"is_in_use": is_in_use
}
scan_dir(settings.UPLOADS_DIR, "Upload")
scan_dir(settings.PROCESSED_DIR, "Processed")
user_files = list(files_map.values())
user_files.sort(key=lambda x: x["created_at"], reverse=True)
return user_files
@router.delete("/my-files/{file_id}")
async def delete_user_file(file_id: str, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
prefix = f"user_{user_id}_"
# Guard: only own files can be deleted
if not file_id.startswith(prefix):
raise HTTPException(status_code=403, detail="Bạn không có quyền xóa tệp này")
deleted = False
for directory in [settings.UPLOADS_DIR, settings.PROCESSED_DIR]:
filepath = os.path.join(directory, file_id)
if os.path.exists(filepath):
try:
os.remove(filepath)
deleted = True
except Exception:
pass
if not deleted:
raise HTTPException(status_code=404, detail="Không tìm thấy tệp trên server")
return {"success": True, "message": "Đã xóa tệp thành công"}