Spaces:
Running on Zero
Running on Zero
| """ | |
| Marine Soundscape Analyzer β Gradio App | |
| Designed for coral reef hydrophone recordings. | |
| Analyses provided: | |
| β’ Waveform | |
| β’ Linear + Mel Spectrogram | |
| β’ Log-Frequency Spectrogram | |
| β’ Power Spectral Density (Welch) | |
| β’ Spectral Centroid over time | |
| β’ MFCC heatmap | |
| β’ Acoustic Complexity Index (ACI) β Pieretti et al. 2011 | |
| β’ Bioacoustic Index (BI) β Boelman et al. 2007 | |
| β’ Normalized Difference Soundscape Index (NDSI) β Kasten et al. 2012 | |
| β’ Acoustic Diversity Index (ADI) β Villanueva-Rivera et al. 2011 | |
| β’ Spectral Entropy (Hf) + Temporal Entropy (Ht) β Sueur et al. 2008 | |
| β’ Summary report table | |
| """ | |
| import warnings | |
| warnings.filterwarnings("ignore") | |
| import os | |
| import numpy as np | |
| import librosa | |
| import librosa.display | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import matplotlib.ticker as mticker | |
| from scipy import signal | |
| import gradio as gr | |
| # This app is CPU-only (librosa/scipy/matplotlib). The `spaces` import is only | |
| # needed if this Space is ever configured with ZeroGPU hardware, which requires | |
| # at least one @spaces.GPU-decorated function to exist at startup. Recommended | |
| # setting is "CPU basic" under Space Settings β Hardware, in which case `spaces` | |
| # won't even be installed and the block below is simply skipped. | |
| try: | |
| import spaces | |
| def _zerogpu_keepalive(): | |
| """No-op so ZeroGPU Spaces pass their startup check. Not used on CPU.""" | |
| return None | |
| except ImportError: | |
| pass | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Constants | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Coral Reef Lagoon palette β deep ocean water with living-coral & seafoam pops | |
| # Deep Ocean #0b3d4a Β· Lagoon #1a5c6b Β· Coral #ff6f59 Β· Seafoam #4cd9c0 Β· Sandy Gold #ffc857 | |
| BG_COLOR = "#0B3D4A" # Deep Ocean Teal β page & figure background | |
| SURFACE = "#123F4D" # Lagoon Teal β card / plot-axis surface (pops against deep ocean) | |
| GRID_COLOR = "#2A7A85" # Reef Teal β dividers, plot grid & borders | |
| TEXT_COLOR = "#E7FBFF" # Sea Foam White β headings & primary text | |
| TEXT_MUTED = "#8FD9E0" # soft aqua β secondary text | |
| ACCENT = "#FF6F59" # Living Coral β primary CTA / plot highlights | |
| ACCENT_HOVER = "#E5563F" # deeper coral β hover/pressed state | |
| SECONDARY = "#4CD9C0" # Seafoam Turquoise β secondary highlight (pairs with coral) | |
| SUCCESS = "#3ADC91" # sea-green β positive / biotic indicator | |
| WARNING = "#FFC857" # Sandy Gold β fresh accent (diversity indicator) | |
| ERROR = "#FF5C5C" # vivid coral-red β errors / negative indicator | |
| N_FFT = 2048 | |
| HOP = 512 | |
| MAX_DUR_S = 300 # clip to 5 min for HuggingFace timeout safety | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Acoustic Index Implementations | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def aci(Sxx: np.ndarray, j_bin: int = 5) -> float: | |
| """Acoustic Complexity Index (Pieretti et al. 2011).""" | |
| total = 0.0 | |
| for j in range(0, Sxx.shape[1] - j_bin, j_bin): | |
| sl = Sxx[:, j : j + j_bin] | |
| denom = sl.sum() | |
| if denom > 0: | |
| total += np.abs(np.diff(sl, axis=1)).sum() / denom | |
| return float(total) | |
| def bioacoustic_index(Sxx: np.ndarray, freqs: np.ndarray, | |
| f_min: float = 2000, f_max: float = 8000) -> float: | |
| """Bioacoustic Index (Boelman et al. 2007).""" | |
| mask = (freqs >= f_min) & (freqs <= f_max) | |
| if not mask.any(): | |
| return 0.0 | |
| sl = Sxx[mask, :] | |
| db = librosa.amplitude_to_db(sl + 1e-10, ref=np.max) | |
| mu = db.mean(axis=1) | |
| shifted = mu - mu.min() | |
| return float(shifted.mean()) | |
| def ndsi(Sxx: np.ndarray, freqs: np.ndarray) -> float: | |
| """Normalized Difference Soundscape Index (Kasten et al. 2012). | |
| Anthropogenic band: 1β2 kHz; Biotic band: 2β11 kHz.""" | |
| anthro = Sxx[(freqs >= 1000) & (freqs <= 2000), :].sum() | |
| bio = Sxx[(freqs >= 2000) & (freqs <= 11000), :].sum() | |
| denom = anthro + bio | |
| return float((bio - anthro) / denom) if denom > 0 else 0.0 | |
| def adi(Sxx: np.ndarray, freqs: np.ndarray, | |
| f_max: float = 10000, db_thresh: float = -50, n_bands: int = 10) -> float: | |
| """Acoustic Diversity Index (Villanueva-Rivera et al. 2011).""" | |
| mask = freqs <= f_max | |
| db = librosa.amplitude_to_db(Sxx[mask, :] + 1e-10, ref=np.max) | |
| mu = db.mean(axis=1) | |
| band_sz = len(mu) // n_bands | |
| if band_sz == 0: | |
| return 0.0 | |
| counts = np.array([ | |
| (mu[i * band_sz : (i + 1) * band_sz] > db_thresh).sum() | |
| for i in range(n_bands) | |
| ], dtype=float) | |
| total = counts.sum() | |
| if total == 0: | |
| return 0.0 | |
| p = counts / total | |
| p = p[p > 0] | |
| return float(-(p * np.log(p)).sum()) | |
| def spectral_entropy(Sxx: np.ndarray) -> float: | |
| """Normalized spectral entropy Hf (Sueur et al. 2008).""" | |
| power = (Sxx ** 2).mean(axis=1) | |
| total = power.sum() | |
| if total == 0: | |
| return 0.0 | |
| p = power / total | |
| p = p[p > 0] | |
| return float(-(p * np.log(p)).sum() / np.log(len(power))) | |
| def temporal_entropy(y: np.ndarray, n_env: int = 1000) -> float: | |
| """Normalized temporal entropy Ht (Sueur et al. 2008).""" | |
| frame = max(1, len(y) // n_env) | |
| env = np.array([ | |
| np.sqrt((y[i : i + frame] ** 2).mean()) | |
| for i in range(0, len(y) - frame, frame) | |
| ]) | |
| total = env.sum() | |
| if total == 0: | |
| return 0.0 | |
| p = env / total | |
| p = p[p > 0] | |
| return float(-(p * np.log(p)).sum() / np.log(len(env))) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Plot helpers | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _make_fig(nrows=1, ncols=1, figsize=(12, 4)): | |
| fig, axes = plt.subplots(nrows, ncols, figsize=figsize, facecolor=BG_COLOR) | |
| return fig, axes | |
| def _style(ax, title="", xlabel="", ylabel=""): | |
| ax.set_facecolor(SURFACE) | |
| ax.set_title(title, color=ACCENT, fontsize=12, fontweight="bold", pad=8) | |
| ax.set_xlabel(xlabel, color=TEXT_COLOR, fontsize=9) | |
| ax.set_ylabel(ylabel, color=TEXT_COLOR, fontsize=9) | |
| ax.tick_params(colors=TEXT_COLOR, labelsize=8) | |
| for sp in ax.spines.values(): | |
| sp.set_edgecolor(GRID_COLOR) | |
| ax.grid(True, alpha=0.18, color=GRID_COLOR) | |
| def _colorbar(fig, im, ax, label="dB"): | |
| cb = fig.colorbar(im, ax=ax, pad=0.02, aspect=25) | |
| cb.set_label(label, color=TEXT_COLOR, fontsize=8) | |
| cb.ax.yaxis.set_tick_params(color=TEXT_COLOR, labelsize=7) | |
| plt.setp(cb.ax.yaxis.get_ticklabels(), color=TEXT_COLOR) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Core Analysis | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyze(file_path): | |
| if file_path is None: | |
| return (None,) * 5 + ("β οΈ Please upload an audio file.",) | |
| # ββ Load ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| y, sr = librosa.load(file_path, sr=None, mono=True, duration=MAX_DUR_S) | |
| except Exception as exc: | |
| return (None,) * 5 + (f"β Could not load file: {exc}",) | |
| duration = len(y) / sr | |
| clipped = duration >= MAX_DUR_S | |
| n_fft_use = min(N_FFT, 2 ** int(np.log2(len(y) / 4))) # safe for short files | |
| # ββ STFT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| D = librosa.stft(y, n_fft=n_fft_use, hop_length=HOP) | |
| Sxx = np.abs(D) | |
| D_db = librosa.amplitude_to_db(Sxx, ref=np.max) | |
| freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft_use) | |
| frame_t = librosa.frames_to_time(np.arange(Sxx.shape[1]), sr=sr, hop_length=HOP) | |
| # ββ Spectral features (used in multiple plots) ββββββββββββββββββββββββββββ | |
| centroid = librosa.feature.spectral_centroid(y=y, sr=sr, hop_length=HOP)[0] | |
| c_times = librosa.frames_to_time(np.arange(len(centroid)), sr=sr, hop_length=HOP) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FIGURE 1 β Waveform | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| t = np.linspace(0, duration, len(y)) | |
| fig1, ax1 = _make_fig(figsize=(13, 3)) | |
| ax1.plot(t, y, color=ACCENT, linewidth=0.45, alpha=0.85) | |
| ax1.fill_between(t, y, 0, alpha=0.15, color=ACCENT) | |
| _style(ax1, "Waveform", "Time (s)", "Amplitude") | |
| ax1.set_xlim(0, duration) | |
| if clipped: | |
| ax1.set_title(f"Waveform (showing first {MAX_DUR_S}s)", color=ACCENT, | |
| fontsize=12, fontweight="bold") | |
| fig1.tight_layout(pad=0.8) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FIGURE 2 β Spectrograms (linear + mel + log) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fmax_mel = min(sr // 2, 20000) | |
| mel_spec = librosa.feature.melspectrogram( | |
| y=y, sr=sr, n_fft=n_fft_use, hop_length=HOP, n_mels=128, fmax=fmax_mel | |
| ) | |
| mel_db = librosa.power_to_db(mel_spec, ref=np.max) | |
| fig2, axes2 = _make_fig(3, 1, figsize=(13, 11)) | |
| CMAP = "GnBu_r" | |
| VRANGE = dict(vmin=-80, vmax=0) | |
| # Linear | |
| im1 = librosa.display.specshow( | |
| D_db, sr=sr, hop_length=HOP, x_axis="time", y_axis="hz", | |
| ax=axes2[0], cmap=CMAP, **VRANGE | |
| ) | |
| _style(axes2[0], "Spectrogram β Linear Frequency", "Time (s)", "Frequency (Hz)") | |
| _colorbar(fig2, im1, axes2[0]) | |
| # Mel | |
| im2 = librosa.display.specshow( | |
| mel_db, sr=sr, hop_length=HOP, x_axis="time", y_axis="mel", | |
| ax=axes2[1], cmap=CMAP, fmax=fmax_mel, **VRANGE | |
| ) | |
| _style(axes2[1], "Spectrogram β Mel Scale", "Time (s)", "Mel Frequency") | |
| _colorbar(fig2, im2, axes2[1]) | |
| # Log | |
| im3 = librosa.display.specshow( | |
| D_db, sr=sr, hop_length=HOP, x_axis="time", y_axis="log", | |
| ax=axes2[2], cmap=CMAP, **VRANGE | |
| ) | |
| _style(axes2[2], "Spectrogram β Log Frequency", "Time (s)", "Frequency (Hz, log)") | |
| _colorbar(fig2, im3, axes2[2]) | |
| fig2.tight_layout(pad=1.2) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FIGURE 3 β PSD Β· Spectral Centroid Β· MFCC | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| f_psd, psd = signal.welch(y, sr, nperseg=min(4096, len(y) // 2)) | |
| psd_db = 10 * np.log10(psd + 1e-20) | |
| mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=20, hop_length=HOP) | |
| mfcc_delta = librosa.feature.delta(mfcc) | |
| fig3, axes3 = _make_fig(3, 1, figsize=(13, 12)) | |
| # PSD | |
| axes3[0].plot(f_psd[1:], psd_db[1:], color=SECONDARY, linewidth=1.3) | |
| marine_bands = [ | |
| (20, 1000, SUCCESS, "Fish & low-freq (20β1 kHz)"), | |
| (1000, 5000, ACCENT, "Snapping shrimp (1β5 kHz)"), | |
| (5000, min(sr / 2, 20000), WARNING, "High-freq biotic (5β20 kHz)"), | |
| ] | |
| for flo, fhi, color, label in marine_bands: | |
| if fhi <= sr / 2 and flo < sr / 2: | |
| axes3[0].axvspan(flo, min(fhi, sr / 2), alpha=0.12, color=color, label=label) | |
| axes3[0].set_xscale("log") | |
| axes3[0].set_xlim(max(20, f_psd[1]), sr / 2) | |
| _style(axes3[0], "Power Spectral Density (Welch)", "Frequency (Hz)", "PSD (dB/Hz)") | |
| axes3[0].legend(fontsize=8, loc="lower left", | |
| facecolor=BG_COLOR, edgecolor=GRID_COLOR, labelcolor=TEXT_COLOR) | |
| # Spectral centroid | |
| axes3[1].plot(c_times, centroid, color=ACCENT, linewidth=1.1, alpha=0.9) | |
| axes3[1].fill_between(c_times, centroid, alpha=0.12, color=ACCENT) | |
| axes3[1].set_xlim(0, duration) | |
| _style(axes3[1], "Spectral Centroid Over Time", "Time (s)", "Frequency (Hz)") | |
| # MFCC | |
| im_mfcc = librosa.display.specshow( | |
| mfcc, sr=sr, hop_length=HOP, x_axis="time", | |
| ax=axes3[2], cmap="coolwarm" | |
| ) | |
| _style(axes3[2], "MFCCs (20 coefficients)", "Time (s)", "MFCC Coefficient") | |
| axes3[2].set_facecolor(SURFACE) | |
| _colorbar(fig3, im_mfcc, axes3[2], label="Amplitude") | |
| fig3.tight_layout(pad=1.2) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FIGURE 4 β Acoustic Indices over time | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Adaptive window: aim for β₯8 windows; each window 5β60 s | |
| win_s = max(5.0, min(60.0, duration / 8)) | |
| win_len = int(sr * win_s) | |
| n_win = max(3, len(y) // win_len) | |
| win_len = len(y) // n_win # recompute for even coverage | |
| aci_v, bi_v, ndsi_v, adi_v, rms_v, centers = [], [], [], [], [], [] | |
| for i in range(n_win): | |
| seg = y[i * win_len : (i + 1) * win_len] | |
| centers.append((i + 0.5) * win_len / sr) | |
| D_s = librosa.stft(seg, n_fft=n_fft_use, hop_length=HOP) | |
| Sxx_s = np.abs(D_s) | |
| aci_v.append(aci(Sxx_s)) | |
| bi_v.append(bioacoustic_index(Sxx_s, freqs)) | |
| ndsi_v.append(ndsi(Sxx_s, freqs)) | |
| adi_v.append(adi(Sxx_s, freqs)) | |
| rms_v.append(float(np.sqrt((seg ** 2).mean()))) | |
| centers = np.array(centers) | |
| bw = win_len / sr * 0.72 | |
| fig4, axes4 = _make_fig(3, 2, figsize=(14, 13)) | |
| def bar_plot(ax, vals, color, title, ylabel): | |
| ax.bar(centers, vals, width=bw, color=color, alpha=0.82) | |
| _style(ax, title, "Time (s)", ylabel) | |
| ax.set_xlim(0, duration) | |
| bar_plot(axes4[0, 0], aci_v, SECONDARY, "Acoustic Complexity Index (ACI)", "ACI") | |
| bar_plot(axes4[0, 1], bi_v, SUCCESS, "Bioacoustic Index (BI)", "BI") | |
| # NDSI β colour by sign | |
| ndsi_colors = [SUCCESS if v >= 0 else ERROR for v in ndsi_v] | |
| axes4[1, 0].bar(centers, ndsi_v, width=bw, color=ndsi_colors, alpha=0.82) | |
| axes4[1, 0].axhline(0, color=TEXT_COLOR, linewidth=0.8, linestyle="--", alpha=0.6) | |
| _style(axes4[1, 0], "NDSI (green > 0 = biotic dominated)", "Time (s)", "NDSI") | |
| axes4[1, 0].set_xlim(0, duration) | |
| bar_plot(axes4[1, 1], adi_v, WARNING, "Acoustic Diversity Index (ADI)", "ADI") | |
| # RMS energy | |
| axes4[2, 0].plot(centers, rms_v, "o-", color=ACCENT, linewidth=1.6, | |
| markersize=5, alpha=0.9) | |
| axes4[2, 0].fill_between(centers, rms_v, alpha=0.15, color=ACCENT) | |
| _style(axes4[2, 0], "RMS Energy Over Time", "Time (s)", "RMS Amplitude") | |
| axes4[2, 0].set_xlim(0, duration) | |
| # Short-time RMS spectrogram (energy heatmap) | |
| rms_frame = librosa.feature.rms(y=y, frame_length=n_fft_use, hop_length=HOP) | |
| rms_db_frame = librosa.amplitude_to_db(rms_frame, ref=np.max) | |
| axes4[2, 1].plot( | |
| librosa.frames_to_time(np.arange(rms_frame.shape[1]), sr=sr, hop_length=HOP), | |
| rms_db_frame[0], color=ACCENT, linewidth=0.8, alpha=0.9 | |
| ) | |
| _style(axes4[2, 1], "Short-time RMS Energy (dBFS)", "Time (s)", "RMS (dB)") | |
| axes4[2, 1].set_xlim(0, duration) | |
| fig4.tight_layout(pad=1.2) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FIGURE 5 β Onset + Bandwidth over time | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| onset_frames = librosa.onset.onset_detect(y=y, sr=sr, hop_length=HOP) | |
| onset_times = librosa.frames_to_time(onset_frames, sr=sr, hop_length=HOP) | |
| bandwidth = librosa.feature.spectral_bandwidth(y=y, sr=sr, hop_length=HOP)[0] | |
| rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr, hop_length=HOP, roll_percent=0.85)[0] | |
| flatness = librosa.feature.spectral_flatness(y=y, hop_length=HOP)[0] | |
| zcr_frame = librosa.feature.zero_crossing_rate(y, hop_length=HOP)[0] | |
| fig5, axes5 = _make_fig(3, 2, figsize=(14, 11)) | |
| axes5[0, 0].plot(c_times, bandwidth, color=SECONDARY, linewidth=0.9, alpha=0.9) | |
| _style(axes5[0, 0], "Spectral Bandwidth Over Time", "Time (s)", "Bandwidth (Hz)") | |
| axes5[0, 0].set_xlim(0, duration) | |
| axes5[0, 1].plot(c_times, rolloff, color=WARNING, linewidth=0.9, alpha=0.9) | |
| _style(axes5[0, 1], "Spectral Rolloff (85%) Over Time", "Time (s)", "Frequency (Hz)") | |
| axes5[0, 1].set_xlim(0, duration) | |
| axes5[1, 0].plot(c_times, flatness, color=TEXT_MUTED, linewidth=0.9, alpha=0.9) | |
| _style(axes5[1, 0], "Spectral Flatness Over Time", "Time (s)", "Flatness [0β1]") | |
| axes5[1, 0].set_xlim(0, duration) | |
| axes5[1, 1].plot(c_times, zcr_frame, color=ACCENT_HOVER, linewidth=0.7, alpha=0.9) | |
| _style(axes5[1, 1], "Zero Crossing Rate Over Time", "Time (s)", "ZCR") | |
| axes5[1, 1].set_xlim(0, duration) | |
| # Onset plot | |
| axes5[2, 0].plot(c_times, rms_db_frame[0], color=ACCENT, linewidth=0.7, alpha=0.7, | |
| label="RMS (dBFS)") | |
| for ot in onset_times: | |
| axes5[2, 0].axvline(ot, color=ERROR, linewidth=0.6, alpha=0.6) | |
| axes5[2, 0].set_xlim(0, duration) | |
| _style(axes5[2, 0], f"Onset Detection ({len(onset_times)} events)", "Time (s)", "RMS (dB)") | |
| axes5[2, 0].text(0.01, 0.96, f"{len(onset_times)} onsets detected", | |
| transform=axes5[2, 0].transAxes, | |
| color=ERROR, fontsize=9, va="top") | |
| # MFCC delta (showing change) | |
| im_delta = librosa.display.specshow( | |
| mfcc_delta, sr=sr, hop_length=HOP, x_axis="time", | |
| ax=axes5[2, 1], cmap="RdBu_r" | |
| ) | |
| axes5[2, 1].set_facecolor(SURFACE) | |
| _style(axes5[2, 1], "MFCC Delta (Rate of Change)", "Time (s)", "MFCC Coefficient") | |
| _colorbar(fig5, im_delta, axes5[2, 1], label="Ξ Amplitude") | |
| fig5.tight_layout(pad=1.2) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Summary Report | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| rms_overall = float(np.sqrt((y ** 2).mean())) | |
| peak = float(np.abs(y).max()) | |
| rms_db_val = 20 * np.log10(rms_overall + 1e-12) | |
| peak_db_val = 20 * np.log10(peak + 1e-12) | |
| dyn_range = 20 * np.log10(peak / (rms_overall + 1e-12)) | |
| zcr_mean = float(librosa.feature.zero_crossing_rate(y).mean()) | |
| sp_c_mean = float(centroid.mean()) | |
| sp_bw_mean = float(bandwidth.mean()) | |
| sp_ro_mean = float(rolloff.mean()) | |
| sp_fl_mean = float(flatness.mean()) | |
| # Top 5 dominant frequencies (mean spectrum) | |
| top5 = freqs[np.argsort((Sxx ** 2).mean(axis=1))[-5:][::-1]] | |
| # Global indices | |
| aci_g = aci(Sxx) | |
| bi_g = bioacoustic_index(Sxx, freqs) | |
| ndsi_g = ndsi(Sxx, freqs) | |
| adi_g = adi(Sxx, freqs) | |
| Hf = spectral_entropy(Sxx) | |
| Ht = temporal_entropy(y) | |
| H_total = Hf * Ht | |
| ndsi_label = ( | |
| "Strong biotic dominance" if ndsi_g > 0.5 else | |
| "Moderate biotic dominance" if ndsi_g > 0.0 else | |
| "Moderate anthropogenic noise" if ndsi_g > -0.5 else | |
| "Strong anthropogenic noise" | |
| ) | |
| aci_label = ( | |
| "Very high complexity" if aci_g > 10000 else | |
| "High complexity" if aci_g > 5000 else | |
| "Moderate complexity" if aci_g > 1000 else | |
| "Low complexity" | |
| ) | |
| clipped_note = ( | |
| f"\n> β οΈ File longer than {MAX_DUR_S}s β analysis performed on first {MAX_DUR_S}s only.\n" | |
| if clipped else "" | |
| ) | |
| report = f"""{clipped_note} | |
| ## π Analysis Report | |
| ### π΅ Basic Information | |
| | Parameter | Value | | |
| |-----------|-------| | |
| | Duration | {duration:.2f} s | | |
| | Sample Rate | {sr:,} Hz | | |
| | Total Samples | {len(y):,} | | |
| | Processing Mode | Mono | | |
| | Analysis Windows | {n_win} Γ {win_len/sr:.1f} s | | |
| --- | |
| ### π Amplitude Statistics | |
| | Parameter | Value | | |
| |-----------|-------| | |
| | RMS Level | {rms_db_val:.1f} dBFS | | |
| | Peak Level | {peak_db_val:.1f} dBFS | | |
| | Dynamic Range | {dyn_range:.1f} dB | | |
| | Zero Crossing Rate | {zcr_mean:.5f} | | |
| | Detected Onsets | {len(onset_times)} events | | |
| --- | |
| ### π Spectral Features (mean over recording) | |
| | Feature | Value | | |
| |---------|-------| | |
| | Spectral Centroid | {sp_c_mean:.1f} Hz | | |
| | Spectral Bandwidth | {sp_bw_mean:.1f} Hz | | |
| | Spectral Rolloff (85%) | {sp_ro_mean:.1f} Hz | | |
| | Spectral Flatness | {sp_fl_mean:.5f} | | |
| | Top 5 Dominant Freqs | {', '.join(f'{f:.0f} Hz' for f in top5)} | | |
| --- | |
| ### 𧬠Acoustic Indices (whole recording) | |
| | Index | Value | Interpretation | | |
| |-------|-------|----------------| | |
| | **ACI** | {aci_g:.1f} | {aci_label} β higher = more varied amplitude patterns | | |
| | **BI** | {bi_g:.2f} | Biological activity intensity in 2β8 kHz band | | |
| | **NDSI** | {ndsi_g:.3f} | {ndsi_label} | | |
| | **ADI** | {adi_g:.3f} | Shannon diversity across frequency bands | | |
| | **Hf** (Spectral Entropy) | {Hf:.4f} | 0 = tonal, 1 = uniform spectrum | | |
| | **Ht** (Temporal Entropy) | {Ht:.4f} | 0 = impulsive, 1 = stationary | | |
| | **H** (Total Entropy) | {H_total:.4f} | Combined soundscape heterogeneity | | |
| --- | |
| ### π Marine Coral Reef Frequency Guide | |
| | Band | Range | Typical Sources | | |
| |------|-------|-----------------| | |
| | Low | 20 β 1,000 Hz | Fish choruses, breaking waves, vessel traffic | | |
| | Snapping Shrimp | 1 β 5 kHz | *Alpheid* snapping shrimp β reef health indicator | | |
| | High Biotic | 5 β 20 kHz | Small crustaceans, urchins, high-frequency fish | | |
| > **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. | |
| --- | |
| *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)* | |
| """ | |
| return fig1, fig2, fig3, fig4, fig5, report | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Gradio Interface | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700;800&family=DM+Sans:wght@400;500;600;700&display=swap'); | |
| body, .gradio-container { | |
| background: linear-gradient(180deg, #04222C 0%, #0B3D4A 45%, #123F4D 100%) !important; | |
| font-family: 'DM Sans', sans-serif !important; | |
| color: #E7FBFF !important; | |
| } | |
| h1, h2, h3, h4 { | |
| font-family: 'Nunito Sans', sans-serif !important; | |
| color: #4CD9C0 !important; | |
| } | |
| h1 { font-size: 2.2rem !important; font-weight: 800 !important; } | |
| h2, h3 { font-weight: 700 !important; } | |
| label, .tab-nav button { | |
| font-family: 'DM Sans', sans-serif !important; | |
| font-weight: 600 !important; | |
| color: #E7FBFF !important; | |
| } | |
| p, .prose, .markdown-body { color: #8FD9E0 !important; } | |
| /* Reduce Gradio's default rounded-corner look across every component | |
| (blocks, inputs, buttons, tabs, dropdowns, audio player, etc.) by | |
| overriding the underlying CSS radius variables the theme relies on. */ | |
| :root, .gradio-container { | |
| --radius-xxs: 2px !important; | |
| --radius-xs: 2px !important; | |
| --radius-sm: 4px !important; | |
| --radius-md: 4px !important; | |
| --radius-lg: 4px !important; | |
| --block-radius: 4px !important; | |
| --button-small-radius: 4px !important; | |
| --button-large-radius: 4px !important; | |
| --input-radius: 4px !important; | |
| --table-radius: 4px !important; | |
| } | |
| /* Cards / surfaces β lagoon teal, popping against the deep-ocean page background */ | |
| .gr-panel, .gr-box, .gr-form, .tabitem, .block, | |
| div[class*="svelte"].block, .form, .wrap.svelte-1ipelgc { | |
| background: #123F4D !important; | |
| border: 1px solid #2A7A85 !important; | |
| border-radius: 4px !important; | |
| } | |
| /* Force every input / upload / textbox surface off pure white, into lagoon teal */ | |
| input, textarea, select, | |
| .gr-input, .gr-box textarea, .gr-box input, | |
| .upload-box, .upload-container, [data-testid="audio"] { | |
| background: #15505E !important; | |
| color: #E7FBFF !important; | |
| border-color: #2A7A85 !important; | |
| border-radius: 4px !important; | |
| } | |
| /* Markdown / table content inside panels */ | |
| table, th, td { color: #E7FBFF !important; border-color: #2A7A85 !important; } | |
| th { background: #FF6F59 !important; color: #FFFFFF !important; } | |
| tr:nth-child(even) td { background: #1A5C6B !important; } | |
| blockquote { background: #15505E !important; border-left: 4px solid #FF6F59 !important; color: #E7FBFF !important; } | |
| code { background: #15505E !important; color: #4CD9C0 !important; } | |
| /* Buttons β Coral Reef Lagoon living-coral primary */ | |
| .gr-button, button.primary { | |
| background: #FF6F59 !important; | |
| border: none !important; | |
| color: #FFFFFF !important; | |
| font-weight: 700; | |
| letter-spacing: 0.01em; | |
| border-radius: 4px !important; | |
| min-height: 48px; | |
| transition: background 0.15s ease-in-out; | |
| } | |
| .gr-button:hover, button.primary:hover { | |
| background: #E5563F !important; | |
| } | |
| /* Tabs β seafoam turquoise active state */ | |
| .tab-nav button.selected { | |
| color: #4CD9C0 !important; | |
| border-color: #4CD9C0 !important; | |
| } | |
| footer { display: none !important; } | |
| """ | |
| _HERE = os.path.dirname(os.path.abspath(__file__)) | |
| _DATA = os.path.join(_HERE, "..", "data") | |
| EXAMPLES = [ | |
| [os.path.join(_DATA, "Invertebrates", "Snapping Shrimp.wav")], | |
| [os.path.join(_DATA, "Invertebrates", "Ghost CrabοΌ Gastric Mill Stridulation.wav")], | |
| [os.path.join(_DATA, "Mammal", "Humpback Whale Song.wav")], | |
| [os.path.join(_DATA, "Mammal", "Fish", "Red Grouper Vocalization.wav")], | |
| ] | |
| # Filter to only examples that actually exist (avoids errors on HuggingFace) | |
| EXAMPLES = [e for e in EXAMPLES if os.path.isfile(e[0])] | |
| with gr.Blocks(title="π Marine Soundscape Analyzer", css=CSS, | |
| theme=gr.themes.Base( | |
| primary_hue="orange", | |
| secondary_hue="teal", | |
| neutral_hue="gray", | |
| radius_size="sm", | |
| font=gr.themes.GoogleFont("Nunito Sans"), | |
| font_mono=gr.themes.GoogleFont("JetBrains Mono"), | |
| ).set( | |
| # Drive every component's box color through real theme tokens | |
| # (more reliable than CSS class guessing) so nothing renders | |
| # on plain white β Coral Reef Lagoon palette throughout. | |
| body_background_fill="#0B3D4A", | |
| body_background_fill_dark="#0B3D4A", | |
| background_fill_primary="#123F4D", | |
| background_fill_primary_dark="#123F4D", | |
| background_fill_secondary="#15505E", | |
| background_fill_secondary_dark="#15505E", | |
| border_color_primary="#2A7A85", | |
| border_color_primary_dark="#2A7A85", | |
| block_background_fill="#123F4D", | |
| block_background_fill_dark="#123F4D", | |
| block_border_color="#2A7A85", | |
| block_border_color_dark="#2A7A85", | |
| block_label_background_fill="#FF6F59", | |
| block_label_text_color="#FFFFFF", | |
| block_title_text_color="#4CD9C0", | |
| body_text_color="#E7FBFF", | |
| body_text_color_dark="#E7FBFF", | |
| body_text_color_subdued="#8FD9E0", | |
| input_background_fill="#15505E", | |
| input_background_fill_dark="#15505E", | |
| input_border_color="#2A7A85", | |
| button_primary_background_fill="#FF6F59", | |
| button_primary_background_fill_hover="#E5563F", | |
| button_primary_text_color="#FFFFFF", | |
| button_secondary_background_fill="#4CD9C0", | |
| button_secondary_text_color="#0B3D4A", | |
| )) as demo: | |
| gr.Markdown(""" | |
| # ππͺΈ Marine Soundscape Analyzer | |
| **Marine Acoustic Analysis Tool** | |
| Upload a hydrophone recording to generate spectrograms, power spectral density, acoustic indices, | |
| and a full analysis report β tailored for coral reef soundscape monitoring. | |
| Supported formats: **WAV Β· MP3 Β· FLAC Β· OGG Β· AIFF** Β· Maximum analysed duration: **5 minutes** | |
| """) | |
| with gr.Row(equal_height=True): | |
| with gr.Column(scale=3): | |
| audio_in = gr.Audio(label="π Upload Sound File", type="filepath") | |
| with gr.Column(scale=1): | |
| gr.Markdown(""" | |
| ### Acoustic Indices | |
| | Index | What it measures | | |
| |-------|-----------------| | |
| | **ACI** | Amplitude complexity | | |
| | **BI** | Biological activity | | |
| | **NDSI** | Biotic vs anthropogenic | | |
| | **ADI** | Frequency diversity | | |
| | **Hf / Ht** | Spectral / temporal entropy | | |
| """) | |
| analyze_btn = gr.Button("π Analyse Recording", variant="primary", size="lg") | |
| with gr.Tabs(): | |
| with gr.Tab("π Waveform"): | |
| plot_wave = gr.Plot() | |
| with gr.Tab("π Spectrograms"): | |
| plot_spec = gr.Plot() | |
| with gr.Tab("π‘ Frequency Analysis + MFCC"): | |
| plot_freq = gr.Plot() | |
| with gr.Tab("𧬠Acoustic Indices"): | |
| plot_idx = gr.Plot() | |
| with gr.Tab("π Temporal Features"): | |
| plot_temp = gr.Plot() | |
| with gr.Tab("π Report"): | |
| report_out = gr.Markdown() | |
| analyze_btn.click( | |
| fn=analyze, | |
| inputs=[audio_in], | |
| outputs=[plot_wave, plot_spec, plot_freq, plot_idx, plot_temp, report_out], | |
| ) | |
| if EXAMPLES: | |
| gr.Examples( | |
| examples=EXAMPLES, | |
| inputs=[audio_in], | |
| label="π§ Example Marine Recordings", | |
| ) | |
| gr.Markdown(""" | |
| <div style="text-align:center;"> | |
| **π Marine Soundscape Analyzer** Β· Built for marine bioacoustic research <br/> | |
| App owner: **Wasurat S. Β· Sittichart S.** | |
| <span style="font-size:0.85em; color:#4F8B91;"> | |
| References: Pieretti et al. 2011 Β· Boelman et al. 2007 Β· Kasten et al. 2012 Β· Villanueva-Rivera et al. 2011 Β· Sueur et al. 2008 | |
| </span> | |
| </div> | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch(share=False) | |