Gary10 commited on
Commit
4376b0e
·
verified ·
1 Parent(s): 4265309

SONICS detect API (FastAPI wrapper, SpecTTTra-alpha-120s)

Browse files
Files changed (1) hide show
  1. app.py +67 -5
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
- y, sr = librosa.load(tmp_path, sr=16000, mono=True)
 
54
  finally:
55
  os.unlink(tmp_path)
56
  except Exception:
57
  raise HTTPException(status_code=400, detail="Could not decode audio")
58
 
59
- if y.size < sr * 3:
60
  raise HTTPException(status_code=400, detail="Audio too short (min 3s)")
61
 
62
- # Same chunking as the official SONICS demo: score the middle max_time window.
 
 
 
 
 
 
 
63
  max_time = model.config.audio.max_time
64
- chunk_samples = int(max_time * sr)
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(y) / sr, 1),
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
  }