WasuratS's picture
Update app.py
310be01 verified
Raw
History Blame Contribute Delete
33.2 kB
"""
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("""
<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)