Real-time physiological stress detection from ECG and respiration signals, with a live web console and per-user calibration.
StressShield reads a BITalino sensor stream over LSL (Lab Streaming Layer), extracts HRV and respiration features every 5 seconds over a rolling 60-second window, and classifies the current state as stress or non-stress using a model trained on the WESAD dataset.
- Live classification — rolling 60 s window, prediction every 5 s
- Web console — stress-probability trend, live ECG and respiration traces, and derived metrics (HR, RMSSD, LF/HF, breath rate, breath amplitude), streamed to the browser over Server-Sent Events
- Per-user calibration — records a relaxed baseline while the user watches a
calming video, then sets a personal threshold at
baseline_mean + 1.5 × std(capped at 0.95). Profiles are stored incalibration.jsonand can be switched or deleted from the UI. - CSV export — record 1–300 s of raw signal and per-window predictions (including all extracted features) and download them as CSV
- Light/dark theme
| Requirement | Detail |
|---|---|
| Sensor | BITalino with ECG and respiration (PZT) channels |
| Software | OpenSignals (r)evolution, streaming over LSL |
| Sampling rate | 1000 Hz |
| Channels | index 1 = respiration, index 2 = ECG |
| LSL stream name | OpenSignals |
These are set as constants at the top of app.py — change CH_ECG, CH_RESP,
FS, or STREAM_NAME if your acquisition setup differs.
git clone https://github.com/sharduldhande/StressShield.git
cd StressShield
pip install -r requirements.txtKey dependencies: flask, pylsl, neurokit2, scikit-learn, joblib, numpy.
- Connect the BITalino, open OpenSignals, and enable LSL streaming.
- Verify the stream is visible:
python lsl_probe.py- Start the console:
python app.py- Open http://localhost:5000.
The first 60 seconds fill the buffer (progress is shown in the UI) before the first prediction appears.
For a terminal-only version without the web UI, run python realtime_stress.py.
- Click + Calibrate, enter a name, and click Start.
- Watch the calibration video and stay relaxed. The last 24 prediction windows (2 minutes) are used as the baseline.
- The computed threshold is saved and the profile becomes selectable from the dropdown.
Without a calibration profile the threshold defaults to 0.5.
The calibration video (
4minbeach.mp4, referenced asCALIB_VIDEO_FILE) is not included in the repository — supply your own and place it in the project root.
ECG (via neurokit2.hrv) — mean NN, SDNN, RMSSD, pNN50, LF power, HF power,
LF/HF ratio
Respiration (via neurokit2.rsp_process) — mean, SD, min and max breathing
rate, mean amplitude
Windows where feature extraction fails (poor signal quality) are reported as errors rather than classified.
Trained on the WESAD
dataset; extracted features are in wesad_features.csv.
stress_model.pkl— full model including EDA featuresstress_model_no_eda.pkl— ECG + respiration only; this is the one the app loads, since the runtime setup has no EDA channel
Each pickle holds a dict with model, scaler, and features keys.
Retrain the EDA-free model with python retrain_no_eda.py.
Notebooks
stressdetect.ipynb— feature engineering and model trainingExplainer.ipynb— model interpretation and analysis
The model is evaluated with leave-one-subject-out (LOSO) cross-validation on the 15 WESAD subjects (S2–S17, excluding S12): train on 14 subjects, test on the held-out one, repeat 15 times. Physiological signals vary far more between people than within one person, so a random train/test split leaks subject identity and inflates accuracy. LOSO is the harder, honest number and is what a new user of the device would actually experience.
| Setting | Value |
|---|---|
| Dataset | WESAD chest RespiBAN (ECG, respiration; EDA dropped for the deployed model) |
| Sampling rate | 700 Hz |
| Window | 60 s, 30 s step (50% overlap) |
| Label rule | majority label in window, ≥ 80% purity; baseline → 0, stress (TSST) → 1 |
| Windows | 883 total (313 stress, 570 non-stress; 35.4% stress) |
| Features | 12: 7 HRV (mean NN, SDNN, RMSSD, pNN50, LF, HF, LF/HF) + 5 respiration (rate mean/SD/min/max, amplitude mean) |
| Classifier | XGBoost, 300 trees, depth 4, lr 0.05, scale_pos_weight for class imbalance |
| Metric | Value |
|---|---|
| Accuracy | 0.89 |
| ROC-AUC | 0.972 |
| Stress precision / recall / F1 | 0.84 / 0.88 / 0.85 |
| Non-stress precision / recall / F1 | 0.93 / 0.91 / 0.92 |
| Macro F1 | 0.89 |
Per-subject AUC (held-out subject):
| S2 | S3 | S4 | S5 | S6 | S7 | S8 | S9 | S10 | S11 | S13 | S14 | S15 | S16 | S17 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.93 | 0.99 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 1.00 | 0.31 | 1.00 | 0.99 | 1.00 | 0.96 | 1.00 | 0.99 |
Fourteen of fifteen subjects score ≥ 0.93 AUC. S10 is a clear outlier (0.31): the model's
stress signature is inverted for this subject, which is exactly the inter-subject variability problem
LOSO is meant to expose. This is the motivation for the per-user calibration step in the app
(see Calibrating a profile): rather than trusting a population threshold of
0.5, each user records a relaxed baseline and the decision threshold is set at
baseline_mean + 1.5 × std.
Reproduce:
python retrain_no_eda.pyprints the full LOSO report for the ECG + respiration model.stressdetect.ipynbcontains the original feature extraction and the EDA-inclusive run.
SHAP values were computed with TreeExplainer on the deployed (no-EDA) model
(Explainer.ipynb). Feature importance, gain-based:
| Rank | Feature | Importance | Physiological reading |
|---|---|---|---|
| 1 | hrv_meanNN |
0.287 | Shorter mean RR interval (higher heart rate) → stress |
| 2 | resp_rate_std |
0.126 | More irregular breathing → stress |
| 3 | hrv_pnn50 |
0.112 | Less beat-to-beat variability (lower vagal tone) → stress |
| 4 | resp_amplitude_mean |
0.109 | Shallower breaths → stress |
| 5 | resp_rate_min |
0.072 | |
| 6 | resp_rate_max |
0.069 | |
| 7 | hrv_rmssd |
0.063 | Lower RMSSD (parasympathetic withdrawal) → stress |
| 8 | resp_rate_mean |
0.053 | |
| 9–12 | hrv_hf, hrv_lf_hf, hrv_sdnn, hrv_lf |
0.02–0.03 | Frequency-domain HRV contributes least |
The top features are the ones a clinician would expect: heart rate, vagal-tone markers (pNN50, RMSSD), and breathing regularity. Frequency-domain HRV (LF, HF, LF/HF) is weak, likely because a 60 s window is too short for a stable spectral estimate. Cardiac and respiratory features together account for the whole model, so the deployed system does not depend on the EDA channel that BITalino setups often lack.
STAR is closed-loop: when the calibrated stress probability crosses the user's threshold, the system fades in a passive ambient-audio intervention (Weightless by Marconi Union, chosen for its documented anxiolytic effect and lyric-free, low-distraction profile) and fades it out when the state returns to non-stress. The intervention was validated in a pilot within-subject AB/BA crossover experiment (GT Human-Systems Integration project, manuscript submitted to ACM 2026).
| Participants | 6, each completing a familiarisation trial (discarded), one control trial and one intervention trial |
| Design | Two-period crossover, order counterbalanced (AB / BA) across participants to cancel practice, fatigue and carry-over effects |
| Stressor / task | Time-constrained square-tracing game: connect four target dots into as many accurate squares as possible under increasing time pressure; scored on straightness, completeness and path deviation |
| Control (A) | Task with live stress prediction but no intervention |
| Intervention (B) | Same task; ambient soundtrack triggered by the model whenever a stress window is detected |
| Primary outcome | Share of 60 s windows classified as stress, and mean stress probability |
| Secondary outcome | Task score |
| Outcome | Control | Intervention | Change |
|---|---|---|---|
| Detected stress windows | 40.7% | 32.2% | −8.5 pp |
| Mean stress probability | 0.470 | 0.376 | −0.094 |
| Mean task score | 73.88 | 76.16 | +2.28 |
Four of six participants scored higher with the intervention; the other two were on par with control. A Cohen's d power analysis on the pilot suggests 41–55 participants would be needed for statistical significance, so these results are exploratory.
A crossover design was chosen over a between-subjects comparison for the same reason LOSO was chosen for evaluation: physiological baselines differ enormously between people, and using each participant as their own control removes that variance.
| Endpoint | Method | Purpose |
|---|---|---|
/stream |
GET | SSE stream of predictions, status and calibration events |
/signals |
GET | SSE stream of downsampled ECG/respiration samples |
/api/profiles |
GET | List calibration profiles |
/api/profiles/<name> |
DELETE | Delete a profile |
/api/select |
POST | Activate a profile |
/api/calibrate |
POST | Begin baseline collection |
/api/calibrate/finish |
POST | Compute and save the threshold |
/api/export/start |
POST | Begin a timed recording |
/api/export/signals |
GET | Download raw signal CSV |
/api/export/predictions |
GET | Download prediction + feature CSV |
- Research and educational use only. Not a medical device, and not validated for clinical or diagnostic use.
- Trained on WESAD, whose participants and lab-induced stressors may not generalise to your users or conditions.
- ECG quality is sensitive to electrode placement and movement; motion artefacts will produce dropped windows or unreliable probabilities.
- Single-user, single-stream: the server assumes one connected device.
- One subject (S10) is misclassified almost entirely; per-user calibration mitigates but does not eliminate this.
- The runtime pipeline uses BITalino at 1000 Hz while training used RespiBAN at 700 Hz; features are
rate-normalised by
neurokit2, but a domain gap remains. - Windows where
neurokit2fails to find R-peaks (poor contact, motion) are dropped rather than classified.
