""" 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 @spaces.GPU(duration=1) 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("""