Spaces:
Running
Running
SONICS detect API (FastAPI wrapper, SpecTTTra-alpha-120s)
Browse files
app.py
CHANGED
|
@@ -9,6 +9,7 @@ from fastapi import FastAPI, File, Header, HTTPException, UploadFile
|
|
| 9 |
MODEL_ID = os.environ.get("MODEL_ID", "awsaf49/sonics-spectttra-alpha-120s")
|
| 10 |
API_KEY = os.environ.get("DETECT_API_KEY", "")
|
| 11 |
MAX_BYTES = 25 * 1024 * 1024
|
|
|
|
| 12 |
|
| 13 |
torch.set_num_threads(2)
|
| 14 |
|
|
@@ -31,6 +32,58 @@ def health():
|
|
| 31 |
return {"ok": True, "model": MODEL_ID, "loaded": model is not None}
|
| 32 |
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
@app.post("/detect")
|
| 35 |
def detect(audio: UploadFile = File(...), x_detect_key: str = Header(default="")):
|
| 36 |
if API_KEY and x_detect_key != API_KEY:
|
|
@@ -50,18 +103,26 @@ def detect(audio: UploadFile = File(...), x_detect_key: str = Header(default="")
|
|
| 50 |
tmp.write(data)
|
| 51 |
tmp_path = tmp.name
|
| 52 |
try:
|
| 53 |
-
|
|
|
|
| 54 |
finally:
|
| 55 |
os.unlink(tmp_path)
|
| 56 |
except Exception:
|
| 57 |
raise HTTPException(status_code=400, detail="Could not decode audio")
|
| 58 |
|
| 59 |
-
if
|
| 60 |
raise HTTPException(status_code=400, detail="Audio too short (min 3s)")
|
| 61 |
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
max_time = model.config.audio.max_time
|
| 64 |
-
chunk_samples = int(max_time *
|
| 65 |
total_chunks = len(y) // chunk_samples
|
| 66 |
middle_idx = total_chunks // 2
|
| 67 |
start = middle_idx * chunk_samples
|
|
@@ -75,6 +136,7 @@ def detect(audio: UploadFile = File(...), x_detect_key: str = Header(default="")
|
|
| 75 |
|
| 76 |
return {
|
| 77 |
"ai_prob": round(ai_prob, 4),
|
| 78 |
-
"duration_s": round(len(
|
| 79 |
"model": MODEL_ID,
|
|
|
|
| 80 |
}
|
|
|
|
| 9 |
MODEL_ID = os.environ.get("MODEL_ID", "awsaf49/sonics-spectttra-alpha-120s")
|
| 10 |
API_KEY = os.environ.get("DETECT_API_KEY", "")
|
| 11 |
MAX_BYTES = 25 * 1024 * 1024
|
| 12 |
+
MODEL_SR = 16000
|
| 13 |
|
| 14 |
torch.set_num_threads(2)
|
| 15 |
|
|
|
|
| 32 |
return {"ok": True, "model": MODEL_ID, "loaded": model is not None}
|
| 33 |
|
| 34 |
|
| 35 |
+
def compute_signals(y: np.ndarray, sr: int) -> dict:
|
| 36 |
+
"""Audio-forensic descriptors computed at the file's native sample rate."""
|
| 37 |
+
out: dict = {"native_sample_rate": int(sr)}
|
| 38 |
+
try:
|
| 39 |
+
S = np.abs(librosa.stft(y, n_fft=2048, hop_length=512))
|
| 40 |
+
freqs = librosa.fft_frequencies(sr=sr, n_fft=2048)
|
| 41 |
+
mag = S.mean(axis=1)
|
| 42 |
+
power = mag**2
|
| 43 |
+
total = float(power.sum()) or 1e-12
|
| 44 |
+
|
| 45 |
+
# Spectral cutoff: frequency below which 99% of energy lives.
|
| 46 |
+
cum = np.cumsum(power)
|
| 47 |
+
idx = int(np.searchsorted(cum, 0.99 * cum[-1]))
|
| 48 |
+
out["spectral_cutoff_hz"] = int(freqs[min(idx, len(freqs) - 1)])
|
| 49 |
+
|
| 50 |
+
# Share of energy above 10 kHz (only meaningful when sr allows it).
|
| 51 |
+
if sr >= 32000:
|
| 52 |
+
out["hf_energy_ratio"] = round(float(power[freqs >= 10000].sum() / total), 5)
|
| 53 |
+
else:
|
| 54 |
+
out["hf_energy_ratio"] = None
|
| 55 |
+
|
| 56 |
+
# Dynamic range: dB spread between loud and quiet frames.
|
| 57 |
+
rms = librosa.feature.rms(y=y, hop_length=512)[0]
|
| 58 |
+
rms = rms[rms > 1e-6]
|
| 59 |
+
if rms.size:
|
| 60 |
+
out["dynamic_range_db"] = round(
|
| 61 |
+
float(20 * np.log10(np.percentile(rms, 95) / max(np.percentile(rms, 10), 1e-9))), 1
|
| 62 |
+
)
|
| 63 |
+
else:
|
| 64 |
+
out["dynamic_range_db"] = None
|
| 65 |
+
|
| 66 |
+
# Spectral flatness: noisiness/synthetic-ness of the average spectrum.
|
| 67 |
+
out["spectral_flatness"] = round(float(librosa.feature.spectral_flatness(y=y).mean()), 4)
|
| 68 |
+
|
| 69 |
+
# Tempo and beat regularity (coefficient of variation of inter-beat intervals).
|
| 70 |
+
try:
|
| 71 |
+
tempo, beats = librosa.beat.beat_track(y=y, sr=sr, hop_length=512)
|
| 72 |
+
tempo_val = float(np.atleast_1d(tempo)[0]) if tempo is not None else 0.0
|
| 73 |
+
out["tempo_bpm"] = round(tempo_val, 1) if tempo_val > 0 else None
|
| 74 |
+
if beats is not None and len(beats) > 8:
|
| 75 |
+
ibis = np.diff(librosa.frames_to_time(beats, sr=sr, hop_length=512))
|
| 76 |
+
out["tempo_cv"] = round(float(np.std(ibis) / max(np.mean(ibis), 1e-9)), 4)
|
| 77 |
+
else:
|
| 78 |
+
out["tempo_cv"] = None
|
| 79 |
+
except Exception:
|
| 80 |
+
out["tempo_bpm"] = None
|
| 81 |
+
out["tempo_cv"] = None
|
| 82 |
+
except Exception:
|
| 83 |
+
pass
|
| 84 |
+
return out
|
| 85 |
+
|
| 86 |
+
|
| 87 |
@app.post("/detect")
|
| 88 |
def detect(audio: UploadFile = File(...), x_detect_key: str = Header(default="")):
|
| 89 |
if API_KEY and x_detect_key != API_KEY:
|
|
|
|
| 103 |
tmp.write(data)
|
| 104 |
tmp_path = tmp.name
|
| 105 |
try:
|
| 106 |
+
# Native rate for forensic signals (high-frequency artifacts live here).
|
| 107 |
+
y_native, sr_native = librosa.load(tmp_path, sr=None, mono=True)
|
| 108 |
finally:
|
| 109 |
os.unlink(tmp_path)
|
| 110 |
except Exception:
|
| 111 |
raise HTTPException(status_code=400, detail="Could not decode audio")
|
| 112 |
|
| 113 |
+
if y_native.size < sr_native * 3:
|
| 114 |
raise HTTPException(status_code=400, detail="Audio too short (min 3s)")
|
| 115 |
|
| 116 |
+
signals = compute_signals(y_native, sr_native)
|
| 117 |
+
|
| 118 |
+
# Model expects 16 kHz; score the middle max_time window (official demo logic).
|
| 119 |
+
y = (
|
| 120 |
+
librosa.resample(y_native, orig_sr=sr_native, target_sr=MODEL_SR)
|
| 121 |
+
if sr_native != MODEL_SR
|
| 122 |
+
else y_native
|
| 123 |
+
)
|
| 124 |
max_time = model.config.audio.max_time
|
| 125 |
+
chunk_samples = int(max_time * MODEL_SR)
|
| 126 |
total_chunks = len(y) // chunk_samples
|
| 127 |
middle_idx = total_chunks // 2
|
| 128 |
start = middle_idx * chunk_samples
|
|
|
|
| 136 |
|
| 137 |
return {
|
| 138 |
"ai_prob": round(ai_prob, 4),
|
| 139 |
+
"duration_s": round(len(y_native) / sr_native, 1),
|
| 140 |
"model": MODEL_ID,
|
| 141 |
+
"signals": signals,
|
| 142 |
}
|