Spaces:
Runtime error
Runtime error
GitHub Actions commited on
Commit ·
314b374
1
Parent(s): 338ec2d
Deploy v0.0.1 from GitHub Actions
Browse files- .env.example +6 -0
- .gitattributes +0 -35
- Dockerfile +15 -10
- README.md +134 -7
- app/database.py +0 -1
- app/main.py +1 -0
- app/models/prediction.py +0 -1
- app/models/user.py +1 -1
- app/services/feature_extractor.py +1 -1
- app/services/ml_service.py +3 -3
- ml_models/ecgfounder_best.pt +3 -0
- ml_models/xgboost_cardiac.joblib +3 -0
- ml_src/README.md +97 -0
- render.yaml +21 -0
- requirements.txt +9 -9
.env.example
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/
|
| 2 |
+
DATABASE_NAME=cardiac_monitor
|
| 3 |
+
JWT_SECRET=change-this-to-a-random-secret-key
|
| 4 |
+
JWT_ALGORITHM=HS256
|
| 5 |
+
JWT_EXPIRY_HOURS=24
|
| 6 |
+
API_KEY=your-device-api-key-here
|
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -13,16 +13,21 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|
| 13 |
COPY app/ app/
|
| 14 |
COPY ml_src/ ml_src/
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
EXPOSE 7860
|
| 27 |
|
| 28 |
-
CMD
|
|
|
|
| 13 |
COPY app/ app/
|
| 14 |
COPY ml_src/ ml_src/
|
| 15 |
|
| 16 |
+
# Copy model files — if they're Git LFS pointers, download the real files from GitHub
|
| 17 |
+
COPY ml_models/ ml_models/
|
| 18 |
+
RUN MODEL_SIZE=$(wc -c < ml_models/ecgfounder_best.pt) && \
|
| 19 |
+
if [ "$MODEL_SIZE" -lt 1000 ]; then \
|
| 20 |
+
echo "Detected LFS pointers — downloading real model files from GitHub..." && \
|
| 21 |
+
apt-get update && apt-get install -y --no-install-recommends curl && \
|
| 22 |
+
curl -L -o ml_models/ecgfounder_best.pt \
|
| 23 |
+
"https://media.githubusercontent.com/media/Sanuka23/cardiac-monitor/main/backend/ml_models/ecgfounder_best.pt" && \
|
| 24 |
+
curl -L -o ml_models/xgboost_cardiac.joblib \
|
| 25 |
+
"https://media.githubusercontent.com/media/Sanuka23/cardiac-monitor/main/backend/ml_models/xgboost_cardiac.joblib" && \
|
| 26 |
+
apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \
|
| 27 |
+
else \
|
| 28 |
+
echo "Model files are real (not LFS pointers) — no download needed"; \
|
| 29 |
+
fi
|
| 30 |
|
| 31 |
EXPOSE 7860
|
| 32 |
|
| 33 |
+
CMD uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}
|
README.md
CHANGED
|
@@ -12,12 +12,139 @@ short_description: Cardiac monitoring API with ML risk prediction
|
|
| 12 |
|
| 13 |
# Cardiac Monitor API
|
| 14 |
|
| 15 |
-
FastAPI backend for ESP32
|
| 16 |
|
| 17 |
-
##
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# Cardiac Monitor API
|
| 14 |
|
| 15 |
+
FastAPI backend for the ESP32 cardiac monitoring system. Handles user authentication, device management, vitals storage, and ML-based cardiac risk prediction.
|
| 16 |
|
| 17 |
+
## Live Deployment
|
| 18 |
|
| 19 |
+
**URL**: `https://sanuka0523-cardiac-monitor-api.hf.space`
|
| 20 |
+
|
| 21 |
+
Hosted on Hugging Face Spaces (Docker SDK). The API docs are available at `/docs` (Swagger UI).
|
| 22 |
+
|
| 23 |
+
## Tech Stack
|
| 24 |
+
|
| 25 |
+
- **Framework**: FastAPI 0.115
|
| 26 |
+
- **Database**: MongoDB Atlas via Motor (async)
|
| 27 |
+
- **Auth**: JWT (python-jose) + bcrypt password hashing
|
| 28 |
+
- **ML**: PyTorch (ECGFounder) + XGBoost ensemble
|
| 29 |
+
- **Signal Processing**: NeuroKit2, SciPy
|
| 30 |
+
|
| 31 |
+
## API Reference
|
| 32 |
+
|
| 33 |
+
### Authentication
|
| 34 |
+
|
| 35 |
+
| Method | Path | Body | Response |
|
| 36 |
+
|--------|------|------|----------|
|
| 37 |
+
| POST | `/api/v1/auth/register` | `{email, password, name}` | `{access_token, token_type}` |
|
| 38 |
+
| POST | `/api/v1/auth/login` | Form: `username, password` | `{access_token, token_type}` |
|
| 39 |
+
| GET | `/api/v1/auth/me` | — | User object |
|
| 40 |
+
| PUT | `/api/v1/auth/profile` | `{health_profile: {...}}` | Updated user |
|
| 41 |
+
|
| 42 |
+
### Devices
|
| 43 |
+
|
| 44 |
+
| Method | Path | Body | Response |
|
| 45 |
+
|--------|------|------|----------|
|
| 46 |
+
| POST | `/api/v1/devices/register` | `{device_id}` | Device object |
|
| 47 |
+
| GET | `/api/v1/devices` | — | List of devices |
|
| 48 |
+
|
| 49 |
+
### Vitals (ESP32 uploads)
|
| 50 |
+
|
| 51 |
+
| Method | Path | Auth | Body |
|
| 52 |
+
|--------|------|------|------|
|
| 53 |
+
| POST | `/api/v1/vitals` | API Key | `{device_id, timestamp, ecg_samples, heart_rate_bpm, spo2_percent, ...}` |
|
| 54 |
+
| GET | `/api/v1/vitals/{device_id}` | JWT | Vitals history (paginated) |
|
| 55 |
+
| GET | `/api/v1/vitals/{device_id}/latest` | JWT | Latest vitals reading |
|
| 56 |
+
|
| 57 |
+
### Predictions
|
| 58 |
+
|
| 59 |
+
| Method | Path | Auth | Response |
|
| 60 |
+
|--------|------|------|----------|
|
| 61 |
+
| GET | `/api/v1/predictions/{device_id}/latest` | JWT | Latest risk prediction |
|
| 62 |
+
| GET | `/api/v1/predictions/{device_id}` | JWT | Prediction history |
|
| 63 |
+
|
| 64 |
+
## Environment Variables
|
| 65 |
+
|
| 66 |
+
| Variable | Description | Required |
|
| 67 |
+
|----------|-------------|----------|
|
| 68 |
+
| `MONGODB_URI` | MongoDB Atlas connection string | Yes |
|
| 69 |
+
| `DATABASE_NAME` | Database name (default: `cardiac_monitor`) | No |
|
| 70 |
+
| `JWT_SECRET` | Secret key for JWT signing | Yes |
|
| 71 |
+
| `API_KEY` | API key for ESP32 device auth | Yes |
|
| 72 |
+
|
| 73 |
+
## Database Collections
|
| 74 |
+
|
| 75 |
+
| Collection | Purpose | Key Fields |
|
| 76 |
+
|------------|---------|------------|
|
| 77 |
+
| `users` | User accounts + health profiles | email, password_hash, health_profile |
|
| 78 |
+
| `devices` | Registered ESP32 devices | device_id, owner_user_id |
|
| 79 |
+
| `vitals` | Raw vitals readings | device_id, timestamp, ecg_samples, heart_rate_bpm |
|
| 80 |
+
| `predictions` | ML risk predictions | vitals_id, risk_score, risk_label, confidence |
|
| 81 |
+
|
| 82 |
+
## ML Pipeline
|
| 83 |
+
|
| 84 |
+
On each vitals upload, the backend runs an ensemble prediction:
|
| 85 |
+
|
| 86 |
+
1. **ECGFounder** (PyTorch): Deep learning model extracts features from raw ECG waveform
|
| 87 |
+
2. **Feature Engineering**: 14 clinical features (HRV, QRS duration, signal quality, etc.)
|
| 88 |
+
3. **XGBoost**: Gradient-boosted classifier on combined features
|
| 89 |
+
4. **Ensemble**: Weighted combination produces final risk score (0.0-1.0) and label
|
| 90 |
+
|
| 91 |
+
Risk labels: `normal`, `low`, `moderate`, `elevated`, `high`
|
| 92 |
+
|
| 93 |
+
## Local Development
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
cd backend
|
| 97 |
+
pip install -r requirements.txt
|
| 98 |
+
|
| 99 |
+
# Set environment variables
|
| 100 |
+
export MONGODB_URI="mongodb+srv://..."
|
| 101 |
+
export JWT_SECRET="your-secret"
|
| 102 |
+
export API_KEY="your-api-key"
|
| 103 |
+
|
| 104 |
+
# Run
|
| 105 |
+
uvicorn app.main:app --reload --port 8000
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
## Docker Deployment
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
docker build -t cardiac-api .
|
| 112 |
+
docker run -p 7860:7860 \
|
| 113 |
+
-e MONGODB_URI="..." \
|
| 114 |
+
-e JWT_SECRET="..." \
|
| 115 |
+
-e API_KEY="..." \
|
| 116 |
+
cardiac-api
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## Project Structure
|
| 120 |
+
|
| 121 |
+
```
|
| 122 |
+
backend/
|
| 123 |
+
├── app/
|
| 124 |
+
│ ├── main.py # FastAPI app, CORS, router mounting
|
| 125 |
+
│ ├── config.py # Pydantic settings (env vars)
|
| 126 |
+
│ ├── database.py # Motor client, index creation
|
| 127 |
+
│ ├── middleware/
|
| 128 |
+
│ │ └── auth.py # JWT + API key verification
|
| 129 |
+
│ ├── models/ # Pydantic request/response models
|
| 130 |
+
│ │ ├── user.py
|
| 131 |
+
│ │ ├── device.py
|
| 132 |
+
│ │ ├── vitals.py
|
| 133 |
+
│ │ └── prediction.py
|
| 134 |
+
│ ├── routes/ # API endpoint handlers
|
| 135 |
+
│ │ ├── auth.py
|
| 136 |
+
│ │ ├── devices.py
|
| 137 |
+
│ │ ├── health.py
|
| 138 |
+
│ │ ├── vitals.py
|
| 139 |
+
│ │ └── predictions.py
|
| 140 |
+
│ └── services/
|
| 141 |
+
│ └── ml_service.py # ML model loading + prediction
|
| 142 |
+
├── ml_src/
|
| 143 |
+
│ ├── ecg_foundation.py # ECGFounder model definition
|
| 144 |
+
│ └── feature_engineer.py # Clinical feature extraction
|
| 145 |
+
├── ml_models/ # Trained model files (LFS)
|
| 146 |
+
│ ├── ecgfounder_best.pt
|
| 147 |
+
│ └── xgboost_cardiac.joblib
|
| 148 |
+
├── Dockerfile
|
| 149 |
+
└── requirements.txt
|
| 150 |
+
```
|
app/database.py
CHANGED
|
@@ -27,7 +27,6 @@ async def connect_db(uri: str, db_name: str):
|
|
| 27 |
|
| 28 |
|
| 29 |
async def close_db():
|
| 30 |
-
global client
|
| 31 |
if client:
|
| 32 |
client.close()
|
| 33 |
|
|
|
|
| 27 |
|
| 28 |
|
| 29 |
async def close_db():
|
|
|
|
| 30 |
if client:
|
| 31 |
client.close()
|
| 32 |
|
app/main.py
CHANGED
|
@@ -40,6 +40,7 @@ app.add_middleware(
|
|
| 40 |
allow_headers=["*"],
|
| 41 |
)
|
| 42 |
|
|
|
|
| 43 |
@app.get("/", include_in_schema=False)
|
| 44 |
async def root():
|
| 45 |
return RedirectResponse(url="/docs")
|
|
|
|
| 40 |
allow_headers=["*"],
|
| 41 |
)
|
| 42 |
|
| 43 |
+
|
| 44 |
@app.get("/", include_in_schema=False)
|
| 45 |
async def root():
|
| 46 |
return RedirectResponse(url="/docs")
|
app/models/prediction.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
from pydantic import BaseModel, Field
|
| 2 |
-
from typing import Optional
|
| 3 |
from datetime import datetime
|
| 4 |
|
| 5 |
|
|
|
|
| 1 |
from pydantic import BaseModel, Field
|
|
|
|
| 2 |
from datetime import datetime
|
| 3 |
|
| 4 |
|
app/models/user.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
from pydantic import BaseModel,
|
| 2 |
from typing import List, Optional
|
| 3 |
from datetime import datetime
|
| 4 |
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
from typing import List, Optional
|
| 3 |
from datetime import datetime
|
| 4 |
|
app/services/feature_extractor.py
CHANGED
|
@@ -10,7 +10,7 @@ import sys
|
|
| 10 |
ML_SRC = os.path.join(os.path.dirname(__file__), "..", "..", "ml_src")
|
| 11 |
sys.path.insert(0, ML_SRC)
|
| 12 |
|
| 13 |
-
from feature_extractor import (
|
| 14 |
extract_ecg_features,
|
| 15 |
features_to_array,
|
| 16 |
FEATURE_NAMES,
|
|
|
|
| 10 |
ML_SRC = os.path.join(os.path.dirname(__file__), "..", "..", "ml_src")
|
| 11 |
sys.path.insert(0, ML_SRC)
|
| 12 |
|
| 13 |
+
from feature_extractor import ( # noqa: E402
|
| 14 |
extract_ecg_features,
|
| 15 |
features_to_array,
|
| 16 |
FEATURE_NAMES,
|
app/services/ml_service.py
CHANGED
|
@@ -15,8 +15,8 @@ BACKEND_ROOT = os.path.join(os.path.dirname(__file__), "..", "..")
|
|
| 15 |
ML_SRC = os.path.join(BACKEND_ROOT, "ml_src")
|
| 16 |
sys.path.insert(0, ML_SRC)
|
| 17 |
|
| 18 |
-
from net1d import Net1D
|
| 19 |
-
from feature_extractor import extract_ecg_features, features_to_array, FEATURE_NAMES
|
| 20 |
|
| 21 |
# Model paths — bundled in backend/ml_models/
|
| 22 |
MODEL_DIR = os.path.join(BACKEND_ROOT, "ml_models")
|
|
@@ -83,7 +83,7 @@ def load_models():
|
|
| 83 |
use_do=False,
|
| 84 |
n_classes=1,
|
| 85 |
)
|
| 86 |
-
state_dict = torch.load(ecg_path, map_location=_device, weights_only=False)
|
| 87 |
_ecg_model.load_state_dict(state_dict)
|
| 88 |
_ecg_model.to(_device)
|
| 89 |
_ecg_model.eval()
|
|
|
|
| 15 |
ML_SRC = os.path.join(BACKEND_ROOT, "ml_src")
|
| 16 |
sys.path.insert(0, ML_SRC)
|
| 17 |
|
| 18 |
+
from net1d import Net1D # noqa: E402
|
| 19 |
+
from feature_extractor import extract_ecg_features, features_to_array, FEATURE_NAMES # noqa: E402
|
| 20 |
|
| 21 |
# Model paths — bundled in backend/ml_models/
|
| 22 |
MODEL_DIR = os.path.join(BACKEND_ROOT, "ml_models")
|
|
|
|
| 83 |
use_do=False,
|
| 84 |
n_classes=1,
|
| 85 |
)
|
| 86 |
+
state_dict = torch.load(ecg_path, map_location=_device, weights_only=False) # nosec B614
|
| 87 |
_ecg_model.load_state_dict(state_dict)
|
| 88 |
_ecg_model.to(_device)
|
| 89 |
_ecg_model.eval()
|
ml_models/ecgfounder_best.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b632fd76127099d8aca8001bf2a816fd0113b65b4fea9a32fbabbeda0a74319a
|
| 3 |
+
size 123072541
|
ml_models/xgboost_cardiac.joblib
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3f6a0c12e66bd9ea73e293602cd85ea1e9195fb5f98200dcb9c4595409341613
|
| 3 |
+
size 769646
|
ml_src/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ML Pipeline
|
| 2 |
+
|
| 3 |
+
Cardiac risk prediction using an ensemble of ECGFounder (deep learning) and XGBoost (gradient boosting).
|
| 4 |
+
|
| 5 |
+
## Model Architecture
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
Raw ECG (1000 samples @ 100Hz)
|
| 9 |
+
│
|
| 10 |
+
├──> ECGFounder (1D CNN)──> 128-dim embedding
|
| 11 |
+
│ │
|
| 12 |
+
├──> Feature Engineering ────────>├──> 14 clinical features
|
| 13 |
+
│ (NeuroKit2 + SciPy) │
|
| 14 |
+
│ v
|
| 15 |
+
│ XGBoost Classifier
|
| 16 |
+
│ │
|
| 17 |
+
└── Ensemble ────────────────────>│──> Risk Score (0.0-1.0)
|
| 18 |
+
(weighted average) Risk Label
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
### ECGFounder
|
| 22 |
+
|
| 23 |
+
- **Architecture**: 1D convolutional neural network (`net1d.py`)
|
| 24 |
+
- **Input**: 1000 ECG samples (10s window at 100Hz), bandpass filtered 0.5-40Hz
|
| 25 |
+
- **Output**: 128-dimensional feature embedding
|
| 26 |
+
- **Model file**: `ml_models/ecgfounder_best.pt` (117 MB)
|
| 27 |
+
|
| 28 |
+
### XGBoost Classifier
|
| 29 |
+
|
| 30 |
+
- **Input**: 14 features (ECG embedding + clinical features)
|
| 31 |
+
- **Output**: Binary risk probability
|
| 32 |
+
- **Model file**: `ml_models/xgboost_cardiac.joblib` (752 KB)
|
| 33 |
+
|
| 34 |
+
## Feature Engineering
|
| 35 |
+
|
| 36 |
+
The `feature_extractor.py` module computes clinical features from raw ECG:
|
| 37 |
+
|
| 38 |
+
| Feature | Source | Description |
|
| 39 |
+
|---------|--------|-------------|
|
| 40 |
+
| Heart Rate | Input | BPM from MAX30100 |
|
| 41 |
+
| SpO2 | Input | Blood oxygen percentage |
|
| 42 |
+
| HRV (SDNN) | NeuroKit2 | Heart rate variability |
|
| 43 |
+
| HRV (RMSSD) | NeuroKit2 | Root mean square of successive RR differences |
|
| 44 |
+
| QRS Duration | NeuroKit2 | Ventricular depolarization time |
|
| 45 |
+
| PR Interval | NeuroKit2 | Atrial-ventricular conduction |
|
| 46 |
+
| Signal Quality | SciPy | SNR estimate of ECG signal |
|
| 47 |
+
| Mean RR | Computed | Average R-R interval |
|
| 48 |
+
| RR Irregularity | Computed | Coefficient of variation of RR |
|
| 49 |
+
| Age | Profile | User age (if available) |
|
| 50 |
+
| Sex | Profile | Binary encoded |
|
| 51 |
+
| BMI | Profile | Computed from height/weight |
|
| 52 |
+
| Comorbidity Score | Profile | Count of risk factors |
|
| 53 |
+
| HR Deviation | History | Z-score vs 24h baseline |
|
| 54 |
+
|
| 55 |
+
## Inference Flow
|
| 56 |
+
|
| 57 |
+
1. Vitals uploaded to `POST /api/v1/vitals`
|
| 58 |
+
2. If ECG leads are connected and >= 100 samples:
|
| 59 |
+
a. Load user health profile from database
|
| 60 |
+
b. Compute 24h and 7d historical baselines
|
| 61 |
+
c. Extract features from ECG waveform
|
| 62 |
+
d. Run ECGFounder for deep features
|
| 63 |
+
e. Combine and run XGBoost
|
| 64 |
+
f. Store prediction in `predictions` collection
|
| 65 |
+
3. Return vitals + prediction in API response
|
| 66 |
+
|
| 67 |
+
## Risk Labels
|
| 68 |
+
|
| 69 |
+
| Score Range | Label | Description |
|
| 70 |
+
|-------------|-------|-------------|
|
| 71 |
+
| 0.00 - 0.20 | normal | No concerning patterns |
|
| 72 |
+
| 0.20 - 0.40 | low | Minor irregularities |
|
| 73 |
+
| 0.40 - 0.60 | moderate | Some risk indicators |
|
| 74 |
+
| 0.60 - 0.80 | elevated | Multiple risk factors |
|
| 75 |
+
| 0.80 - 1.00 | high | Significant cardiac risk |
|
| 76 |
+
|
| 77 |
+
## Files
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
ml_src/
|
| 81 |
+
├── __init__.py
|
| 82 |
+
├── net1d.py # ECGFounder 1D CNN model definition
|
| 83 |
+
└── feature_extractor.py # Clinical feature extraction
|
| 84 |
+
|
| 85 |
+
ml_models/
|
| 86 |
+
├── ecgfounder_best.pt # Trained ECGFounder weights (Git LFS)
|
| 87 |
+
└── xgboost_cardiac.joblib # Trained XGBoost model (Git LFS)
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
## Dependencies
|
| 91 |
+
|
| 92 |
+
- `torch` (CPU-only, installed from pytorch.org/whl/cpu)
|
| 93 |
+
- `xgboost >= 2.0.0`
|
| 94 |
+
- `neurokit2 >= 0.2.7`
|
| 95 |
+
- `scipy >= 1.14.1`
|
| 96 |
+
- `numpy >= 1.26.4`
|
| 97 |
+
- `joblib >= 1.3.0`
|
render.yaml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
- type: web
|
| 3 |
+
name: cardiac-monitor-api
|
| 4 |
+
runtime: docker
|
| 5 |
+
dockerfilePath: ./Dockerfile
|
| 6 |
+
plan: starter
|
| 7 |
+
region: oregon
|
| 8 |
+
healthCheckPath: /api/v1/health
|
| 9 |
+
envVars:
|
| 10 |
+
- key: MONGODB_URI
|
| 11 |
+
sync: false
|
| 12 |
+
- key: DATABASE_NAME
|
| 13 |
+
value: cardiac_monitor
|
| 14 |
+
- key: JWT_SECRET
|
| 15 |
+
sync: false
|
| 16 |
+
- key: JWT_ALGORITHM
|
| 17 |
+
value: HS256
|
| 18 |
+
- key: JWT_EXPIRY_HOURS
|
| 19 |
+
value: "24"
|
| 20 |
+
- key: API_KEY
|
| 21 |
+
sync: false
|
requirements.txt
CHANGED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
-
fastapi=
|
| 2 |
-
uvicorn[standard]=
|
| 3 |
-
motor=
|
| 4 |
pymongo>=4.9,<4.10
|
| 5 |
-
pydantic=
|
| 6 |
-
pydantic-settings=
|
| 7 |
-
python-jose[cryptography]=
|
| 8 |
-
bcrypt=
|
| 9 |
-
python-multipart=
|
| 10 |
numpy==1.26.4
|
| 11 |
scipy==1.14.1
|
| 12 |
-
httpx=
|
| 13 |
xgboost>=2.0.0
|
| 14 |
neurokit2>=0.2.7
|
| 15 |
joblib>=1.3.0
|
|
|
|
| 1 |
+
fastapi>=0.115.6
|
| 2 |
+
uvicorn[standard]>=0.34.0
|
| 3 |
+
motor>=3.6.0
|
| 4 |
pymongo>=4.9,<4.10
|
| 5 |
+
pydantic>=2.10.0
|
| 6 |
+
pydantic-settings>=2.7.0
|
| 7 |
+
python-jose[cryptography]>=3.5.0
|
| 8 |
+
bcrypt>=4.2.0
|
| 9 |
+
python-multipart>=0.0.22
|
| 10 |
numpy==1.26.4
|
| 11 |
scipy==1.14.1
|
| 12 |
+
httpx>=0.28.1
|
| 13 |
xgboost>=2.0.0
|
| 14 |
neurokit2>=0.2.7
|
| 15 |
joblib>=1.3.0
|