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:
+88
-4
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user