Gap 11: A/B reference track — /reference-analyze LUFS/dBTP/centroid + RefCompareModal A/B listen (5 test, 174 passed)
This commit is contained in:
@@ -485,6 +485,90 @@ async def list_user_files(req: MyFilesRequest, current_user: dict = Depends(get_
|
||||
user_files.sort(key=lambda x: x["created_at"], reverse=True)
|
||||
return user_files
|
||||
|
||||
class ReferenceAnalyzeRequest(BaseModel):
|
||||
path: Optional[str] = None # local path (media/browse server mode)
|
||||
file_id: Optional[str] = None # uploaded/processed file id
|
||||
|
||||
def _spectral_centroid_hz(audio, sr):
|
||||
"""Tone proxy: mean spectral centroid (Hz) over 1 s windows.
|
||||
Mono downmix + decimate to ~8 kHz (bounded cost). 0.0 for silence."""
|
||||
import numpy as np
|
||||
from scipy.signal import resample_poly
|
||||
x = audio
|
||||
if x.ndim > 1:
|
||||
x = np.mean(x, axis=0)
|
||||
if x.size == 0 or not np.any(x):
|
||||
return 0.0
|
||||
g = max(1, int(round(sr / 8000.0)))
|
||||
x = resample_poly(x, 1, g)
|
||||
sr8 = float(sr) / g
|
||||
win = int(round(1.0 * sr8))
|
||||
if win < 64 or x.size < win:
|
||||
return 0.0
|
||||
hann = np.hanning(win)
|
||||
freqs = np.fft.rfftfreq(win, 1.0 / sr8)
|
||||
cents = []
|
||||
step = win // 2
|
||||
for i in range(0, x.size - win, step):
|
||||
mag = np.abs(np.fft.rfft(x[i:i + win] * hann))
|
||||
msum = float(mag.sum())
|
||||
if msum > 1e-9:
|
||||
cents.append(float((freqs * mag).sum() / msum))
|
||||
if not cents:
|
||||
return 0.0
|
||||
return float(np.mean(cents))
|
||||
|
||||
@router.post("/reference-analyze")
|
||||
async def analyze_reference(req: ReferenceAnalyzeRequest, current_user: Optional[dict] = Depends(get_optional_user)):
|
||||
"""Gap 11: A/B reference track - analyze LUFS/dBTP/centroid of a
|
||||
reference file (local path from media/browse, or uploaded file_id),
|
||||
return compare metrics + playback url for the A/B toggle."""
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
from urllib.parse import quote
|
||||
from app.core import loudness
|
||||
path = ""
|
||||
url = ""
|
||||
if req.file_id:
|
||||
path = _resolve_storage_path(req.file_id)
|
||||
if not path:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
url = "/api/v1/audio/download/%s" % _safe_file_id(req.file_id)
|
||||
elif req.path:
|
||||
path = os.path.normpath(req.path)
|
||||
if not os.path.isfile(path):
|
||||
raise HTTPException(status_code=404, detail="Khong tim thay file")
|
||||
ext = os.path.splitext(path)[1].lower().lstrip(".")
|
||||
if ext not in ALLOWED_AUDIO_EXTENSIONS:
|
||||
raise HTTPException(status_code=400, detail="Dinh dang khong ho tro: %s" % ext)
|
||||
url = "/api/v1/media/file?path=" + quote(path)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Can path hoac file_id")
|
||||
try:
|
||||
info = sf.info(path)
|
||||
if info.samplerate and info.frames / info.samplerate > 900:
|
||||
raise HTTPException(status_code=413, detail="File qua dai (>15 phut)")
|
||||
data, sr = sf.read(path, dtype="float32", always_2d=True)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail="Khong doc duoc audio: %s" % e)
|
||||
audio = np.asarray(data.T, dtype=np.float64) # (ch, n)
|
||||
lufs = loudness.integrated_lufs(audio, sr)
|
||||
tp = loudness.true_peak_db(audio, sr)
|
||||
centroid = await asyncio.to_thread(_spectral_centroid_hz, audio, sr)
|
||||
dur = float(info.frames / max(1, info.samplerate or 1))
|
||||
return {
|
||||
"filename": os.path.basename(path),
|
||||
"duration": round(dur, 2),
|
||||
"sample_rate": int(info.samplerate or 0),
|
||||
"channels": int(info.channels or 0),
|
||||
"lufs": round(lufs, 2) if np.isfinite(lufs) else None,
|
||||
"true_peak_db": round(tp, 2) if np.isfinite(tp) else None,
|
||||
"centroid_hz": round(centroid, 0) if centroid > 0 else None,
|
||||
"url": url,
|
||||
}
|
||||
|
||||
@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"]
|
||||
|
||||
Reference in New Issue
Block a user