A high-performance, full-stack web application designed for loading, decoding, streaming, and rendering multi-channel European Data Format (EDF) physiological recordings (e.g., EEG, EOG, EMG) in real time.
The application utilizes MNE-Python for lazy-loaded file parsing, FastAPI + SSE (Server-Sent Events) for binary chunk streaming, and a Next.js + ApexCharts frontend for declarative stacked waveform representation.
Below is a demonstration of the application rendering EEG waveforms, shifting the dynamic timeline axis on the fly, and scrolling through all 36 channels with baseline stacking:
┌────────────────────────────────────────────────────────┐
│ Browser (Next.js App) │
└──────┬──────────────────────────────────────────┬──────┘
│ │
(Fetch Metadata) (SSE Stream Connection)
│ │
▼ ▼
GET /api/v1/edf/metadata GET /api/v1/edf/stream?params...
┌────────────────────────────────────────────────────────┐
│ FastAPI Web Server (Port 8000) │
│ Delegates to internal EDFService wrapper │
└──────────────────────────┬─────────────────────────────┘
│
(Reads slices of EDF)
│
▼
┌──────────────────────────┐
│ MNE-Python EDF I/O │
└────────────┬─────────────┘
│
(Loads bytes on demand)
▼
[ SC4001E0-PSG.edf ]
- Backend SSE instead of raw gRPC-Web: Browsers cannot speak raw gRPC natively due to HTTP/2 binary framing constraints without a proxy (like Envoy). This project exposes REST endpoints and a Server-Sent Events (SSE) stream returning JSON packets with base64-encoded binary sample arrays. This matches the proto schema, bypasses complex proxy setups, and runs over standard HTTP/1.1.
- Standalone gRPC Server: A companion standalone gRPC server (
grpc_server.pyon:50051) remains active for native gRPC clients, native mobile apps, or command-line utilities likegrpcurl. - Async Thread Offloading: MNE-Python operations are blocking and synchronous. The backend uses
asyncio.to_threadto run MNE operations in thread pools, preventing the FastAPI async event loop from getting blocked.
- Multi-Channel Stacked EEG Display: Stacked vertical rendering of up to 36 channels with customizable baseline offsets to prevent overlapping.
-
Smart Backend Scaling & DC Removal:
- Subtracts the baseline offset (mean of the first 10 seconds of calibration) for each channel to center waveforms around zero.
- Multiplies EEG/EOG/EMG channels by
$10^6$ (V → µV) on the backend so they are transmitted in readable physiological scales. Non-EEG channels (e.g., Temperature, Respiration) remain in their native units.
- Declarative React Rendering: Completely decoupled from imperative rendering loops, avoiding race conditions that cause waveforms to flicker or disappear.
-
Dynamic Top-Mounted X-Axis: The time-series x-axis labels (
Time (s)) are shown at the top of the chart, dynamically shifting in bounds (minXtomaxX) as you navigate. - Debounced Navigation & Seek: Dragging the seek bar slider or changing window limits triggers a single 200ms debounced network fetch, avoiding server spam.
-
Constrained Height Layout: The viewport height is constrained to
100vh, enabling the sidebar checklist of 36 channels to scroll independently without shifting layout cards off-screen.
grpc-test/
├── backend/
│ ├── app/
│ │ ├── core/
│ │ │ ├── config.py # Configuration & env parser
│ │ │ └── grpc_config.py # gRPC server parameters
│ │ ├── grpc/ # Stubs and native gRPC implementation
│ │ │ ├── generated/ # Auto-generated python protobuf stubs
│ │ │ ├── server.py # gRPC server instantiator
│ │ │ └── servicer.py # Servicer handling channel buffers
│ │ ├── services/
│ │ │ └── edf_service.py # MNE core reader, DC offset removal & scaling
│ │ └── api/v1/
│ │ └── router.py # GET /edf/metadata & GET /edf/stream (SSE)
│ ├── proto/
│ │ └── edf_stream.proto # Protobuf protocol specification
│ ├── scripts/
│ │ └── generate_proto.sh # Stub generator script
│ ├── .env # Active environment settings
│ ├── main.py # FastAPI startup entry point
│ ├── grpc_server.py # Standalone gRPC startup entry point
│ └── requirements.txt # Backend requirements (mne, numpy, fastapi, etc.)
│
├── frontend/
│ ├── app/
│ │ ├── globals.css # Custom HSL-based dark design system
│ │ ├── layout.tsx # SEO tags, fonts, and HTML wrappers
│ │ └── page.tsx # Main entry point mounting EDFViewer
│ ├── components/edf-viewer/
│ │ ├── EDFViewer.tsx # Component orchestrating page state
│ │ ├── WaveformChart.tsx # Declarative ApexChart wrapping coordinate data
│ │ ├── ChannelSelector.tsx # Grouped scrollable channel checklist
│ │ ├── TimeControls.tsx # Seek bar slider, prev/next buttons
│ │ └── MetadataPanel.tsx # Recording stats & subject info
│ ├── lib/
│ │ ├── grpc/
│ │ │ └── edf-client.ts # SSE parser converting base64 -> Float32Arrays
│ │ └── hooks/
│ │ ├── useEDFMetadata.ts # Fetch metadata hook
│ │ └── useEDFStream.ts # Accumulating stream chunk buffer hook
│ ├── proto/
│ │ └── edf_stream.ts # TS interfaces matching proto definitions
│ └── .env.local # Frontend env variables
│
├── SC4001E0-PSG.edf # Active physiological EDF file
└── aaaaamrj_s001_t000.edf # Alternative EEG EDF file
- Python 3.12+ (with
venvsupport) - Node.js 18+ & npm
Navigate to the backend folder, create and activate a virtual environment, and install dependencies:
cd backend
# Create virtual environment
python3 -m venv .venv
# Activate virtual environment
source .venv/bin/activate
# Install requirements
pip install -r requirements.txtEnsure your backend/.env points to the active EDF file:
PORT=8000
EDF_FILE_PATH=/Users/subhajithait/Documents/testing/grpc-test/SC4001E0-PSG.edfRun the FastAPI web server:
uvicorn main:app --reload --port 8000(Optional) To start the standalone gRPC server:
python grpc_server.pyNavigate to the frontend folder, install Node modules, and run the development server:
cd ../frontend
# Install dependencies
npm install
# Run the development server
npm run devOpen http://localhost:3000 in your browser.
- Mounting:
useEDFMetadatafetches metadata from the backend. The frontend initialises the selected channels and automatically queries the first10 secondsof the recording. - Streaming: The frontend initiates a connection to
/api/v1/edf/streampassing parameters:start_sample,window_samples, andchannel_indices. - Chunking & Transfer:
- The backend reads slices of EDF data using MNE.
- Subtracts cached DC baseline offset and applies scale multipliers (
V → µV). - Serialises the raw
float32array to raw bytes (data_f32.tobytes()). - Encodes the bytes as Base64 and yields the chunk via SSE.
- Decoding & Offsets:
- The frontend decodes the base64 string back into binary values using
atob. - Reinterprets the binary buffer into standard floats:
const floats = new Float32Array(bytes.buffer); - Maps each channel into stacked coordinates
{ x: timestamp, y: amplitude + channelOffset }. - Passes the series to ApexCharts, which updates reactively.
- The frontend decodes the base64 string back into binary values using