WasuratS commited on
Commit
d457caf
Β·
verified Β·
1 Parent(s): f372854

Initial commit app.py

Browse files
Files changed (1) hide show
  1. app.py +597 -0
app.py ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Marine Soundscape Analyzer β€” Gradio App
3
+ Designed for coral reef hydrophone recordings.
4
+
5
+ Analyses provided:
6
+ β€’ Waveform
7
+ β€’ Linear + Mel Spectrogram
8
+ β€’ Log-Frequency Spectrogram
9
+ β€’ Power Spectral Density (Welch)
10
+ β€’ Spectral Centroid over time
11
+ β€’ MFCC heatmap
12
+ β€’ Acoustic Complexity Index (ACI) – Pieretti et al. 2011
13
+ β€’ Bioacoustic Index (BI) – Boelman et al. 2007
14
+ β€’ Normalized Difference Soundscape Index (NDSI) – Kasten et al. 2012
15
+ β€’ Acoustic Diversity Index (ADI) – Villanueva-Rivera et al. 2011
16
+ β€’ Spectral Entropy (Hf) + Temporal Entropy (Ht) – Sueur et al. 2008
17
+ β€’ Summary report table
18
+ """
19
+
20
+ import warnings
21
+ warnings.filterwarnings("ignore")
22
+
23
+ import os
24
+ import numpy as np
25
+ import librosa
26
+ import librosa.display
27
+ import matplotlib
28
+ matplotlib.use("Agg")
29
+ import matplotlib.pyplot as plt
30
+ import matplotlib.ticker as mticker
31
+ from scipy import signal
32
+ import gradio as gr
33
+
34
+ # ──────────────────────────────────────────────────────────────────────────────
35
+ # Constants
36
+ # ──────────────────────────────────────────────────────────────────────────────
37
+ BG_COLOR = "#0b1e35"
38
+ GRID_COLOR = "#1e3a5f"
39
+ TEXT_COLOR = "#cfe8ff"
40
+ ACCENT = "#00d4ff"
41
+ N_FFT = 2048
42
+ HOP = 512
43
+ MAX_DUR_S = 300 # clip to 5 min for HuggingFace timeout safety
44
+
45
+ # ──────────────────────────────────────────────────────────────────────────────
46
+ # Acoustic Index Implementations
47
+ # ──────────────────────────────────────────────────────────────────────────────
48
+
49
+ def aci(Sxx: np.ndarray, j_bin: int = 5) -> float:
50
+ """Acoustic Complexity Index (Pieretti et al. 2011)."""
51
+ total = 0.0
52
+ for j in range(0, Sxx.shape[1] - j_bin, j_bin):
53
+ sl = Sxx[:, j : j + j_bin]
54
+ denom = sl.sum()
55
+ if denom > 0:
56
+ total += np.abs(np.diff(sl, axis=1)).sum() / denom
57
+ return float(total)
58
+
59
+
60
+ def bioacoustic_index(Sxx: np.ndarray, freqs: np.ndarray,
61
+ f_min: float = 2000, f_max: float = 8000) -> float:
62
+ """Bioacoustic Index (Boelman et al. 2007)."""
63
+ mask = (freqs >= f_min) & (freqs <= f_max)
64
+ if not mask.any():
65
+ return 0.0
66
+ sl = Sxx[mask, :]
67
+ db = librosa.amplitude_to_db(sl + 1e-10, ref=np.max)
68
+ mu = db.mean(axis=1)
69
+ shifted = mu - mu.min()
70
+ return float(shifted.mean())
71
+
72
+
73
+ def ndsi(Sxx: np.ndarray, freqs: np.ndarray) -> float:
74
+ """Normalized Difference Soundscape Index (Kasten et al. 2012).
75
+ Anthropogenic band: 1–2 kHz; Biotic band: 2–11 kHz."""
76
+ anthro = Sxx[(freqs >= 1000) & (freqs <= 2000), :].sum()
77
+ bio = Sxx[(freqs >= 2000) & (freqs <= 11000), :].sum()
78
+ denom = anthro + bio
79
+ return float((bio - anthro) / denom) if denom > 0 else 0.0
80
+
81
+
82
+ def adi(Sxx: np.ndarray, freqs: np.ndarray,
83
+ f_max: float = 10000, db_thresh: float = -50, n_bands: int = 10) -> float:
84
+ """Acoustic Diversity Index (Villanueva-Rivera et al. 2011)."""
85
+ mask = freqs <= f_max
86
+ db = librosa.amplitude_to_db(Sxx[mask, :] + 1e-10, ref=np.max)
87
+ mu = db.mean(axis=1)
88
+ band_sz = len(mu) // n_bands
89
+ if band_sz == 0:
90
+ return 0.0
91
+ counts = np.array([
92
+ (mu[i * band_sz : (i + 1) * band_sz] > db_thresh).sum()
93
+ for i in range(n_bands)
94
+ ], dtype=float)
95
+ total = counts.sum()
96
+ if total == 0:
97
+ return 0.0
98
+ p = counts / total
99
+ p = p[p > 0]
100
+ return float(-(p * np.log(p)).sum())
101
+
102
+
103
+ def spectral_entropy(Sxx: np.ndarray) -> float:
104
+ """Normalized spectral entropy Hf (Sueur et al. 2008)."""
105
+ power = (Sxx ** 2).mean(axis=1)
106
+ total = power.sum()
107
+ if total == 0:
108
+ return 0.0
109
+ p = power / total
110
+ p = p[p > 0]
111
+ return float(-(p * np.log(p)).sum() / np.log(len(power)))
112
+
113
+
114
+ def temporal_entropy(y: np.ndarray, n_env: int = 1000) -> float:
115
+ """Normalized temporal entropy Ht (Sueur et al. 2008)."""
116
+ frame = max(1, len(y) // n_env)
117
+ env = np.array([
118
+ np.sqrt((y[i : i + frame] ** 2).mean())
119
+ for i in range(0, len(y) - frame, frame)
120
+ ])
121
+ total = env.sum()
122
+ if total == 0:
123
+ return 0.0
124
+ p = env / total
125
+ p = p[p > 0]
126
+ return float(-(p * np.log(p)).sum() / np.log(len(env)))
127
+
128
+
129
+ # ──────────────────────────────────────────────────────────────────────────────
130
+ # Plot helpers
131
+ # ──────────────────────────────────────────────────────────────────────────────
132
+
133
+ def _make_fig(nrows=1, ncols=1, figsize=(12, 4)):
134
+ fig, axes = plt.subplots(nrows, ncols, figsize=figsize, facecolor=BG_COLOR)
135
+ return fig, axes
136
+
137
+
138
+ def _style(ax, title="", xlabel="", ylabel=""):
139
+ ax.set_facecolor(BG_COLOR)
140
+ ax.set_title(title, color=ACCENT, fontsize=12, fontweight="bold", pad=8)
141
+ ax.set_xlabel(xlabel, color=TEXT_COLOR, fontsize=9)
142
+ ax.set_ylabel(ylabel, color=TEXT_COLOR, fontsize=9)
143
+ ax.tick_params(colors=TEXT_COLOR, labelsize=8)
144
+ for sp in ax.spines.values():
145
+ sp.set_edgecolor(GRID_COLOR)
146
+ ax.grid(True, alpha=0.18, color=GRID_COLOR)
147
+
148
+
149
+ def _colorbar(fig, im, ax, label="dB"):
150
+ cb = fig.colorbar(im, ax=ax, pad=0.02, aspect=25)
151
+ cb.set_label(label, color=TEXT_COLOR, fontsize=8)
152
+ cb.ax.yaxis.set_tick_params(color=TEXT_COLOR, labelsize=7)
153
+ plt.setp(cb.ax.yaxis.get_ticklabels(), color=TEXT_COLOR)
154
+
155
+
156
+ # ──────────────────────────────────────────────────────────────────────────────
157
+ # Core Analysis
158
+ # ──────────────────────────────────────────────────────────────────────────────
159
+
160
+ def analyze(file_path):
161
+ if file_path is None:
162
+ return (None,) * 5 + ("⚠️ Please upload an audio file.",)
163
+
164
+ # ── Load ──────────────────────────────────────────────────────────────────
165
+ try:
166
+ y, sr = librosa.load(file_path, sr=None, mono=True, duration=MAX_DUR_S)
167
+ except Exception as exc:
168
+ return (None,) * 5 + (f"❌ Could not load file: {exc}",)
169
+
170
+ duration = len(y) / sr
171
+ clipped = duration >= MAX_DUR_S
172
+ n_fft_use = min(N_FFT, 2 ** int(np.log2(len(y) / 4))) # safe for short files
173
+
174
+ # ── STFT ──────────────────────────────────────────────────────────────────
175
+ D = librosa.stft(y, n_fft=n_fft_use, hop_length=HOP)
176
+ Sxx = np.abs(D)
177
+ D_db = librosa.amplitude_to_db(Sxx, ref=np.max)
178
+ freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft_use)
179
+ frame_t = librosa.frames_to_time(np.arange(Sxx.shape[1]), sr=sr, hop_length=HOP)
180
+
181
+ # ── Spectral features (used in multiple plots) ────────────────────────────
182
+ centroid = librosa.feature.spectral_centroid(y=y, sr=sr, hop_length=HOP)[0]
183
+ c_times = librosa.frames_to_time(np.arange(len(centroid)), sr=sr, hop_length=HOP)
184
+
185
+ # ══════════════════════════════════════════════════════════════════════════
186
+ # FIGURE 1 β€” Waveform
187
+ # ══════════════════════════════════════════════════════════════════════════
188
+ t = np.linspace(0, duration, len(y))
189
+ fig1, ax1 = _make_fig(figsize=(13, 3))
190
+ ax1.plot(t, y, color=ACCENT, linewidth=0.45, alpha=0.85)
191
+ ax1.fill_between(t, y, 0, alpha=0.15, color=ACCENT)
192
+ _style(ax1, "Waveform", "Time (s)", "Amplitude")
193
+ ax1.set_xlim(0, duration)
194
+ if clipped:
195
+ ax1.set_title(f"Waveform (showing first {MAX_DUR_S}s)", color=ACCENT,
196
+ fontsize=12, fontweight="bold")
197
+ fig1.tight_layout(pad=0.8)
198
+
199
+ # ══════════════════════════════════════════════════════════════════════════
200
+ # FIGURE 2 β€” Spectrograms (linear + mel + log)
201
+ # ══════════════════════════════════════════════════════════════════════════
202
+ fmax_mel = min(sr // 2, 20000)
203
+ mel_spec = librosa.feature.melspectrogram(
204
+ y=y, sr=sr, n_fft=n_fft_use, hop_length=HOP, n_mels=128, fmax=fmax_mel
205
+ )
206
+ mel_db = librosa.power_to_db(mel_spec, ref=np.max)
207
+
208
+ fig2, axes2 = _make_fig(3, 1, figsize=(13, 11))
209
+ CMAP = "magma"
210
+ VRANGE = dict(vmin=-80, vmax=0)
211
+
212
+ # Linear
213
+ im1 = librosa.display.specshow(
214
+ D_db, sr=sr, hop_length=HOP, x_axis="time", y_axis="hz",
215
+ ax=axes2[0], cmap=CMAP, **VRANGE
216
+ )
217
+ _style(axes2[0], "Spectrogram β€” Linear Frequency", "Time (s)", "Frequency (Hz)")
218
+ _colorbar(fig2, im1, axes2[0])
219
+
220
+ # Mel
221
+ im2 = librosa.display.specshow(
222
+ mel_db, sr=sr, hop_length=HOP, x_axis="time", y_axis="mel",
223
+ ax=axes2[1], cmap=CMAP, fmax=fmax_mel, **VRANGE
224
+ )
225
+ _style(axes2[1], "Spectrogram β€” Mel Scale", "Time (s)", "Mel Frequency")
226
+ _colorbar(fig2, im2, axes2[1])
227
+
228
+ # Log
229
+ im3 = librosa.display.specshow(
230
+ D_db, sr=sr, hop_length=HOP, x_axis="time", y_axis="log",
231
+ ax=axes2[2], cmap=CMAP, **VRANGE
232
+ )
233
+ _style(axes2[2], "Spectrogram β€” Log Frequency", "Time (s)", "Frequency (Hz, log)")
234
+ _colorbar(fig2, im3, axes2[2])
235
+
236
+ fig2.tight_layout(pad=1.2)
237
+
238
+ # ══════════════════════════════════════════════════════════════════════════
239
+ # FIGURE 3 β€” PSD Β· Spectral Centroid Β· MFCC
240
+ # ══════════════════════════════════════════════════════════════════════════
241
+ f_psd, psd = signal.welch(y, sr, nperseg=min(4096, len(y) // 2))
242
+ psd_db = 10 * np.log10(psd + 1e-20)
243
+
244
+ mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20, hop_length=HOP)
245
+ mfcc_delta = librosa.feature.delta(mfcc)
246
+
247
+ fig3, axes3 = _make_fig(3, 1, figsize=(13, 12))
248
+
249
+ # PSD
250
+ axes3[0].plot(f_psd[1:], psd_db[1:], color="#ff7f0e", linewidth=1.3)
251
+ marine_bands = [
252
+ (20, 1000, "#2ca02c", "Fish & low-freq (20–1 kHz)"),
253
+ (1000, 5000, "#ff7f0e", "Snapping shrimp (1–5 kHz)"),
254
+ (5000, min(sr / 2, 20000), "#d62728", "High-freq biotic (5–20 kHz)"),
255
+ ]
256
+ for flo, fhi, color, label in marine_bands:
257
+ if fhi <= sr / 2 and flo < sr / 2:
258
+ axes3[0].axvspan(flo, min(fhi, sr / 2), alpha=0.12, color=color, label=label)
259
+ axes3[0].set_xscale("log")
260
+ axes3[0].set_xlim(max(20, f_psd[1]), sr / 2)
261
+ _style(axes3[0], "Power Spectral Density (Welch)", "Frequency (Hz)", "PSD (dB/Hz)")
262
+ axes3[0].legend(fontsize=8, loc="lower left",
263
+ facecolor=BG_COLOR, edgecolor=GRID_COLOR, labelcolor=TEXT_COLOR)
264
+
265
+ # Spectral centroid
266
+ axes3[1].plot(c_times, centroid, color="#9467bd", linewidth=1.1, alpha=0.9)
267
+ axes3[1].fill_between(c_times, centroid, alpha=0.12, color="#9467bd")
268
+ axes3[1].set_xlim(0, duration)
269
+ _style(axes3[1], "Spectral Centroid Over Time", "Time (s)", "Frequency (Hz)")
270
+
271
+ # MFCC
272
+ im_mfcc = librosa.display.specshow(
273
+ mfcc, sr=sr, hop_length=HOP, x_axis="time",
274
+ ax=axes3[2], cmap="coolwarm"
275
+ )
276
+ _style(axes3[2], "MFCCs (20 coefficients)", "Time (s)", "MFCC Coefficient")
277
+ axes3[2].set_facecolor(BG_COLOR)
278
+ _colorbar(fig3, im_mfcc, axes3[2], label="Amplitude")
279
+
280
+ fig3.tight_layout(pad=1.2)
281
+
282
+ # ══════════════════════════════════════════════════════════════════════════
283
+ # FIGURE 4 β€” Acoustic Indices over time
284
+ # ══════════════════════════════════════════════════════════════════════════
285
+ # Adaptive window: aim for β‰₯8 windows; each window 5–60 s
286
+ win_s = max(5.0, min(60.0, duration / 8))
287
+ win_len = int(sr * win_s)
288
+ n_win = max(3, len(y) // win_len)
289
+ win_len = len(y) // n_win # recompute for even coverage
290
+
291
+ aci_v, bi_v, ndsi_v, adi_v, rms_v, centers = [], [], [], [], [], []
292
+ for i in range(n_win):
293
+ seg = y[i * win_len : (i + 1) * win_len]
294
+ centers.append((i + 0.5) * win_len / sr)
295
+ D_s = librosa.stft(seg, n_fft=n_fft_use, hop_length=HOP)
296
+ Sxx_s = np.abs(D_s)
297
+ aci_v.append(aci(Sxx_s))
298
+ bi_v.append(bioacoustic_index(Sxx_s, freqs))
299
+ ndsi_v.append(ndsi(Sxx_s, freqs))
300
+ adi_v.append(adi(Sxx_s, freqs))
301
+ rms_v.append(float(np.sqrt((seg ** 2).mean())))
302
+
303
+ centers = np.array(centers)
304
+ bw = win_len / sr * 0.72
305
+
306
+ fig4, axes4 = _make_fig(3, 2, figsize=(14, 13))
307
+
308
+ def bar_plot(ax, vals, color, title, ylabel):
309
+ ax.bar(centers, vals, width=bw, color=color, alpha=0.82)
310
+ _style(ax, title, "Time (s)", ylabel)
311
+ ax.set_xlim(0, duration)
312
+
313
+ bar_plot(axes4[0, 0], aci_v, "#1f77b4", "Acoustic Complexity Index (ACI)", "ACI")
314
+ bar_plot(axes4[0, 1], bi_v, "#2ca02c", "Bioacoustic Index (BI)", "BI")
315
+
316
+ # NDSI β€” colour by sign
317
+ ndsi_colors = ["#2ca02c" if v >= 0 else "#d62728" for v in ndsi_v]
318
+ axes4[1, 0].bar(centers, ndsi_v, width=bw, color=ndsi_colors, alpha=0.82)
319
+ axes4[1, 0].axhline(0, color=TEXT_COLOR, linewidth=0.8, linestyle="--", alpha=0.6)
320
+ _style(axes4[1, 0], "NDSI (green > 0 = biotic dominated)", "Time (s)", "NDSI")
321
+ axes4[1, 0].set_xlim(0, duration)
322
+
323
+ bar_plot(axes4[1, 1], adi_v, "#ff7f0e", "Acoustic Diversity Index (ADI)", "ADI")
324
+
325
+ # RMS energy
326
+ axes4[2, 0].plot(centers, rms_v, "o-", color="#e377c2", linewidth=1.6,
327
+ markersize=5, alpha=0.9)
328
+ axes4[2, 0].fill_between(centers, rms_v, alpha=0.15, color="#e377c2")
329
+ _style(axes4[2, 0], "RMS Energy Over Time", "Time (s)", "RMS Amplitude")
330
+ axes4[2, 0].set_xlim(0, duration)
331
+
332
+ # Short-time RMS spectrogram (energy heatmap)
333
+ rms_frame = librosa.feature.rms(y=y, frame_length=n_fft_use, hop_length=HOP)
334
+ rms_db_frame = librosa.amplitude_to_db(rms_frame, ref=np.max)
335
+ axes4[2, 1].plot(
336
+ librosa.frames_to_time(np.arange(rms_frame.shape[1]), sr=sr, hop_length=HOP),
337
+ rms_db_frame[0], color=ACCENT, linewidth=0.8, alpha=0.9
338
+ )
339
+ _style(axes4[2, 1], "Short-time RMS Energy (dBFS)", "Time (s)", "RMS (dB)")
340
+ axes4[2, 1].set_xlim(0, duration)
341
+
342
+ fig4.tight_layout(pad=1.2)
343
+
344
+ # ══════════════════════════════════════════════════════════════════════════
345
+ # FIGURE 5 β€” Onset + Bandwidth over time
346
+ # ══════════════════════════════════════════════════════════════════════════
347
+ onset_frames = librosa.onset.onset_detect(y=y, sr=sr, hop_length=HOP)
348
+ onset_times = librosa.frames_to_time(onset_frames, sr=sr, hop_length=HOP)
349
+
350
+ bandwidth = librosa.feature.spectral_bandwidth(y=y, sr=sr, hop_length=HOP)[0]
351
+ rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr, hop_length=HOP, roll_percent=0.85)[0]
352
+ flatness = librosa.feature.spectral_flatness(y=y, hop_length=HOP)[0]
353
+ zcr_frame = librosa.feature.zero_crossing_rate(y, hop_length=HOP)[0]
354
+
355
+ fig5, axes5 = _make_fig(3, 2, figsize=(14, 11))
356
+
357
+ axes5[0, 0].plot(c_times, bandwidth, color="#17becf", linewidth=0.9, alpha=0.9)
358
+ _style(axes5[0, 0], "Spectral Bandwidth Over Time", "Time (s)", "Bandwidth (Hz)")
359
+ axes5[0, 0].set_xlim(0, duration)
360
+
361
+ axes5[0, 1].plot(c_times, rolloff, color="#bcbd22", linewidth=0.9, alpha=0.9)
362
+ _style(axes5[0, 1], "Spectral Rolloff (85%) Over Time", "Time (s)", "Frequency (Hz)")
363
+ axes5[0, 1].set_xlim(0, duration)
364
+
365
+ axes5[1, 0].plot(c_times, flatness, color="#7f7f7f", linewidth=0.9, alpha=0.9)
366
+ _style(axes5[1, 0], "Spectral Flatness Over Time", "Time (s)", "Flatness [0–1]")
367
+ axes5[1, 0].set_xlim(0, duration)
368
+
369
+ axes5[1, 1].plot(c_times, zcr_frame, color="#8c564b", linewidth=0.7, alpha=0.9)
370
+ _style(axes5[1, 1], "Zero Crossing Rate Over Time", "Time (s)", "ZCR")
371
+ axes5[1, 1].set_xlim(0, duration)
372
+
373
+ # Onset plot
374
+ axes5[2, 0].plot(c_times, rms_db_frame[0], color=ACCENT, linewidth=0.7, alpha=0.7,
375
+ label="RMS (dBFS)")
376
+ for ot in onset_times:
377
+ axes5[2, 0].axvline(ot, color="#ff4444", linewidth=0.6, alpha=0.6)
378
+ axes5[2, 0].set_xlim(0, duration)
379
+ _style(axes5[2, 0], f"Onset Detection ({len(onset_times)} events)", "Time (s)", "RMS (dB)")
380
+ axes5[2, 0].text(0.01, 0.96, f"{len(onset_times)} onsets detected",
381
+ transform=axes5[2, 0].transAxes,
382
+ color="#ff4444", fontsize=9, va="top")
383
+
384
+ # MFCC delta (showing change)
385
+ im_delta = librosa.display.specshow(
386
+ mfcc_delta, sr=sr, hop_length=HOP, x_axis="time",
387
+ ax=axes5[2, 1], cmap="RdBu_r"
388
+ )
389
+ axes5[2, 1].set_facecolor(BG_COLOR)
390
+ _style(axes5[2, 1], "MFCC Delta (Rate of Change)", "Time (s)", "MFCC Coefficient")
391
+ _colorbar(fig5, im_delta, axes5[2, 1], label="Ξ” Amplitude")
392
+
393
+ fig5.tight_layout(pad=1.2)
394
+
395
+ # ══════════════════════════════════════════════════════════════════════════
396
+ # Summary Report
397
+ # ══════════════════════════════════════════════════════════════════════════
398
+ rms_overall = float(np.sqrt((y ** 2).mean()))
399
+ peak = float(np.abs(y).max())
400
+ rms_db_val = 20 * np.log10(rms_overall + 1e-12)
401
+ peak_db_val = 20 * np.log10(peak + 1e-12)
402
+ dyn_range = 20 * np.log10(peak / (rms_overall + 1e-12))
403
+ zcr_mean = float(librosa.feature.zero_crossing_rate(y).mean())
404
+ sp_c_mean = float(centroid.mean())
405
+ sp_bw_mean = float(bandwidth.mean())
406
+ sp_ro_mean = float(rolloff.mean())
407
+ sp_fl_mean = float(flatness.mean())
408
+
409
+ # Top 5 dominant frequencies (mean spectrum)
410
+ top5 = freqs[np.argsort((Sxx ** 2).mean(axis=1))[-5:][::-1]]
411
+
412
+ # Global indices
413
+ aci_g = aci(Sxx)
414
+ bi_g = bioacoustic_index(Sxx, freqs)
415
+ ndsi_g = ndsi(Sxx, freqs)
416
+ adi_g = adi(Sxx, freqs)
417
+ Hf = spectral_entropy(Sxx)
418
+ Ht = temporal_entropy(y)
419
+ H_total = Hf * Ht
420
+
421
+ ndsi_label = (
422
+ "Strong biotic dominance" if ndsi_g > 0.5 else
423
+ "Moderate biotic dominance" if ndsi_g > 0.0 else
424
+ "Moderate anthropogenic noise" if ndsi_g > -0.5 else
425
+ "Strong anthropogenic noise"
426
+ )
427
+ aci_label = (
428
+ "Very high complexity" if aci_g > 10000 else
429
+ "High complexity" if aci_g > 5000 else
430
+ "Moderate complexity" if aci_g > 1000 else
431
+ "Low complexity"
432
+ )
433
+
434
+ clipped_note = (
435
+ f"\n> ⚠️ File longer than {MAX_DUR_S}s β€” analysis performed on first {MAX_DUR_S}s only.\n"
436
+ if clipped else ""
437
+ )
438
+
439
+ report = f"""{clipped_note}
440
+ ## πŸ“‹ Analysis Report
441
+
442
+ ### 🎡 Basic Information
443
+ | Parameter | Value |
444
+ |-----------|-------|
445
+ | Duration | {duration:.2f} s |
446
+ | Sample Rate | {sr:,} Hz |
447
+ | Total Samples | {len(y):,} |
448
+ | Processing Mode | Mono |
449
+ | Analysis Windows | {n_win} Γ— {win_len/sr:.1f} s |
450
+
451
+ ---
452
+
453
+ ### πŸ“ˆ Amplitude Statistics
454
+ | Parameter | Value |
455
+ |-----------|-------|
456
+ | RMS Level | {rms_db_val:.1f} dBFS |
457
+ | Peak Level | {peak_db_val:.1f} dBFS |
458
+ | Dynamic Range | {dyn_range:.1f} dB |
459
+ | Zero Crossing Rate | {zcr_mean:.5f} |
460
+ | Detected Onsets | {len(onset_times)} events |
461
+
462
+ ---
463
+
464
+ ### 🌊 Spectral Features (mean over recording)
465
+ | Feature | Value |
466
+ |---------|-------|
467
+ | Spectral Centroid | {sp_c_mean:.1f} Hz |
468
+ | Spectral Bandwidth | {sp_bw_mean:.1f} Hz |
469
+ | Spectral Rolloff (85%) | {sp_ro_mean:.1f} Hz |
470
+ | Spectral Flatness | {sp_fl_mean:.5f} |
471
+ | Top 5 Dominant Freqs | {', '.join(f'{f:.0f} Hz' for f in top5)} |
472
+
473
+ ---
474
+
475
+ ### 🧬 Acoustic Indices (whole recording)
476
+ | Index | Value | Interpretation |
477
+ |-------|-------|----------------|
478
+ | **ACI** | {aci_g:.1f} | {aci_label} β€” higher = more varied amplitude patterns |
479
+ | **BI** | {bi_g:.2f} | Biological activity intensity in 2–8 kHz band |
480
+ | **NDSI** | {ndsi_g:.3f} | {ndsi_label} |
481
+ | **ADI** | {adi_g:.3f} | Shannon diversity across frequency bands |
482
+ | **Hf** (Spectral Entropy) | {Hf:.4f} | 0 = tonal, 1 = uniform spectrum |
483
+ | **Ht** (Temporal Entropy) | {Ht:.4f} | 0 = impulsive, 1 = stationary |
484
+ | **H** (Total Entropy) | {H_total:.4f} | Combined soundscape heterogeneity |
485
+
486
+ ---
487
+
488
+ ### 🐠 Marine Coral Reef Frequency Guide
489
+ | Band | Range | Typical Sources |
490
+ |------|-------|-----------------|
491
+ | Low | 20 – 1,000 Hz | Fish choruses, breaking waves, vessel traffic |
492
+ | Snapping Shrimp | 1 – 5 kHz | *Alpheid* snapping shrimp β€” reef health indicator |
493
+ | High Biotic | 5 – 20 kHz | Small crustaceans, urchins, high-frequency fish |
494
+
495
+ > **Reef health note:** Healthy reefs typically show strong broadband energy from snapping shrimp (1–20 kHz crackling), high ACI, and positive NDSI. Degraded reefs tend to be quieter and more tonally uniform.
496
+
497
+ ---
498
+ *Indices: ACI (Pieretti et al. 2011) Β· BI (Boelman et al. 2007) Β· NDSI (Kasten et al. 2012) Β· ADI (Villanueva-Rivera et al. 2011) Β· H (Sueur et al. 2008)*
499
+ """
500
+
501
+ return fig1, fig2, fig3, fig4, fig5, report
502
+
503
+
504
+ # ──────────────────────────────────────────────────────────────────────────────
505
+ # Gradio Interface
506
+ # ──────────────────────────────────────────────────────────────────────────────
507
+
508
+ CSS = """
509
+ body, .gradio-container {
510
+ background: linear-gradient(160deg, #071324 0%, #0b1e35 60%, #071324 100%) !important;
511
+ }
512
+ h1 { font-size: 2rem !important; }
513
+ .gr-button { font-weight: 600; }
514
+ footer { display: none !important; }
515
+ """
516
+
517
+ _HERE = os.path.dirname(os.path.abspath(__file__))
518
+ _DATA = os.path.join(_HERE, "..", "data")
519
+
520
+ EXAMPLES = [
521
+ [os.path.join(_DATA, "Invertebrates", "Snapping Shrimp.wav")],
522
+ [os.path.join(_DATA, "Invertebrates", "Ghost Crab: Gastric Mill Stridulation.wav")],
523
+ [os.path.join(_DATA, "Mammal", "Humpback Whale Song.wav")],
524
+ [os.path.join(_DATA, "Mammal", "Fish", "Red Grouper Vocalization.wav")],
525
+ ]
526
+ # Filter to only examples that actually exist (avoids errors on HuggingFace)
527
+ EXAMPLES = [e for e in EXAMPLES if os.path.isfile(e[0])]
528
+
529
+ with gr.Blocks(title="🌊 Marine Soundscape Analyzer", css=CSS,
530
+ theme=gr.themes.Base(
531
+ primary_hue="cyan",
532
+ secondary_hue="blue",
533
+ neutral_hue="slate",
534
+ font=gr.themes.GoogleFont("Inter"),
535
+ )) as demo:
536
+
537
+ gr.Markdown("""
538
+ # 🌊 Marine Soundscape Analyzer
539
+ **Coral Reef Acoustic Analysis Tool**
540
+
541
+ Upload a hydrophone recording to generate spectrograms, power spectral density, acoustic indices,
542
+ and a full analysis report β€” tailored for coral reef soundscape monitoring.
543
+
544
+ Supported formats: **WAV Β· MP3 Β· FLAC Β· OGG Β· AIFF** Β· Maximum analysed duration: **5 minutes**
545
+ """)
546
+
547
+ with gr.Row(equal_height=True):
548
+ with gr.Column(scale=3):
549
+ audio_in = gr.Audio(label="πŸ“ Upload Sound File", type="filepath")
550
+ with gr.Column(scale=1):
551
+ gr.Markdown("""
552
+ ### Acoustic Indices
553
+ | Index | What it measures |
554
+ |-------|-----------------|
555
+ | **ACI** | Amplitude complexity |
556
+ | **BI** | Biological activity |
557
+ | **NDSI** | Biotic vs anthropogenic |
558
+ | **ADI** | Frequency diversity |
559
+ | **Hf / Ht** | Spectral / temporal entropy |
560
+ """)
561
+
562
+ analyze_btn = gr.Button("πŸ” Analyse Recording", variant="primary", size="lg")
563
+
564
+ with gr.Tabs():
565
+ with gr.Tab("πŸ“Š Waveform"):
566
+ plot_wave = gr.Plot()
567
+ with gr.Tab("πŸ”Š Spectrograms"):
568
+ plot_spec = gr.Plot()
569
+ with gr.Tab("πŸ“‘ Frequency Analysis + MFCC"):
570
+ plot_freq = gr.Plot()
571
+ with gr.Tab("🧬 Acoustic Indices"):
572
+ plot_idx = gr.Plot()
573
+ with gr.Tab("πŸ“ Temporal Features"):
574
+ plot_temp = gr.Plot()
575
+ with gr.Tab("πŸ“‹ Report"):
576
+ report_out = gr.Markdown()
577
+
578
+ analyze_btn.click(
579
+ fn=analyze,
580
+ inputs=[audio_in],
581
+ outputs=[plot_wave, plot_spec, plot_freq, plot_idx, plot_temp, report_out],
582
+ )
583
+
584
+ if EXAMPLES:
585
+ gr.Examples(
586
+ examples=EXAMPLES,
587
+ inputs=[audio_in],
588
+ label="🎧 Example Marine Recordings",
589
+ )
590
+
591
+ gr.Markdown("""
592
+ ---
593
+ *Built for marine bioacoustic research Β· References: Pieretti et al. 2011, Boelman et al. 2007, Kasten et al. 2012, Villanueva-Rivera et al. 2011, Sueur et al. 2008*
594
+ """)
595
+
596
+ if __name__ == "__main__":
597
+ demo.launch(share=False)