Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

18 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌊 AquaListen - AI-Powered Coral Reef Health Monitoring System

AquaListen is an advanced, non-invasive marine conservation platform that leverages deep learning to analyze underwater acoustics and assess coral reef ecosystem health. By analyzing biological soundscapes, AquaListen classifies reef environments into three core ecological states: Healthy, Stressed, and Ambient.


Table of Contents

  1. Overview
  2. System Architecture
  3. ML Inference Pipeline
  4. End-to-End Data Flow
  5. Frontend Component Tree
  6. Node.js Server Architecture
  7. Python API Architecture
  8. Folder Relationships
  9. Classification Decision Tree
  10. ML Pipeline β€” Verified from Code
  11. Google SurfPerch vs. AquaListen
  12. API Reference & Payload Specs
  13. Environment & Configuration Reference
  14. Operational & Deployment Guide
  15. Port Assignments
  16. Troubleshooting & Common Issues

Overview

AquaListen is a full-stack web application that classifies coral reef health from hydrophone audio recordings. It combines Google SurfPerch (a 10,932-class bird/wildlife acoustic taxonomy model served as a TensorFlow SavedModel) with AquaListen's custom ecological mapping logic to produce three reef health categories: healthy, stressed, and ambient.

The system consists of:

Component Technology Port
ML Inference API Python Β· FastAPI Β· TensorFlow 8000
Database + Web Server Node.js Β· Express Β· MongoDB Atlas 3002
Frontend Dashboard React Β· Vite Β· TailwindCSS served by Node on 3002

Note on the frontend port: In development, Vite is mounted as middleware inside the Express server (setupVite(app, server)), so the dashboard is served from the same origin as the API, http://localhost:3002. Vite's HMR socket runs internally and is not exposed as its own public port.


1. System Architecture & Component Mapping

AquaListen runs in a decoupled configuration where heavy DSP (Digital Signal Processing) and ML workloads are handled by Python/FastAPI, while user requests, state storage, and configuration are managed by Node.js/Express/Mongoose.

1.1 Complete Component Overview

graph TD
    Client[React/Vite Frontend - served on Node :3002] -->|1. Upload Wav| FastAPI[FastAPI Server - Port 8000]
    Client -->|4. Fetch Dashboard/Alerts/Sites| NodeExpress[Node/Express Server - Port 3002]

    subgraph Python Inference Engine
        FastAPI -->|2. Resample & Feature Extract| Librosa[Librosa DSP]
        FastAPI -->|3. Evaluate Soundscape| TF[TensorFlow SavedModel]
    end

    FastAPI -->|5. HTTP POST /api/predictions| NodeExpress

    subgraph Data Orchestration Layer
        NodeExpress -->|6. Mongoose ODM| MongoDB[(MongoDB Atlas)]
        NodeExpress -->|7. Auto-Generate Alert| Alerts[Alert Generator Heuristics]
    end
Loading

1.2 High-Level System Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        USER BROWSER                              β”‚
β”‚              React + Vite + TailwindCSS + shadcn/ui              β”‚
β”‚                    served from localhost:3002                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚                          β”‚
        Audio upload                 All data reads
        POST /predict            (stats, sites, alerts,
               β”‚                   predictions)
               β–Ό                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Python FastAPI       β”‚   β”‚  Node.js Express                     β”‚
β”‚  localhost:8000       β”‚   β”‚  localhost:3002                      β”‚
β”‚                       β”‚   β”‚                                       β”‚
β”‚  ML Inference         │──▢│  POST /api/predictions               β”‚
β”‚  Audio Processing     β”‚   β”‚  (save result to DB)                 β”‚
β”‚  Feature Extraction   β”‚   β”‚                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚                            β”‚
               β”‚                            β–Ό
               β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚              β”‚  MongoDB Atlas           β”‚
               β”‚              β”‚  (cloud)                 β”‚
               β”‚              β”‚                          β”‚
               β”‚              β”‚  Collections:            β”‚
               β”‚              β”‚  β€’ reefSites             β”‚
               β”‚              β”‚  β€’ predictions           β”‚
               β”‚              β”‚  β€’ alerts                β”‚
               β”‚              β”‚  β€’ users                 β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. ML Inference Pipeline

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚  Audio File   β”‚
                        β”‚  .wav .mp3    β”‚
                        β”‚  .flac .m4a   β”‚
                        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                               β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  librosa.load()         β”‚  [AquaListen]
                  β”‚  sr = native            β”‚
                  β”‚  No resampling here     β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                               β–Ό
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚  extract_audio_         β”‚  [AquaListen]
                  β”‚  features()             β”‚
                  β”‚                         β”‚
                  β”‚  Pad/truncate to        β”‚
                  β”‚  1.88 s * sr            β”‚
                  β”‚                         β”‚
                  β”‚  Computes:              β”‚
                  β”‚  β€’ spectral_centroid    β”‚
                  β”‚  β€’ spectral_bandwidth   β”‚
                  β”‚  β€’ zero_crossing_rate   β”‚
                  β”‚  β€’ mfcc (13 coeffs)     β”‚
                  β”‚  β€’ spectral_rolloff     β”‚
                  β”‚  β€’ log-mel spectrogram  β”‚
                  β”‚    (128 mels, computed  β”‚
                  β”‚     but NOT used by TF) β”‚
                  β”‚  β€’ raw_audio copy       β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚                                β”‚
               β–Ό                                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  compute_anthropogenic_   β”‚    β”‚  predict_with_aqualisten_       β”‚
β”‚  noise_score()            β”‚    β”‚  model()                        β”‚
β”‚                           β”‚    β”‚                                  β”‚
β”‚  [AquaListen]             β”‚    β”‚  [AquaListen wrapper]            β”‚
β”‚                           β”‚    β”‚                                  β”‚
β”‚  From raw_audio:          β”‚    β”‚  raw_audio β†’                     β”‚
β”‚  FFT β†’ low-freq ratio     β”‚    β”‚  pad/truncate to 160,000         β”‚
β”‚                           β”‚    β”‚  samples (10 s @ 16 kHz)          β”‚
β”‚  Indicators:              β”‚    β”‚                                  β”‚
β”‚  β€’ centroid  (wt=0.30)    β”‚    β”‚  batch dim β†’ (1, 160000)         β”‚
β”‚  β€’ bandwidth (wt=0.20)    β”‚    β”‚  tf.float32 tensor                β”‚
β”‚  β€’ ZCR       (wt=0.10)    β”‚    β”‚                                  β”‚
β”‚  β€’ lowfreq_r (wt=0.40)    β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚                           β”‚                    β”‚
β”‚  Linear ramp scoring      β”‚                    β–Ό
β”‚  β†’ score in [0.0, 1.0]    β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  [Google SurfPerch]              β”‚
             β”‚                   β”‚  tf.saved_model.load()           β”‚
             β”‚                   β”‚  signatures['serving_default']   β”‚
             β”‚                   β”‚  input key: 'inputs'             β”‚
             β”‚                   β”‚                                  β”‚
             β”‚                   β”‚  Internal (inferred from         β”‚
             β”‚                   β”‚  SurfPerch paper, not visible     β”‚
             β”‚                   β”‚  in SavedModel graph):            β”‚
             β”‚                   β”‚  β€’ STFT                          β”‚
             β”‚                   β”‚  β€’ Log-mel spectrogram            β”‚
             β”‚                   β”‚  β€’ EfficientNet-B0 backbone       β”‚
             β”‚                   β”‚                                  β”‚
             β”‚                   β”‚  Outputs:                        β”‚
             β”‚                   β”‚  output_0: (1, 10932) logits      β”‚
             β”‚                   β”‚  output_1: (1, 1280) embedding    β”‚
             β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚                                   β”‚
             β”‚                                   β–Ό
             β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
             β”‚                   β”‚  process_aqualisten_             β”‚
             β”‚                   β”‚  predictions()                  β”‚
             β”‚                   β”‚                                  β”‚
             β”‚                   β”‚  [AquaListen]                    β”‚
             β”‚                   β”‚                                  β”‚
             β”‚                   β”‚  softmax(output_0 logits)        β”‚
             β”‚                   β”‚  β†’ class_probs [10932]           β”‚
             β”‚                   β”‚                                  β”‚
             β”‚                   β”‚  top-10 indices & probs          β”‚
             β”‚                   β”‚                                  β”‚
             β”‚                   β”‚  Shannon Entropy                 β”‚
             β”‚                   β”‚  Richness (prob > 0.005)          β”‚
             β”‚                   β”‚  Dominance (max/sum_top10)        β”‚
             β”‚                   β”‚                                  β”‚
             β”‚                   β”‚  Rule-based mapping β†’             β”‚
             β”‚                   β”‚  health_status + confidence       β”‚
             β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚                                   β”‚
             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
                            β–Ό
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚  classify_reef_health() β”‚
               β”‚                         β”‚
               β”‚  [AquaListen]           β”‚
               β”‚                         β”‚
               β”‚  Fusion Policy:         β”‚
               β”‚  anthro < 0.3  β†’ trust  β”‚
               β”‚  0.3–0.6 β†’ βˆ’5–15%      β”‚
               β”‚  0.6–0.8 β†’ βˆ’15–25%     β”‚
               β”‚  β‰₯0.8 + low conf        β”‚
               β”‚    β†’ override stressed β”‚
               β”‚  β‰₯0.8 + high conf       β”‚
               β”‚    β†’ trust, βˆ’15%       β”‚
               β”‚                         β”‚
               β”‚  Final:                 β”‚
               β”‚  health_status          β”‚
               β”‚  confidence [0.5,0.95]  β”‚
               β”‚  diagnostics dict       β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
                            β–Ό
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚  POST /api/predictions  β”‚
               β”‚  β†’ Node.js :3002        β”‚
               β”‚  β†’ MongoDB               β”‚
               β”‚  β†’ Auto-alert if         β”‚
               β”‚    stressed + conf>0.7   β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. End-to-End Data Flow (Sequence)

Browser          Python :8000        Node.js :3002        MongoDB
   β”‚                   β”‚                    β”‚                 β”‚
   β”‚  POST /predict     β”‚                    β”‚                 β”‚
   β”‚  (multipart/form)  β”‚                    β”‚                 β”‚
   │──────────────────▢│                    β”‚                 β”‚
   β”‚                   β”‚                    β”‚                 β”‚
   β”‚                   β”‚ librosa.load()      β”‚                 β”‚
   β”‚                   β”‚ extract_features()  β”‚                 β”‚
   β”‚                   β”‚ anthro_score()      β”‚                 β”‚
   β”‚                   β”‚ surfperch infer      β”‚                 β”‚
   β”‚                   β”‚ ecological map       β”‚                 β”‚
   β”‚                   β”‚ fusion               β”‚                 β”‚
   β”‚                   β”‚                    β”‚                 β”‚
   β”‚                   β”‚  GET /sites         β”‚                 β”‚
   β”‚                   │───────────────────▢│                 β”‚
   β”‚                   β”‚                    │── find() ──────▢│
   β”‚                   β”‚                    │◀── sites ───────│
   β”‚                   │◀── sites ──────────│                 β”‚
   β”‚                   β”‚                    β”‚                 β”‚
   β”‚                   β”‚  POST /api/predictions                β”‚
   β”‚                   │───────────────────▢│                 β”‚
   β”‚                   β”‚                    │── insertOne() ─▢│
   β”‚                   β”‚                    β”‚  (prediction)    β”‚
   β”‚                   β”‚                    β”‚                 β”‚
   β”‚                   β”‚                    β”‚  if stressed     β”‚
   β”‚                   β”‚                    β”‚  + conf > 0.7:   β”‚
   β”‚                   β”‚                    │── insertOne() ─▢│
   β”‚                   β”‚                    β”‚  (alert)         β”‚
   β”‚                   β”‚                    │◀── ok ──────────│
   β”‚                   │◀── 200 ok ─────────│                 β”‚
   β”‚                   β”‚                    β”‚                 β”‚
   │◀── JSON response ─│                    β”‚                 β”‚
   β”‚  (prediction +     β”‚                    β”‚                 β”‚
   β”‚   diagnostics)     β”‚                    β”‚                 β”‚
   β”‚                   β”‚                    β”‚                 β”‚
   β”‚  GET /dashboard/    β”‚                    β”‚                 β”‚
   β”‚  stats             β”‚                    β”‚                 β”‚
   │──────────────────────────────────────▢│                 β”‚
   β”‚                   β”‚                    │── aggregate ───▢│
   β”‚                   β”‚                    │◀── stats ───────│
   │◀── stats ─────────────────────────────│                 β”‚

4. Frontend Component Tree

App.tsx
β”œβ”€β”€ QueryClientProvider        (@tanstack/react-query)
β”œβ”€β”€ ThemeProvider               (next-themes, key: "aqua-listen-theme")
β”œβ”€β”€ TooltipProvider
β”œβ”€β”€ SidebarProvider
β”‚   └── SitesProvider           (context: sites array)
β”‚       β”œβ”€β”€ AppSidebar
β”‚       β”‚   └── SidebarMenu
β”‚       β”‚       β”œβ”€β”€ Dashboard   β†’ /
β”‚       β”‚       β”œβ”€β”€ Upload      β†’ /upload
β”‚       β”‚       β”œβ”€β”€ Batch       β†’ /batch
β”‚       β”‚       β”œβ”€β”€ Sites       β†’ /sites
β”‚       β”‚       β”œβ”€β”€ Alerts      β†’ /alerts
β”‚       β”‚       β”œβ”€β”€ Model Info  β†’ /model-info
β”‚       β”‚       └── Settings    β†’ /settings
β”‚       β”œβ”€β”€ header
β”‚       β”‚   β”œβ”€β”€ SidebarTrigger
β”‚       β”‚   └── ThemeToggle
β”‚       └── main (Router)
β”‚           β”œβ”€β”€ Dashboard.tsx
β”‚           β”‚   β”œβ”€β”€ ModelStatusBanner
β”‚           β”‚   β”œβ”€β”€ DashboardStats       ← live: GET :3002/dashboard/stats
β”‚           β”‚   β”œβ”€β”€ HealthStatusCard[]   ← live: GET :3002/predictions/recent
β”‚           β”‚   β”œβ”€β”€ AreaChart            ← MOCK trend data
β”‚           β”‚   β”œβ”€β”€ LineChart            ← MOCK confidence data
β”‚           β”‚   β”œβ”€β”€ ConfidenceGauge      ← hardcoded 84.7
β”‚           β”‚   β”œβ”€β”€ AlertCard[]          ← MOCK alert data
β”‚           β”‚   └── QuickUploadModal
β”‚           β”œβ”€β”€ Upload.tsx
β”‚           β”‚   β”œβ”€β”€ FileUploadArea       ← drag/drop, fake progress
β”‚           β”‚   β”œβ”€β”€ SpectrogramViewer    ← MOCK canvas spectrogram
β”‚           β”‚   β”œβ”€β”€ HealthStatusCard     ← real result after upload
β”‚           β”‚   └── Technical Details    ← real result after upload
β”‚           β”œβ”€β”€ Batch.tsx
β”‚           β”œβ”€β”€ Sites.tsx                ← live: GET :3002/sites
β”‚           β”œβ”€β”€ Alerts.tsx               ← live: GET :3002/alerts
β”‚           β”œβ”€β”€ ModelInfo.tsx            ← MOCK data only
β”‚           └── Settings.tsx
└── Toaster

5. Node.js Server Architecture

server/index.ts
β”œβ”€β”€ CORS middleware (wildcard *)
β”œβ”€β”€ express.json()
β”œβ”€β”€ Request logger middleware
β”œβ”€β”€ registerRoutes(app)          ← server/routes.ts
β”‚   β”œβ”€β”€ GET  /health
β”‚   β”œβ”€β”€ GET  /dashboard/stats
β”‚   β”œβ”€β”€ GET  /sites
β”‚   β”œβ”€β”€ GET  /predictions/recent
β”‚   β”œβ”€β”€ GET  /alerts
β”‚   β”œβ”€β”€ GET  /model/info          ← MOCK static JSON
β”‚   β”œβ”€β”€ POST /predict             ← MOCK random result
β”‚   β”œβ”€β”€ POST /seed-database
β”‚   β”œβ”€β”€ POST /add-dynamic-data
β”‚   └── POST /api/predictions     ← real save from Python
β”œβ”€β”€ setupVite(app, server)        ← dev: Vite HMR proxy, mounted as Express middleware
└── listen(:3002, '127.0.0.1')

storage = new MongoStorage()      ← server/storage.ts line 232
   └── MongoStorage               ← server/mongodb-storage.ts
       β”œβ”€β”€ ensureConnection()     β†’ connectToDatabase()
       β”‚   └── mongoose.connect(MONGODB_ATLAS_URI)
       β”œβ”€β”€ initializeSampleData() (5 reef sites if empty)
       β”œβ”€β”€ getAllSites()
       β”œβ”€β”€ getAllPredictions()
       β”œβ”€β”€ createPrediction()     + auto-alert
       β”œβ”€β”€ getAllAlerts()
       β”œβ”€β”€ getUnreadAlerts()
       β”œβ”€β”€ createAlert()
       β”œβ”€β”€ markAlertAsRead()
       └── getDashboardStats()

6. Python API Architecture

aqualisten_api.py
β”‚
β”œβ”€β”€ CONSTANTS (top of file)
β”‚   β”œβ”€β”€ ANTHRO_CENTROID_THRESHOLD       = 500 Hz
β”‚   β”œβ”€β”€ ANTHRO_BANDWIDTH_THRESHOLD      = 600 Hz
β”‚   β”œβ”€β”€ ANTHRO_ZCR_THRESHOLD            = 0.03
β”‚   β”œβ”€β”€ ANTHRO_LOWFREQ_CUTOFF           = 500 Hz
β”‚   β”œβ”€β”€ ANTHRO_LOWFREQ_RATIO_THRESHOLD  = 0.80
β”‚   β”œβ”€β”€ ANTHRO_WEIGHT_CENTROID          = 0.30
β”‚   β”œβ”€β”€ ANTHRO_WEIGHT_BANDWIDTH         = 0.20
β”‚   β”œβ”€β”€ ANTHRO_WEIGHT_ZCR               = 0.10
β”‚   β”œβ”€β”€ ANTHRO_WEIGHT_LOWFREQ_RATIO     = 0.40
β”‚   β”œβ”€β”€ ANTHRO_SCORE_WARN               = 0.3
β”‚   β”œβ”€β”€ ANTHRO_SCORE_MODERATE           = 0.6
β”‚   β”œβ”€β”€ ANTHRO_SCORE_OVERRIDE           = 0.8
β”‚   β”œβ”€β”€ ANTHRO_ML_CONFIDENCE_FLOOR      = 0.60
β”‚   └── DEBUG_PREDICTIONS               = False
β”‚
β”œβ”€β”€ SimpleNodeJSStorage
β”‚   β”œβ”€β”€ base_url = "http://localhost:3002"
β”‚   β”œβ”€β”€ getAllSites()      β†’ GET :3002/sites
β”‚   └── createPrediction() β†’ POST :3002/api/predictions
β”‚
β”œβ”€β”€ FastAPI app (CORS: all origins)
β”‚
β”œβ”€β”€ GLOBALS
β”‚   β”œβ”€β”€ aqualisten_model         (tf.SavedModel)
β”‚   β”œβ”€β”€ reef_classifications     (dict of DataFrames)
β”‚   β”œβ”€β”€ model_loaded             (bool)
β”‚   β”œβ”€β”€ cached_model_signature   (dict, set at startup)
β”‚   └── latest_embedding         (ndarray, (1,1280), unused)
β”‚
β”œβ”€β”€ load_aqualisten_model()
β”‚   β”œβ”€β”€ tf.saved_model.load("models/savedmodel")
β”‚   β”œβ”€β”€ _cache_model_signature()
β”‚   └── pd.read_csv() for each *.csv in models/
β”‚
β”œβ”€β”€ _cache_model_signature()      ← called once at startup
β”œβ”€β”€ create_reef_health_mapping()  ← fallback keyword dict
β”‚
β”œβ”€β”€ predict_with_aqualisten_model(audio_features)
β”‚   β”œβ”€β”€ raw_audio from features dict
β”‚   β”œβ”€β”€ pad/truncate β†’ 160,000 samples
β”‚   β”œβ”€β”€ tf.convert_to_tensor(float32)
β”‚   β”œβ”€β”€ signatures['serving_default'](inputs=tensor)
β”‚   β”œβ”€β”€ extract output_0 β†’ predictions ndarray
β”‚   β”œβ”€β”€ cache output_1 β†’ latest_embedding
β”‚   └── β†’ process_aqualisten_predictions()
β”‚
β”œβ”€β”€ process_aqualisten_predictions(predictions)
β”‚   β”œβ”€β”€ softmax normalization
β”‚   β”œβ”€β”€ top-10 extraction
β”‚   β”œβ”€β”€ entropy, richness, dominance
β”‚   β”œβ”€β”€ normalize entropy to [0.25, 0.70]
β”‚   └── β†’ (health_status, confidence)
β”‚
β”œβ”€β”€ _score_below(value, threshold, ramp_width)
β”œβ”€β”€ _score_above(value, threshold, ramp_width)
β”‚
β”œβ”€β”€ compute_anthropogenic_noise_score(audio_features)
β”‚   β”œβ”€β”€ spectral_centroid, bandwidth, ZCR from features
β”‚   β”œβ”€β”€ FFT of raw_audio β†’ low_freq_ratio
β”‚   β”œβ”€β”€ score each via _score_below / _score_above
β”‚   └── β†’ weighted sum dict
β”‚
β”œβ”€β”€ classify_reef_health(audio_features, filename)
β”‚   β”œβ”€β”€ [Primary] predict_with_aqualisten_model()
β”‚   β”œβ”€β”€ [Primary] compute_anthropogenic_noise_score()
β”‚   β”œβ”€β”€ Fusion policy β†’ final health_status, confidence
β”‚   β”œβ”€β”€ [Fallback 1] filename keyword matching
β”‚   β”œβ”€β”€ [Fallback 2] spectral feature rules
β”‚   └── [Fallback 3] weighted random choice
β”‚
β”œβ”€β”€ extract_audio_features(audio, sr)
β”‚
└── ENDPOINTS
    β”œβ”€β”€ @startup  β†’ load_aqualisten_model()
    β”œβ”€β”€ GET  /
    β”œβ”€β”€ GET  /health
    β”œβ”€β”€ POST /predict            ← main
    β”œβ”€β”€ POST /batch_predict
    β”œβ”€β”€ GET  /model/info
    β”œβ”€β”€ GET  /dashboard/stats
    β”œβ”€β”€ GET  /sites
    β”œβ”€β”€ GET  /predictions/recent
    β”œβ”€β”€ GET  /alerts
    β”œβ”€β”€ GET  /uploads
    └── GET  /uploads/recent

7. Folder Relationships

AquaListen/               ← Project root
β”‚
β”œβ”€β”€ aqualisten_api.py      Standalone Python service
β”œβ”€β”€ models/                Model files and CSVs
β”‚   └── savedmodel/        Loaded by Python at startup
β”‚
└── app/                   Node.js monorepo
    β”œβ”€β”€ client/            React SPA (bundled by Vite)
    β”‚   └── src/
    β”‚       β”œβ”€β”€ lib/api.ts   Knows about both :8000 and :3002
    β”‚       β”œβ”€β”€ pages/       Route-level components
    β”‚       └── components/  Reusable UI components
    β”œβ”€β”€ server/            Express.js backend
    β”‚   β”œβ”€β”€ routes.ts        API routes
    β”‚   β”œβ”€β”€ storage.ts       IStorage interface
    β”‚   β”œβ”€β”€ mongodb-storage.ts  Active implementation
    β”‚   └── models.ts        Mongoose schemas
    └── shared/
        └── schema.ts       TypeScript types (Drizzle syntax, used for types only)

8. Classification Decision Tree (Verified)

                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚  Model loaded?           β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           YESβ”‚        β”‚NO
                              β–Ό        β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ SurfPerch infer    β”‚  β”‚ Fallback 1:          β”‚
              β”‚ + anthro score     β”‚  β”‚ Filename keywords    β”‚
              β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚ (fish/boat/ambient..) β”‚
                     β”‚               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               ok?   β”‚                     foundβ”‚   not found
               β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”                   β”‚        β”‚
             YES           NO                  β”‚        β–Ό
               β”‚            β”‚              return  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚            β–Ό               early  β”‚ Fallback 2:  β”‚
               β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚ spectral      β”‚
               β”‚      β”‚ Fallback 1  β”‚             β”‚ feature rules β”‚
               β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚                                         β”‚
               β–Ό                                         β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                            β–Ό
   β”‚  Fusion Policy          β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  anthro_score < 0.3?    β”‚                   β”‚ Fallback 3:      β”‚
   β”‚    β†’ trust ML           β”‚                   β”‚ Weighted random  β”‚
   β”‚  0.3–0.6?               β”‚                   β”‚ 45/30/25 split   β”‚
   β”‚    β†’ mild penalty       β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β”‚  0.6–0.8?               β”‚
   β”‚    β†’ moderate penalty   β”‚
   β”‚  β‰₯ 0.8?                 β”‚
   β”‚    conf < 0.60?         β”‚
   β”‚      β†’ override to      β”‚
   β”‚        stressed         β”‚
   β”‚    conf β‰₯ 0.60?         β”‚
   β”‚      β†’ trust + βˆ’15%    β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β–Ό
        Final: health_status
               confidence
               diagnostics

ML Pipeline β€” Verified from Code

Stage 1 Β· Audio Ingestion [AquaListen]

Source: aqualisten_api.py β†’ POST /predict

  • Accepts: .wav, .mp3, .flac, .m4a
  • librosa.load(io.BytesIO(file_contents), sr=None) β€” loads at native sample rate (no resampling occurs at this stage)
  • extract_audio_features(audio, sr) runs next

Stage 2 Β· Feature Extraction [AquaListen]

Source: extract_audio_features() lines 715–749

Librosa extracts:

Feature Method
Spectral Centroid librosa.feature.spectral_centroid
Spectral Bandwidth librosa.feature.spectral_bandwidth
Zero Crossing Rate librosa.feature.zero_crossing_rate
MFCC (13 coeffs) librosa.feature.mfcc(n_mfcc=13)
Spectral Rolloff librosa.feature.spectral_rolloff
Log-Mel Spectrogram librosa.feature.melspectrogram(n_mels=128, n_fft=1024, hop_length=512) β†’ power_to_db
Raw audio copy stored in features['raw_audio']

Important: The audio is first clipped/padded to 1.88 seconds (int(1.88 * sr)) at this stage. The mel spectrogram is computed but NOT passed to the TF model β€” the model receives raw PCM.

Stage 3 Β· SurfPerch Inference [Google SurfPerch]

Source: predict_with_aqualisten_model() lines 247–330

raw_audio  β†’  pad/truncate to 160,000 samples (10 s @ 16 kHz)
           β†’  add batch dim: shape (1, 160000)
           β†’  tf.convert_to_tensor(dtype=float32)
           β†’  aqualisten_model.signatures['serving_default'](inputs=tensor)

Model: tf.saved_model.load("models/savedmodel") Signature key: serving_default Input key: inputs

Outputs:

Key Shape Content
output_0 (1, 10932) Log-probability logits over 10,932 eBird 2021 species
output_1 (1, 1280) Embedding vector β€” cached as latest_embedding, not yet used

SurfPerch does NOT output healthy/stressed/ambient. It outputs taxonomic species probabilities. The ecological mapping is entirely AquaListen custom logic. Its internal architecture (STFT β†’ log-mel β†’ EfficientNet-B0) is inferred from the published SurfPerch paper and is not directly visible in the SavedModel graph.

Stage 4 Β· Ecological Mapping [AquaListen]

Source: process_aqualisten_predictions() lines 332–435

  1. Softmax normalization of output_0 log-probs: exp(logits - max) / sum
  2. Top-10 class probabilities extracted
  3. Three diversity metrics computed:
Metric Formula
Shannon Entropy βˆ’Ξ£ pΒ·log(p+1e-10) over top-10
Richness count of classes where prob > 0.005 across all 10,932
Dominance max_prob / sum(top10_probs)
  1. Entropy normalized to observed range [0.25, 0.70]

Classification rules (in order):

Condition Label Confidence formula
norm_entropy > 0.4 AND richness β‰₯ 10 healthy clip(0.65 + norm_entropy Γ— 0.3, 0.5, 0.95)
norm_entropy < 0.3 AND richness β‰₯ 10 ambient 0.75 + (1 - norm_entropy) Γ— 0.15
norm_entropy < 0.3 OR richness < 8 stressed 0.70 + (1 - norm_entropy) Γ— 0.2
dominance > 0.4 stressed clip(0.65 + dominance Γ— 0.25, 0.5, 0.95)
total_confidence < 0.06 ambient 0.60 + (1 - total_conf) Γ— 0.3
norm_entropy > 0.7 healthy clip(0.80 + norm_entropy Γ— 0.15, 0.5, 0.95)
else ambient 0.60 + norm_entropy Γ— 0.15

Stage 5 Β· Anthropogenic Noise Scoring [AquaListen]

Source: compute_anthropogenic_noise_score() lines 473–570

Four weighted acoustic indicators (smooth linear ramp scoring, not binary):

Indicator Weight Threshold Direction
Spectral Centroid 0.30 500 Hz below = anthropogenic
Spectral Bandwidth 0.20 600 Hz below = anthropogenic
Zero Crossing Rate 0.10 0.03 below = anthropogenic
Low-Freq Energy Ratio 0.40 0.80 above = anthropogenic

Low-freq ratio computed via FFT: Ξ£(|FFT|Β² for freqs ≀ 500 Hz) / Ξ£(|FFT|Β²)

Stage 6 Β· Fusion [AquaListen]

Source: classify_reef_health() lines 572–713

Anthro Score Action
< 0.3 Trust ML completely
0.3 – 0.6 Penalty 5–15% off confidence
0.6 – 0.8 Penalty 15–25% off confidence
β‰₯ 0.8 AND ML confidence < 0.60 Override to stressed
β‰₯ 0.8 AND ML confidence β‰₯ 0.60 Trust ML, βˆ’15% penalty

Final confidence clamped to [0.50, 0.95].

Fallback Chain [AquaListen]

If model is not loaded (in order):

  1. Filename keyword matching against reef_health_map dict
  2. Rule-based on spectral centroid + ZCR thresholds
  3. Weighted random choice: 45% healthy, 30% stressed, 25% ambient

9. What Google SurfPerch Is vs. What AquaListen Is

[Google SurfPerch]

  • A pre-trained TensorFlow SavedModel
  • Trained on 10,932 bird/wildlife species from the eBird 2021 taxonomy
  • Input: raw PCM waveform, 10 seconds at 16 kHz β†’ shape (1, 160000)
  • Output 0: log-probability over 10,932 species β†’ shape (1, 10932)
  • Output 1: 1,280-dimensional embedding vector
  • Internal architecture: STFT β†’ Log-Mel β†’ EfficientNet-B0 (inferred from the SurfPerch paper β€” not directly confirmed from the SavedModel graph)
  • Does NOT classify reefs as healthy/stressed/ambient
  • Does NOT know about coral, reefs, or underwater acoustics specifically

[AquaListen Custom Code]

  • Wraps SurfPerch inference
  • Audio ingestion, feature extraction (librosa)
  • Padding/truncation logic (1.88s for features; 160k samples for model)
  • Softmax normalization of SurfPerch log-probs
  • Ecological mapping via Shannon entropy, richness, dominance
  • Anthropogenic noise scorer (4 weighted acoustic indicators + FFT)
  • Graduated fusion policy (confidence modulation)
  • Fallback classification chain (keyword β†’ spectral rules β†’ random)
  • REST API (FastAPI)
  • Node.js bridge (saves results to MongoDB)
  • React dashboard (visualization)

10. API Reference & Payload Specs

10.1 Python API (aqualisten_api.py)

POST /predict

Runs ML classification on an uploaded audio file.

  • Content-Type: multipart/form-data
  • Request Body:
    • file: Binary file (.wav, .mp3, .flac)
  • Response Model (200 OK):
{
  "success": true,
  "prediction": {
    "health_status": "stressed",
    "confidence": 0.824
  },
  "acoustic_features": {
    "spectral_centroid_hz": 1820.5,
    "spectral_bandwidth_hz": 950.2,
    "zero_crossing_rate": 0.089
  },
  "file_info": {
    "filename": "coral_stressed_recording.wav",
    "duration_seconds": 10.0,
    "sample_rate": 16000
  },
  "processing": {
    "model_used": "SurfPerch SavedModel",
    "processing_time_seconds": 0.354,
    "timestamp": "2026-07-05T14:41:46Z"
  }
}

10.2 Node.js API (app/server/routes.ts)

GET /api/dashboard/stats

Consolidates statistics for the dashboard view.

  • Response Model (200 OK):
{
  "totalSites": 5,
  "healthySites": 3,
  "totalPredictions": 30,
  "activeAlerts": 4,
  "globalAverage": 84.7
}

POST /api/predictions

Persists a prediction result to MongoDB, updates the corresponding reef site's status, and creates alerts if necessary.

  • Content-Type: application/json
  • Request Body:
{
  "siteId": "65b8e90f2b87f2e1a3d90141",
  "filename": "live_recording_001.wav",
  "healthStatus": "stressed",
  "confidence": 0.76,
  "audioFeatures": "{\"spectral_centroid\": 1420.5, \"spectral_bandwidth\": 800.1, \"zero_crossing_rate\": 0.065}",
  "processingTime": 3.2
}
  • Response Model (200 OK):
{
  "success": true,
  "prediction": {
    "id": "60c72b2f9b1d8a23d4c1d8a1",
    "siteId": "65b8e90f2b87f2e1a3d90141",
    "filename": "live_recording_001.wav",
    "healthStatus": "stressed",
    "confidence": 0.76,
    "audioFeatures": "{\"spectral_centroid\": 1420.5, \"spectral_bandwidth\": 800.1, \"zero_crossing_rate\": 0.065}",
    "processingTime": 3.2
  },
  "message": "Prediction saved successfully"
}

11. Environment & Configuration Reference

Create an .env file in the app/ folder to manage database and server settings:

# MongoDB persistence URI (Atlas connection string)
DATABASE_URL=mongodb+srv://<username>:<password>@cluster.mongodb.net/aqualisten

# Port number for Node.js Express server
PORT=3002

# Runtime environment (development / production)
NODE_ENV=development

12. Operational & Deployment Guide

12.1 Running Locally (Development Mode)

Prerequisites: Ensure MongoDB Atlas is accessible (or a local MongoDB instance is running) and that you have Node.js, Python 3.10+, and ffmpeg installed on your system.

βš™οΈ Backend β€” Node.js / Express + Frontend (Port 3002)

Open a terminal and run:

# 1. Navigate to the app directory
cd app

# 2. Install dependencies
npm install

# 3. Create a .env file (if not already present)
#    Copy the example and fill in your MongoDB connection string
cp .env.example .env
#    Then edit .env and set:
#      DATABASE_URL=mongodb+srv://<username>:<password>@cluster.mongodb.net/aqualisten
#      PORT=3002
#      NODE_ENV=development

# 4. Seed the database with default reef sites
npm run seed

# 5. (Optional) Add simulated monitoring data for demo
npm run add-data

# 6. Start the Node.js backend (Vite frontend is mounted automatically)
npm run dev

The Express server β€” serving both the API and the React/Vite frontend β€” starts at http://localhost:3002.

🐍 Backend β€” Python / FastAPI (Port 8000)

Open a separate terminal and run:

# 1. Navigate to the project root directory (where aqualisten_api.py is located)
cd AquaListen

# 2. Create a Python virtual environment (first time only)
python -m venv venv

# 3. Activate the virtual environment
# Windows (CMD):
venv\Scripts\activate.bat
# Windows (PowerShell):
.\venv\Scripts\Activate.ps1
# macOS / Linux:
source venv/bin/activate

# 4. Install Python dependencies (first time only)
pip install -r requirements.txt

# 5. Start the FastAPI server with hot-reload
uvicorn aqualisten_api:app --reload --host 0.0.0.0 --port 8000

The FastAPI ML inference server starts at http://localhost:8000.

Alternative (Windows): You can also run start_system.bat from the root directory, which activates the virtual environment and launches the server automatically.

πŸš€ Quick Start Summary

Component Directory Install Run URL
Frontend + Node.js Backend app/ npm install npm run dev http://localhost:3002
FastAPI Backend Root (/) pip install -r requirements.txt uvicorn aqualisten_api:app --reload --port 8000 http://localhost:8000

Note: The Frontend and Node.js Backend share the same npm run dev command β€” Vite is mounted as middleware inside Express, so there's a single port for both. You need two terminals total: one for the Node.js server (frontend + API) and one for the FastAPI ML service.

12.2 Production Deployment Architecture

                                  [ HTTPS Requests ]
                                           β”‚
                                           β–Ό
                                   [ Nginx Reverse Proxy ]
                                           β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β–Ό (Static Assets / API)                       β–Ό (Heavy ML Compute)
        [ PM2: Node.js Clusters ]                      [ Gunicorn/Uvicorn Workers ]
             (Port 3002)                                      (Port 8000)
                    β”‚                                             β”‚
                    β–Ό                                             β–Ό
          [( MongoDB Atlas )]                            [ TensorFlow Models ]
  • Nginx Reverse Proxy: Directs static assets and database queries to the Node.js service, and routes /predict ML inference requests to the Python service.
  • Process Management:
    • Node.js: Scale horizontally using PM2 clusters:
      pm2 start dist/server/index.js -i max
    • Python: Run Gunicorn with Uvicorn workers to handle concurrent inference requests:
      gunicorn aqualisten_api:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

13. Port Assignments

Service Port Started by
Node.js Express (DB + Frontend) 3002 npm run dev in app/
Python FastAPI (ML) 8000 uvicorn via python aqualisten_api.py
Vite HMR (dev only, internal) internal managed by server/vite.ts
MongoDB cloud MongoDB Atlas (no local port)

14. Troubleshooting & Common Issues

  • Librosa Audio Decoding Errors:
    • Problem: File formats like MP3 or M4A fail to load with a NoBackendError.
    • Solution: Install ffmpeg on the host system and ensure it is added to your environment PATH variables.
  • TensorFlow Model Initialization Failures:
    • Problem: The server fails to start, displaying model loading warnings or errors.
    • Solution: Verify that the files in the savedmodel/ folder are complete. The fallback heuristic logic will automatically handle requests if the model is missing or fails to load.
  • CORS Blocked Requests:
    • Problem: The browser console displays CORS errors when the frontend makes requests to the backend.
    • Solution: Ensure FastAPI and Express are configured to allow cross-origin requests from the client URL during development.

About

🌊 AquaListen is an AI-powered system that monitors coral reef health using underwater sound analysis. It detects marine activity, identifies stress levels, and provides real-time insights to support smarter, non-invasive ocean conservation.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages