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.
- Overview
- System Architecture
- ML Inference Pipeline
- End-to-End Data Flow
- Frontend Component Tree
- Node.js Server Architecture
- Python API Architecture
- Folder Relationships
- Classification Decision Tree
- ML Pipeline β Verified from Code
- Google SurfPerch vs. AquaListen
- API Reference & Payload Specs
- Environment & Configuration Reference
- Operational & Deployment Guide
- Port Assignments
- Troubleshooting & Common Issues
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.
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.
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
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β
ββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββ
β 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 β
ββββββββββββββββββββββββββ
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 ββββββββββββββββββββββββββββββ β
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
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()
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
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)
βββββββββββββββββββββββββββ
β 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
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
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.
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.
Source: process_aqualisten_predictions() lines 332β435
- Softmax normalization of
output_0log-probs:exp(logits - max) / sum - Top-10 class probabilities extracted
- 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) |
- 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 |
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|Β²)
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].
If model is not loaded (in order):
- Filename keyword matching against
reef_health_mapdict - Rule-based on spectral centroid + ZCR thresholds
- Weighted random choice: 45% healthy, 30% stressed, 25% ambient
- 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
- 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)
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"
}
}Consolidates statistics for the dashboard view.
- Response Model (200 OK):
{
"totalSites": 5,
"healthySites": 3,
"totalPredictions": 30,
"activeAlerts": 4,
"globalAverage": 84.7
}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"
}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=developmentPrerequisites: 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.
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 devThe Express server β serving both the API and the React/Vite frontend β starts at http://localhost:3002.
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 8000The FastAPI ML inference server starts at http://localhost:8000.
Alternative (Windows): You can also run
start_system.batfrom the root directory, which activates the virtual environment and launches the server automatically.
| 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 devcommand β 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.
[ 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
/predictML 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
- Node.js: Scale horizontally using PM2 clusters:
| 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) |
- Librosa Audio Decoding Errors:
- Problem: File formats like MP3 or M4A fail to load with a
NoBackendError. - Solution: Install
ffmpegon the host system and ensure it is added to your environmentPATHvariables.
- Problem: File formats like MP3 or M4A fail to load with a
- 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.