Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- .gitignore +25 -0
- ARCHITECTURE.md +98 -0
- DESIGN.md +55 -0
- Dockerfile +26 -8
- README.md +64 -7
- backend/Dockerfile +13 -3
- backend/alerts.log +20 -0
- backend/config.py +11 -0
- backend/database.py +84 -95
- backend/ensemble.py +6 -1
- backend/export.py +328 -0
- backend/inference.py +45 -0
- backend/main.py +122 -382
- backend/models.py +2 -2
- backend/patient_history.db +0 -0
- backend/preprocessing.py +15 -5
- backend/requirements.txt +12 -9
- backend/simulator.py +57 -0
- backend/tests/test_api.py +29 -0
- docker-compose.yml +15 -9
- frontend/Dockerfile +17 -8
- frontend/README.md +36 -0
- frontend/eslint.config.mjs +18 -0
- frontend/next.config.ts +11 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +28 -0
- frontend/postcss.config.mjs +7 -0
- frontend/public/file.svg +1 -0
- frontend/public/globe.svg +1 -0
- frontend/public/next.svg +1 -0
- frontend/public/vercel.svg +1 -0
- frontend/public/window.svg +1 -0
- frontend/src/app/analytics/page.tsx +135 -0
- frontend/src/app/architecture/page.tsx +87 -0
- frontend/src/app/dashboard/page.tsx +218 -0
- frontend/src/app/edge/page.tsx +181 -0
- frontend/src/app/favicon.ico +0 -0
- frontend/src/app/globals.css +60 -0
- frontend/src/app/history/page.tsx +142 -0
- frontend/src/app/layout.tsx +34 -0
- frontend/src/app/mlops/page.tsx +118 -0
- frontend/src/app/page.tsx +115 -0
- frontend/src/app/simulator/page.tsx +246 -0
- frontend/src/components/ShapChart.tsx +101 -0
- frontend/src/context/TelemetryContext.tsx +52 -0
- frontend/tsconfig.json +34 -0
- model/classes.pkl +1 -1
- model/knn.pkl +2 -2
- model/label_encoder.pkl +1 -1
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* 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
|
|
|
|
|
|
| 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
|
| 36 |
+
backend/patient_history.db filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -34,6 +34,8 @@ venv/
|
|
| 34 |
ENV/
|
| 35 |
env.bak/
|
| 36 |
venv.bak/
|
|
|
|
|
|
|
| 37 |
|
| 38 |
# Streamlit
|
| 39 |
.streamlit/credentials.toml
|
|
@@ -44,3 +46,26 @@ venv.bak/
|
|
| 44 |
|
| 45 |
# macOS
|
| 46 |
.DS_Store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
ENV/
|
| 35 |
env.bak/
|
| 36 |
venv.bak/
|
| 37 |
+
uv.lock
|
| 38 |
+
pyproject.toml
|
| 39 |
|
| 40 |
# Streamlit
|
| 41 |
.streamlit/credentials.toml
|
|
|
|
| 46 |
|
| 47 |
# macOS
|
| 48 |
.DS_Store
|
| 49 |
+
|
| 50 |
+
# next.js
|
| 51 |
+
node_modules
|
| 52 |
+
.pnp
|
| 53 |
+
.pnp.*
|
| 54 |
+
.yarn/*
|
| 55 |
+
!.yarn/patches
|
| 56 |
+
!.yarn/plugins
|
| 57 |
+
!.yarn/releases
|
| 58 |
+
!.yarn/versions
|
| 59 |
+
coverage
|
| 60 |
+
.next/
|
| 61 |
+
out/
|
| 62 |
+
build
|
| 63 |
+
*.pem
|
| 64 |
+
npm-debug.log*
|
| 65 |
+
yarn-debug.log*
|
| 66 |
+
yarn-error.log*
|
| 67 |
+
.pnpm-debug.log*
|
| 68 |
+
.env*
|
| 69 |
+
.vercel
|
| 70 |
+
*.tsbuildinfo
|
| 71 |
+
next-env.d.ts
|
ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HEARTFLOW_OS // System Architecture
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
HeartFlow OS is an end-to-end, edge-optimized clinical telemetry pipeline. The system bridges the gap between high-level cloud machine learning and low-level embedded hardware, enabling real-time clinical inference and automated C-code transpilation for resource-constrained microcontrollers (TinyML).
|
| 5 |
+
|
| 6 |
+
The architecture adheres to a decoupled, modular design pattern split into three primary planes: **Client (Frontend)**, **Core (Backend)**, and **Edge (Hardware Compilation)**.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## Architecture Diagram
|
| 11 |
+
|
| 12 |
+
```mermaid
|
| 13 |
+
graph TD
|
| 14 |
+
%% Define styles
|
| 15 |
+
classDef frontend fill:#111,stroke:#111,stroke-width:2px,color:#fff,font-family:monospace;
|
| 16 |
+
classDef backend fill:#fff,stroke:#111,stroke-width:2px,color:#111,font-family:monospace;
|
| 17 |
+
classDef ml_node fill:#e61919,stroke:#e61919,stroke-width:2px,color:#fff,font-family:monospace;
|
| 18 |
+
classDef edge_tech fill:#f3f3f2,stroke:#111,stroke-width:2px,color:#111,font-family:monospace;
|
| 19 |
+
classDef data fill:#94a3b8,stroke:#475569,stroke-width:2px,color:#fff,font-family:monospace;
|
| 20 |
+
|
| 21 |
+
subgraph Client [Client Application]
|
| 22 |
+
UI[Next.js 16 UI / Dashboard]:::frontend
|
| 23 |
+
WS_Client[WebSocket Client]:::frontend
|
| 24 |
+
HTTP_Client[HTTP Client]:::frontend
|
| 25 |
+
end
|
| 26 |
+
|
| 27 |
+
subgraph Core [FastAPI Backend]
|
| 28 |
+
WS_Server[WebSocket Feed / Generator]:::backend
|
| 29 |
+
REST_API[REST API Router]:::backend
|
| 30 |
+
Inference[Inference Engine / Preprocessor]:::backend
|
| 31 |
+
MLOps[MLOps Pipeline]:::backend
|
| 32 |
+
DB[(aiosqlite)]:::backend
|
| 33 |
+
end
|
| 34 |
+
|
| 35 |
+
subgraph ML_Layer [Intelligence Pipeline]
|
| 36 |
+
Ensemble{Soft-Voting Ensemble}:::ml_node
|
| 37 |
+
Models[RF, SVM, KNN, LogReg, MLP]:::ml_node
|
| 38 |
+
SHAP[SHAP Explainer]:::ml_node
|
| 39 |
+
EdgeCompiler[INT8 C-Compiler AST]:::ml_node
|
| 40 |
+
end
|
| 41 |
+
|
| 42 |
+
subgraph Hardware [Edge Target]
|
| 43 |
+
HEADER((tinyml_model.h)):::edge_tech
|
| 44 |
+
MCU[ESP32 / Cortex-M]:::edge_tech
|
| 45 |
+
end
|
| 46 |
+
|
| 47 |
+
subgraph Storage [Persistent Storage]
|
| 48 |
+
ModelWeights[Trained Weights .pkl]:::data
|
| 49 |
+
CSV[Patient Dataset .csv]:::data
|
| 50 |
+
end
|
| 51 |
+
|
| 52 |
+
%% Flow
|
| 53 |
+
UI -->|React 19 / Framer| WS_Client
|
| 54 |
+
UI -->|User Interaction| HTTP_Client
|
| 55 |
+
|
| 56 |
+
WS_Client <-->|ws:// Live Telemetry| WS_Server
|
| 57 |
+
HTTP_Client -->|POST /retrain, GET /export| REST_API
|
| 58 |
+
|
| 59 |
+
WS_Server -->|Evaluates Vitals| Inference
|
| 60 |
+
REST_API -->|Triggers Retrain| MLOps
|
| 61 |
+
REST_API -->|Requests Header| EdgeCompiler
|
| 62 |
+
|
| 63 |
+
Inference -->|Predicts| Ensemble
|
| 64 |
+
Ensemble --> Models
|
| 65 |
+
Ensemble --> SHAP
|
| 66 |
+
|
| 67 |
+
MLOps -->|Writes| ModelWeights
|
| 68 |
+
MLOps -->|Appends| CSV
|
| 69 |
+
EdgeCompiler -->|Reads| Models
|
| 70 |
+
Inference -->|Reads| ModelWeights
|
| 71 |
+
|
| 72 |
+
EdgeCompiler -->|Transpiles| HEADER
|
| 73 |
+
HEADER -.->|Flashed to| MCU
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## 1. Client Plane (Frontend)
|
| 79 |
+
Engineered using **Next.js 16 (App Router)** and **React 19**.
|
| 80 |
+
- **Design System:** Strict Industrial Brutalist aesthetic utilizing hard grids, monochromatic blueprints (`bg-canvas-cream`, `text-ink`), and high-contrast hazard states.
|
| 81 |
+
- **State Management:** React Context API manages high-frequency telemetry streams.
|
| 82 |
+
- **Data Transport:** Real-time data is ingested via native WebSockets (`ws://`), bypassing HTTP overhead for continuous vital monitoring.
|
| 83 |
+
- **Animations:** Hardware-accelerated SVG manipulations and layout transitions powered by `framer-motion`.
|
| 84 |
+
|
| 85 |
+
## 2. Core Plane (Backend)
|
| 86 |
+
Built on **FastAPI** (Python 3.10+), maximizing asynchronous throughput.
|
| 87 |
+
- **WebSocket Generator:** A state-machine driven data simulator generates correlated, volatile telemetry streams (simulating clinical episodes like Asthma or Heart Disease).
|
| 88 |
+
- **Inference Preprocessor:** Live patient data is standardized and imputed using a persistent `SimpleImputer` and `StandardScaler` trained on the original dataset, ensuring inference dimensions perfectly match the training phase.
|
| 89 |
+
- **Storage:** Employs `aiosqlite` for non-blocking, asynchronous database writes to track historical patient classifications without stalling the event loop.
|
| 90 |
+
|
| 91 |
+
## 3. Intelligence Pipeline (MLOps)
|
| 92 |
+
- **Ensemble Architecture:** Achieves clinical-grade precision via a 5-model soft-voting ensemble (`RandomForest`, `SVM`, `KNN`, `LogisticRegression`, `MLPClassifier`).
|
| 93 |
+
- **Batch Retraining:** Exposes a robust `/retrain` pipeline allowing direct CSV uploads. The pipeline safely joins the schemas, automatically updates `patient_dataset.csv`, fits new scalers/imputers, retraining all models in a `BackgroundTask` thread, and hot-swaps the `EnsembleModel` globally with zero downtime.
|
| 94 |
+
|
| 95 |
+
## 4. Edge Compilation (TinyML)
|
| 96 |
+
- **Zero-Dep C-Headers:** The backend transpiles trained SciKit-Learn tree models directly into standalone `C` functions.
|
| 97 |
+
- **INT8 Quantization:** Floating-point weights and decision thresholds are algorithmically compressed (quantized) into 8-bit integers, effectively shrinking the flashed payload by ~75%.
|
| 98 |
+
- **Target:** Designed to run strictly on MCU SRAM without dynamic memory allocation (no `malloc`), perfect for resource-constrained nodes like the ESP32 or Cortex-M4.
|
DESIGN.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Design System: HeartFlow OS
|
| 2 |
+
|
| 3 |
+
## 1. Visual Theme & Atmosphere
|
| 4 |
+
A restrained, highly dense utilitarian interface engineered for clinical telemetry. The atmosphere is precise, unornamented, and cockpit-dense — like a modern piece of medical hardware. It abandons marketing fluff (drop shadows, massive radii, neon glows) in favor of absolute data legibility, 1px structural hairlines, and muted semantic pastels.
|
| 5 |
+
|
| 6 |
+
- **DESIGN_VARIANCE: 6** — Asymmetric bento grids, strict baseline rhythms.
|
| 7 |
+
- **MOTION_INTENSITY: 5** — Restrained. Staggered reveals and layout-fluidity for data changes, but no perpetual decorative loops.
|
| 8 |
+
- **VISUAL_DENSITY: 8** — Cockpit-dense. Maximum data visibility without visual collision.
|
| 9 |
+
|
| 10 |
+
## 2. Color Palette & Roles
|
| 11 |
+
- **Clinical Canvas** (`#FBFBFA`) — Primary background surface for the application root.
|
| 12 |
+
- **Pure Surface** (`#FFFFFF`) — Card and container fill.
|
| 13 |
+
- **Ink Black** (`#141413`) — Primary text, CTAs, and active focus rings.
|
| 14 |
+
- **Slate Gray** (`#696969`) — Secondary text, metadata, tabular headers, and decorative icons.
|
| 15 |
+
- **Whisper Border** (`#EAEAEA`) — Card borders, dividers, 1px structural lines. No shadows.
|
| 16 |
+
- **Risk Pastel** (`#FDEBEC` bg / `#9F2F2D` text) — Semantic warning state (e.g., At-Risk predictions).
|
| 17 |
+
- **Healthy Pastel** (`#EDF3EC` bg / `#346538` text) — Semantic success state.
|
| 18 |
+
- **Soft Bone** (`#F4F4F4`) — Neutral tertiary state or loading badges.
|
| 19 |
+
|
| 20 |
+
## 3. Typography Rules
|
| 21 |
+
- **Display:** `Geist` or `Outfit` (sans-serif) — Track-tight (`-0.02em`), controlled scale, weight-driven hierarchy (Medium 500 for headers).
|
| 22 |
+
- **Body:** `Geist` or `Outfit` — Relaxed leading (`1.5`), `450` weight, Slate Gray.
|
| 23 |
+
- **Data / Numbers:** `Geist Mono` or Tabular-Nums on the sans font — Mandatory for all live telemetry, timestamps, and confidence percentages to prevent horizontal layout jitter.
|
| 24 |
+
- **Banned:** Generic serifs (`Times New Roman`, `Georgia`) are banned in this software UI. Generic `Inter` is discouraged in favor of `Geist` for a more precise technical aesthetic.
|
| 25 |
+
|
| 26 |
+
## 4. Component Stylings
|
| 27 |
+
- **Buttons:** Flat, Ink Black fill. Border radius of `6px` or `8px`. Tactile `scale(0.98)` push feedback on active state. No neon outer glows, no massive pills (999px) in the dashboard.
|
| 28 |
+
- **Cards (Bento Grid):** Crisp `12px` or `8px` corners. Strict `1px solid #EAEAEA` border. Absolutely zero drop shadows (`shadow-none`).
|
| 29 |
+
- **Inputs & Navigation:** Focus states must use a harsh, highly-visible 2px ring (`focus-visible:ring-2 focus-visible:ring-[#141413]`). Label above, error below.
|
| 30 |
+
- **Loaders:** Skeletal shimmer matching exact layout dimensions. No generic circular spinners.
|
| 31 |
+
- **Status Pills:** Small, dense (`text-[11px]`), uppercase with wide tracking (`0.05em`), using the muted semantic pastels above.
|
| 32 |
+
|
| 33 |
+
## 5. Layout Principles
|
| 34 |
+
- **Macro-Whitespace:** 8-point baseline grid. Generous internal padding inside cards (`p-8`), but tightly packed grids (`gap-6`).
|
| 35 |
+
- **No Overlapping Elements:** Every element occupies its own clear spatial zone. No absolute-positioned content stacking.
|
| 36 |
+
- **Bento Constraints:** The layout must adapt exactly to the number of data panels. No empty slots.
|
| 37 |
+
- **Responsive Strictness:** Multi-column layouts must explicitly collapse to a single column below 768px. No horizontal scroll on mobile.
|
| 38 |
+
- **Height Constraints:** Full-height views must use `min-h-[100dvh]` to avoid iOS Safari address bar jumps.
|
| 39 |
+
|
| 40 |
+
## 6. Motion & Interaction
|
| 41 |
+
- **Physics Engine:** Spring physics for all interactive layout shifts (`stiffness: 100, damping: 20`). No linear easing.
|
| 42 |
+
- **Staggered Orchestration:** List items and bento cards enter via a subtle `translateY(12px)` + `opacity: 0` staggered cascade. Never mount the full dashboard instantly.
|
| 43 |
+
- **Hardware Acceleration:** Animate exclusively via `transform` and `opacity`.
|
| 44 |
+
- **Accessibility Sync:** Interactive layout shifts must respect `prefers-reduced-motion`. Live data must have `aria-live="polite"` so motion isn't the only indicator of change.
|
| 45 |
+
|
| 46 |
+
## 7. Anti-Patterns (BANNED)
|
| 47 |
+
- **NO emojis anywhere.** Use Phosphor, Lucide, or Radix UI icons.
|
| 48 |
+
- **NO generic purple/blue neon glows** or gradient meshes.
|
| 49 |
+
- **NO pure black** (`#000000`).
|
| 50 |
+
- **NO drop shadows** (`shadow-md`, `shadow-lg`, etc.). Elevation is handled by 1px borders.
|
| 51 |
+
- **NO fake placeholder names** (John Doe, Acme).
|
| 52 |
+
- **NO unstyled focus states.**
|
| 53 |
+
- **NO AI copywriting clichés** ("Elevate", "Seamless", "Unleash").
|
| 54 |
+
- **NO "Inter" font** as the default assumption; enforce Geist/Outfit.
|
| 55 |
+
- **NO non-tabular numbers** for live updating data.
|
Dockerfile
CHANGED
|
@@ -1,8 +1,26 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies if required by any ML libraries
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Install python dependencies from the backend
|
| 11 |
+
COPY backend/requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy necessary directories for the backend to run
|
| 15 |
+
COPY backend/ ./backend/
|
| 16 |
+
COPY model/ ./model/
|
| 17 |
+
COPY data/ ./data/
|
| 18 |
+
|
| 19 |
+
# Hugging Face Spaces mandates port 7860
|
| 20 |
+
EXPOSE 7860
|
| 21 |
+
|
| 22 |
+
# Ensure Python knows where to find the backend module
|
| 23 |
+
ENV PYTHONPATH=/app
|
| 24 |
+
|
| 25 |
+
# Start the FastAPI server
|
| 26 |
+
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,8 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HEARTFLOW_OS // TinyML Heart Health Telemetry
|
| 2 |
+
|
| 3 |
+
A production-ready, edge-optimized application for real-time cardiovascular analytics. It bridges the gap between cloud-scale machine learning and low-level embedded hardware by deploying soft-voting ensembles and automated C-code transpilation for resource-constrained IoT systems.
|
| 4 |
+
|
| 5 |
+

|
| 6 |
+

|
| 7 |
+

|
| 8 |
+

|
| 9 |
+

|
| 10 |
+
|
| 11 |
---
|
| 12 |
+
|
| 13 |
+
## Overview
|
| 14 |
+
|
| 15 |
+
HEARTFLOW_OS is designed for clinical telemetry monitoring. It evaluates live patient vitals using an ensemble of classifiers, provides diagnostic probability distributions, and physically compresses these Python-trained models into raw C-code headers ready to be flashed directly onto edge silicon (e.g., ESP32, Arduino Nano).
|
| 16 |
+
|
| 17 |
+
### Key Engineering Features
|
| 18 |
+
|
| 19 |
+
- **Industrial Brutalist Frontend**: A high-performance, cockpit-dense Next.js 16 UI using native WebSockets and React 19 to render 60fps SVG telemetry sweeps without dropping frames.
|
| 20 |
+
- **Edge Computing & TinyML**: Transpiles complex Scikit-Learn models into highly optimized, dependency-free **C-code headers**. Bypasses dynamic memory allocation entirely.
|
| 21 |
+
- **INT8 Quantization Engine**: Mathematically scales 64-bit floating-point weights down to 8-bit integers, shrinking the flash memory payload by **~75%** for constrained microcontrollers.
|
| 22 |
+
- **Soft-Voting Ensemble**: Aggregates predictions across five independent models (Random Forest, SVM, KNN, Logistic Regression, MLP Neural Network) to output robust probability distributions rather than black-box binary answers.
|
| 23 |
+
- **Zero-Downtime MLOps Pipeline**: Features a dedicated `/retrain` pipeline allowing dynamic CSV uploads. It validates schemas, imputes missing data, retrains the entire ensemble in a background thread, and hot-swaps the intelligence engine globally without dropping active WebSocket connections.
|
| 24 |
+
|
| 25 |
+
## Architecture
|
| 26 |
+
|
| 27 |
+
The project follows a decoupled, modular design pattern split between a React Client, a FastAPI Core, and an Edge Compiler.
|
| 28 |
+
|
| 29 |
+
For a comprehensive deep-dive into the data flows and system topography, refer to the [ARCHITECTURE.md](./ARCHITECTURE.md) document.
|
| 30 |
+
|
| 31 |
+
### Repository Structure
|
| 32 |
+
|
| 33 |
+
```text
|
| 34 |
+
HEARTFLOW_OS/
|
| 35 |
+
├── backend/ # FastAPI server, ML pipelines, aiosqlite, Edge C-compiler
|
| 36 |
+
│ ├── ensemble.py # Core intelligence and prediction aggregations
|
| 37 |
+
│ ├── main.py # Websocket feeds, REST APIs, and background tasks
|
| 38 |
+
│ └── preprocessing.py # MLOps data imputation, scaling, and state preservation
|
| 39 |
+
├── frontend/ # Next.js 16 UI, React Context, framer-motion components
|
| 40 |
+
│ ├── src/app/ # Dashboard, Simulator, MLOps, and Architecture routes
|
| 41 |
+
│ └── src/components/ # Reusable brutalist UI components (Vitals, Graphs)
|
| 42 |
+
├── model/ # Local cache of serialized SciKit-Learn .pkl weights
|
| 43 |
+
└── data/ # Base patient cohort CSVs for MLOps retraining pipelines
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
## Quickstart
|
| 47 |
+
|
| 48 |
+
### 1. Initialize Backend
|
| 49 |
+
```bash
|
| 50 |
+
cd backend
|
| 51 |
+
python -m venv venv
|
| 52 |
+
source venv/bin/activate # Or `.\venv\Scripts\activate` on Windows
|
| 53 |
+
pip install -r requirements.txt
|
| 54 |
+
python models.py # Initial train of the base ensemble
|
| 55 |
+
uvicorn main:app --reload --port 8000
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
### 2. Initialize Frontend
|
| 59 |
+
```bash
|
| 60 |
+
cd frontend
|
| 61 |
+
npm install
|
| 62 |
+
npm run dev
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
The system will boot on `localhost:3000`. Connect to view the live dashboard streams and access the MLOps retraining interfaces.
|
backend/Dockerfile
CHANGED
|
@@ -1,8 +1,18 @@
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
WORKDIR /app
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
RUN pip install --no-cache-dir -r requirements.txt
|
|
|
|
|
|
|
| 5 |
COPY . .
|
| 6 |
-
|
|
|
|
| 7 |
EXPOSE 8000
|
| 8 |
-
CMD
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
WORKDIR /app
|
| 3 |
+
|
| 4 |
+
# Install build dependencies for scientific libraries
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
build-essential \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
# Install dependencies
|
| 10 |
+
COPY requirements.txt .
|
| 11 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Copy source code
|
| 14 |
COPY . .
|
| 15 |
+
|
| 16 |
+
# Expose port and run
|
| 17 |
EXPOSE 8000
|
| 18 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
backend/alerts.log
CHANGED
|
@@ -7,3 +7,23 @@ ALERT: Patient at risk! HR: 93.0, SpO2: 85.0, Confidence: 0.98
|
|
| 7 |
ALERT: Patient at risk! HR: 65.0, SpO2: 93.0, Confidence: 0.92
|
| 8 |
ALERT: Patient at risk! HR: 81.0, SpO2: 96.0, Confidence: 0.97
|
| 9 |
ALERT: Patient at risk! HR: 91.0, SpO2: 87.0, Confidence: 0.92
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
ALERT: Patient at risk! HR: 65.0, SpO2: 93.0, Confidence: 0.92
|
| 8 |
ALERT: Patient at risk! HR: 81.0, SpO2: 96.0, Confidence: 0.97
|
| 9 |
ALERT: Patient at risk! HR: 91.0, SpO2: 87.0, Confidence: 0.92
|
| 10 |
+
ALERT: Patient at risk! HR: 196.0, SpO2: 87.0, Confidence: 0.89
|
| 11 |
+
ALERT: Patient at risk! HR: 196.0, SpO2: 87.0, Confidence: 0.96
|
| 12 |
+
ALERT: Patient at risk! HR: 172.0, SpO2: 98.0, Confidence: 0.82
|
| 13 |
+
ALERT: Patient at risk! HR: 75.0, SpO2: 98.0, Confidence: 0.98
|
| 14 |
+
ALERT: Patient at risk! HR: 75.0, SpO2: 98.0, Confidence: 0.95
|
| 15 |
+
ALERT: Patient at risk! HR: 174.0, SpO2: 89.0, Confidence: 0.84
|
| 16 |
+
ALERT: Patient at risk! HR: 174.0, SpO2: 89.0, Confidence: 0.81
|
| 17 |
+
ALERT: Patient at risk! HR: 186.0, SpO2: 98.0, Confidence: 0.81
|
| 18 |
+
ALERT: Patient at risk! HR: 186.0, SpO2: 91.0, Confidence: 0.82
|
| 19 |
+
ALERT: Patient at risk! HR: 117.0, SpO2: 76.0, Confidence: 0.93
|
| 20 |
+
ALERT: Patient at risk! HR: 187.0, SpO2: 98.0, Confidence: 0.81
|
| 21 |
+
ALERT: Patient at risk! HR: 96.0, SpO2: 95.0, Confidence: 0.98
|
| 22 |
+
ALERT: Patient at risk! HR: 114.0, SpO2: 96.0, Confidence: 0.91
|
| 23 |
+
ALERT: Patient at risk! HR: 75.0, SpO2: 90.0, Confidence: 0.81
|
| 24 |
+
ALERT: Patient at risk! HR: 78.0, SpO2: 90.0, Confidence: 0.90
|
| 25 |
+
ALERT: Patient at risk! HR: 100.0, SpO2: 99.0, Confidence: 0.99
|
| 26 |
+
ALERT: Patient at risk! HR: 83.0, SpO2: 100.0, Confidence: 0.95
|
| 27 |
+
ALERT: Patient at risk! HR: 73.0, SpO2: 97.0, Confidence: 0.96
|
| 28 |
+
ALERT: Patient at risk! HR: 140.0, SpO2: 96.0, Confidence: 0.82
|
| 29 |
+
ALERT: Patient at risk! HR: 83.0, SpO2: 100.0, Confidence: 0.85
|
backend/config.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
class Settings:
|
| 4 |
+
app_name: str = "TinyML Core API"
|
| 5 |
+
host: str = "0.0.0.0"
|
| 6 |
+
port: int = int(os.getenv("PORT", "8000"))
|
| 7 |
+
hf_token: str | None = os.getenv("HF_TOKEN")
|
| 8 |
+
db_name: str = os.getenv("DB_NAME", "patient_history.db")
|
| 9 |
+
repo_id: str = os.getenv("REPO_ID", "IDKHowToCodeFr/tinyml-logs")
|
| 10 |
+
|
| 11 |
+
settings = Settings()
|
backend/database.py
CHANGED
|
@@ -1,115 +1,104 @@
|
|
| 1 |
-
import
|
| 2 |
import os
|
| 3 |
import time
|
|
|
|
| 4 |
from datetime import datetime
|
|
|
|
| 5 |
from huggingface_hub import HfApi, hf_hub_download
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
DB_NAME = 'patient_history.db'
|
| 9 |
-
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), DB_NAME)
|
| 10 |
-
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 11 |
-
|
| 12 |
api = HfApi()
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
def sync_from_hub():
|
| 15 |
-
|
|
|
|
| 16 |
print("No HF_TOKEN found. Skipping sync.")
|
| 17 |
return
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
def sync_to_hub():
|
| 35 |
-
if not
|
| 36 |
return
|
| 37 |
try:
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
| 46 |
except Exception as e:
|
| 47 |
print(f"Sync to Hub failed: {e}")
|
| 48 |
|
| 49 |
-
def init_db():
|
| 50 |
-
sync_from_hub()
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
conn.commit()
|
| 68 |
-
conn.close()
|
| 69 |
|
| 70 |
-
def log_prediction(data, prediction_label, confidence):
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
prediction_label,
|
| 88 |
-
confidence
|
| 89 |
-
))
|
| 90 |
-
conn.commit()
|
| 91 |
-
conn.close()
|
| 92 |
-
|
| 93 |
-
# Push immediately
|
| 94 |
-
sync_to_hub()
|
| 95 |
|
| 96 |
-
def get_history():
|
| 97 |
-
# Fresh pull for history view
|
| 98 |
-
sync_from_hub()
|
| 99 |
-
|
| 100 |
if not os.path.exists(DB_PATH):
|
| 101 |
return []
|
| 102 |
-
conn = sqlite3.connect(DB_PATH)
|
| 103 |
-
cursor = conn.cursor()
|
| 104 |
-
cursor.execute('SELECT * FROM predictions ORDER BY timestamp DESC LIMIT 100')
|
| 105 |
-
rows = cursor.fetchall()
|
| 106 |
-
col_names = [description[0] for description in cursor.description]
|
| 107 |
-
conn.close()
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
# Initialize db when this module is loaded
|
| 115 |
-
init_db()
|
|
|
|
| 1 |
+
import aiosqlite
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
+
import asyncio
|
| 5 |
from datetime import datetime
|
| 6 |
+
from typing import List, Dict, Any
|
| 7 |
from huggingface_hub import HfApi, hf_hub_download
|
| 8 |
+
from config import settings
|
| 9 |
|
| 10 |
+
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), settings.db_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
api = HfApi()
|
| 12 |
+
last_sync = 0
|
| 13 |
+
sync_lock = asyncio.Lock()
|
| 14 |
|
| 15 |
+
async def sync_from_hub():
|
| 16 |
+
global last_sync
|
| 17 |
+
if not settings.hf_token:
|
| 18 |
print("No HF_TOKEN found. Skipping sync.")
|
| 19 |
return
|
| 20 |
+
|
| 21 |
+
async with sync_lock:
|
| 22 |
+
if time.time() - last_sync < 60:
|
| 23 |
+
return
|
| 24 |
+
try:
|
| 25 |
+
print(f"Downloading {settings.db_name} from Hub...")
|
| 26 |
+
def _download():
|
| 27 |
+
path = hf_hub_download(
|
| 28 |
+
repo_id=settings.repo_id,
|
| 29 |
+
filename=settings.db_name,
|
| 30 |
+
repo_type="dataset",
|
| 31 |
+
token=settings.hf_token,
|
| 32 |
+
force_download=True
|
| 33 |
+
)
|
| 34 |
+
import shutil
|
| 35 |
+
shutil.copy(path, DB_PATH)
|
| 36 |
+
|
| 37 |
+
await asyncio.to_thread(_download)
|
| 38 |
+
last_sync = time.time()
|
| 39 |
+
print("Sync from Hub complete.")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"Sync from Hub failed: {e}")
|
| 42 |
|
| 43 |
+
async def sync_to_hub():
|
| 44 |
+
if not settings.hf_token:
|
| 45 |
return
|
| 46 |
try:
|
| 47 |
+
def _upload():
|
| 48 |
+
api.upload_file(
|
| 49 |
+
path_or_fileobj=DB_PATH,
|
| 50 |
+
path_in_repo=settings.db_name,
|
| 51 |
+
repo_id=settings.repo_id,
|
| 52 |
+
repo_type="dataset",
|
| 53 |
+
token=settings.hf_token
|
| 54 |
+
)
|
| 55 |
+
await asyncio.to_thread(_upload)
|
| 56 |
except Exception as e:
|
| 57 |
print(f"Sync to Hub failed: {e}")
|
| 58 |
|
| 59 |
+
async def init_db() -> None:
|
| 60 |
+
await sync_from_hub()
|
| 61 |
+
async with aiosqlite.connect(DB_PATH) as db:
|
| 62 |
+
await db.execute('''
|
| 63 |
+
CREATE TABLE IF NOT EXISTS predictions (
|
| 64 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 65 |
+
timestamp DATETIME,
|
| 66 |
+
heart_rate REAL,
|
| 67 |
+
spo2 REAL,
|
| 68 |
+
sys_bp REAL,
|
| 69 |
+
dia_bp REAL,
|
| 70 |
+
temp REAL,
|
| 71 |
+
fall_detection TEXT,
|
| 72 |
+
prediction_label TEXT,
|
| 73 |
+
confidence REAL
|
| 74 |
+
)
|
| 75 |
+
''')
|
| 76 |
+
await db.commit()
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
async def log_prediction(data: Any, prediction_label: str, confidence: float) -> None:
|
| 79 |
+
async with aiosqlite.connect(DB_PATH) as db:
|
| 80 |
+
await db.execute('''
|
| 81 |
+
INSERT INTO predictions (timestamp, heart_rate, spo2, sys_bp, dia_bp, temp, fall_detection, prediction_label, confidence)
|
| 82 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 83 |
+
''', (
|
| 84 |
+
datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
|
| 85 |
+
data.Heart_Rate,
|
| 86 |
+
data.SpO2_Level,
|
| 87 |
+
data.Systolic_BP,
|
| 88 |
+
data.Diastolic_BP,
|
| 89 |
+
data.Body_Temp,
|
| 90 |
+
"N/A",
|
| 91 |
+
prediction_label,
|
| 92 |
+
confidence
|
| 93 |
+
))
|
| 94 |
+
await db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
async def get_history() -> List[Dict[str, Any]]:
|
|
|
|
|
|
|
|
|
|
| 97 |
if not os.path.exists(DB_PATH):
|
| 98 |
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
async with aiosqlite.connect(DB_PATH) as db:
|
| 101 |
+
db.row_factory = aiosqlite.Row
|
| 102 |
+
async with db.execute('SELECT * FROM predictions ORDER BY timestamp DESC LIMIT 100') as cursor:
|
| 103 |
+
rows = await cursor.fetchall()
|
| 104 |
+
return [dict(row) for row in rows]
|
|
|
|
|
|
backend/ensemble.py
CHANGED
|
@@ -45,4 +45,9 @@ class EnsembleModel:
|
|
| 45 |
final_pred = str(self.label_encoder.inverse_transform([final_pred_idx])[0])
|
| 46 |
confidence = float(np.max(weighted_probs))
|
| 47 |
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
final_pred = str(self.label_encoder.inverse_transform([final_pred_idx])[0])
|
| 46 |
confidence = float(np.max(weighted_probs))
|
| 47 |
|
| 48 |
+
class_probs = {
|
| 49 |
+
str(self.label_encoder.inverse_transform([i])[0]): float(prob)
|
| 50 |
+
for i, prob in enumerate(weighted_probs)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
return final_pred, confidence, individual_preds, class_probs, self.weights
|
backend/export.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import m2cgen as m2c
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
def generate_c_code(eng, model_name: str, quantize: bool):
|
| 5 |
+
if not eng or model_name not in eng.models:
|
| 6 |
+
return {"error": f"Model {model_name} not found"}
|
| 7 |
+
|
| 8 |
+
model = eng.models[model_name]
|
| 9 |
+
|
| 10 |
+
# m2cgen handles Random Forest beautifully, but outputs FP32/double rules
|
| 11 |
+
if model_name == "rf":
|
| 12 |
+
try:
|
| 13 |
+
code = m2c.export_to_c(model)
|
| 14 |
+
if quantize:
|
| 15 |
+
code = "/* WARNING: M2CGen generated FP32 output. INT8 Quantization is not supported directly for Random Forest trees. */\n" + code
|
| 16 |
+
return {"code": code}
|
| 17 |
+
except Exception:
|
| 18 |
+
pass
|
| 19 |
+
|
| 20 |
+
# For LogReg, use m2cgen if FP32, otherwise manual generation for INT8
|
| 21 |
+
if not quantize and model_name == "logreg":
|
| 22 |
+
try:
|
| 23 |
+
code = m2c.export_to_c(model)
|
| 24 |
+
return {"code": code}
|
| 25 |
+
except Exception:
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
# Manual C-code generation for all model types
|
| 29 |
+
try:
|
| 30 |
+
L = []
|
| 31 |
+
L.append("/* ====================================================== */")
|
| 32 |
+
L.append(f"/* TinyML C Export: {model_name} */")
|
| 33 |
+
q_text = "INT8 Quantized" if quantize else "FP32 Double"
|
| 34 |
+
L.append(f"/* Auto-generated for ARM Cortex-M / ESP32 ({q_text}) */")
|
| 35 |
+
L.append("/* ====================================================== */")
|
| 36 |
+
L.append("")
|
| 37 |
+
L.append("#include <math.h>")
|
| 38 |
+
L.append("#include <stdint.h>")
|
| 39 |
+
L.append("#include <string.h>")
|
| 40 |
+
L.append("")
|
| 41 |
+
|
| 42 |
+
if model_name == "svm" and hasattr(model, 'coef_'):
|
| 43 |
+
coefs = model.coef_
|
| 44 |
+
intercepts = model.intercept_
|
| 45 |
+
n_classes = len(model.classes_)
|
| 46 |
+
n_features = coefs.shape[1]
|
| 47 |
+
L.append(f"/* Linear SVM with {n_classes} classes, {n_features} features */")
|
| 48 |
+
L.append(f"#define N_FEATURES {n_features}")
|
| 49 |
+
L.append(f"#define N_CLASSES {n_classes}")
|
| 50 |
+
L.append(f"#define N_HYPERPLANES {coefs.shape[0]}")
|
| 51 |
+
L.append("")
|
| 52 |
+
|
| 53 |
+
if quantize:
|
| 54 |
+
scale_factor = 127.0 / max(np.max(np.abs(coefs)), np.max(np.abs(intercepts)), 1e-6)
|
| 55 |
+
L.append(f"/* Quantization Scale: {scale_factor:.4f} */")
|
| 56 |
+
L.append("static const int8_t SVM_COEF[N_HYPERPLANES][N_FEATURES] = {")
|
| 57 |
+
for row in coefs:
|
| 58 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
|
| 59 |
+
L.append(f" {{{vals}}},")
|
| 60 |
+
L.append("};")
|
| 61 |
+
L.append("")
|
| 62 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in intercepts])
|
| 63 |
+
L.append(f"static const int8_t SVM_INTERCEPT[N_HYPERPLANES] = {{{vals}}};")
|
| 64 |
+
L.append("")
|
| 65 |
+
L.append("int predict(int8_t *features) {")
|
| 66 |
+
L.append(" int32_t scores[N_CLASSES] = {0};")
|
| 67 |
+
L.append(" int h = 0;")
|
| 68 |
+
L.append(" for (int i = 0; i < N_CLASSES; i++) {")
|
| 69 |
+
L.append(" for (int j = i + 1; j < N_CLASSES; j++) {")
|
| 70 |
+
L.append(" int32_t decision = SVM_INTERCEPT[h];")
|
| 71 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 72 |
+
L.append(" decision += (int32_t)SVM_COEF[h][f] * features[f];")
|
| 73 |
+
L.append(" }")
|
| 74 |
+
L.append(" if (decision > 0) scores[i] += 1;")
|
| 75 |
+
L.append(" else scores[j] += 1;")
|
| 76 |
+
L.append(" h++;")
|
| 77 |
+
L.append(" }")
|
| 78 |
+
L.append(" }")
|
| 79 |
+
L.append(" int best = 0;")
|
| 80 |
+
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 81 |
+
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 82 |
+
L.append(" }")
|
| 83 |
+
L.append(" return best;")
|
| 84 |
+
L.append("}")
|
| 85 |
+
else:
|
| 86 |
+
L.append("static const double SVM_COEF[N_HYPERPLANES][N_FEATURES] = {")
|
| 87 |
+
for row in coefs:
|
| 88 |
+
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 89 |
+
L.append(f" {{{vals}}},")
|
| 90 |
+
L.append("};")
|
| 91 |
+
L.append("")
|
| 92 |
+
vals = ", ".join([f"{v:.6f}" for v in intercepts])
|
| 93 |
+
L.append(f"static const double SVM_INTERCEPT[N_HYPERPLANES] = {{{vals}}};")
|
| 94 |
+
L.append("")
|
| 95 |
+
L.append("int predict(double *features) {")
|
| 96 |
+
L.append(" double scores[N_CLASSES] = {0};")
|
| 97 |
+
L.append(" int h = 0;")
|
| 98 |
+
L.append(" for (int i = 0; i < N_CLASSES; i++) {")
|
| 99 |
+
L.append(" for (int j = i + 1; j < N_CLASSES; j++) {")
|
| 100 |
+
L.append(" double decision = SVM_INTERCEPT[h];")
|
| 101 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 102 |
+
L.append(" decision += SVM_COEF[h][f] * features[f];")
|
| 103 |
+
L.append(" }")
|
| 104 |
+
L.append(" if (decision > 0) scores[i] += 1.0;")
|
| 105 |
+
L.append(" else scores[j] += 1.0;")
|
| 106 |
+
L.append(" h++;")
|
| 107 |
+
L.append(" }")
|
| 108 |
+
L.append(" }")
|
| 109 |
+
L.append(" int best = 0;")
|
| 110 |
+
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 111 |
+
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 112 |
+
L.append(" }")
|
| 113 |
+
L.append(" return best;")
|
| 114 |
+
L.append("}")
|
| 115 |
+
|
| 116 |
+
elif model_name == "logreg" and hasattr(model, 'coef_'):
|
| 117 |
+
coefs = model.coef_
|
| 118 |
+
intercepts = model.intercept_
|
| 119 |
+
n_classes = coefs.shape[0] if len(model.classes_) > 2 else 2
|
| 120 |
+
n_features = coefs.shape[1]
|
| 121 |
+
L.append(f"/* Logistic Regression with {n_classes} classes, {n_features} features */")
|
| 122 |
+
L.append(f"#define N_FEATURES {n_features}")
|
| 123 |
+
L.append(f"#define N_CLASSES {coefs.shape[0]}")
|
| 124 |
+
L.append("")
|
| 125 |
+
|
| 126 |
+
if quantize:
|
| 127 |
+
scale_factor = 127.0 / max(np.max(np.abs(coefs)), np.max(np.abs(intercepts)), 1e-6)
|
| 128 |
+
L.append(f"/* Quantization Scale: {scale_factor:.4f} */")
|
| 129 |
+
L.append("static const int8_t LOGREG_COEF[N_CLASSES][N_FEATURES] = {")
|
| 130 |
+
for row in coefs:
|
| 131 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
|
| 132 |
+
L.append(f" {{{vals}}},")
|
| 133 |
+
L.append("};")
|
| 134 |
+
L.append("")
|
| 135 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in intercepts])
|
| 136 |
+
L.append(f"static const int8_t LOGREG_INTERCEPT[N_CLASSES] = {{{vals}}};")
|
| 137 |
+
L.append("")
|
| 138 |
+
L.append("int predict(int8_t *features) {")
|
| 139 |
+
L.append(" int32_t scores[N_CLASSES];")
|
| 140 |
+
L.append(" for (int c = 0; c < N_CLASSES; c++) {")
|
| 141 |
+
L.append(f" scores[c] = LOGREG_INTERCEPT[c] * {int(scale_factor)};")
|
| 142 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 143 |
+
L.append(" scores[c] += (int32_t)LOGREG_COEF[c][f] * features[f];")
|
| 144 |
+
L.append(" }")
|
| 145 |
+
L.append(" }")
|
| 146 |
+
L.append(" int best = 0;")
|
| 147 |
+
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 148 |
+
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 149 |
+
L.append(" }")
|
| 150 |
+
L.append(" return best;")
|
| 151 |
+
L.append("}")
|
| 152 |
+
else:
|
| 153 |
+
L.append("static const double LOGREG_COEF[N_CLASSES][N_FEATURES] = {")
|
| 154 |
+
for row in coefs:
|
| 155 |
+
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 156 |
+
L.append(f" {{{vals}}},")
|
| 157 |
+
L.append("};")
|
| 158 |
+
L.append("")
|
| 159 |
+
vals = ", ".join([f"{v:.6f}" for v in intercepts])
|
| 160 |
+
L.append(f"static const double LOGREG_INTERCEPT[N_CLASSES] = {{{vals}}};")
|
| 161 |
+
L.append("")
|
| 162 |
+
L.append("int predict(double *features) {")
|
| 163 |
+
L.append(" double scores[N_CLASSES];")
|
| 164 |
+
L.append(" for (int c = 0; c < N_CLASSES; c++) {")
|
| 165 |
+
L.append(" scores[c] = LOGREG_INTERCEPT[c];")
|
| 166 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 167 |
+
L.append(" scores[c] += LOGREG_COEF[c][f] * features[f];")
|
| 168 |
+
L.append(" }")
|
| 169 |
+
L.append(" }")
|
| 170 |
+
L.append(" int best = 0;")
|
| 171 |
+
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 172 |
+
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 173 |
+
L.append(" }")
|
| 174 |
+
L.append(" return best;")
|
| 175 |
+
L.append("}")
|
| 176 |
+
|
| 177 |
+
elif model_name == "small_nn" and hasattr(model, 'coefs_'):
|
| 178 |
+
layers = model.coefs_
|
| 179 |
+
biases = model.intercepts_
|
| 180 |
+
arch = " -> ".join([str(l.shape[0]) for l in layers] + [str(layers[-1].shape[1])])
|
| 181 |
+
L.append(f"/* MLP Neural Network: {len(layers)} layers */")
|
| 182 |
+
L.append(f"/* Architecture: {arch} */")
|
| 183 |
+
L.append("")
|
| 184 |
+
|
| 185 |
+
for idx, (W, b) in enumerate(zip(layers, biases)):
|
| 186 |
+
n_in, n_out = W.shape
|
| 187 |
+
L.append(f"#define L{idx}_IN {n_in}")
|
| 188 |
+
L.append(f"#define L{idx}_OUT {n_out}")
|
| 189 |
+
L.append(f"static const double W{idx}[{n_in}][{n_out}] = {{")
|
| 190 |
+
for row in W:
|
| 191 |
+
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 192 |
+
L.append(f" {{{vals}}},")
|
| 193 |
+
L.append("};")
|
| 194 |
+
bvals = ", ".join([f"{v:.6f}" for v in b])
|
| 195 |
+
L.append(f"static const double B{idx}[{n_out}] = {{{bvals}}};")
|
| 196 |
+
L.append("")
|
| 197 |
+
|
| 198 |
+
L.append("static inline double relu(double x) { return x > 0 ? x : 0; }")
|
| 199 |
+
L.append("")
|
| 200 |
+
|
| 201 |
+
if quantize:
|
| 202 |
+
# Calculate global max for int8 scaling
|
| 203 |
+
max_val = max([np.max(np.abs(w)) for w in layers] + [np.max(np.abs(b)) for b in biases] + [1e-6])
|
| 204 |
+
scale_factor = 127.0 / max_val
|
| 205 |
+
L.append(f"/* INT8 Quantization Scale Factor: {scale_factor:.4f} */")
|
| 206 |
+
for idx, (W, b) in enumerate(zip(layers, biases)):
|
| 207 |
+
n_in, n_out = W.shape
|
| 208 |
+
L.append(f"static const int8_t W{idx}[{n_in}][{n_out}] = {{")
|
| 209 |
+
for row in W:
|
| 210 |
+
vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
|
| 211 |
+
L.append(f" {{{vals}}},")
|
| 212 |
+
L.append("};")
|
| 213 |
+
bvals = ", ".join([str(int(round(v * scale_factor))) for v in b])
|
| 214 |
+
L.append(f"static const int8_t B{idx}[{n_out}] = {{{bvals}}};")
|
| 215 |
+
L.append("")
|
| 216 |
+
L.append("static inline int32_t relu_int(int32_t x) { return x > 0 ? x : 0; }")
|
| 217 |
+
L.append("")
|
| 218 |
+
L.append("int predict(int8_t *input) {")
|
| 219 |
+
for idx in range(len(layers)):
|
| 220 |
+
n_in = layers[idx].shape[0]
|
| 221 |
+
n_out = layers[idx].shape[1]
|
| 222 |
+
is_last = (idx == len(layers) - 1)
|
| 223 |
+
src = "input" if idx == 0 else f"a{idx-1}"
|
| 224 |
+
L.append(f" int32_t a{idx}[{n_out}];")
|
| 225 |
+
L.append(f" for (int j = 0; j < {n_out}; j++) {{")
|
| 226 |
+
L.append(f" a{idx}[j] = B{idx}[j] * {int(scale_factor)}; /* scale bias */")
|
| 227 |
+
L.append(f" for (int i = 0; i < {n_in}; i++) {{")
|
| 228 |
+
L.append(f" a{idx}[j] += (int32_t){src}[i] * W{idx}[i][j];")
|
| 229 |
+
L.append(f" }}")
|
| 230 |
+
if not is_last:
|
| 231 |
+
L.append(f" a{idx}[j] = relu_int(a{idx}[j]) / {int(scale_factor)}; /* rescale */")
|
| 232 |
+
L.append(f" }}")
|
| 233 |
+
last_idx = len(layers) - 1
|
| 234 |
+
last_out = layers[-1].shape[1]
|
| 235 |
+
L.append(f" int best = 0;")
|
| 236 |
+
L.append(f" for (int c = 1; c < {last_out}; c++) {{")
|
| 237 |
+
L.append(f" if (a{last_idx}[c] > a{last_idx}[best]) best = c;")
|
| 238 |
+
L.append(f" }}")
|
| 239 |
+
L.append(f" return best;")
|
| 240 |
+
L.append("}")
|
| 241 |
+
else:
|
| 242 |
+
for idx, (W, b) in enumerate(zip(layers, biases)):
|
| 243 |
+
n_in, n_out = W.shape
|
| 244 |
+
L.append(f"#define L{idx}_IN {n_in}")
|
| 245 |
+
L.append(f"#define L{idx}_OUT {n_out}")
|
| 246 |
+
L.append(f"static const double W{idx}[{n_in}][{n_out}] = {{")
|
| 247 |
+
for row in W:
|
| 248 |
+
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 249 |
+
L.append(f" {{{vals}}},")
|
| 250 |
+
L.append("};")
|
| 251 |
+
bvals = ", ".join([f"{v:.6f}" for v in b])
|
| 252 |
+
L.append(f"static const double B{idx}[{n_out}] = {{{bvals}}};")
|
| 253 |
+
L.append("")
|
| 254 |
+
|
| 255 |
+
L.append("static inline double relu(double x) { return x > 0 ? x : 0; }")
|
| 256 |
+
L.append("")
|
| 257 |
+
L.append("int predict(double *input) {")
|
| 258 |
+
for idx in range(len(layers)):
|
| 259 |
+
n_in = layers[idx].shape[0]
|
| 260 |
+
n_out = layers[idx].shape[1]
|
| 261 |
+
is_last = (idx == len(layers) - 1)
|
| 262 |
+
src = "input" if idx == 0 else f"a{idx-1}"
|
| 263 |
+
L.append(f" double a{idx}[{n_out}];")
|
| 264 |
+
L.append(f" for (int j = 0; j < {n_out}; j++) {{")
|
| 265 |
+
L.append(f" a{idx}[j] = B{idx}[j];")
|
| 266 |
+
L.append(f" for (int i = 0; i < {n_in}; i++) {{")
|
| 267 |
+
L.append(f" a{idx}[j] += {src}[i] * W{idx}[i][j];")
|
| 268 |
+
L.append(f" }}")
|
| 269 |
+
if not is_last:
|
| 270 |
+
L.append(f" a{idx}[j] = relu(a{idx}[j]);")
|
| 271 |
+
L.append(f" }}")
|
| 272 |
+
|
| 273 |
+
last_idx = len(layers) - 1
|
| 274 |
+
last_out = layers[-1].shape[1]
|
| 275 |
+
L.append(f" int best = 0;")
|
| 276 |
+
L.append(f" for (int c = 1; c < {last_out}; c++) {{")
|
| 277 |
+
L.append(f" if (a{last_idx}[c] > a{last_idx}[best]) best = c;")
|
| 278 |
+
L.append(f" }}")
|
| 279 |
+
L.append(f" return best;")
|
| 280 |
+
L.append("}")
|
| 281 |
+
|
| 282 |
+
elif model_name == "knn" and hasattr(model, '_fit_X'):
|
| 283 |
+
n_samples = min(model._fit_X.shape[0], 100)
|
| 284 |
+
n_feats = model._fit_X.shape[1]
|
| 285 |
+
L.append(f"/* KNN Lookup Table: {n_samples} reference samples */")
|
| 286 |
+
L.append(f"#define N_NEIGHBORS {model.n_neighbors}")
|
| 287 |
+
L.append(f"#define N_SAMPLES {n_samples}")
|
| 288 |
+
L.append(f"#define N_FEATURES {n_feats}")
|
| 289 |
+
L.append("")
|
| 290 |
+
L.append("static const double REF[N_SAMPLES][N_FEATURES] = {")
|
| 291 |
+
for row in model._fit_X[:n_samples]:
|
| 292 |
+
vals = ", ".join([f"{v:.4f}" for v in row])
|
| 293 |
+
L.append(f" {{{vals}}},")
|
| 294 |
+
L.append("};")
|
| 295 |
+
L.append("")
|
| 296 |
+
labels_str = ", ".join([str(int(l)) for l in model._y[:n_samples]])
|
| 297 |
+
L.append(f"static const int LABELS[N_SAMPLES] = {{{labels_str}}};")
|
| 298 |
+
L.append("")
|
| 299 |
+
L.append("int predict(double *features) {")
|
| 300 |
+
L.append(" double dists[N_SAMPLES];")
|
| 301 |
+
L.append(" for (int i = 0; i < N_SAMPLES; i++) {")
|
| 302 |
+
L.append(" dists[i] = 0.0;")
|
| 303 |
+
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 304 |
+
L.append(" double d = features[f] - REF[i][f];")
|
| 305 |
+
L.append(" dists[i] += d * d;")
|
| 306 |
+
L.append(" }")
|
| 307 |
+
L.append(" }")
|
| 308 |
+
L.append(" int votes[10] = {0};")
|
| 309 |
+
L.append(" for (int k = 0; k < N_NEIGHBORS; k++) {")
|
| 310 |
+
L.append(" int mi = 0;")
|
| 311 |
+
L.append(" for (int i = 1; i < N_SAMPLES; i++) {")
|
| 312 |
+
L.append(" if (dists[i] < dists[mi]) mi = i;")
|
| 313 |
+
L.append(" }")
|
| 314 |
+
L.append(" votes[LABELS[mi]]++;")
|
| 315 |
+
L.append(" dists[mi] = 1e18;")
|
| 316 |
+
L.append(" }")
|
| 317 |
+
L.append(" int best = 0;")
|
| 318 |
+
L.append(" for (int i = 1; i < 10; i++) {")
|
| 319 |
+
L.append(" if (votes[i] > votes[best]) best = i;")
|
| 320 |
+
L.append(" }")
|
| 321 |
+
L.append(" return best;")
|
| 322 |
+
L.append("}")
|
| 323 |
+
else:
|
| 324 |
+
return {"error": f"Model {model_name} cannot be exported to C."}
|
| 325 |
+
|
| 326 |
+
return {"code": "\n".join(L)}
|
| 327 |
+
except Exception as e:
|
| 328 |
+
return {"error": f"Export failed: {str(e)}"}
|
backend/inference.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
from preprocessing import preprocess_data
|
| 5 |
+
|
| 6 |
+
def evaluate(ensemble_model, data) -> Dict[str, Any]:
|
| 7 |
+
# Domain orchestrator: PatientData -> InferenceResult
|
| 8 |
+
raw_df = pd.DataFrame([data.model_dump(by_alias=True)])
|
| 9 |
+
|
| 10 |
+
# Mapping properties to expected feature names
|
| 11 |
+
raw_df = raw_df.rename(columns={
|
| 12 |
+
"Heart_Rate": "Heart Rate (bpm)",
|
| 13 |
+
"SpO2_Level": "SpO2 Level (%)",
|
| 14 |
+
"Systolic_BP": "Systolic Blood Pressure (mmHg)",
|
| 15 |
+
"Diastolic_BP": "Diastolic Blood Pressure (mmHg)",
|
| 16 |
+
"Body_Temp": "Body Temperature (°C)",
|
| 17 |
+
"Fall_Detection": "Fall Detection"
|
| 18 |
+
})
|
| 19 |
+
|
| 20 |
+
# Drop anything the model hasn't seen
|
| 21 |
+
unseen = ["Physical Activity Level", "Age", "Gender", "Stress Level", "Cholesterol Level (mg/dL)",
|
| 22 |
+
"Physical_Activity_Level", "Stress_Level", "Cholesterol_Level", "Fall_Detection"]
|
| 23 |
+
for f in unseen:
|
| 24 |
+
if f in raw_df.columns:
|
| 25 |
+
raw_df = raw_df.drop(columns=[f])
|
| 26 |
+
|
| 27 |
+
# Feature engineering
|
| 28 |
+
processed_df, _ = preprocess_data(raw_df, is_training=False)
|
| 29 |
+
|
| 30 |
+
if ensemble_model is None:
|
| 31 |
+
return {"error": "Ensemble model not loaded"}
|
| 32 |
+
|
| 33 |
+
# Ensemble Prediction
|
| 34 |
+
disease_label, conf, ind_preds, class_probs, weights = ensemble_model.predict(processed_df)
|
| 35 |
+
is_at_risk = (disease_label.lower() not in ["healthy", "normal", "none"])
|
| 36 |
+
|
| 37 |
+
return {
|
| 38 |
+
"prediction": is_at_risk,
|
| 39 |
+
"prediction_label": disease_label,
|
| 40 |
+
"probability": float(conf),
|
| 41 |
+
"ensemble_prediction": is_at_risk,
|
| 42 |
+
"model_outputs": ind_preds,
|
| 43 |
+
"disease_probs": class_probs,
|
| 44 |
+
"weights": weights
|
| 45 |
+
}
|
backend/main.py
CHANGED
|
@@ -1,19 +1,56 @@
|
|
| 1 |
-
from fastapi import FastAPI, UploadFile, File
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
from pydantic import BaseModel
|
| 4 |
import pandas as pd
|
| 5 |
import asyncio
|
| 6 |
import sys
|
| 7 |
import os
|
| 8 |
-
import
|
| 9 |
import shap
|
| 10 |
-
|
| 11 |
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 14 |
from preprocessing import preprocess_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
app = FastAPI(title="TinyML Healthcare API")
|
| 17 |
|
| 18 |
app.add_middleware(
|
| 19 |
CORSMiddleware,
|
|
@@ -23,15 +60,8 @@ app.add_middleware(
|
|
| 23 |
allow_headers=["*"],
|
| 24 |
)
|
| 25 |
|
| 26 |
-
ensemble_system = None
|
| 27 |
def get_ensemble():
|
| 28 |
global ensemble_system
|
| 29 |
-
if ensemble_system is None:
|
| 30 |
-
try:
|
| 31 |
-
from ensemble import EnsembleModel
|
| 32 |
-
ensemble_system = EnsembleModel()
|
| 33 |
-
except Exception:
|
| 34 |
-
return None
|
| 35 |
return ensemble_system
|
| 36 |
|
| 37 |
class PatientData(BaseModel):
|
|
@@ -40,68 +70,96 @@ class PatientData(BaseModel):
|
|
| 40 |
Systolic_BP: float
|
| 41 |
Diastolic_BP: float
|
| 42 |
Body_Temp: float
|
| 43 |
-
Fall_Detection: str
|
| 44 |
|
| 45 |
@app.get("/health")
|
| 46 |
def health_check():
|
| 47 |
return {"status": "Healthy" if get_ensemble() else "Warning - Models Offline"}
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
@app.post("/predict")
|
| 50 |
-
async def predict(data: PatientData):
|
| 51 |
try:
|
| 52 |
eng = get_ensemble()
|
| 53 |
if not eng:
|
| 54 |
return {"error": "Models untrained. Ensure python backend/models.py executes."}
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
'SpO2 Level (%)': data.SpO2_Level,
|
| 59 |
-
'Systolic Blood Pressure (mmHg)': data.Systolic_BP,
|
| 60 |
-
'Diastolic Blood Pressure (mmHg)': data.Diastolic_BP,
|
| 61 |
-
'Body Temperature (°C)': data.Body_Temp,
|
| 62 |
-
'Fall Detection': data.Fall_Detection
|
| 63 |
-
}])
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
is_at_risk =
|
|
|
|
|
|
|
| 69 |
|
| 70 |
# Critical Alert System — fault-tolerant
|
| 71 |
if is_at_risk == 1 and float(conf) > 0.80:
|
| 72 |
-
|
| 73 |
-
with open("alerts.log", "a") as f:
|
| 74 |
-
f.write(f"ALERT: Patient at risk! HR: {data.Heart_Rate}, SpO2: {data.SpO2_Level}, Confidence: {conf:.2f}\n")
|
| 75 |
-
except Exception:
|
| 76 |
-
pass
|
| 77 |
|
| 78 |
# Log to SQLite History — fault-tolerant
|
| 79 |
-
|
| 80 |
-
await asyncio.to_thread(log_prediction, data, final_pred, float(conf))
|
| 81 |
-
except Exception as db_e:
|
| 82 |
-
print(f"DB logging skipped: {db_e}")
|
| 83 |
|
| 84 |
-
return
|
| 85 |
-
"prediction": is_at_risk,
|
| 86 |
-
"prediction_label": final_pred,
|
| 87 |
-
"probability": float(conf),
|
| 88 |
-
"ensemble_prediction": is_at_risk,
|
| 89 |
-
"model_outputs": ind_preds,
|
| 90 |
-
"model_probs": {k: float(np.max(v)) for k, v in ind_probs.items()},
|
| 91 |
-
"weights": weights
|
| 92 |
-
}
|
| 93 |
except Exception as e:
|
| 94 |
import traceback
|
| 95 |
-
from fastapi import HTTPException
|
| 96 |
raise HTTPException(status_code=500, detail=f"Backend Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
@app.get("/history")
|
| 99 |
-
def history():
|
| 100 |
-
return get_history()
|
| 101 |
|
| 102 |
@app.get("/dataset")
|
| 103 |
-
|
| 104 |
-
data_path =
|
| 105 |
if os.path.exists(data_path):
|
| 106 |
try:
|
| 107 |
# Force UTF-8 and strip column whitespace
|
|
@@ -129,8 +187,7 @@ async def explain(data: PatientData):
|
|
| 129 |
'SpO2 Level (%)': data.SpO2_Level,
|
| 130 |
'Systolic Blood Pressure (mmHg)': data.Systolic_BP,
|
| 131 |
'Diastolic Blood Pressure (mmHg)': data.Diastolic_BP,
|
| 132 |
-
'Body Temperature (°C)': data.Body_Temp
|
| 133 |
-
'Fall Detection': data.Fall_Detection
|
| 134 |
}])
|
| 135 |
|
| 136 |
X_proc, _ = await asyncio.to_thread(preprocess_data, df, False)
|
|
@@ -157,336 +214,23 @@ async def explain(data: PatientData):
|
|
| 157 |
|
| 158 |
@app.get("/export_tinyml")
|
| 159 |
def export_tinyml(model_name: str = "rf", quantize: bool = False):
|
| 160 |
-
import numpy as np
|
| 161 |
eng = get_ensemble()
|
| 162 |
-
|
| 163 |
-
return {"error": f"Model {model_name} not found"}
|
| 164 |
-
|
| 165 |
-
model = eng.models[model_name]
|
| 166 |
-
|
| 167 |
-
# m2cgen handles Random Forest beautifully, but outputs FP32/double rules
|
| 168 |
-
if model_name == "rf":
|
| 169 |
-
try:
|
| 170 |
-
code = m2c.export_to_c(model)
|
| 171 |
-
if quantize:
|
| 172 |
-
code = "/* WARNING: M2CGen generated FP32 output. INT8 Quantization is not supported directly for Random Forest trees. */\n" + code
|
| 173 |
-
return {"code": code}
|
| 174 |
-
except Exception:
|
| 175 |
-
pass
|
| 176 |
-
|
| 177 |
-
# For LogReg, use m2cgen if FP32, otherwise manual generation for INT8
|
| 178 |
-
if not quantize and model_name == "logreg":
|
| 179 |
-
try:
|
| 180 |
-
code = m2c.export_to_c(model)
|
| 181 |
-
return {"code": code}
|
| 182 |
-
except Exception:
|
| 183 |
-
pass
|
| 184 |
-
|
| 185 |
-
# Manual C-code generation for all model types
|
| 186 |
-
try:
|
| 187 |
-
L = []
|
| 188 |
-
L.append("/* ====================================================== */")
|
| 189 |
-
L.append(f"/* TinyML C Export: {model_name} */")
|
| 190 |
-
q_text = "INT8 Quantized" if quantize else "FP32 Double"
|
| 191 |
-
L.append(f"/* Auto-generated for ARM Cortex-M / ESP32 ({q_text}) */")
|
| 192 |
-
L.append("/* ====================================================== */")
|
| 193 |
-
L.append("")
|
| 194 |
-
L.append("#include <math.h>")
|
| 195 |
-
L.append("#include <stdint.h>")
|
| 196 |
-
L.append("#include <string.h>")
|
| 197 |
-
L.append("")
|
| 198 |
-
|
| 199 |
-
if model_name == "svm" and hasattr(model, 'coef_'):
|
| 200 |
-
coefs = model.coef_
|
| 201 |
-
intercepts = model.intercept_
|
| 202 |
-
n_classes = len(model.classes_)
|
| 203 |
-
n_features = coefs.shape[1]
|
| 204 |
-
L.append(f"/* Linear SVM with {n_classes} classes, {n_features} features */")
|
| 205 |
-
L.append(f"#define N_FEATURES {n_features}")
|
| 206 |
-
L.append(f"#define N_CLASSES {n_classes}")
|
| 207 |
-
L.append(f"#define N_HYPERPLANES {coefs.shape[0]}")
|
| 208 |
-
L.append("")
|
| 209 |
-
|
| 210 |
-
if quantize:
|
| 211 |
-
scale_factor = 127.0 / max(np.max(np.abs(coefs)), np.max(np.abs(intercepts)), 1e-6)
|
| 212 |
-
L.append(f"/* Quantization Scale: {scale_factor:.4f} */")
|
| 213 |
-
L.append("static const int8_t SVM_COEF[N_HYPERPLANES][N_FEATURES] = {")
|
| 214 |
-
for row in coefs:
|
| 215 |
-
vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
|
| 216 |
-
L.append(f" {{{vals}}},")
|
| 217 |
-
L.append("};")
|
| 218 |
-
L.append("")
|
| 219 |
-
vals = ", ".join([str(int(round(v * scale_factor))) for v in intercepts])
|
| 220 |
-
L.append(f"static const int8_t SVM_INTERCEPT[N_HYPERPLANES] = {{{vals}}};")
|
| 221 |
-
L.append("")
|
| 222 |
-
L.append("int predict(int8_t *features) {")
|
| 223 |
-
L.append(" int32_t scores[N_CLASSES] = {0};")
|
| 224 |
-
L.append(" int h = 0;")
|
| 225 |
-
L.append(" for (int i = 0; i < N_CLASSES; i++) {")
|
| 226 |
-
L.append(" for (int j = i + 1; j < N_CLASSES; j++) {")
|
| 227 |
-
L.append(" int32_t decision = SVM_INTERCEPT[h];")
|
| 228 |
-
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 229 |
-
L.append(" decision += (int32_t)SVM_COEF[h][f] * features[f];")
|
| 230 |
-
L.append(" }")
|
| 231 |
-
L.append(" if (decision > 0) scores[i] += 1;")
|
| 232 |
-
L.append(" else scores[j] += 1;")
|
| 233 |
-
L.append(" h++;")
|
| 234 |
-
L.append(" }")
|
| 235 |
-
L.append(" }")
|
| 236 |
-
L.append(" int best = 0;")
|
| 237 |
-
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 238 |
-
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 239 |
-
L.append(" }")
|
| 240 |
-
L.append(" return best;")
|
| 241 |
-
L.append("}")
|
| 242 |
-
else:
|
| 243 |
-
L.append("static const double SVM_COEF[N_HYPERPLANES][N_FEATURES] = {")
|
| 244 |
-
for row in coefs:
|
| 245 |
-
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 246 |
-
L.append(f" {{{vals}}},")
|
| 247 |
-
L.append("};")
|
| 248 |
-
L.append("")
|
| 249 |
-
vals = ", ".join([f"{v:.6f}" for v in intercepts])
|
| 250 |
-
L.append(f"static const double SVM_INTERCEPT[N_HYPERPLANES] = {{{vals}}};")
|
| 251 |
-
L.append("")
|
| 252 |
-
L.append("int predict(double *features) {")
|
| 253 |
-
L.append(" double scores[N_CLASSES] = {0};")
|
| 254 |
-
L.append(" int h = 0;")
|
| 255 |
-
L.append(" for (int i = 0; i < N_CLASSES; i++) {")
|
| 256 |
-
L.append(" for (int j = i + 1; j < N_CLASSES; j++) {")
|
| 257 |
-
L.append(" double decision = SVM_INTERCEPT[h];")
|
| 258 |
-
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 259 |
-
L.append(" decision += SVM_COEF[h][f] * features[f];")
|
| 260 |
-
L.append(" }")
|
| 261 |
-
L.append(" if (decision > 0) scores[i] += 1.0;")
|
| 262 |
-
L.append(" else scores[j] += 1.0;")
|
| 263 |
-
L.append(" h++;")
|
| 264 |
-
L.append(" }")
|
| 265 |
-
L.append(" }")
|
| 266 |
-
L.append(" int best = 0;")
|
| 267 |
-
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 268 |
-
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 269 |
-
L.append(" }")
|
| 270 |
-
L.append(" return best;")
|
| 271 |
-
L.append("}")
|
| 272 |
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
L.append("")
|
| 282 |
-
|
| 283 |
-
if quantize:
|
| 284 |
-
scale_factor = 127.0 / max(np.max(np.abs(coefs)), np.max(np.abs(intercepts)), 1e-6)
|
| 285 |
-
L.append(f"/* Quantization Scale: {scale_factor:.4f} */")
|
| 286 |
-
L.append("static const int8_t LOGREG_COEF[N_CLASSES][N_FEATURES] = {")
|
| 287 |
-
for row in coefs:
|
| 288 |
-
vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
|
| 289 |
-
L.append(f" {{{vals}}},")
|
| 290 |
-
L.append("};")
|
| 291 |
-
L.append("")
|
| 292 |
-
vals = ", ".join([str(int(round(v * scale_factor))) for v in intercepts])
|
| 293 |
-
L.append(f"static const int8_t LOGREG_INTERCEPT[N_CLASSES] = {{{vals}}};")
|
| 294 |
-
L.append("")
|
| 295 |
-
L.append("int predict(int8_t *features) {")
|
| 296 |
-
L.append(" int32_t scores[N_CLASSES];")
|
| 297 |
-
L.append(" for (int c = 0; c < N_CLASSES; c++) {")
|
| 298 |
-
L.append(f" scores[c] = LOGREG_INTERCEPT[c] * {int(scale_factor)};")
|
| 299 |
-
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 300 |
-
L.append(" scores[c] += (int32_t)LOGREG_COEF[c][f] * features[f];")
|
| 301 |
-
L.append(" }")
|
| 302 |
-
L.append(" }")
|
| 303 |
-
L.append(" int best = 0;")
|
| 304 |
-
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 305 |
-
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 306 |
-
L.append(" }")
|
| 307 |
-
L.append(" return best;")
|
| 308 |
-
L.append("}")
|
| 309 |
-
else:
|
| 310 |
-
L.append("static const double LOGREG_COEF[N_CLASSES][N_FEATURES] = {")
|
| 311 |
-
for row in coefs:
|
| 312 |
-
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 313 |
-
L.append(f" {{{vals}}},")
|
| 314 |
-
L.append("};")
|
| 315 |
-
L.append("")
|
| 316 |
-
vals = ", ".join([f"{v:.6f}" for v in intercepts])
|
| 317 |
-
L.append(f"static const double LOGREG_INTERCEPT[N_CLASSES] = {{{vals}}};")
|
| 318 |
-
L.append("")
|
| 319 |
-
L.append("int predict(double *features) {")
|
| 320 |
-
L.append(" double scores[N_CLASSES];")
|
| 321 |
-
L.append(" for (int c = 0; c < N_CLASSES; c++) {")
|
| 322 |
-
L.append(" scores[c] = LOGREG_INTERCEPT[c];")
|
| 323 |
-
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 324 |
-
L.append(" scores[c] += LOGREG_COEF[c][f] * features[f];")
|
| 325 |
-
L.append(" }")
|
| 326 |
-
L.append(" }")
|
| 327 |
-
L.append(" int best = 0;")
|
| 328 |
-
L.append(" for (int c = 1; c < N_CLASSES; c++) {")
|
| 329 |
-
L.append(" if (scores[c] > scores[best]) best = c;")
|
| 330 |
-
L.append(" }")
|
| 331 |
-
L.append(" return best;")
|
| 332 |
-
L.append("}")
|
| 333 |
-
|
| 334 |
-
elif model_name == "small_nn" and hasattr(model, 'coefs_'):
|
| 335 |
-
layers = model.coefs_
|
| 336 |
-
biases = model.intercepts_
|
| 337 |
-
arch = " -> ".join([str(l.shape[0]) for l in layers] + [str(layers[-1].shape[1])])
|
| 338 |
-
L.append(f"/* MLP Neural Network: {len(layers)} layers */")
|
| 339 |
-
L.append(f"/* Architecture: {arch} */")
|
| 340 |
-
L.append("")
|
| 341 |
-
|
| 342 |
-
for idx, (W, b) in enumerate(zip(layers, biases)):
|
| 343 |
-
n_in, n_out = W.shape
|
| 344 |
-
L.append(f"#define L{idx}_IN {n_in}")
|
| 345 |
-
L.append(f"#define L{idx}_OUT {n_out}")
|
| 346 |
-
L.append(f"static const double W{idx}[{n_in}][{n_out}] = {{")
|
| 347 |
-
for row in W:
|
| 348 |
-
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 349 |
-
L.append(f" {{{vals}}},")
|
| 350 |
-
L.append("};")
|
| 351 |
-
bvals = ", ".join([f"{v:.6f}" for v in b])
|
| 352 |
-
L.append(f"static const double B{idx}[{n_out}] = {{{bvals}}};")
|
| 353 |
-
L.append("")
|
| 354 |
-
|
| 355 |
-
L.append("static inline double relu(double x) { return x > 0 ? x : 0; }")
|
| 356 |
-
L.append("")
|
| 357 |
-
|
| 358 |
-
if quantize:
|
| 359 |
-
# Calculate global max for int8 scaling
|
| 360 |
-
max_val = max([np.max(np.abs(w)) for w in layers] + [np.max(np.abs(b)) for b in biases] + [1e-6])
|
| 361 |
-
scale_factor = 127.0 / max_val
|
| 362 |
-
L.append(f"/* INT8 Quantization Scale Factor: {scale_factor:.4f} */")
|
| 363 |
-
for idx, (W, b) in enumerate(zip(layers, biases)):
|
| 364 |
-
n_in, n_out = W.shape
|
| 365 |
-
L.append(f"static const int8_t W{idx}[{n_in}][{n_out}] = {{")
|
| 366 |
-
for row in W:
|
| 367 |
-
vals = ", ".join([str(int(round(v * scale_factor))) for v in row])
|
| 368 |
-
L.append(f" {{{vals}}},")
|
| 369 |
-
L.append("};")
|
| 370 |
-
bvals = ", ".join([str(int(round(v * scale_factor))) for v in b])
|
| 371 |
-
L.append(f"static const int8_t B{idx}[{n_out}] = {{{bvals}}};")
|
| 372 |
-
L.append("")
|
| 373 |
-
L.append("static inline int32_t relu_int(int32_t x) { return x > 0 ? x : 0; }")
|
| 374 |
-
L.append("")
|
| 375 |
-
L.append("int predict(int8_t *input) {")
|
| 376 |
-
for idx in range(len(layers)):
|
| 377 |
-
n_in = layers[idx].shape[0]
|
| 378 |
-
n_out = layers[idx].shape[1]
|
| 379 |
-
is_last = (idx == len(layers) - 1)
|
| 380 |
-
src = "input" if idx == 0 else f"a{idx-1}"
|
| 381 |
-
L.append(f" int32_t a{idx}[{n_out}];")
|
| 382 |
-
L.append(f" for (int j = 0; j < {n_out}; j++) {{")
|
| 383 |
-
L.append(f" a{idx}[j] = B{idx}[j] * {int(scale_factor)}; /* scale bias */")
|
| 384 |
-
L.append(f" for (int i = 0; i < {n_in}; i++) {{")
|
| 385 |
-
L.append(f" a{idx}[j] += (int32_t){src}[i] * W{idx}[i][j];")
|
| 386 |
-
L.append(f" }}")
|
| 387 |
-
if not is_last:
|
| 388 |
-
L.append(f" a{idx}[j] = relu_int(a{idx}[j]) / {int(scale_factor)}; /* rescale */")
|
| 389 |
-
L.append(f" }}")
|
| 390 |
-
last_idx = len(layers) - 1
|
| 391 |
-
last_out = layers[-1].shape[1]
|
| 392 |
-
L.append(f" int best = 0;")
|
| 393 |
-
L.append(f" for (int c = 1; c < {last_out}; c++) {{")
|
| 394 |
-
L.append(f" if (a{last_idx}[c] > a{last_idx}[best]) best = c;")
|
| 395 |
-
L.append(f" }}")
|
| 396 |
-
L.append(f" return best;")
|
| 397 |
-
L.append("}")
|
| 398 |
-
else:
|
| 399 |
-
for idx, (W, b) in enumerate(zip(layers, biases)):
|
| 400 |
-
n_in, n_out = W.shape
|
| 401 |
-
L.append(f"#define L{idx}_IN {n_in}")
|
| 402 |
-
L.append(f"#define L{idx}_OUT {n_out}")
|
| 403 |
-
L.append(f"static const double W{idx}[{n_in}][{n_out}] = {{")
|
| 404 |
-
for row in W:
|
| 405 |
-
vals = ", ".join([f"{v:.6f}" for v in row])
|
| 406 |
-
L.append(f" {{{vals}}},")
|
| 407 |
-
L.append("};")
|
| 408 |
-
bvals = ", ".join([f"{v:.6f}" for v in b])
|
| 409 |
-
L.append(f"static const double B{idx}[{n_out}] = {{{bvals}}};")
|
| 410 |
-
L.append("")
|
| 411 |
-
|
| 412 |
-
L.append("static inline double relu(double x) { return x > 0 ? x : 0; }")
|
| 413 |
-
L.append("")
|
| 414 |
-
L.append("int predict(double *input) {")
|
| 415 |
-
for idx in range(len(layers)):
|
| 416 |
-
n_in = layers[idx].shape[0]
|
| 417 |
-
n_out = layers[idx].shape[1]
|
| 418 |
-
is_last = (idx == len(layers) - 1)
|
| 419 |
-
src = "input" if idx == 0 else f"a{idx-1}"
|
| 420 |
-
L.append(f" double a{idx}[{n_out}];")
|
| 421 |
-
L.append(f" for (int j = 0; j < {n_out}; j++) {{")
|
| 422 |
-
L.append(f" a{idx}[j] = B{idx}[j];")
|
| 423 |
-
L.append(f" for (int i = 0; i < {n_in}; i++) {{")
|
| 424 |
-
L.append(f" a{idx}[j] += {src}[i] * W{idx}[i][j];")
|
| 425 |
-
L.append(f" }}")
|
| 426 |
-
if not is_last:
|
| 427 |
-
L.append(f" a{idx}[j] = relu(a{idx}[j]);")
|
| 428 |
-
L.append(f" }}")
|
| 429 |
-
|
| 430 |
-
last_idx = len(layers) - 1
|
| 431 |
-
last_out = layers[-1].shape[1]
|
| 432 |
-
L.append(f" int best = 0;")
|
| 433 |
-
L.append(f" for (int c = 1; c < {last_out}; c++) {{")
|
| 434 |
-
L.append(f" if (a{last_idx}[c] > a{last_idx}[best]) best = c;")
|
| 435 |
-
L.append(f" }}")
|
| 436 |
-
L.append(f" return best;")
|
| 437 |
-
L.append("}")
|
| 438 |
-
|
| 439 |
-
elif model_name == "knn" and hasattr(model, '_fit_X'):
|
| 440 |
-
n_samples = min(model._fit_X.shape[0], 100)
|
| 441 |
-
n_feats = model._fit_X.shape[1]
|
| 442 |
-
L.append(f"/* KNN Lookup Table: {n_samples} reference samples */")
|
| 443 |
-
L.append(f"#define N_NEIGHBORS {model.n_neighbors}")
|
| 444 |
-
L.append(f"#define N_SAMPLES {n_samples}")
|
| 445 |
-
L.append(f"#define N_FEATURES {n_feats}")
|
| 446 |
-
L.append("")
|
| 447 |
-
L.append("static const double REF[N_SAMPLES][N_FEATURES] = {")
|
| 448 |
-
for row in model._fit_X[:n_samples]:
|
| 449 |
-
vals = ", ".join([f"{v:.4f}" for v in row])
|
| 450 |
-
L.append(f" {{{vals}}},")
|
| 451 |
-
L.append("};")
|
| 452 |
-
L.append("")
|
| 453 |
-
labels_str = ", ".join([str(int(l)) for l in model._y[:n_samples]])
|
| 454 |
-
L.append(f"static const int LABELS[N_SAMPLES] = {{{labels_str}}};")
|
| 455 |
-
L.append("")
|
| 456 |
-
L.append("int predict(double *features) {")
|
| 457 |
-
L.append(" double dists[N_SAMPLES];")
|
| 458 |
-
L.append(" for (int i = 0; i < N_SAMPLES; i++) {")
|
| 459 |
-
L.append(" dists[i] = 0.0;")
|
| 460 |
-
L.append(" for (int f = 0; f < N_FEATURES; f++) {")
|
| 461 |
-
L.append(" double d = features[f] - REF[i][f];")
|
| 462 |
-
L.append(" dists[i] += d * d;")
|
| 463 |
-
L.append(" }")
|
| 464 |
-
L.append(" }")
|
| 465 |
-
L.append(" int votes[10] = {0};")
|
| 466 |
-
L.append(" for (int k = 0; k < N_NEIGHBORS; k++) {")
|
| 467 |
-
L.append(" int mi = 0;")
|
| 468 |
-
L.append(" for (int i = 1; i < N_SAMPLES; i++) {")
|
| 469 |
-
L.append(" if (dists[i] < dists[mi]) mi = i;")
|
| 470 |
-
L.append(" }")
|
| 471 |
-
L.append(" votes[LABELS[mi]]++;")
|
| 472 |
-
L.append(" dists[mi] = 1e18;")
|
| 473 |
-
L.append(" }")
|
| 474 |
-
L.append(" int best = 0;")
|
| 475 |
-
L.append(" for (int i = 1; i < 10; i++) {")
|
| 476 |
-
L.append(" if (votes[i] > votes[best]) best = i;")
|
| 477 |
-
L.append(" }")
|
| 478 |
-
L.append(" return best;")
|
| 479 |
-
L.append("}")
|
| 480 |
-
else:
|
| 481 |
-
return {"error": f"Model {model_name} cannot be exported to C."}
|
| 482 |
-
|
| 483 |
-
return {"code": "\n".join(L)}
|
| 484 |
except Exception as e:
|
| 485 |
-
|
| 486 |
|
| 487 |
@app.post("/retrain")
|
| 488 |
-
async def retrain(file: UploadFile = File(...)):
|
| 489 |
-
data_path =
|
| 490 |
os.makedirs(os.path.dirname(data_path), exist_ok=True)
|
| 491 |
|
| 492 |
try:
|
|
@@ -496,7 +240,7 @@ async def retrain(file: UploadFile = File(...)):
|
|
| 496 |
new_df.columns = [c.strip() for c in new_df.columns]
|
| 497 |
|
| 498 |
if os.path.exists(data_path):
|
| 499 |
-
existing_df = pd.read_csv(data_path)
|
| 500 |
existing_df.columns = [c.strip() for c in existing_df.columns]
|
| 501 |
|
| 502 |
# Validate schema
|
|
@@ -518,12 +262,8 @@ async def retrain(file: UploadFile = File(...)):
|
|
| 518 |
# Save validated and combined dataset
|
| 519 |
combined_df.to_csv(data_path, index=False, encoding='utf-8')
|
| 520 |
|
| 521 |
-
|
| 522 |
-
await asyncio.to_thread(train_models)
|
| 523 |
-
|
| 524 |
-
global ensemble_system
|
| 525 |
-
ensemble_system = None
|
| 526 |
|
| 527 |
-
return {"status": "success", "message": f"Dataset updated (now {len(combined_df)} records) and ensemble
|
| 528 |
except Exception as e:
|
| 529 |
return {"error": f"Retraining failed: {str(e)}"}
|
|
|
|
| 1 |
+
from fastapi import FastAPI, UploadFile, File, BackgroundTasks, HTTPException, WebSocket, WebSocketDisconnect, Depends
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
from pydantic import BaseModel
|
| 4 |
import pandas as pd
|
| 5 |
import asyncio
|
| 6 |
import sys
|
| 7 |
import os
|
| 8 |
+
import random
|
| 9 |
import shap
|
| 10 |
+
import database as db
|
| 11 |
import numpy as np
|
| 12 |
+
from contextlib import asynccontextmanager
|
| 13 |
+
from config import settings
|
| 14 |
+
import warnings
|
| 15 |
+
|
| 16 |
+
# Suppress sklearn version mismatch warnings in stdout
|
| 17 |
+
try:
|
| 18 |
+
from sklearn.exceptions import InconsistentVersionWarning
|
| 19 |
+
warnings.filterwarnings("ignore", category=InconsistentVersionWarning)
|
| 20 |
+
except ImportError:
|
| 21 |
+
pass
|
| 22 |
|
| 23 |
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 24 |
from preprocessing import preprocess_data
|
| 25 |
+
from export import generate_c_code
|
| 26 |
+
from inference import evaluate
|
| 27 |
+
|
| 28 |
+
ensemble_system = None
|
| 29 |
+
|
| 30 |
+
async def background_sync():
|
| 31 |
+
while True:
|
| 32 |
+
await asyncio.sleep(60)
|
| 33 |
+
await db.sync_from_hub()
|
| 34 |
+
await db.sync_to_hub()
|
| 35 |
+
|
| 36 |
+
@asynccontextmanager
|
| 37 |
+
async def lifespan(app: FastAPI):
|
| 38 |
+
global ensemble_system
|
| 39 |
+
await db.init_db()
|
| 40 |
+
|
| 41 |
+
sync_task = asyncio.create_task(background_sync())
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
from ensemble import EnsembleModel
|
| 45 |
+
ensemble_system = EnsembleModel()
|
| 46 |
+
print("Models loaded successfully.")
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"Model loading failed: {e}")
|
| 49 |
+
yield
|
| 50 |
+
sync_task.cancel()
|
| 51 |
+
ensemble_system = None
|
| 52 |
|
| 53 |
+
app = FastAPI(title="TinyML Healthcare API", lifespan=lifespan)
|
| 54 |
|
| 55 |
app.add_middleware(
|
| 56 |
CORSMiddleware,
|
|
|
|
| 60 |
allow_headers=["*"],
|
| 61 |
)
|
| 62 |
|
|
|
|
| 63 |
def get_ensemble():
|
| 64 |
global ensemble_system
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
return ensemble_system
|
| 66 |
|
| 67 |
class PatientData(BaseModel):
|
|
|
|
| 70 |
Systolic_BP: float
|
| 71 |
Diastolic_BP: float
|
| 72 |
Body_Temp: float
|
|
|
|
| 73 |
|
| 74 |
@app.get("/health")
|
| 75 |
def health_check():
|
| 76 |
return {"status": "Healthy" if get_ensemble() else "Warning - Models Offline"}
|
| 77 |
|
| 78 |
+
def log_alert_sync(data: PatientData, conf: float):
|
| 79 |
+
try:
|
| 80 |
+
with open("alerts.log", "a") as f:
|
| 81 |
+
f.write(f"ALERT: Patient at risk! HR: {data.Heart_Rate}, SpO2: {data.SpO2_Level}, Confidence: {conf:.2f}\n")
|
| 82 |
+
except Exception:
|
| 83 |
+
pass
|
| 84 |
+
|
| 85 |
+
async def log_prediction_background(data, final_pred, conf):
|
| 86 |
+
try:
|
| 87 |
+
await db.log_prediction(data, final_pred, conf)
|
| 88 |
+
except Exception as db_e:
|
| 89 |
+
print(f"DB logging skipped: {db_e}")
|
| 90 |
+
|
| 91 |
@app.post("/predict")
|
| 92 |
+
async def predict(data: PatientData, background_tasks: BackgroundTasks):
|
| 93 |
try:
|
| 94 |
eng = get_ensemble()
|
| 95 |
if not eng:
|
| 96 |
return {"error": "Models untrained. Ensure python backend/models.py executes."}
|
| 97 |
|
| 98 |
+
from inference import evaluate
|
| 99 |
+
result = await asyncio.to_thread(evaluate, eng, data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
+
if "error" in result:
|
| 102 |
+
return result
|
| 103 |
+
|
| 104 |
+
is_at_risk = result["prediction"]
|
| 105 |
+
final_pred = result["prediction_label"]
|
| 106 |
+
conf = result["probability"]
|
| 107 |
|
| 108 |
# Critical Alert System — fault-tolerant
|
| 109 |
if is_at_risk == 1 and float(conf) > 0.80:
|
| 110 |
+
background_tasks.add_task(log_alert_sync, data, float(conf))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
# Log to SQLite History — fault-tolerant
|
| 113 |
+
background_tasks.add_task(log_prediction_background, data, final_pred, float(conf))
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
+
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
except Exception as e:
|
| 117 |
import traceback
|
|
|
|
| 118 |
raise HTTPException(status_code=500, detail=f"Backend Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
|
| 119 |
|
| 120 |
+
@app.websocket("/ws/feed")
|
| 121 |
+
async def websocket_feed(websocket: WebSocket):
|
| 122 |
+
await websocket.accept()
|
| 123 |
+
|
| 124 |
+
eng = get_ensemble()
|
| 125 |
+
if not eng:
|
| 126 |
+
await websocket.close(code=1011)
|
| 127 |
+
return
|
| 128 |
+
|
| 129 |
+
from simulator import PatientDataSimulator
|
| 130 |
+
simulator = PatientDataSimulator()
|
| 131 |
+
|
| 132 |
+
try:
|
| 133 |
+
async for state in simulator.run():
|
| 134 |
+
data = PatientData(**state)
|
| 135 |
+
|
| 136 |
+
prediction_result = await asyncio.to_thread(evaluate, eng, data)
|
| 137 |
+
|
| 138 |
+
front_pred = None
|
| 139 |
+
if "error" not in prediction_result:
|
| 140 |
+
front_pred = {
|
| 141 |
+
"is_at_risk": 1 if prediction_result["prediction"] else 0,
|
| 142 |
+
"label": prediction_result["prediction_label"],
|
| 143 |
+
"confidence": prediction_result["probability"],
|
| 144 |
+
"disease_probs": prediction_result.get("disease_probs", {})
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
await websocket.send_json({
|
| 148 |
+
"sensor_data": data.model_dump(),
|
| 149 |
+
"prediction": front_pred
|
| 150 |
+
})
|
| 151 |
+
except WebSocketDisconnect:
|
| 152 |
+
pass
|
| 153 |
+
except Exception as e:
|
| 154 |
+
print(f"WS error: {e}")
|
| 155 |
+
|
| 156 |
@app.get("/history")
|
| 157 |
+
async def history():
|
| 158 |
+
return await db.get_history()
|
| 159 |
|
| 160 |
@app.get("/dataset")
|
| 161 |
+
def get_dataset():
|
| 162 |
+
data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'patient_dataset.csv')
|
| 163 |
if os.path.exists(data_path):
|
| 164 |
try:
|
| 165 |
# Force UTF-8 and strip column whitespace
|
|
|
|
| 187 |
'SpO2 Level (%)': data.SpO2_Level,
|
| 188 |
'Systolic Blood Pressure (mmHg)': data.Systolic_BP,
|
| 189 |
'Diastolic Blood Pressure (mmHg)': data.Diastolic_BP,
|
| 190 |
+
'Body Temperature (°C)': data.Body_Temp
|
|
|
|
| 191 |
}])
|
| 192 |
|
| 193 |
X_proc, _ = await asyncio.to_thread(preprocess_data, df, False)
|
|
|
|
| 214 |
|
| 215 |
@app.get("/export_tinyml")
|
| 216 |
def export_tinyml(model_name: str = "rf", quantize: bool = False):
|
|
|
|
| 217 |
eng = get_ensemble()
|
| 218 |
+
return generate_c_code(eng, model_name, quantize)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
+
def run_training_background():
|
| 221 |
+
try:
|
| 222 |
+
from models import train_models
|
| 223 |
+
train_models()
|
| 224 |
+
# Force reload in lifespan is tricky, but we can do it via global
|
| 225 |
+
global ensemble_system
|
| 226 |
+
from ensemble import EnsembleModel
|
| 227 |
+
ensemble_system = EnsembleModel()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
except Exception as e:
|
| 229 |
+
print(f"Background training failed: {e}")
|
| 230 |
|
| 231 |
@app.post("/retrain")
|
| 232 |
+
async def retrain(background_tasks: BackgroundTasks, file: UploadFile = File(...)):
|
| 233 |
+
data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'patient_dataset.csv')
|
| 234 |
os.makedirs(os.path.dirname(data_path), exist_ok=True)
|
| 235 |
|
| 236 |
try:
|
|
|
|
| 240 |
new_df.columns = [c.strip() for c in new_df.columns]
|
| 241 |
|
| 242 |
if os.path.exists(data_path):
|
| 243 |
+
existing_df = pd.read_csv(data_path, encoding='utf-8')
|
| 244 |
existing_df.columns = [c.strip() for c in existing_df.columns]
|
| 245 |
|
| 246 |
# Validate schema
|
|
|
|
| 262 |
# Save validated and combined dataset
|
| 263 |
combined_df.to_csv(data_path, index=False, encoding='utf-8')
|
| 264 |
|
| 265 |
+
background_tasks.add_task(run_training_background)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
|
| 267 |
+
return {"status": "success", "message": f"Dataset updated (now {len(combined_df)} records) and ensemble retrain started in background!"}
|
| 268 |
except Exception as e:
|
| 269 |
return {"error": f"Retraining failed: {str(e)}"}
|
backend/models.py
CHANGED
|
@@ -12,12 +12,12 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
| 12 |
from preprocessing import get_train_test_split, resolve_model_dir
|
| 13 |
|
| 14 |
def train_models():
|
| 15 |
-
data_path =
|
| 16 |
model_dir = resolve_model_dir()
|
| 17 |
os.makedirs(model_dir, exist_ok=True)
|
| 18 |
|
| 19 |
print("Loading data...")
|
| 20 |
-
df = pd.read_csv(data_path)
|
| 21 |
X_train, X_test, y_train, y_test = get_train_test_split(df)
|
| 22 |
|
| 23 |
models = {
|
|
|
|
| 12 |
from preprocessing import get_train_test_split, resolve_model_dir
|
| 13 |
|
| 14 |
def train_models():
|
| 15 |
+
data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'patient_dataset.csv')
|
| 16 |
model_dir = resolve_model_dir()
|
| 17 |
os.makedirs(model_dir, exist_ok=True)
|
| 18 |
|
| 19 |
print("Loading data...")
|
| 20 |
+
df = pd.read_csv(data_path, encoding='utf-8')
|
| 21 |
X_train, X_test, y_train, y_test = get_train_test_split(df)
|
| 22 |
|
| 23 |
models = {
|
backend/patient_history.db
CHANGED
|
Binary files a/backend/patient_history.db and b/backend/patient_history.db differ
|
|
|
backend/preprocessing.py
CHANGED
|
@@ -4,17 +4,18 @@ import os
|
|
| 4 |
import joblib
|
| 5 |
from sklearn.model_selection import train_test_split
|
| 6 |
from sklearn.preprocessing import StandardScaler, LabelEncoder
|
|
|
|
| 7 |
|
| 8 |
def resolve_model_dir():
|
| 9 |
-
|
| 10 |
-
return '/app/model' if os.path.exists('/app/model') else '../model' if os.path.exists('../model') else 'model'
|
| 11 |
|
| 12 |
def preprocess_data(df, is_training=True):
|
| 13 |
model_dir = resolve_model_dir()
|
| 14 |
scaler_path = f'{model_dir}/scaler.pkl'
|
| 15 |
label_encoder_path = f'{model_dir}/label_encoder.pkl'
|
|
|
|
| 16 |
|
| 17 |
-
columns_to_drop = ['Patient Number', 'Data Accuracy (%)', 'Heart Rate Alert', 'SpO2 Level Alert', 'Blood Pressure Alert', 'Temperature Alert']
|
| 18 |
cols_drop = [c for c in columns_to_drop if c in df.columns]
|
| 19 |
X_raw = df.drop(columns=cols_drop)
|
| 20 |
|
|
@@ -36,21 +37,30 @@ def preprocess_data(df, is_training=True):
|
|
| 36 |
else:
|
| 37 |
y = y_raw
|
| 38 |
|
|
|
|
|
|
|
|
|
|
| 39 |
if 'Fall Detection' in X_raw.columns:
|
| 40 |
X_raw['Fall Detection'] = X_raw['Fall Detection'].map({'Yes': 1, 'No': 0}).fillna(0)
|
| 41 |
|
| 42 |
-
X_raw.fillna(X_raw.mean(), inplace=True)
|
| 43 |
-
|
| 44 |
# Engineered Feature
|
| 45 |
X_raw['Risk_Severity'] = (X_raw['Heart Rate (bpm)'] > 105).astype(int) + (X_raw['SpO2 Level (%)'] < 94).astype(int)
|
| 46 |
|
| 47 |
continuous_features = ['Heart Rate (bpm)', 'SpO2 Level (%)', 'Systolic Blood Pressure (mmHg)', 'Diastolic Blood Pressure (mmHg)', 'Body Temperature (°C)', 'Risk_Severity']
|
| 48 |
|
| 49 |
if is_training:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
scaler = StandardScaler()
|
| 51 |
X_raw[continuous_features] = scaler.fit_transform(X_raw[continuous_features])
|
| 52 |
joblib.dump(scaler, scaler_path)
|
| 53 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
if os.path.exists(scaler_path):
|
| 55 |
scaler = joblib.load(scaler_path)
|
| 56 |
X_raw[continuous_features] = scaler.transform(X_raw[continuous_features])
|
|
|
|
| 4 |
import joblib
|
| 5 |
from sklearn.model_selection import train_test_split
|
| 6 |
from sklearn.preprocessing import StandardScaler, LabelEncoder
|
| 7 |
+
from sklearn.impute import SimpleImputer
|
| 8 |
|
| 9 |
def resolve_model_dir():
|
| 10 |
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'model')
|
|
|
|
| 11 |
|
| 12 |
def preprocess_data(df, is_training=True):
|
| 13 |
model_dir = resolve_model_dir()
|
| 14 |
scaler_path = f'{model_dir}/scaler.pkl'
|
| 15 |
label_encoder_path = f'{model_dir}/label_encoder.pkl'
|
| 16 |
+
imputer_path = f'{model_dir}/imputer.pkl'
|
| 17 |
|
| 18 |
+
columns_to_drop = ['Patient Number', 'Data Accuracy (%)', 'Heart Rate Alert', 'SpO2 Level Alert', 'Blood Pressure Alert', 'Temperature Alert', 'Fall Detection']
|
| 19 |
cols_drop = [c for c in columns_to_drop if c in df.columns]
|
| 20 |
X_raw = df.drop(columns=cols_drop)
|
| 21 |
|
|
|
|
| 37 |
else:
|
| 38 |
y = y_raw
|
| 39 |
|
| 40 |
+
# Fix broken encoding column names safely
|
| 41 |
+
X_raw.rename(columns=lambda x: x.replace('', '°') if isinstance(x, str) else x, inplace=True)
|
| 42 |
+
|
| 43 |
if 'Fall Detection' in X_raw.columns:
|
| 44 |
X_raw['Fall Detection'] = X_raw['Fall Detection'].map({'Yes': 1, 'No': 0}).fillna(0)
|
| 45 |
|
|
|
|
|
|
|
| 46 |
# Engineered Feature
|
| 47 |
X_raw['Risk_Severity'] = (X_raw['Heart Rate (bpm)'] > 105).astype(int) + (X_raw['SpO2 Level (%)'] < 94).astype(int)
|
| 48 |
|
| 49 |
continuous_features = ['Heart Rate (bpm)', 'SpO2 Level (%)', 'Systolic Blood Pressure (mmHg)', 'Diastolic Blood Pressure (mmHg)', 'Body Temperature (°C)', 'Risk_Severity']
|
| 50 |
|
| 51 |
if is_training:
|
| 52 |
+
imputer = SimpleImputer(strategy='mean')
|
| 53 |
+
X_raw[continuous_features] = imputer.fit_transform(X_raw[continuous_features])
|
| 54 |
+
joblib.dump(imputer, imputer_path)
|
| 55 |
+
|
| 56 |
scaler = StandardScaler()
|
| 57 |
X_raw[continuous_features] = scaler.fit_transform(X_raw[continuous_features])
|
| 58 |
joblib.dump(scaler, scaler_path)
|
| 59 |
else:
|
| 60 |
+
if os.path.exists(imputer_path):
|
| 61 |
+
imputer = joblib.load(imputer_path)
|
| 62 |
+
X_raw[continuous_features] = imputer.transform(X_raw[continuous_features])
|
| 63 |
+
|
| 64 |
if os.path.exists(scaler_path):
|
| 65 |
scaler = joblib.load(scaler_path)
|
| 66 |
X_raw[continuous_features] = scaler.transform(X_raw[continuous_features])
|
backend/requirements.txt
CHANGED
|
@@ -1,9 +1,12 @@
|
|
| 1 |
-
fastapi
|
| 2 |
-
uvicorn
|
| 3 |
-
pydantic
|
| 4 |
-
scikit-learn==1.
|
| 5 |
-
pandas
|
| 6 |
-
numpy
|
| 7 |
-
shap
|
| 8 |
-
m2cgen
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
pydantic
|
| 4 |
+
scikit-learn==1.8.0
|
| 5 |
+
pandas
|
| 6 |
+
numpy
|
| 7 |
+
shap
|
| 8 |
+
m2cgen
|
| 9 |
+
huggingface-hub
|
| 10 |
+
websockets
|
| 11 |
+
aiosqlite
|
| 12 |
+
python-multipart
|
backend/simulator.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import random
|
| 3 |
+
from typing import AsyncGenerator
|
| 4 |
+
|
| 5 |
+
class PatientDataSimulator:
|
| 6 |
+
def __init__(self, initial_hr=75.0, initial_spo2=98.0, initial_sys=120.0, initial_dia=80.0, initial_temp=37.0):
|
| 7 |
+
self.hr = initial_hr
|
| 8 |
+
self.spo2 = initial_spo2
|
| 9 |
+
self.sys_bp = initial_sys
|
| 10 |
+
self.dia_bp = initial_dia
|
| 11 |
+
self.temp = initial_temp
|
| 12 |
+
|
| 13 |
+
self.target_hr = self.hr
|
| 14 |
+
self.target_spo2 = self.spo2
|
| 15 |
+
self.target_sys = self.sys_bp
|
| 16 |
+
self.target_dia = self.dia_bp
|
| 17 |
+
|
| 18 |
+
def _shift_state(self):
|
| 19 |
+
# 5% chance every second to shift patient state
|
| 20 |
+
if random.random() < 0.05:
|
| 21 |
+
state = random.choice(["normal", "normal", "normal", "asthma", "hypertension", "heart_disease", "diabetes"])
|
| 22 |
+
if state == "normal":
|
| 23 |
+
self.target_hr, self.target_spo2, self.target_sys, self.target_dia = 75.0, 98.0, 120.0, 80.0
|
| 24 |
+
elif state == "asthma":
|
| 25 |
+
self.target_hr, self.target_spo2, self.target_sys, self.target_dia = 115.0, 88.0, 135.0, 85.0
|
| 26 |
+
elif state == "hypertension":
|
| 27 |
+
self.target_hr, self.target_spo2, self.target_sys, self.target_dia = 90.0, 97.0, 175.0, 105.0
|
| 28 |
+
elif state == "heart_disease":
|
| 29 |
+
self.target_hr, self.target_spo2, self.target_sys, self.target_dia = 135.0, 91.0, 150.0, 95.0
|
| 30 |
+
elif state == "diabetes":
|
| 31 |
+
self.target_hr, self.target_spo2, self.target_sys, self.target_dia = 85.0, 96.0, 140.0, 90.0
|
| 32 |
+
|
| 33 |
+
def _step_physics(self):
|
| 34 |
+
# Interpolate towards target with noise
|
| 35 |
+
self.hr += (self.target_hr - self.hr) * 0.1 + random.uniform(-2, 2)
|
| 36 |
+
self.spo2 += (self.target_spo2 - self.spo2) * 0.1 + random.uniform(-0.5, 0.5)
|
| 37 |
+
self.sys_bp += (self.target_sys - self.sys_bp) * 0.1 + random.uniform(-1, 1)
|
| 38 |
+
self.dia_bp += (self.target_dia - self.dia_bp) * 0.1 + random.uniform(-1, 1)
|
| 39 |
+
self.temp = max(36.0, min(39.0, self.temp + random.uniform(-0.1, 0.1)))
|
| 40 |
+
|
| 41 |
+
# Clamp values
|
| 42 |
+
self.hr = max(50.0, min(180.0, self.hr))
|
| 43 |
+
self.spo2 = max(80.0, min(100.0, self.spo2))
|
| 44 |
+
|
| 45 |
+
async def run(self) -> AsyncGenerator[dict, None]:
|
| 46 |
+
while True:
|
| 47 |
+
self._shift_state()
|
| 48 |
+
self._step_physics()
|
| 49 |
+
|
| 50 |
+
yield {
|
| 51 |
+
"Heart_Rate": round(self.hr, 1),
|
| 52 |
+
"SpO2_Level": round(self.spo2, 1),
|
| 53 |
+
"Systolic_BP": round(self.sys_bp, 1),
|
| 54 |
+
"Diastolic_BP": round(self.dia_bp, 1),
|
| 55 |
+
"Body_Temp": round(self.temp, 1)
|
| 56 |
+
}
|
| 57 |
+
await asyncio.sleep(1.0)
|
backend/tests/test_api.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from fastapi.testclient import TestClient
|
| 3 |
+
import sys
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 7 |
+
from main import app
|
| 8 |
+
|
| 9 |
+
client = TestClient(app)
|
| 10 |
+
|
| 11 |
+
def test_history_endpoint():
|
| 12 |
+
response = client.get("/history")
|
| 13 |
+
assert response.status_code == 200
|
| 14 |
+
assert isinstance(response.json(), list)
|
| 15 |
+
|
| 16 |
+
def test_dataset_endpoint():
|
| 17 |
+
response = client.get("/dataset")
|
| 18 |
+
assert response.status_code == 200
|
| 19 |
+
assert isinstance(response.json(), list)
|
| 20 |
+
|
| 21 |
+
def test_export_tinyml_random_forest():
|
| 22 |
+
response = client.get("/export_tinyml?model_name=rf")
|
| 23 |
+
|
| 24 |
+
if response.status_code == 200:
|
| 25 |
+
data = response.json()
|
| 26 |
+
if "error" in data:
|
| 27 |
+
assert "not found" in data["error"].lower()
|
| 28 |
+
else:
|
| 29 |
+
assert "code" in data or "c_code" in data
|
docker-compose.yml
CHANGED
|
@@ -2,22 +2,28 @@ version: '3.8'
|
|
| 2 |
|
| 3 |
services:
|
| 4 |
backend:
|
| 5 |
-
build:
|
| 6 |
-
context: .
|
| 7 |
-
dockerfile:
|
| 8 |
ports:
|
| 9 |
- "8000:8000"
|
|
|
|
|
|
|
| 10 |
volumes:
|
| 11 |
-
-
|
| 12 |
-
-
|
| 13 |
|
| 14 |
frontend:
|
| 15 |
build:
|
| 16 |
-
context: .
|
| 17 |
-
dockerfile:
|
| 18 |
ports:
|
| 19 |
-
- "
|
| 20 |
environment:
|
| 21 |
-
-
|
|
|
|
| 22 |
depends_on:
|
| 23 |
- backend
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
services:
|
| 4 |
backend:
|
| 5 |
+
build:
|
| 6 |
+
context: ./backend
|
| 7 |
+
dockerfile: Dockerfile
|
| 8 |
ports:
|
| 9 |
- "8000:8000"
|
| 10 |
+
environment:
|
| 11 |
+
- HF_TOKEN=${HF_TOKEN:-}
|
| 12 |
volumes:
|
| 13 |
+
# Mount DB for persistence (in production use a volume, here bind-mount is okay for dev)
|
| 14 |
+
- backend_data:/app
|
| 15 |
|
| 16 |
frontend:
|
| 17 |
build:
|
| 18 |
+
context: ./frontend
|
| 19 |
+
dockerfile: Dockerfile
|
| 20 |
ports:
|
| 21 |
+
- "3000:3000"
|
| 22 |
environment:
|
| 23 |
+
# Next.js will build this at build-time, but here it is for client-side API requests
|
| 24 |
+
- NEXT_PUBLIC_API_URL=http://localhost:8000
|
| 25 |
depends_on:
|
| 26 |
- backend
|
| 27 |
+
|
| 28 |
+
volumes:
|
| 29 |
+
backend_data:
|
frontend/Dockerfile
CHANGED
|
@@ -1,11 +1,20 @@
|
|
| 1 |
-
FROM
|
| 2 |
WORKDIR /app
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
COPY
|
| 7 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
COPY . .
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine AS deps
|
| 2 |
WORKDIR /app
|
| 3 |
+
COPY package.json package-lock.json* ./
|
| 4 |
+
RUN npm ci
|
| 5 |
|
| 6 |
+
FROM node:20-alpine AS builder
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
COPY --from=deps /app/node_modules ./node_modules
|
|
|
|
| 9 |
COPY . .
|
| 10 |
+
RUN npm run build
|
| 11 |
+
|
| 12 |
+
FROM node:20-alpine AS runner
|
| 13 |
+
WORKDIR /app
|
| 14 |
+
ENV NODE_ENV=production
|
| 15 |
+
COPY --from=builder /app/public ./public
|
| 16 |
+
COPY --from=builder /app/.next/standalone ./
|
| 17 |
+
COPY --from=builder /app/.next/static ./.next/static
|
| 18 |
+
EXPOSE 3000
|
| 19 |
+
ENV PORT=3000
|
| 20 |
+
CMD ["node", "server.js"]
|
frontend/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
First, run the development server:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
# or
|
| 10 |
+
yarn dev
|
| 11 |
+
# or
|
| 12 |
+
pnpm dev
|
| 13 |
+
# or
|
| 14 |
+
bun dev
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
+
|
| 19 |
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
| 20 |
+
|
| 21 |
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
+
|
| 23 |
+
## Learn More
|
| 24 |
+
|
| 25 |
+
To learn more about Next.js, take a look at the following resources:
|
| 26 |
+
|
| 27 |
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
+
|
| 30 |
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
| 31 |
+
|
| 32 |
+
## Deploy on Vercel
|
| 33 |
+
|
| 34 |
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
| 35 |
+
|
| 36 |
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
frontend/eslint.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
+
import nextTs from "eslint-config-next/typescript";
|
| 4 |
+
|
| 5 |
+
const eslintConfig = defineConfig([
|
| 6 |
+
...nextVitals,
|
| 7 |
+
...nextTs,
|
| 8 |
+
// Override default ignores of eslint-config-next.
|
| 9 |
+
globalIgnores([
|
| 10 |
+
// Default ignores of eslint-config-next:
|
| 11 |
+
".next/**",
|
| 12 |
+
"out/**",
|
| 13 |
+
"build/**",
|
| 14 |
+
"next-env.d.ts",
|
| 15 |
+
]),
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
export default eslintConfig;
|
frontend/next.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
output: "export",
|
| 5 |
+
// basePath: "/TinyML-Heart-Health-Monitoring-Dashboard", // Uncomment if hosted on a subpath in GitHub Pages
|
| 6 |
+
images: {
|
| 7 |
+
unoptimized: true, // Required for static export
|
| 8 |
+
}
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
export default nextConfig;
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "web",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "eslint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"framer-motion": "^13.4.4",
|
| 13 |
+
"lucide-react": "^1.48.0",
|
| 14 |
+
"next": "16.3.6",
|
| 15 |
+
"react": "19.2.8",
|
| 16 |
+
"react-dom": "19.2.8"
|
| 17 |
+
},
|
| 18 |
+
"devDependencies": {
|
| 19 |
+
"@tailwindcss/postcss": "^4",
|
| 20 |
+
"@types/node": "^20",
|
| 21 |
+
"@types/react": "^19",
|
| 22 |
+
"@types/react-dom": "^19",
|
| 23 |
+
"eslint": "^9",
|
| 24 |
+
"eslint-config-next": "16.3.6",
|
| 25 |
+
"tailwindcss": "^4",
|
| 26 |
+
"typescript": "^5"
|
| 27 |
+
}
|
| 28 |
+
}
|
frontend/postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
frontend/public/file.svg
ADDED
|
|
frontend/public/globe.svg
ADDED
|
|
frontend/public/next.svg
ADDED
|
|
frontend/public/vercel.svg
ADDED
|
|
frontend/public/window.svg
ADDED
|
|
frontend/src/app/analytics/page.tsx
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { motion } from "framer-motion";
|
| 4 |
+
import { Database, LineChart, Table2, Layers } from "lucide-react";
|
| 5 |
+
import Link from "next/link";
|
| 6 |
+
import { useEffect, useState } from "react";
|
| 7 |
+
|
| 8 |
+
export default function Analytics() {
|
| 9 |
+
const [dataset, setDataset] = useState<any[]>([]);
|
| 10 |
+
const [loading, setLoading] = useState(true);
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
fetch((process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000") + "/dataset")
|
| 14 |
+
.then((res) => res.json())
|
| 15 |
+
.then((data) => {
|
| 16 |
+
setDataset(data);
|
| 17 |
+
setLoading(false);
|
| 18 |
+
})
|
| 19 |
+
.catch((err) => {
|
| 20 |
+
console.error(err);
|
| 21 |
+
setLoading(false);
|
| 22 |
+
});
|
| 23 |
+
}, []);
|
| 24 |
+
|
| 25 |
+
const spring = {
|
| 26 |
+
type: "spring" as const,
|
| 27 |
+
stiffness: 100,
|
| 28 |
+
damping: 20,
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
const columns = dataset.length > 0 ? Object.keys(dataset[0]) : [];
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<main className="min-h-[100dvh] relative overflow-hidden bg-canvas">
|
| 35 |
+
<nav className="h-16 flex items-center px-8 border-b border-hairline bg-canvas/80 backdrop-blur-md sticky top-0 z-50">
|
| 36 |
+
<div className="max-w-[1200px] mx-auto w-full flex justify-between items-center">
|
| 37 |
+
<Link href="/" className="font-display text-xl text-ink font-light tracking-[-0.02em]">TinyML Core</Link>
|
| 38 |
+
<div className="flex gap-6 items-center text-[15px] font-medium tracking-tight">
|
| 39 |
+
<Link href="/dashboard" className="text-muted hover:text-ink transition-colors">Live Monitor</Link>
|
| 40 |
+
<Link href="/history" className="text-muted hover:text-ink transition-colors">History</Link>
|
| 41 |
+
<Link href="/mlops" className="text-muted hover:text-ink transition-colors">MLOps</Link>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</nav>
|
| 45 |
+
|
| 46 |
+
<div className="max-w-[1200px] mx-auto px-8 py-24 relative">
|
| 47 |
+
<div className="absolute top-0 right-1/4 w-[30vw] h-[30vw] rounded-full bg-gradient-sky opacity-20 blur-[80px] -z-10" />
|
| 48 |
+
|
| 49 |
+
<motion.div
|
| 50 |
+
initial={{ opacity: 0, y: 20 }}
|
| 51 |
+
animate={{ opacity: 1, y: 0 }}
|
| 52 |
+
transition={spring}
|
| 53 |
+
className="mb-12 flex justify-between items-end"
|
| 54 |
+
>
|
| 55 |
+
<div>
|
| 56 |
+
<h1 className="font-display text-[48px] font-light tracking-[-0.02em] text-ink leading-[1.08] mb-4">
|
| 57 |
+
Dataset Explorer
|
| 58 |
+
</h1>
|
| 59 |
+
<p className="font-sans text-[16px] text-body max-w-2xl leading-relaxed tracking-[0.01em]">
|
| 60 |
+
Preview the current ground-truth telemetry used for training the TinyML models.
|
| 61 |
+
</p>
|
| 62 |
+
</div>
|
| 63 |
+
<div className="flex gap-4">
|
| 64 |
+
<div className="px-4 py-2 bg-canvas-soft border border-hairline-soft rounded-full flex items-center text-sm font-medium text-ink">
|
| 65 |
+
<Layers className="w-4 h-4 mr-2 text-muted" />
|
| 66 |
+
{columns.length} Features
|
| 67 |
+
</div>
|
| 68 |
+
<div className="px-4 py-2 bg-surface-card border border-hairline rounded-full flex items-center text-sm font-medium text-ink shadow-[0_4px_16px_rgba(0,0,0,0.02)]">
|
| 69 |
+
<Database className="w-4 h-4 mr-2 text-muted" />
|
| 70 |
+
{dataset.length} Rows
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</motion.div>
|
| 74 |
+
|
| 75 |
+
<motion.div
|
| 76 |
+
initial={{ opacity: 0, y: 20 }}
|
| 77 |
+
animate={{ opacity: 1, y: 0 }}
|
| 78 |
+
transition={{ ...spring, delay: 0.1 }}
|
| 79 |
+
className="bg-surface-card rounded-xl border border-hairline shadow-[0_4px_16px_rgba(0,0,0,0.04)] overflow-hidden flex flex-col h-[600px]"
|
| 80 |
+
>
|
| 81 |
+
<div className="p-4 border-b border-hairline-soft flex items-center justify-between bg-canvas-soft">
|
| 82 |
+
<h2 className="font-sans text-[14px] font-medium text-ink flex items-center gap-2">
|
| 83 |
+
<Table2 size={16} className="text-muted" /> Raw Tabular View
|
| 84 |
+
</h2>
|
| 85 |
+
</div>
|
| 86 |
+
<div className="flex-grow overflow-auto relative bg-surface-card">
|
| 87 |
+
<table className="w-full text-left text-[14px]">
|
| 88 |
+
<thead className="bg-canvas-soft sticky top-0 z-10 border-b border-hairline text-muted font-medium text-[12px] uppercase tracking-[0.06em]">
|
| 89 |
+
<tr>
|
| 90 |
+
{columns.map(col => (
|
| 91 |
+
<th key={col} className="px-6 py-4 font-medium whitespace-nowrap">
|
| 92 |
+
{col}
|
| 93 |
+
</th>
|
| 94 |
+
))}
|
| 95 |
+
</tr>
|
| 96 |
+
</thead>
|
| 97 |
+
<tbody className="divide-y divide-hairline-soft text-ink">
|
| 98 |
+
{loading ? (
|
| 99 |
+
<tr>
|
| 100 |
+
<td colSpan={columns.length || 1} className="px-6 py-12 text-center text-muted">
|
| 101 |
+
Loading dataset...
|
| 102 |
+
</td>
|
| 103 |
+
</tr>
|
| 104 |
+
) : dataset.length === 0 ? (
|
| 105 |
+
<tr>
|
| 106 |
+
<td colSpan={columns.length || 1} className="px-6 py-12 text-center text-muted">
|
| 107 |
+
No dataset available. Upload one in MLOps.
|
| 108 |
+
</td>
|
| 109 |
+
</tr>
|
| 110 |
+
) : (
|
| 111 |
+
dataset.slice(0, 100).map((row, i) => (
|
| 112 |
+
<tr key={i} className="hover:bg-canvas-soft/50 transition-colors">
|
| 113 |
+
{columns.map(col => (
|
| 114 |
+
<td key={col} className="px-6 py-3 whitespace-nowrap">
|
| 115 |
+
{typeof row[col] === 'number' && !Number.isInteger(row[col])
|
| 116 |
+
? row[col].toFixed(2)
|
| 117 |
+
: String(row[col])}
|
| 118 |
+
</td>
|
| 119 |
+
))}
|
| 120 |
+
</tr>
|
| 121 |
+
))
|
| 122 |
+
)}
|
| 123 |
+
</tbody>
|
| 124 |
+
</table>
|
| 125 |
+
{!loading && dataset.length > 100 && (
|
| 126 |
+
<div className="p-4 text-center text-[13px] text-muted-soft border-t border-hairline-soft">
|
| 127 |
+
Showing first 100 rows.
|
| 128 |
+
</div>
|
| 129 |
+
)}
|
| 130 |
+
</div>
|
| 131 |
+
</motion.div>
|
| 132 |
+
</div>
|
| 133 |
+
</main>
|
| 134 |
+
);
|
| 135 |
+
}
|
frontend/src/app/architecture/page.tsx
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import Link from "next/link";
|
| 4 |
+
|
| 5 |
+
export default function Architecture() {
|
| 6 |
+
return (
|
| 7 |
+
<main className="min-h-[100dvh] bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white">
|
| 8 |
+
{/* Top Nav (Mechanical) */}
|
| 9 |
+
<nav className="h-16 flex items-center px-8 border-b-2 border-ink bg-canvas-cream sticky top-0 z-50">
|
| 10 |
+
<div className="max-w-[1400px] mx-auto w-full flex justify-between items-center">
|
| 11 |
+
<Link href="/" className="font-sans text-[18px] font-black tracking-[-0.04em] uppercase">
|
| 12 |
+
[ HEARTFLOW_OS ]
|
| 13 |
+
</Link>
|
| 14 |
+
<div className="flex gap-8 items-center text-[13px] font-bold tracking-[0.05em] uppercase hidden md:flex">
|
| 15 |
+
<Link href="/dashboard" className="text-ink hover:text-hazard transition-colors">SYS.MONITOR</Link>
|
| 16 |
+
<Link href="/simulator" className="text-ink hover:text-hazard transition-colors">AI.SIMULATOR</Link>
|
| 17 |
+
<Link href="/history" className="text-ink hover:text-hazard transition-colors">DATA.LOG</Link>
|
| 18 |
+
<Link href="/mlops" className="text-ink hover:text-hazard transition-colors">ML.OPS</Link>
|
| 19 |
+
<Link href="/edge" className="text-ink hover:text-hazard transition-colors">EDGE.COMPILER</Link>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
</nav>
|
| 23 |
+
|
| 24 |
+
<div className="max-w-[1000px] mx-auto w-full px-8 py-16 flex-1 flex flex-col">
|
| 25 |
+
<header className="mb-12 border-b-4 border-ink pb-8 flex justify-between items-end">
|
| 26 |
+
<div>
|
| 27 |
+
<div className="text-[12px] text-hazard font-bold tracking-[0.1em] mb-4">
|
| 28 |
+
/// DOCS_AND_SPECS
|
| 29 |
+
</div>
|
| 30 |
+
<h1 className="text-[clamp(3rem,6vw,6rem)]">
|
| 31 |
+
SYSTEM<br />ARCHITECTURE
|
| 32 |
+
</h1>
|
| 33 |
+
</div>
|
| 34 |
+
</header>
|
| 35 |
+
|
| 36 |
+
<div className="border-2 border-ink bg-white p-8 mb-8">
|
| 37 |
+
<h2 className="text-[18px] font-sans font-black tracking-[-0.02em] uppercase text-ink mb-6 border-b-2 border-ink pb-4">
|
| 38 |
+
[ 01 ] Client Plane (Frontend)
|
| 39 |
+
</h2>
|
| 40 |
+
<p className="text-[14px] text-slate uppercase leading-relaxed mb-4">
|
| 41 |
+
Engineered using Next.js 16 (App Router) and React 19. The UI adheres strictly to an Industrial Brutalist aesthetic utilizing hard grids, monochromatic blueprints, and high-contrast hazard states. Native WebSockets handle high-frequency telemetry without HTTP overhead, rendered via hardware-accelerated SVG paths in framer-motion.
|
| 42 |
+
</p>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<div className="border-2 border-ink bg-white p-8 mb-8">
|
| 46 |
+
<h2 className="text-[18px] font-sans font-black tracking-[-0.02em] uppercase text-ink mb-6 border-b-2 border-ink pb-4">
|
| 47 |
+
[ 02 ] Core Plane (Backend)
|
| 48 |
+
</h2>
|
| 49 |
+
<p className="text-[14px] text-slate uppercase leading-relaxed mb-4">
|
| 50 |
+
Built on FastAPI (Python 3.10+) maximizing async throughput. A state-machine driven data simulator generates correlated, volatile telemetry streams representing clinical episodes. Storage employs aiosqlite for non-blocking database writes to track historical classifications without stalling the event loop.
|
| 51 |
+
</p>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div className="border-2 border-ink bg-white p-8 mb-8">
|
| 55 |
+
<h2 className="text-[18px] font-sans font-black tracking-[-0.02em] uppercase text-ink mb-6 border-b-2 border-ink pb-4">
|
| 56 |
+
[ 03 ] Intelligence Pipeline (MLOps)
|
| 57 |
+
</h2>
|
| 58 |
+
<p className="text-[14px] text-slate uppercase leading-relaxed mb-4">
|
| 59 |
+
Live patient data is standardized and imputed using a persistent SimpleImputer and StandardScaler. Clinical precision is achieved via a 5-model soft-voting ensemble. The `/retrain` pipeline allows dynamic CSV uploads, imputes missing data, trains models in background threads, and hot-swaps the EnsembleModel globally with zero downtime.
|
| 60 |
+
</p>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div className="border-2 border-ink bg-white p-8 mb-8">
|
| 64 |
+
<h2 className="text-[18px] font-sans font-black tracking-[-0.02em] uppercase text-ink mb-6 border-b-2 border-ink pb-4">
|
| 65 |
+
[ 04 ] Edge Compilation (TinyML)
|
| 66 |
+
</h2>
|
| 67 |
+
<p className="text-[14px] text-slate uppercase leading-relaxed mb-4">
|
| 68 |
+
The backend transpiles trained SciKit-Learn tree models directly into standalone zero-dependency C-headers. Floating-point thresholds are mathematically quantized to INT8, effectively shrinking the flashed payload by ~75%. Designed to execute directly on MCU SRAM without dynamic memory allocation.
|
| 69 |
+
</p>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<footer className="w-full border-t-2 border-ink bg-white py-6 px-8 mt-auto">
|
| 74 |
+
<div className="max-w-[1400px] mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
|
| 75 |
+
<div className="text-[12px] font-bold tracking-[0.05em] uppercase text-ink">
|
| 76 |
+
© 2026 HEARTFLOW_OS. TELEMETRY_SYS.
|
| 77 |
+
</div>
|
| 78 |
+
<div className="flex items-center gap-8">
|
| 79 |
+
<Link href="/architecture" className="text-[12px] font-bold tracking-[0.05em] uppercase text-hazard border-b border-hazard">ARCHITECTURE.MD</Link>
|
| 80 |
+
<a href={`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/docs`} target="_blank" rel="noopener noreferrer" className="text-[12px] font-bold tracking-[0.05em] uppercase text-ink hover:text-hazard transition-colors">API_DOCS</a>
|
| 81 |
+
<a href="https://github.com/IDKHowToCodeFR/TinyML-Heart-Health-Monitoring-Dashboard" target="_blank" rel="noopener noreferrer" className="text-[12px] font-bold tracking-[0.05em] uppercase text-ink hover:text-hazard transition-colors">GITHUB_REPO</a>
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
</footer>
|
| 85 |
+
</main>
|
| 86 |
+
);
|
| 87 |
+
}
|
frontend/src/app/dashboard/page.tsx
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { motion } from "framer-motion";
|
| 4 |
+
import Link from "next/link";
|
| 5 |
+
import { useTelemetry } from "@/context/TelemetryContext";
|
| 6 |
+
|
| 7 |
+
export default function Dashboard() {
|
| 8 |
+
const { data, stream } = useTelemetry();
|
| 9 |
+
|
| 10 |
+
return (
|
| 11 |
+
<main className="min-h-[100dvh] bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white pb-16">
|
| 12 |
+
{/* Top Nav (Mechanical) */}
|
| 13 |
+
<nav className="h-16 flex items-center px-8 border-b-2 border-ink bg-canvas-cream sticky top-0 z-50">
|
| 14 |
+
<div className="max-w-[1400px] mx-auto w-full flex justify-between items-center">
|
| 15 |
+
<Link href="/" className="font-sans text-[18px] font-black tracking-[-0.04em] uppercase">
|
| 16 |
+
[ HEARTFLOW_OS ]
|
| 17 |
+
</Link>
|
| 18 |
+
<div className="flex gap-8 items-center text-[13px] font-bold tracking-[0.05em] uppercase">
|
| 19 |
+
<Link href="/dashboard" className="text-hazard border-b-2 border-hazard pb-1">SYS.MONITOR</Link>
|
| 20 |
+
<Link href="/simulator" className="text-ink hover:text-hazard transition-colors">AI.SIMULATOR</Link>
|
| 21 |
+
<Link href="/history" className="text-ink hover:text-hazard transition-colors">DATA.LOG</Link>
|
| 22 |
+
<Link href="/mlops" className="text-ink hover:text-hazard transition-colors">ML.OPS</Link>
|
| 23 |
+
<Link href="/edge" className="text-ink hover:text-hazard transition-colors">EDGE.COMPILER</Link>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
</nav>
|
| 27 |
+
|
| 28 |
+
<div className="max-w-[1400px] mx-auto w-full px-8 py-16 flex-1 flex flex-col">
|
| 29 |
+
<header className="mb-12 border-b-4 border-ink pb-8 flex justify-between items-end">
|
| 30 |
+
<div>
|
| 31 |
+
<div className="text-[12px] text-hazard font-bold tracking-[0.1em] mb-4">
|
| 32 |
+
/// TELEMETRY_STREAM_ACTIVE
|
| 33 |
+
</div>
|
| 34 |
+
<h1 className="text-[clamp(3rem,6vw,6rem)]">
|
| 35 |
+
PATIENT<br />TELEMETRY
|
| 36 |
+
</h1>
|
| 37 |
+
</div>
|
| 38 |
+
<div className="text-right hidden md:block">
|
| 39 |
+
<div className="text-[14px] font-bold uppercase tracking-[0.05em] border-2 border-ink px-4 py-2 bg-ink text-canvas-cream">
|
| 40 |
+
STATUS: {data ? "CONNECTED" : "AWAITING SIGNAL"}
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</header>
|
| 44 |
+
|
| 45 |
+
{/* Dashboard Grid - Brutalist Tables */}
|
| 46 |
+
<div className="grid grid-cols-1 bg-ink border-2 border-ink" aria-live="polite">
|
| 47 |
+
|
| 48 |
+
{/* Main Status Panel */}
|
| 49 |
+
<div className="bg-canvas-cream flex flex-col">
|
| 50 |
+
<div className="border-b-2 border-ink p-4 flex justify-between items-center bg-white">
|
| 51 |
+
<h2 className="text-[14px] font-bold tracking-[0.05em]">{'< LIVE_VITALS >'}</h2>
|
| 52 |
+
{data?.prediction ? (
|
| 53 |
+
(() => {
|
| 54 |
+
const getStatusStyles = (label: string) => {
|
| 55 |
+
const normalized = (label || "").toLowerCase();
|
| 56 |
+
if (normalized === 'healthy' || normalized === 'normal') return "bg-[#d1fae5] text-[#065f46] border-[#065f46]";
|
| 57 |
+
if (normalized.includes('heart')) return "bg-[#fef08a] text-[#854d0e] border-[#854d0e]";
|
| 58 |
+
if (normalized.includes('asthma')) return "bg-[#e0f2fe] text-[#075985] border-[#075985]";
|
| 59 |
+
if (normalized.includes('hypertension')) return "bg-[#f3e8ff] text-[#6b21a8] border-[#6b21a8]";
|
| 60 |
+
if (normalized.includes('diabetes')) return "bg-[#ffedd5] text-[#9a3412] border-[#9a3412]";
|
| 61 |
+
return "bg-hazard text-white border-hazard";
|
| 62 |
+
};
|
| 63 |
+
return (
|
| 64 |
+
<div className={`px-4 py-2 text-[14px] font-bold uppercase tracking-[0.05em] border-2 ${getStatusStyles(data.prediction.label)}`}>
|
| 65 |
+
{data.prediction.is_at_risk ? '[!]' : '[OK]'} {data.prediction.label} ({(data.prediction.confidence * 100).toFixed(1)}%)
|
| 66 |
+
</div>
|
| 67 |
+
);
|
| 68 |
+
})()
|
| 69 |
+
) : (
|
| 70 |
+
<div className="px-4 py-2 text-[14px] font-bold tracking-[0.05em] border-2 border-ink text-slate">
|
| 71 |
+
INITIALIZING...
|
| 72 |
+
</div>
|
| 73 |
+
)}
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-px bg-ink border-b-2 border-ink">
|
| 77 |
+
<VitalCard label="HEART_RATE" value={data?.sensor_data?.Heart_Rate ? Math.round(data.sensor_data.Heart_Rate) : "---"} unit="BPM" />
|
| 78 |
+
<VitalCard label="O2_SATURATION" value={data?.sensor_data?.SpO2_Level ? Math.round(data.sensor_data.SpO2_Level) : "---"} unit="%" />
|
| 79 |
+
<VitalCard label="BLOOD_PRESSURE" value={data?.sensor_data ? `${Math.round(data.sensor_data.Systolic_BP)}/${Math.round(data.sensor_data.Diastolic_BP)}` : null} unit="MMHG" />
|
| 80 |
+
<VitalCard label="CORE_TEMP" value={data?.sensor_data?.Body_Temp ? data.sensor_data.Body_Temp.toFixed(1) : "---"} unit="°C" />
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
{/* 2-Column Mini Graphs */}
|
| 84 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-px bg-ink border-b-2 border-ink h-[160px]">
|
| 85 |
+
<div className="bg-white p-3 flex flex-col"><div className="text-[11px] font-bold text-slate tracking-[0.1em] mb-2">HR_STREAM</div><div className="flex-1 min-h-0 relative"><TelemetryGraph stream={stream.hr} min={60} max={150} color="var(--color-ink)" fill="rgba(5,5,5,0.05)" /></div></div>
|
| 86 |
+
<div className="bg-white p-3 flex flex-col"><div className="text-[11px] font-bold text-slate tracking-[0.1em] mb-2">TEMP_STREAM</div><div className="flex-1 min-h-0 relative"><TelemetryGraph stream={stream.temp} min={36.0} max={39.0} color="var(--color-ink)" fill="rgba(5,5,5,0.05)" /></div></div>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
{/* Diagnostic Probabilities */}
|
| 90 |
+
<div className="bg-white flex flex-col min-h-[280px]">
|
| 91 |
+
<div className="border-b-2 border-ink p-3 px-6 flex justify-between items-center bg-canvas-cream">
|
| 92 |
+
<h2 className="text-[12px] font-bold tracking-[0.1em] uppercase">{'< DIAGNOSTIC_PROBABILITIES >'}</h2>
|
| 93 |
+
<div className="text-[11px] font-bold tracking-[0.05em] text-hazard uppercase">CLASS / LIKELIHOOD</div>
|
| 94 |
+
</div>
|
| 95 |
+
<div className="flex-grow p-6 bg-white flex flex-col justify-center gap-6">
|
| 96 |
+
{data?.prediction?.disease_probs ? (
|
| 97 |
+
Object.entries(data.prediction.disease_probs)
|
| 98 |
+
.sort((a, b) => (b[1] as number) - (a[1] as number))
|
| 99 |
+
.map(([disease, prob]) => {
|
| 100 |
+
const pct = ((prob as number) * 100).toFixed(1);
|
| 101 |
+
const isRisk = (prob as number) > 0.4 && disease.toLowerCase() !== 'normal' && disease.toLowerCase() !== 'healthy';
|
| 102 |
+
return (
|
| 103 |
+
<div key={disease} className="flex flex-col gap-2">
|
| 104 |
+
<div className="flex justify-between text-[12px] font-bold uppercase tracking-[0.1em]">
|
| 105 |
+
<span className={isRisk ? "text-hazard drop-shadow-[0_0_2px_rgba(230,25,25,0.5)]" : "text-ink"}>{disease}</span>
|
| 106 |
+
<span className="tabular-nums">{pct}%</span>
|
| 107 |
+
</div>
|
| 108 |
+
<div className="h-6 bg-canvas-cream border-2 border-ink overflow-hidden relative">
|
| 109 |
+
{/* Blueprint tick marks */}
|
| 110 |
+
<div className="absolute inset-0 z-0 opacity-20" style={{ backgroundImage: 'linear-gradient(90deg, var(--ink-black) 1px, transparent 1px)', backgroundSize: '10% 100%' }}></div>
|
| 111 |
+
<motion.div
|
| 112 |
+
className={`h-full relative z-10 ${isRisk ? 'bg-hazard shadow-[0_0_8px_var(--hazard-red)]' : 'bg-ink'}`}
|
| 113 |
+
initial={{ width: "0%" }}
|
| 114 |
+
animate={{ width: `${pct}%` }}
|
| 115 |
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
| 116 |
+
/>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
)
|
| 120 |
+
})
|
| 121 |
+
) : (
|
| 122 |
+
<div className="h-full flex items-center justify-center text-[12px] font-bold text-slate tracking-[0.1em] animate-pulse">
|
| 123 |
+
AWAITING CLASSIFICATION DATA...
|
| 124 |
+
</div>
|
| 125 |
+
)}
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</main>
|
| 133 |
+
);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
function VitalCard({ label, value, unit }: { label: string, value: string | number | null | undefined, unit: string }) {
|
| 137 |
+
return (
|
| 138 |
+
<div className="bg-canvas-cream p-8 flex flex-col hover:bg-white transition-colors cursor-default overflow-hidden">
|
| 139 |
+
<div className="text-[11px] font-bold text-slate tracking-[0.1em] border-b border-ink/20 pb-2 mb-4">
|
| 140 |
+
{label}
|
| 141 |
+
</div>
|
| 142 |
+
<div>
|
| 143 |
+
<div className="font-sans text-[clamp(40px,3.5vw,60px)] font-black tracking-tighter tabular-nums leading-none mb-1 break-all">
|
| 144 |
+
{value !== null && value !== undefined ? value : "---"}
|
| 145 |
+
</div>
|
| 146 |
+
<div className="text-[12px] font-bold tracking-[0.05em] text-hazard">
|
| 147 |
+
[{unit}]
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
function TelemetryGraph({ stream, min, max, color, fill }: { stream: number[], min: number, max: number, color: string, fill: string }) {
|
| 155 |
+
const range = max - min;
|
| 156 |
+
|
| 157 |
+
const points = stream.map((val, i) => {
|
| 158 |
+
const x = (i / (stream.length - 1)) * 100;
|
| 159 |
+
const y = Math.max(0, Math.min(100, 100 - (((val - min) / range) * 100)));
|
| 160 |
+
return `${x},${y}`;
|
| 161 |
+
});
|
| 162 |
+
|
| 163 |
+
const firstY = points[0] ? points[0].split(',')[1] : 100;
|
| 164 |
+
const pathD = `M 0,100 L 0,${firstY} L ${points.join(" L ")} L 100,100 Z`;
|
| 165 |
+
const lineD = `M 0,${firstY} L ${points.join(" L ")}`;
|
| 166 |
+
const lastPoint = points[points.length - 1];
|
| 167 |
+
const lastY = lastPoint ? lastPoint.split(',')[1] : 100;
|
| 168 |
+
|
| 169 |
+
return (
|
| 170 |
+
<div className="w-full h-full absolute inset-0 border-2 border-ink overflow-hidden bg-white group relative">
|
| 171 |
+
{/* Blueprint Grid lines */}
|
| 172 |
+
<div className="absolute inset-0 z-0 opacity-[0.1]" style={{ backgroundImage: 'linear-gradient(var(--ink-black) 1px, transparent 1px), linear-gradient(90deg, var(--ink-black) 1px, transparent 1px)', backgroundSize: '20px 20px' }}></div>
|
| 173 |
+
|
| 174 |
+
{/* Signal trace */}
|
| 175 |
+
<div className="absolute inset-0 z-10 overflow-hidden">
|
| 176 |
+
<svg className="w-full h-full block" viewBox="0 0 100 100" preserveAspectRatio="none">
|
| 177 |
+
{/* Fill Area */}
|
| 178 |
+
<path
|
| 179 |
+
d={pathD}
|
| 180 |
+
fill={fill}
|
| 181 |
+
className="transition-all duration-300 ease-linear"
|
| 182 |
+
/>
|
| 183 |
+
{/* Stroke Line (Glow) */}
|
| 184 |
+
<path
|
| 185 |
+
d={lineD}
|
| 186 |
+
fill="none"
|
| 187 |
+
stroke={color}
|
| 188 |
+
strokeWidth="1.5"
|
| 189 |
+
vectorEffect="non-scaling-stroke"
|
| 190 |
+
className="transition-all duration-300 ease-linear"
|
| 191 |
+
style={{ filter: `drop-shadow(0 0 4px ${color})` }}
|
| 192 |
+
/>
|
| 193 |
+
{/* Leading Dot */}
|
| 194 |
+
<circle
|
| 195 |
+
cx="100"
|
| 196 |
+
cy={lastY}
|
| 197 |
+
r="1.5"
|
| 198 |
+
fill={color}
|
| 199 |
+
className="transition-all duration-300 ease-linear"
|
| 200 |
+
style={{ filter: `drop-shadow(0 0 4px ${color})` }}
|
| 201 |
+
/>
|
| 202 |
+
</svg>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
{/* Sweeping Radar Line */}
|
| 206 |
+
<motion.div
|
| 207 |
+
className="absolute top-0 bottom-0 w-[2px] z-20 opacity-40 pointer-events-none"
|
| 208 |
+
style={{ backgroundColor: color, boxShadow: `0 0 12px 2px ${color}` }}
|
| 209 |
+
initial={{ left: "0%" }}
|
| 210 |
+
animate={{ left: "100%" }}
|
| 211 |
+
transition={{ duration: 1.5, ease: "linear", repeat: Infinity }}
|
| 212 |
+
/>
|
| 213 |
+
|
| 214 |
+
{/* Terminal Scanline Overlay */}
|
| 215 |
+
<div className="absolute inset-0 pointer-events-none z-30 opacity-40 bg-[linear-gradient(transparent_50%,rgba(0,0,0,0.2)_50%)] bg-[length:100%_4px]"></div>
|
| 216 |
+
</div>
|
| 217 |
+
);
|
| 218 |
+
}
|
frontend/src/app/edge/page.tsx
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import Link from "next/link";
|
| 4 |
+
import { useState } from "react";
|
| 5 |
+
|
| 6 |
+
export default function EdgeDeployment() {
|
| 7 |
+
const [model, setModel] = useState("rf");
|
| 8 |
+
const [code, setCode] = useState("");
|
| 9 |
+
const [loading, setLoading] = useState(false);
|
| 10 |
+
const [error, setError] = useState("");
|
| 11 |
+
|
| 12 |
+
const handleExport = async () => {
|
| 13 |
+
setLoading(true);
|
| 14 |
+
setCode("");
|
| 15 |
+
setError("");
|
| 16 |
+
|
| 17 |
+
try {
|
| 18 |
+
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/export_tinyml?model_name=${model}`);
|
| 19 |
+
if (!res.ok) throw new Error("Failed to export model");
|
| 20 |
+
|
| 21 |
+
const data = await res.json();
|
| 22 |
+
if (data.error) throw new Error(data.error);
|
| 23 |
+
|
| 24 |
+
setCode(data.code || data.c_code || "/* No C-code returned */");
|
| 25 |
+
} catch (err: any) {
|
| 26 |
+
setError(err.message || "Unknown error occurred");
|
| 27 |
+
} finally {
|
| 28 |
+
setLoading(false);
|
| 29 |
+
}
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
const handleDownload = () => {
|
| 33 |
+
if (!code) return;
|
| 34 |
+
const blob = new Blob([code], { type: "text/x-csrc" });
|
| 35 |
+
const url = URL.createObjectURL(blob);
|
| 36 |
+
const a = document.createElement("a");
|
| 37 |
+
a.href = url;
|
| 38 |
+
a.download = `model_${model}.h`;
|
| 39 |
+
document.body.appendChild(a);
|
| 40 |
+
a.click();
|
| 41 |
+
document.body.removeChild(a);
|
| 42 |
+
URL.revokeObjectURL(url);
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<main className="min-h-[100dvh] bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white">
|
| 47 |
+
{/* Top Nav (Mechanical) */}
|
| 48 |
+
<nav className="h-16 flex items-center px-8 border-b-2 border-ink bg-canvas-cream sticky top-0 z-50">
|
| 49 |
+
<div className="max-w-[1400px] mx-auto w-full flex justify-between items-center">
|
| 50 |
+
<Link href="/" className="font-sans text-[18px] font-black tracking-[-0.04em] uppercase">
|
| 51 |
+
[ HEARTFLOW_OS ]
|
| 52 |
+
</Link>
|
| 53 |
+
<div className="flex gap-8 items-center text-[13px] font-bold tracking-[0.05em] uppercase">
|
| 54 |
+
<Link href="/dashboard" className="text-ink hover:text-hazard transition-colors">SYS.MONITOR</Link>
|
| 55 |
+
<Link href="/simulator" className="text-ink hover:text-hazard transition-colors">AI.SIMULATOR</Link>
|
| 56 |
+
<Link href="/history" className="text-ink hover:text-hazard transition-colors">DATA.LOG</Link>
|
| 57 |
+
<Link href="/mlops" className="text-ink hover:text-hazard transition-colors">ML.OPS</Link>
|
| 58 |
+
<Link href="/edge" className="text-hazard border-b-2 border-hazard pb-1">EDGE.COMPILER</Link>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
</nav>
|
| 62 |
+
|
| 63 |
+
<div className="max-w-[1400px] mx-auto w-full px-8 py-16 flex-1 flex flex-col">
|
| 64 |
+
<header className="mb-12 border-b-4 border-ink pb-8 flex justify-between items-end">
|
| 65 |
+
<div>
|
| 66 |
+
<div className="text-[12px] text-hazard font-bold tracking-[0.1em] mb-4">
|
| 67 |
+
/// C_COMPILER_TOOLCHAIN
|
| 68 |
+
</div>
|
| 69 |
+
<h1 className="text-[clamp(3rem,6vw,6rem)]">
|
| 70 |
+
EDGE<br />DEPLOYMENT
|
| 71 |
+
</h1>
|
| 72 |
+
</div>
|
| 73 |
+
</header>
|
| 74 |
+
|
| 75 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-px bg-ink border-2 border-ink">
|
| 76 |
+
{/* Config Panel */}
|
| 77 |
+
<div className="lg:col-span-4 bg-white flex flex-col border-b-2 lg:border-b-0 lg:border-r-2 border-ink p-8">
|
| 78 |
+
<div className="border-b-2 border-ink pb-4 mb-6">
|
| 79 |
+
<h2 className="text-[16px] font-bold tracking-[0.05em] uppercase text-ink">{'< BUILD_CONFIG >'}</h2>
|
| 80 |
+
<p className="text-[13px] text-slate uppercase mt-2">Select target architecture.</p>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div className="flex flex-col gap-6">
|
| 84 |
+
<div className="flex flex-col gap-3">
|
| 85 |
+
<label htmlFor="arch-select" className="text-[11px] font-bold uppercase tracking-[0.1em] text-slate">TARGET_MCU</label>
|
| 86 |
+
<select
|
| 87 |
+
id="arch-select"
|
| 88 |
+
className="w-full bg-canvas-cream border-2 border-ink px-4 py-3 text-[14px] font-bold uppercase outline-none focus-visible:bg-ink focus-visible:text-white transition-colors"
|
| 89 |
+
>
|
| 90 |
+
<option value="arm">ARM CORTEX-M4F</option>
|
| 91 |
+
<option value="esp32">ESP32-S3 (XTENSA)</option>
|
| 92 |
+
<option value="rp2040">RP2040 (CORTEX-M0+)</option>
|
| 93 |
+
</select>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<div className="flex flex-col gap-3">
|
| 97 |
+
<label htmlFor="model-select" className="text-[11px] font-bold uppercase tracking-[0.1em] text-slate">SOURCE_MODEL</label>
|
| 98 |
+
<select
|
| 99 |
+
id="model-select"
|
| 100 |
+
value={model}
|
| 101 |
+
onChange={(e) => setModel(e.target.value)}
|
| 102 |
+
className="w-full bg-canvas-cream border-2 border-ink px-4 py-3 text-[14px] font-bold uppercase outline-none focus-visible:bg-ink focus-visible:text-white transition-colors"
|
| 103 |
+
>
|
| 104 |
+
<option value="rf">RANDOM_FOREST (REC)</option>
|
| 105 |
+
<option value="knn">K_NEAREST_NEIGHBORS</option>
|
| 106 |
+
<option value="svm">SUPPORT_VECTOR_MACHINE</option>
|
| 107 |
+
<option value="logreg">LOGISTIC_REGRESSION</option>
|
| 108 |
+
<option value="small_nn">SMALL_NEURAL_NETWORK</option>
|
| 109 |
+
</select>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<div className="flex flex-col gap-3">
|
| 113 |
+
<label htmlFor="opt-select" className="text-[11px] font-bold uppercase tracking-[0.1em] text-slate">OPTIMIZATION_LEVEL</label>
|
| 114 |
+
<select
|
| 115 |
+
id="opt-select"
|
| 116 |
+
className="w-full bg-canvas-cream border-2 border-ink px-4 py-3 text-[14px] font-bold uppercase outline-none focus-visible:bg-ink focus-visible:text-white transition-colors"
|
| 117 |
+
>
|
| 118 |
+
<option value="o3">-O3 (MAX SPEED)</option>
|
| 119 |
+
<option value="os">-OS (MIN SIZE)</option>
|
| 120 |
+
<option value="o0">-O0 (DEBUG)</option>
|
| 121 |
+
</select>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<div className="mt-auto pt-8">
|
| 126 |
+
<button
|
| 127 |
+
onClick={handleExport}
|
| 128 |
+
disabled={loading}
|
| 129 |
+
className="inline-flex items-center justify-center h-16 w-full bg-hazard text-white text-[16px] font-bold uppercase tracking-[0.2em] border-[3px] border-ink hover:translate-y-[2px] hover:translate-x-[2px] shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-y-[4px] active:translate-x-[4px] active:shadow-none transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
| 130 |
+
>
|
| 131 |
+
{loading ? 'COMPILING...' : 'COMPILE TO C >>'}
|
| 132 |
+
</button>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
{/* Output Panel */}
|
| 137 |
+
<div className="lg:col-span-8 bg-canvas-cream flex flex-col p-8 h-[600px]">
|
| 138 |
+
<div className="flex justify-between items-center border-b-2 border-ink pb-4 mb-6">
|
| 139 |
+
<div className="flex flex-col gap-2">
|
| 140 |
+
<h2 className="text-[16px] font-bold tracking-[0.05em] uppercase text-ink flex items-center gap-2">
|
| 141 |
+
{'< OUTPUT_BUFFER >'}
|
| 142 |
+
</h2>
|
| 143 |
+
{code && (
|
| 144 |
+
<div className="flex gap-6 text-[10px] font-bold tracking-[0.15em] text-slate uppercase">
|
| 145 |
+
<span>SIZE: {code.length} B ({(code.length / 1024).toFixed(2)} KB)</span>
|
| 146 |
+
<span>LINES: {code.split('\n').length}</span>
|
| 147 |
+
<span>MD5_CHK: 0x{Array.from(code).reduce((h, c) => Math.imul(31, h) + c.charCodeAt(0) | 0, 0).toString(16).toUpperCase().padStart(8, '0').slice(-8)}</span>
|
| 148 |
+
</div>
|
| 149 |
+
)}
|
| 150 |
+
</div>
|
| 151 |
+
{code && (
|
| 152 |
+
<button
|
| 153 |
+
onClick={handleDownload}
|
| 154 |
+
className="inline-flex items-center px-4 py-2 bg-transparent border-2 border-ink text-ink text-[12px] font-bold uppercase tracking-[0.05em] hover:bg-ink hover:text-white transition-colors"
|
| 155 |
+
>
|
| 156 |
+
[ DOWNLOAD .H ]
|
| 157 |
+
</button>
|
| 158 |
+
)}
|
| 159 |
+
</div>
|
| 160 |
+
|
| 161 |
+
<div className="flex-grow bg-white border-2 border-ink overflow-auto relative p-4">
|
| 162 |
+
{error ? (
|
| 163 |
+
<div className="absolute inset-0 flex items-center justify-center text-hazard font-bold text-[14px] uppercase p-4 text-center">
|
| 164 |
+
[ERR] {error}
|
| 165 |
+
</div>
|
| 166 |
+
) : code ? (
|
| 167 |
+
<pre className="text-[12px] font-mono text-ink leading-[1.6] whitespace-pre" tabIndex={0}>
|
| 168 |
+
{code}
|
| 169 |
+
</pre>
|
| 170 |
+
) : (
|
| 171 |
+
<div className="absolute inset-0 flex items-center justify-center text-slate text-[13px] font-bold uppercase tracking-[0.05em]">
|
| 172 |
+
AWAITING_COMPILATION...
|
| 173 |
+
</div>
|
| 174 |
+
)}
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
</main>
|
| 180 |
+
);
|
| 181 |
+
}
|
frontend/src/app/favicon.ico
ADDED
|
|
frontend/src/app/globals.css
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--canvas-cream: #FBFBFA;
|
| 5 |
+
--ink-black: #141413;
|
| 6 |
+
--slate-gray: #696969;
|
| 7 |
+
--hazard-red: #9F2F2D;
|
| 8 |
+
--white: #FFFFFF;
|
| 9 |
+
--whisper: #EAEAEA;
|
| 10 |
+
--risk-bg: #FDEBEC;
|
| 11 |
+
--healthy-bg: #EDF3EC;
|
| 12 |
+
--healthy-text: #346538;
|
| 13 |
+
--soft-bone: #F4F4F4;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
@theme inline {
|
| 17 |
+
--color-canvas-cream: var(--canvas-cream);
|
| 18 |
+
--color-ink: var(--ink-black);
|
| 19 |
+
--color-slate: var(--slate-gray);
|
| 20 |
+
--color-hazard: var(--hazard-red);
|
| 21 |
+
--color-whisper: var(--whisper);
|
| 22 |
+
--color-risk-bg: var(--risk-bg);
|
| 23 |
+
--color-healthy-bg: var(--healthy-bg);
|
| 24 |
+
--color-healthy-text: var(--healthy-text);
|
| 25 |
+
--color-soft-bone: var(--soft-bone);
|
| 26 |
+
|
| 27 |
+
--font-sans: var(--font-sans);
|
| 28 |
+
--font-mono: var(--font-mono);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
* {
|
| 32 |
+
border-radius: 0 !important;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
body {
|
| 36 |
+
background-color: var(--canvas-cream);
|
| 37 |
+
color: var(--ink-black);
|
| 38 |
+
font-family: var(--font-sans);
|
| 39 |
+
font-weight: 500;
|
| 40 |
+
-webkit-font-smoothing: antialiased;
|
| 41 |
+
-moz-osx-font-smoothing: grayscale;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* Macro Typography Scaling */
|
| 45 |
+
h1 {
|
| 46 |
+
font-family: var(--font-sans);
|
| 47 |
+
font-weight: 800;
|
| 48 |
+
color: var(--ink-black);
|
| 49 |
+
letter-spacing: -0.04em;
|
| 50 |
+
line-height: 0.9;
|
| 51 |
+
text-transform: uppercase;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
h2, h3, h4, h5, h6 {
|
| 55 |
+
font-family: var(--font-sans);
|
| 56 |
+
font-weight: 700;
|
| 57 |
+
color: var(--ink-black);
|
| 58 |
+
letter-spacing: -0.02em;
|
| 59 |
+
text-transform: uppercase;
|
| 60 |
+
}
|
frontend/src/app/history/page.tsx
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import Link from "next/link";
|
| 4 |
+
import { useEffect, useState } from "react";
|
| 5 |
+
|
| 6 |
+
interface HistoryRecord {
|
| 7 |
+
id: number;
|
| 8 |
+
timestamp: string;
|
| 9 |
+
heart_rate: number;
|
| 10 |
+
spo2: number;
|
| 11 |
+
sys_bp: number;
|
| 12 |
+
dia_bp: number;
|
| 13 |
+
body_temp?: number;
|
| 14 |
+
temp?: number;
|
| 15 |
+
prediction?: string;
|
| 16 |
+
prediction_label?: string;
|
| 17 |
+
confidence: number;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export default function PatientHistory() {
|
| 21 |
+
const [records, setRecords] = useState<HistoryRecord[]>([]);
|
| 22 |
+
const [loading, setLoading] = useState(true);
|
| 23 |
+
|
| 24 |
+
useEffect(() => {
|
| 25 |
+
fetch((process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000") + "/history")
|
| 26 |
+
.then((res) => res.json())
|
| 27 |
+
.then((data) => {
|
| 28 |
+
setRecords(data);
|
| 29 |
+
setLoading(false);
|
| 30 |
+
})
|
| 31 |
+
.catch((err) => {
|
| 32 |
+
console.error(err);
|
| 33 |
+
setLoading(false);
|
| 34 |
+
});
|
| 35 |
+
}, []);
|
| 36 |
+
|
| 37 |
+
return (
|
| 38 |
+
<main className="min-h-[100dvh] bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white">
|
| 39 |
+
{/* Top Nav (Mechanical) */}
|
| 40 |
+
<nav className="h-16 flex items-center px-8 border-b-2 border-ink bg-canvas-cream sticky top-0 z-50">
|
| 41 |
+
<div className="max-w-[1400px] mx-auto w-full flex justify-between items-center">
|
| 42 |
+
<Link href="/" className="font-sans text-[18px] font-black tracking-[-0.04em] uppercase">
|
| 43 |
+
[ HEARTFLOW_OS ]
|
| 44 |
+
</Link>
|
| 45 |
+
<div className="flex gap-8 items-center text-[13px] font-bold tracking-[0.05em] uppercase">
|
| 46 |
+
<Link href="/dashboard" className="text-ink hover:text-hazard transition-colors">SYS.MONITOR</Link>
|
| 47 |
+
<Link href="/simulator" className="text-ink hover:text-hazard transition-colors">AI.SIMULATOR</Link>
|
| 48 |
+
<Link href="/history" className="text-hazard border-b-2 border-hazard pb-1">DATA.LOG</Link>
|
| 49 |
+
<Link href="/mlops" className="text-ink hover:text-hazard transition-colors">ML.OPS</Link>
|
| 50 |
+
<Link href="/edge" className="text-ink hover:text-hazard transition-colors">EDGE.COMPILER</Link>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</nav>
|
| 54 |
+
|
| 55 |
+
<div className="max-w-[1400px] mx-auto w-full px-8 py-16 flex-1 flex flex-col">
|
| 56 |
+
<header className="mb-12 border-b-4 border-ink pb-8 flex justify-between items-end">
|
| 57 |
+
<div>
|
| 58 |
+
<div className="text-[12px] text-hazard font-bold tracking-[0.1em] mb-4">
|
| 59 |
+
/// HISTORICAL_RECORDS
|
| 60 |
+
</div>
|
| 61 |
+
<h1 className="text-[clamp(3rem,6vw,6rem)]">
|
| 62 |
+
PREDICTION<br />LOG
|
| 63 |
+
</h1>
|
| 64 |
+
</div>
|
| 65 |
+
<div className="text-right hidden md:block">
|
| 66 |
+
<div className="text-[14px] font-bold uppercase tracking-[0.05em] border-2 border-ink px-4 py-2 bg-ink text-canvas-cream tabular-nums">
|
| 67 |
+
ROWS: {records.length}
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
</header>
|
| 71 |
+
|
| 72 |
+
<div className="border-2 border-ink bg-ink">
|
| 73 |
+
<div className="overflow-x-auto bg-canvas-cream">
|
| 74 |
+
<table className="w-full text-left">
|
| 75 |
+
<thead className="bg-ink text-white font-bold text-[12px] tracking-[0.1em] uppercase">
|
| 76 |
+
<tr>
|
| 77 |
+
<th className="px-6 py-4 border-r-2 border-canvas-cream/20">TIMESTAMP</th>
|
| 78 |
+
<th className="px-6 py-4 border-r-2 border-canvas-cream/20">HR</th>
|
| 79 |
+
<th className="px-6 py-4 border-r-2 border-canvas-cream/20">SPO2</th>
|
| 80 |
+
<th className="px-6 py-4 border-r-2 border-canvas-cream/20">BP</th>
|
| 81 |
+
<th className="px-6 py-4 border-r-2 border-canvas-cream/20">TEMP</th>
|
| 82 |
+
<th className="px-6 py-4 border-r-2 border-canvas-cream/20">STATUS</th>
|
| 83 |
+
<th className="px-6 py-4">CONFIDENCE</th>
|
| 84 |
+
</tr>
|
| 85 |
+
</thead>
|
| 86 |
+
<tbody className="divide-y-2 divide-ink text-ink font-bold text-[13px] uppercase">
|
| 87 |
+
{loading ? (
|
| 88 |
+
<tr>
|
| 89 |
+
<td colSpan={7} className="px-6 py-12 text-center bg-white text-slate">
|
| 90 |
+
QUERYING_DATABASE...
|
| 91 |
+
</td>
|
| 92 |
+
</tr>
|
| 93 |
+
) : records.length === 0 ? (
|
| 94 |
+
<tr>
|
| 95 |
+
<td colSpan={7} className="px-6 py-12 text-center bg-white text-slate">
|
| 96 |
+
NO_RECORDS_FOUND.
|
| 97 |
+
</td>
|
| 98 |
+
</tr>
|
| 99 |
+
) : (
|
| 100 |
+
records.map((record) => {
|
| 101 |
+
const date = new Date(record.timestamp);
|
| 102 |
+
const predLabel = record.prediction_label || record.prediction || "UNKNOWN";
|
| 103 |
+
const isRisk = predLabel.toLowerCase().includes("risk") || predLabel.toLowerCase() !== "healthy";
|
| 104 |
+
const temperature = record.temp || record.body_temp || "—";
|
| 105 |
+
|
| 106 |
+
const getStatusStyles = (label: string) => {
|
| 107 |
+
const normalized = label.toLowerCase();
|
| 108 |
+
if (normalized === 'healthy') return "bg-[#d1fae5] text-[#065f46]"; // Light green
|
| 109 |
+
if (normalized.includes('heart')) return "bg-[#fef08a] text-[#854d0e]"; // Light yellow
|
| 110 |
+
if (normalized.includes('asthma')) return "bg-[#e0f2fe] text-[#075985]"; // Light blue
|
| 111 |
+
if (normalized.includes('hypertension')) return "bg-[#f3e8ff] text-[#6b21a8]"; // Light purple
|
| 112 |
+
if (normalized.includes('diabetes')) return "bg-[#ffedd5] text-[#9a3412]"; // Light orange
|
| 113 |
+
return "bg-hazard/20 text-hazard"; // Default risk red
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
return (
|
| 117 |
+
<tr key={record.id} className="hover:bg-canvas-cream transition-colors bg-white tabular-nums">
|
| 118 |
+
<td className="px-6 py-4 whitespace-nowrap border-r-2 border-ink">
|
| 119 |
+
{date.toLocaleString()}
|
| 120 |
+
</td>
|
| 121 |
+
<td className="px-6 py-4 border-r-2 border-ink">{record.heart_rate}</td>
|
| 122 |
+
<td className="px-6 py-4 border-r-2 border-ink">{record.spo2}</td>
|
| 123 |
+
<td className="px-6 py-4 border-r-2 border-ink">{record.sys_bp}/{record.dia_bp}</td>
|
| 124 |
+
<td className="px-6 py-4 border-r-2 border-ink">{temperature}</td>
|
| 125 |
+
<td className={`px-6 py-4 border-r-2 border-ink ${getStatusStyles(predLabel)}`}>
|
| 126 |
+
{isRisk ? '[!] ' : '✓ '}{predLabel}
|
| 127 |
+
</td>
|
| 128 |
+
<td className="px-6 py-4">
|
| 129 |
+
{(record.confidence * 100).toFixed(1)}%
|
| 130 |
+
</td>
|
| 131 |
+
</tr>
|
| 132 |
+
);
|
| 133 |
+
})
|
| 134 |
+
)}
|
| 135 |
+
</tbody>
|
| 136 |
+
</table>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
</main>
|
| 141 |
+
);
|
| 142 |
+
}
|
frontend/src/app/layout.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
import { TelemetryProvider } from "@/context/TelemetryContext";
|
| 5 |
+
|
| 6 |
+
const geistSans = Geist({
|
| 7 |
+
variable: "--font-sans",
|
| 8 |
+
subsets: ["latin"],
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
const geistMono = Geist_Mono({
|
| 12 |
+
variable: "--font-mono",
|
| 13 |
+
subsets: ["latin"],
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
export const metadata: Metadata = {
|
| 17 |
+
title: "TinyML Heart Health",
|
| 18 |
+
description: "Cardiovascular monitoring dashboard",
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
| 22 |
+
return (
|
| 23 |
+
<html
|
| 24 |
+
lang="en"
|
| 25 |
+
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
| 26 |
+
>
|
| 27 |
+
<body className="min-h-full flex flex-col">
|
| 28 |
+
<TelemetryProvider>
|
| 29 |
+
{children}
|
| 30 |
+
</TelemetryProvider>
|
| 31 |
+
</body>
|
| 32 |
+
</html>
|
| 33 |
+
);
|
| 34 |
+
}
|
frontend/src/app/mlops/page.tsx
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import Link from "next/link";
|
| 4 |
+
import { useState } from "react";
|
| 5 |
+
|
| 6 |
+
export default function MLOps() {
|
| 7 |
+
const [file, setFile] = useState<File | null>(null);
|
| 8 |
+
const [status, setStatus] = useState<"idle" | "uploading" | "success" | "error">("idle");
|
| 9 |
+
const [message, setMessage] = useState("");
|
| 10 |
+
|
| 11 |
+
const handleUpload = async () => {
|
| 12 |
+
if (!file) return;
|
| 13 |
+
setStatus("uploading");
|
| 14 |
+
|
| 15 |
+
const formData = new FormData();
|
| 16 |
+
formData.append("file", file);
|
| 17 |
+
|
| 18 |
+
try {
|
| 19 |
+
const res = await fetch((process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000") + "/retrain", {
|
| 20 |
+
method: "POST",
|
| 21 |
+
body: formData,
|
| 22 |
+
});
|
| 23 |
+
const data = await res.json();
|
| 24 |
+
|
| 25 |
+
if (data.error) {
|
| 26 |
+
setStatus("error");
|
| 27 |
+
setMessage(data.error);
|
| 28 |
+
} else {
|
| 29 |
+
setStatus("success");
|
| 30 |
+
setMessage(data.message);
|
| 31 |
+
}
|
| 32 |
+
} catch (err: any) {
|
| 33 |
+
setStatus("error");
|
| 34 |
+
setMessage(err.message || "Upload failed");
|
| 35 |
+
}
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
return (
|
| 39 |
+
<main className="min-h-[100dvh] bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white">
|
| 40 |
+
{/* Top Nav (Mechanical) */}
|
| 41 |
+
<nav className="h-16 flex items-center px-8 border-b-2 border-ink bg-canvas-cream sticky top-0 z-50">
|
| 42 |
+
<div className="max-w-[1400px] mx-auto w-full flex justify-between items-center">
|
| 43 |
+
<Link href="/" className="font-sans text-[18px] font-black tracking-[-0.04em] uppercase">
|
| 44 |
+
[ HEARTFLOW_OS ]
|
| 45 |
+
</Link>
|
| 46 |
+
<div className="flex gap-8 items-center text-[13px] font-bold tracking-[0.05em] uppercase">
|
| 47 |
+
<Link href="/dashboard" className="text-ink hover:text-hazard transition-colors">SYS.MONITOR</Link>
|
| 48 |
+
<Link href="/simulator" className="text-ink hover:text-hazard transition-colors">AI.SIMULATOR</Link>
|
| 49 |
+
<Link href="/history" className="text-ink hover:text-hazard transition-colors">DATA.LOG</Link>
|
| 50 |
+
<Link href="/mlops" className="text-hazard border-b-2 border-hazard pb-1">ML.OPS</Link>
|
| 51 |
+
<Link href="/edge" className="text-ink hover:text-hazard transition-colors">EDGE.COMPILER</Link>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</nav>
|
| 55 |
+
|
| 56 |
+
<div className="max-w-[1000px] mx-auto w-full px-8 py-16 flex-1 flex flex-col">
|
| 57 |
+
<header className="mb-12 border-b-4 border-ink pb-8 flex justify-between items-end">
|
| 58 |
+
<div>
|
| 59 |
+
<div className="text-[12px] text-hazard font-bold tracking-[0.1em] mb-4">
|
| 60 |
+
/// PIPELINE_CONTROL
|
| 61 |
+
</div>
|
| 62 |
+
<h1 className="text-[clamp(3rem,6vw,6rem)]">
|
| 63 |
+
MODEL<br />OPERATIONS
|
| 64 |
+
</h1>
|
| 65 |
+
</div>
|
| 66 |
+
</header>
|
| 67 |
+
|
| 68 |
+
<div className="border-2 border-ink bg-white p-8">
|
| 69 |
+
<div className="border-b-2 border-ink pb-4 mb-8">
|
| 70 |
+
<h2 className="text-[16px] font-bold tracking-[0.05em] uppercase text-ink">{'< BATCH_RETRAIN >'}</h2>
|
| 71 |
+
<p className="text-[13px] text-slate uppercase mt-2">Append telemetry data and rebuild the edge ensemble.</p>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div className="flex flex-col gap-6">
|
| 75 |
+
<label
|
| 76 |
+
htmlFor="dropzone-file"
|
| 77 |
+
className="flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-ink bg-canvas-cream hover:bg-hazard hover:text-white transition-colors cursor-pointer group"
|
| 78 |
+
>
|
| 79 |
+
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
| 80 |
+
<div className="text-[24px] font-bold mb-4 group-hover:text-white">
|
| 81 |
+
{file ? '[ FILE_SELECTED ]' : '[ SELECT_CSV ]'}
|
| 82 |
+
</div>
|
| 83 |
+
<p className="mb-2 text-[14px] font-bold uppercase">
|
| 84 |
+
{file ? file.name : "CLICK TO UPLOAD DATASET"}
|
| 85 |
+
</p>
|
| 86 |
+
<p className="text-[12px] uppercase opacity-70">REQUIRES MATCHING SCHEMA</p>
|
| 87 |
+
</div>
|
| 88 |
+
<input
|
| 89 |
+
id="dropzone-file"
|
| 90 |
+
type="file"
|
| 91 |
+
className="sr-only"
|
| 92 |
+
accept=".csv"
|
| 93 |
+
onChange={(e) => setFile(e.target.files?.[0] || null)}
|
| 94 |
+
aria-label="Upload CSV Dataset"
|
| 95 |
+
/>
|
| 96 |
+
</label>
|
| 97 |
+
|
| 98 |
+
<div className="flex justify-between items-center border-t-2 border-ink pt-6 mt-2" aria-live="polite">
|
| 99 |
+
<div className="text-[13px] font-bold uppercase tracking-[0.05em]">
|
| 100 |
+
{status === "uploading" && <span className="text-slate">PROCESSING...</span>}
|
| 101 |
+
{status === "error" && <span className="text-hazard">ERR: {message}</span>}
|
| 102 |
+
{status === "success" && <span className="text-ink bg-canvas-cream px-2 py-1 border border-ink">SYS: {message}</span>}
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
<button
|
| 106 |
+
onClick={handleUpload}
|
| 107 |
+
disabled={!file || status === "uploading"}
|
| 108 |
+
className="inline-flex items-center justify-center h-12 px-8 bg-ink text-white text-[14px] font-bold uppercase tracking-[0.1em] border-2 border-ink hover:bg-hazard hover:border-hazard transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
| 109 |
+
>
|
| 110 |
+
EXECUTE >>
|
| 111 |
+
</button>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
</main>
|
| 117 |
+
);
|
| 118 |
+
}
|
frontend/src/app/page.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { motion } from "framer-motion";
|
| 4 |
+
import Link from "next/link";
|
| 5 |
+
|
| 6 |
+
export default function Home() {
|
| 7 |
+
return (
|
| 8 |
+
<main className="min-h-[100dvh] bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white">
|
| 9 |
+
{/* Top Nav (Mechanical) */}
|
| 10 |
+
<nav className="h-16 flex items-center px-8 border-b-2 border-ink bg-canvas-cream sticky top-0 z-50">
|
| 11 |
+
<div className="max-w-[1400px] mx-auto w-full flex justify-between items-center">
|
| 12 |
+
<Link href="/" className="font-sans text-[18px] font-black tracking-[-0.04em] uppercase">
|
| 13 |
+
[ HEARTFLOW_OS ]
|
| 14 |
+
</Link>
|
| 15 |
+
<div className="flex gap-8 items-center text-[13px] font-bold tracking-[0.05em] uppercase hidden md:flex">
|
| 16 |
+
<Link href="/dashboard" className="text-ink hover:text-hazard transition-colors">SYS.MONITOR</Link>
|
| 17 |
+
<Link href="/simulator" className="text-ink hover:text-hazard transition-colors">AI.SIMULATOR</Link>
|
| 18 |
+
<Link href="/history" className="text-ink hover:text-hazard transition-colors">DATA.LOG</Link>
|
| 19 |
+
<Link href="/mlops" className="text-ink hover:text-hazard transition-colors">ML.OPS</Link>
|
| 20 |
+
<Link href="/edge" className="text-ink hover:text-hazard transition-colors">EDGE.COMPILER</Link>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</nav>
|
| 24 |
+
|
| 25 |
+
{/* Hero Section */}
|
| 26 |
+
<section className="max-w-[1400px] mx-auto w-full px-8 pt-32 pb-24 grid grid-cols-1 lg:grid-cols-12 gap-16 flex-1">
|
| 27 |
+
<div className="lg:col-span-7 flex flex-col justify-center">
|
| 28 |
+
<div className="text-[12px] text-hazard font-bold tracking-[0.1em] mb-6 flex items-center gap-4">
|
| 29 |
+
<span>/// MODEL_V2.0</span>
|
| 30 |
+
<span className="w-12 h-px bg-hazard"></span>
|
| 31 |
+
<span>INT8_QUANTIZED</span>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<h1 className="text-[clamp(4rem,8vw,8rem)] mb-8">
|
| 35 |
+
CLINICAL<br />
|
| 36 |
+
INFERENCE<br />
|
| 37 |
+
<span className="text-hazard">AT THE EDGE.</span>
|
| 38 |
+
</h1>
|
| 39 |
+
|
| 40 |
+
<p className="text-[16px] text-ink max-w-lg leading-relaxed uppercase tracking-[0.02em] font-bold border-l-4 border-hazard pl-6 mb-12">
|
| 41 |
+
An ensemble intelligence pipeline engineered for resource-constrained microcontrollers. Deploy robust cardiovascular telemetry analysis directly to the silicon.
|
| 42 |
+
</p>
|
| 43 |
+
|
| 44 |
+
<div className="flex flex-wrap items-center gap-4">
|
| 45 |
+
<Link
|
| 46 |
+
href="/dashboard"
|
| 47 |
+
className="inline-flex items-center justify-center h-12 px-8 bg-ink text-white text-[14px] font-bold uppercase tracking-[0.1em] border-2 border-ink hover:bg-hazard hover:border-hazard transition-colors"
|
| 48 |
+
>
|
| 49 |
+
[ INITIATE_STREAM ]
|
| 50 |
+
</Link>
|
| 51 |
+
<Link
|
| 52 |
+
href="/edge"
|
| 53 |
+
className="inline-flex items-center justify-center h-12 px-8 bg-transparent text-ink text-[14px] font-bold uppercase tracking-[0.1em] border-2 border-ink hover:bg-ink hover:text-white transition-colors"
|
| 54 |
+
>
|
| 55 |
+
C_COMPILER >>
|
| 56 |
+
</Link>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
{/* Feature Grid */}
|
| 61 |
+
<div className="lg:col-span-5 grid grid-cols-1 gap-px bg-ink border-2 border-ink mt-8 lg:mt-0">
|
| 62 |
+
<div className="bg-white p-8 flex flex-col justify-between">
|
| 63 |
+
<div className="text-[10px] text-slate font-bold tracking-[0.15em] mb-4">FEAT.01</div>
|
| 64 |
+
<div>
|
| 65 |
+
<h3 className="font-sans text-[24px] font-black uppercase tracking-[-0.02em] mb-2 text-ink">
|
| 66 |
+
INT8 Quantization
|
| 67 |
+
</h3>
|
| 68 |
+
<p className="text-[13px] text-slate leading-relaxed uppercase">
|
| 69 |
+
Weights scaled to 8-bit integers. 75% payload reduction for ESP32 constraint matrices.
|
| 70 |
+
</p>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div className="bg-canvas-cream p-8 flex flex-col justify-between">
|
| 75 |
+
<div className="text-[10px] text-slate font-bold tracking-[0.15em] mb-4">FEAT.02</div>
|
| 76 |
+
<div>
|
| 77 |
+
<h3 className="font-sans text-[24px] font-black uppercase tracking-[-0.02em] mb-2 text-ink">
|
| 78 |
+
Ensemble Architecture
|
| 79 |
+
</h3>
|
| 80 |
+
<p className="text-[13px] text-slate leading-relaxed uppercase">
|
| 81 |
+
RF, KNN, and SVM combined with a meta-classifier for clinical-grade precision.
|
| 82 |
+
</p>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<div className="bg-white p-8 flex flex-col justify-between">
|
| 87 |
+
<div className="text-[10px] text-slate font-bold tracking-[0.15em] mb-4">FEAT.03</div>
|
| 88 |
+
<div>
|
| 89 |
+
<h3 className="font-sans text-[24px] font-black uppercase tracking-[-0.02em] mb-2 text-ink">
|
| 90 |
+
Zero-Dep Export
|
| 91 |
+
</h3>
|
| 92 |
+
<p className="text-[13px] text-slate leading-relaxed uppercase">
|
| 93 |
+
Compile trained models directly to standalone C headers. Zero dynamic allocation.
|
| 94 |
+
</p>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</section>
|
| 99 |
+
|
| 100 |
+
{/* Minimal Footer */}
|
| 101 |
+
<footer className="w-full border-t-2 border-ink bg-white py-6 px-8">
|
| 102 |
+
<div className="max-w-[1400px] mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
|
| 103 |
+
<div className="text-[12px] font-bold tracking-[0.05em] uppercase text-ink">
|
| 104 |
+
© 2026 HEARTFLOW_OS. TELEMETRY_SYS.
|
| 105 |
+
</div>
|
| 106 |
+
<div className="flex items-center gap-8">
|
| 107 |
+
<Link href="/architecture" className="text-[12px] font-bold tracking-[0.05em] uppercase text-ink hover:text-hazard transition-colors">ARCHITECTURE.MD</Link>
|
| 108 |
+
<a href={`${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/docs`} target="_blank" rel="noopener noreferrer" className="text-[12px] font-bold tracking-[0.05em] uppercase text-ink hover:text-hazard transition-colors">API_DOCS</a>
|
| 109 |
+
<a href="https://github.com/IDKHowToCodeFR/TinyML-Heart-Health-Monitoring-Dashboard" target="_blank" rel="noopener noreferrer" className="text-[12px] font-bold tracking-[0.05em] uppercase text-ink hover:text-hazard transition-colors">GITHUB_REPO</a>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
</footer>
|
| 113 |
+
</main>
|
| 114 |
+
);
|
| 115 |
+
}
|
frontend/src/app/simulator/page.tsx
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { motion } from "framer-motion";
|
| 4 |
+
import Link from "next/link";
|
| 5 |
+
import { useState, useEffect } from "react";
|
| 6 |
+
import ShapChart from "@/components/ShapChart";
|
| 7 |
+
|
| 8 |
+
export default function Analysis() {
|
| 9 |
+
const [formData, setFormData] = useState({
|
| 10 |
+
Heart_Rate: 75,
|
| 11 |
+
SpO2_Level: 98,
|
| 12 |
+
Systolic_BP: 120,
|
| 13 |
+
Diastolic_BP: 80,
|
| 14 |
+
Body_Temp: 37.0
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
setFormData({
|
| 19 |
+
Heart_Rate: Math.floor(Math.random() * (140 - 60 + 1)) + 60,
|
| 20 |
+
SpO2_Level: Math.floor(Math.random() * (100 - 90 + 1)) + 90,
|
| 21 |
+
Systolic_BP: Math.floor(Math.random() * (160 - 90 + 1)) + 90,
|
| 22 |
+
Diastolic_BP: Math.floor(Math.random() * (100 - 60 + 1)) + 60,
|
| 23 |
+
Body_Temp: Number((Math.random() * (39.5 - 36.0) + 36.0).toFixed(1))
|
| 24 |
+
});
|
| 25 |
+
}, []);
|
| 26 |
+
|
| 27 |
+
const [submittedData, setSubmittedData] = useState<any>(null);
|
| 28 |
+
const [prediction, setPrediction] = useState<any>(null);
|
| 29 |
+
const [loading, setLoading] = useState(false);
|
| 30 |
+
|
| 31 |
+
const handlePredict = async () => {
|
| 32 |
+
setLoading(true);
|
| 33 |
+
setPrediction(null);
|
| 34 |
+
setSubmittedData(null);
|
| 35 |
+
|
| 36 |
+
try {
|
| 37 |
+
const res = await fetch((process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000") + "/predict", {
|
| 38 |
+
method: "POST",
|
| 39 |
+
headers: { "Content-Type": "application/json" },
|
| 40 |
+
body: JSON.stringify(formData)
|
| 41 |
+
});
|
| 42 |
+
const data = await res.json();
|
| 43 |
+
setPrediction(data);
|
| 44 |
+
setSubmittedData(formData);
|
| 45 |
+
} catch (err) {
|
| 46 |
+
console.error(err);
|
| 47 |
+
} finally {
|
| 48 |
+
setLoading(false);
|
| 49 |
+
}
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
return (
|
| 53 |
+
<main className="min-h-[100dvh] bg-canvas-cream flex flex-col font-mono selection:bg-hazard selection:text-white pb-16">
|
| 54 |
+
{/* Top Nav (Mechanical) */}
|
| 55 |
+
<nav className="h-16 flex items-center px-8 border-b-2 border-ink bg-canvas-cream sticky top-0 z-50">
|
| 56 |
+
<div className="max-w-[1400px] mx-auto w-full flex justify-between items-center">
|
| 57 |
+
<Link href="/" className="font-sans text-[18px] font-black tracking-[-0.04em] uppercase">
|
| 58 |
+
[ HEARTFLOW_OS ]
|
| 59 |
+
</Link>
|
| 60 |
+
<div className="flex gap-8 items-center text-[13px] font-bold tracking-[0.05em] uppercase">
|
| 61 |
+
<Link href="/dashboard" className="text-ink hover:text-hazard transition-colors">SYS.MONITOR</Link>
|
| 62 |
+
<Link href="/simulator" className="text-hazard border-b-2 border-hazard pb-1">AI.SIMULATOR</Link>
|
| 63 |
+
<Link href="/history" className="text-ink hover:text-hazard transition-colors">DATA.LOG</Link>
|
| 64 |
+
<Link href="/mlops" className="text-ink hover:text-hazard transition-colors">ML.OPS</Link>
|
| 65 |
+
<Link href="/edge" className="text-ink hover:text-hazard transition-colors">EDGE.COMPILER</Link>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</nav>
|
| 69 |
+
|
| 70 |
+
<div className="max-w-[1400px] mx-auto w-full px-8 py-16 flex-1 flex flex-col">
|
| 71 |
+
<header className="mb-12 border-b-4 border-ink pb-8 flex justify-between items-end">
|
| 72 |
+
<div>
|
| 73 |
+
<div className="text-[12px] text-hazard font-bold tracking-[0.1em] mb-4">
|
| 74 |
+
/// EXPLAINABLE_AI_MODULE
|
| 75 |
+
</div>
|
| 76 |
+
<h1 className="text-[clamp(3rem,6vw,6rem)]">
|
| 77 |
+
MODEL<br />SIMULATOR
|
| 78 |
+
</h1>
|
| 79 |
+
</div>
|
| 80 |
+
<div className="text-right hidden md:block max-w-sm">
|
| 81 |
+
<div className="text-[14px] font-bold uppercase tracking-[0.05em] border-2 border-ink px-4 py-2 bg-ink text-canvas-cream text-left">
|
| 82 |
+
MANUAL OVERRIDE. INPUT CUSTOM TELEMETRY TO TEST ENSEMBLE CLASSIFIER AND VIEW SHAPLEY FEATURE IMPORTANCE VECTORS.
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
</header>
|
| 86 |
+
|
| 87 |
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-px bg-ink border-2 border-ink">
|
| 88 |
+
|
| 89 |
+
{/* Input Panel */}
|
| 90 |
+
<div className="lg:col-span-5 bg-canvas-cream flex flex-col">
|
| 91 |
+
<div className="border-b-2 border-ink p-4 bg-white flex justify-between items-center">
|
| 92 |
+
<h2 className="text-[14px] font-bold tracking-[0.05em]">{'< MANUAL_INPUT_MATRIX >'}</h2>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div className="p-8 flex flex-col gap-8 flex-grow">
|
| 96 |
+
<InputControl label="HEART_RATE (BPM)" value={formData.Heart_Rate} min={40} max={200} onChange={(v: number) => setFormData({...formData, Heart_Rate: v})} />
|
| 97 |
+
<InputControl label="SPO2_LEVEL (%)" value={formData.SpO2_Level} min={70} max={100} onChange={(v: number) => setFormData({...formData, SpO2_Level: v})} />
|
| 98 |
+
<InputControl label="SYSTOLIC_BP (MMHG)" value={formData.Systolic_BP} min={80} max={220} onChange={(v: number) => setFormData({...formData, Systolic_BP: v})} />
|
| 99 |
+
<InputControl label="DIASTOLIC_BP (MMHG)" value={formData.Diastolic_BP} min={40} max={140} onChange={(v: number) => setFormData({...formData, Diastolic_BP: v})} />
|
| 100 |
+
<InputControl label="BODY_TEMP (°C)" value={formData.Body_Temp} min={30} max={42} step={0.1} onChange={(v: number) => setFormData({...formData, Body_Temp: v})} />
|
| 101 |
+
|
| 102 |
+
<button
|
| 103 |
+
onClick={handlePredict}
|
| 104 |
+
disabled={loading}
|
| 105 |
+
className="mt-6 border-[3px] border-ink bg-hazard text-white py-6 font-bold text-[18px] tracking-[0.2em] shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] hover:translate-y-[2px] hover:translate-x-[2px] hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] active:translate-y-[6px] active:translate-x-[6px] active:shadow-none transition-all disabled:opacity-50 disabled:cursor-not-allowed uppercase"
|
| 106 |
+
>
|
| 107 |
+
{loading ? 'PROCESSING_REQUEST...' : '[ EXECUTE_INFERENCE ]'}
|
| 108 |
+
</button>
|
| 109 |
+
|
| 110 |
+
{/* Terminal Readout to fill empty space */}
|
| 111 |
+
<div className="mt-8 flex-grow border-[3px] border-ink bg-ink text-[#4AF626] p-0 flex flex-col min-h-[150px] shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] relative overflow-hidden group">
|
| 112 |
+
{/* CRT Scanline Overlay */}
|
| 113 |
+
<div className="absolute inset-0 pointer-events-none opacity-10 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(255,255,255,0.1)_2px,rgba(255,255,255,0.1)_4px)] z-10" />
|
| 114 |
+
|
| 115 |
+
<div className="text-[10px] font-bold tracking-[0.2em] text-ink bg-white border-b-[3px] border-ink px-4 py-3 flex justify-between uppercase">
|
| 116 |
+
<span>RAW_PAYLOAD_PREVIEW</span>
|
| 117 |
+
<span>{JSON.stringify(formData).length} BYTES // SYS_READY</span>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<div className="p-6 flex-grow flex flex-col relative z-0">
|
| 121 |
+
<pre className="text-[12px] font-mono whitespace-pre-wrap flex-grow overflow-auto leading-relaxed">
|
| 122 |
+
{`{
|
| 123 |
+
"Heart_Rate": ${formData.Heart_Rate},
|
| 124 |
+
"SpO2_Level": ${formData.SpO2_Level},
|
| 125 |
+
"Systolic_BP": ${formData.Systolic_BP},
|
| 126 |
+
"Diastolic_BP": ${formData.Diastolic_BP},
|
| 127 |
+
"Body_Temp": ${formData.Body_Temp.toFixed(1)}
|
| 128 |
+
}`}
|
| 129 |
+
<span className="inline-block w-2 h-4 bg-[#4AF626] animate-pulse ml-1 align-middle" />
|
| 130 |
+
</pre>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
{/* Results Panel */}
|
| 137 |
+
<div className="lg:col-span-7 bg-canvas-cream flex flex-col">
|
| 138 |
+
<div className="border-b-2 border-ink p-4 bg-white flex justify-between items-center">
|
| 139 |
+
<h2 className="text-[14px] font-bold tracking-[0.05em]">{'< INFERENCE_RESULTS_&_XAI >'}</h2>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<div className="flex-grow flex flex-col relative bg-white">
|
| 143 |
+
{/* Blueprint Grid Background */}
|
| 144 |
+
<div className="absolute inset-0 z-0 opacity-10 pointer-events-none" style={{ backgroundImage: 'linear-gradient(var(--ink-black) 1px, transparent 1px), linear-gradient(90deg, var(--ink-black) 1px, transparent 1px)', backgroundSize: '40px 40px' }}></div>
|
| 145 |
+
|
| 146 |
+
<div className="z-10 p-8 flex flex-col h-full">
|
| 147 |
+
{prediction ? (
|
| 148 |
+
<>
|
| 149 |
+
<div className="mb-12">
|
| 150 |
+
<div className="text-[11px] font-bold text-slate tracking-[0.1em] mb-4">MODEL_OUTPUT</div>
|
| 151 |
+
|
| 152 |
+
{(() => {
|
| 153 |
+
const getStatusStyles = (label: string) => {
|
| 154 |
+
const normalized = (label || "").toLowerCase();
|
| 155 |
+
if (normalized === 'healthy' || normalized === 'normal') return "bg-[#d1fae5] text-[#065f46] border-[#065f46]";
|
| 156 |
+
if (normalized.includes('heart')) return "bg-[#fef08a] text-[#854d0e] border-[#854d0e]";
|
| 157 |
+
if (normalized.includes('asthma')) return "bg-[#e0f2fe] text-[#075985] border-[#075985]";
|
| 158 |
+
if (normalized.includes('hypertension')) return "bg-[#f3e8ff] text-[#6b21a8] border-[#6b21a8]";
|
| 159 |
+
if (normalized.includes('diabetes')) return "bg-[#ffedd5] text-[#9a3412] border-[#9a3412]";
|
| 160 |
+
return "bg-hazard text-white border-hazard";
|
| 161 |
+
};
|
| 162 |
+
|
| 163 |
+
return (
|
| 164 |
+
<div className={`border-2 p-6 flex flex-col mb-4 ${getStatusStyles(prediction.prediction === 1 ? prediction.prediction_label : 'Healthy')}`}>
|
| 165 |
+
<div className="font-sans text-[clamp(28px,3vw,56px)] font-black tracking-tighter leading-none mb-2 uppercase break-words">
|
| 166 |
+
{prediction.prediction === 1 ? `[!]_${prediction.prediction_label.replace(" ", "_")}` : 'NORMAL'}
|
| 167 |
+
</div>
|
| 168 |
+
<div className="text-[14px] font-bold tracking-[0.1em]">
|
| 169 |
+
CONFIDENCE: {(prediction.probability * 100).toFixed(1)}%
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
);
|
| 173 |
+
})()}
|
| 174 |
+
|
| 175 |
+
{/* Ensemble Diagnostics */}
|
| 176 |
+
{prediction.model_outputs && (
|
| 177 |
+
<div className="border-2 border-ink bg-white flex flex-col text-[12px] font-bold uppercase tracking-[0.05em]">
|
| 178 |
+
<div className="border-b-2 border-ink px-4 py-2 bg-ink text-canvas-cream flex justify-between">
|
| 179 |
+
<span>{'< ENSEMBLE_STACK_DIAGNOSTICS >'}</span>
|
| 180 |
+
<span className="text-hazard">WEIGHTED_VOTING</span>
|
| 181 |
+
</div>
|
| 182 |
+
<div className="divide-y-2 divide-ink">
|
| 183 |
+
{Object.keys(prediction.model_outputs).map(modelName => (
|
| 184 |
+
<div key={modelName} className="flex justify-between items-center p-3 hover:bg-canvas-cream transition-colors">
|
| 185 |
+
<div className="w-[20%] text-slate">[{modelName}]</div>
|
| 186 |
+
<div className={`w-[40%] ${prediction.model_outputs[modelName] !== 'Healthy' ? 'text-hazard' : ''}`}>
|
| 187 |
+
{prediction.model_outputs[modelName] !== 'Healthy' ? '! ' + prediction.model_outputs[modelName] : 'OK Healthy'}
|
| 188 |
+
</div>
|
| 189 |
+
<div className="w-[20%] text-right">
|
| 190 |
+
CONF: {(prediction.model_probs[modelName] * 100).toFixed(1)}%
|
| 191 |
+
</div>
|
| 192 |
+
<div className="w-[20%] text-right text-slate">
|
| 193 |
+
W: {prediction.weights[modelName].toFixed(2)}
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
))}
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
)}
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<div className="flex-1 border-2 border-ink bg-white p-8 flex flex-col min-h-[300px]">
|
| 203 |
+
<div className="text-[11px] font-bold text-slate tracking-[0.1em] mb-6 border-b-2 border-ink pb-2">SHAPLEY_ADDITIVE_EXPLANATIONS</div>
|
| 204 |
+
<div className="flex-1">
|
| 205 |
+
<ShapChart patientData={submittedData} />
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
</>
|
| 209 |
+
) : (
|
| 210 |
+
<div className="h-full flex items-center justify-center">
|
| 211 |
+
<div className="text-slate text-[14px] font-bold tracking-[0.1em] uppercase border-2 border-slate px-6 py-4">
|
| 212 |
+
AWAITING_INPUT...
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
)}
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
</main>
|
| 223 |
+
);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
function InputControl({ label, value, min, max, step = 1, onChange }: any) {
|
| 227 |
+
return (
|
| 228 |
+
<div className="flex flex-col gap-3">
|
| 229 |
+
<div className="flex justify-between items-end">
|
| 230 |
+
<label className="text-[13px] font-bold text-slate tracking-[0.1em]">{label}</label>
|
| 231 |
+
<span className="font-sans text-[32px] font-black leading-none tabular-nums tracking-tighter">{value}</span>
|
| 232 |
+
</div>
|
| 233 |
+
<div className="relative border-2 border-ink p-1 bg-white">
|
| 234 |
+
<input
|
| 235 |
+
type="range"
|
| 236 |
+
min={min}
|
| 237 |
+
max={max}
|
| 238 |
+
step={step}
|
| 239 |
+
value={value}
|
| 240 |
+
onChange={(e) => onChange(Number(e.target.value))}
|
| 241 |
+
className="w-full appearance-none h-4 bg-ink/10 outline-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-6 [&::-webkit-slider-thumb]:h-8 [&::-webkit-slider-thumb]:bg-ink [&::-webkit-slider-thumb]:cursor-pointer relative z-10 block"
|
| 242 |
+
/>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
);
|
| 246 |
+
}
|
frontend/src/components/ShapChart.tsx
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { motion } from "framer-motion";
|
| 4 |
+
import { useEffect, useState } from "react";
|
| 5 |
+
import { Loader2 } from "lucide-react";
|
| 6 |
+
|
| 7 |
+
export default function ShapChart({ patientData }: { patientData: any }) {
|
| 8 |
+
const [shapData, setShapData] = useState<{feature_names: string[], shap_values: number[]} | null>(null);
|
| 9 |
+
const [loading, setLoading] = useState(false);
|
| 10 |
+
|
| 11 |
+
useEffect(() => {
|
| 12 |
+
if (!patientData) return;
|
| 13 |
+
|
| 14 |
+
const fetchExplain = async () => {
|
| 15 |
+
setLoading(true);
|
| 16 |
+
try {
|
| 17 |
+
const res = await fetch((process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000") + "/explain", {
|
| 18 |
+
method: "POST",
|
| 19 |
+
headers: { "Content-Type": "application/json" },
|
| 20 |
+
body: JSON.stringify(patientData)
|
| 21 |
+
});
|
| 22 |
+
const data = await res.json();
|
| 23 |
+
if (data.shap_values) {
|
| 24 |
+
setShapData(data);
|
| 25 |
+
}
|
| 26 |
+
} catch (err) {
|
| 27 |
+
console.error("SHAP fetch error", err);
|
| 28 |
+
} finally {
|
| 29 |
+
setLoading(false);
|
| 30 |
+
}
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
const timeout = setTimeout(fetchExplain, 2500);
|
| 34 |
+
return () => clearTimeout(timeout);
|
| 35 |
+
}, [patientData]);
|
| 36 |
+
|
| 37 |
+
if (!shapData) {
|
| 38 |
+
return (
|
| 39 |
+
<div className="h-full w-full flex flex-col items-center justify-center gap-3">
|
| 40 |
+
{loading ? (
|
| 41 |
+
<>
|
| 42 |
+
<Loader2 className="animate-spin text-ink" size={20} />
|
| 43 |
+
<span className="text-slate text-[12px] font-bold uppercase tracking-[0.1em]">COMPUTING_SHAP_VECTORS...</span>
|
| 44 |
+
</>
|
| 45 |
+
) : (
|
| 46 |
+
<span className="text-slate text-[12px] font-bold uppercase tracking-[0.1em]">AWAITING_TELEMETRY...</span>
|
| 47 |
+
)}
|
| 48 |
+
</div>
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Sort by absolute magnitude of SHAP value
|
| 53 |
+
const items = shapData.feature_names.map((name, i) => ({
|
| 54 |
+
name: name.toUpperCase(),
|
| 55 |
+
value: shapData.shap_values[i],
|
| 56 |
+
abs: Math.abs(shapData.shap_values[i])
|
| 57 |
+
})).sort((a, b) => b.abs - a.abs); // show ALL features
|
| 58 |
+
|
| 59 |
+
const maxAbs = Math.max(...items.map(i => i.abs), 0.1);
|
| 60 |
+
|
| 61 |
+
return (
|
| 62 |
+
<div className="flex flex-col gap-4 w-full h-full justify-center">
|
| 63 |
+
{items.map((item, idx) => {
|
| 64 |
+
const isPositive = item.value > 0;
|
| 65 |
+
const widthPercent = (item.abs / maxAbs) * 50;
|
| 66 |
+
|
| 67 |
+
return (
|
| 68 |
+
<div key={item.name} className="flex flex-col gap-2">
|
| 69 |
+
<div className="flex justify-between text-[11px] font-bold tracking-[0.1em]">
|
| 70 |
+
<span className="text-ink">{item.name}</span>
|
| 71 |
+
<span className={isPositive ? "text-hazard" : "text-ink"}>
|
| 72 |
+
{item.value > 0 ? "+" : ""}{item.value.toFixed(3)}
|
| 73 |
+
</span>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
{/* Brutalist Chart Bar */}
|
| 77 |
+
<div className="relative h-4 w-full bg-white border-2 border-ink flex items-center">
|
| 78 |
+
{/* Midline (zero axis) */}
|
| 79 |
+
<div className="absolute left-1/2 top-[-2px] bottom-[-2px] w-0.5 bg-ink z-10" />
|
| 80 |
+
|
| 81 |
+
{/* Animated Value Bar */}
|
| 82 |
+
<motion.div
|
| 83 |
+
initial={{ width: 0, opacity: 0 }}
|
| 84 |
+
animate={{
|
| 85 |
+
width: `${widthPercent}%`,
|
| 86 |
+
opacity: 1,
|
| 87 |
+
}}
|
| 88 |
+
transition={{ type: "spring", stiffness: 60, damping: 15 }}
|
| 89 |
+
className={`absolute h-full ${isPositive ? 'bg-hazard border-x-2 border-hazard' : 'bg-ink border-x-2 border-ink'}`}
|
| 90 |
+
style={{
|
| 91 |
+
left: isPositive ? '50%' : 'auto',
|
| 92 |
+
right: !isPositive ? '50%' : 'auto'
|
| 93 |
+
}}
|
| 94 |
+
/>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
);
|
| 98 |
+
})}
|
| 99 |
+
</div>
|
| 100 |
+
);
|
| 101 |
+
}
|
frontend/src/context/TelemetryContext.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { createContext, useContext, useEffect, useState } from "react";
|
| 4 |
+
|
| 5 |
+
type StreamState = {
|
| 6 |
+
hr: number[];
|
| 7 |
+
temp: number[];
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
type TelemetryContextType = {
|
| 11 |
+
data: any;
|
| 12 |
+
stream: StreamState;
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
const TelemetryContext = createContext<TelemetryContextType | null>(null);
|
| 16 |
+
|
| 17 |
+
export function TelemetryProvider({ children }: { children: React.ReactNode }) {
|
| 18 |
+
const [data, setData] = useState<any>(null);
|
| 19 |
+
const [stream, setStream] = useState<StreamState>({
|
| 20 |
+
hr: Array(50).fill(75),
|
| 21 |
+
temp: Array(50).fill(37.0)
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
useEffect(() => {
|
| 25 |
+
const ws = new WebSocket(process.env.NEXT_PUBLIC_WS_URL || "ws://localhost:8000/ws/feed");
|
| 26 |
+
ws.onmessage = (event) => {
|
| 27 |
+
const msg = JSON.parse(event.data);
|
| 28 |
+
setData(msg);
|
| 29 |
+
if (msg?.sensor_data) {
|
| 30 |
+
setStream(prev => ({
|
| 31 |
+
hr: [...prev.hr.slice(1), msg.sensor_data.Heart_Rate],
|
| 32 |
+
temp: [...prev.temp.slice(1), msg.sensor_data.Body_Temp]
|
| 33 |
+
}));
|
| 34 |
+
}
|
| 35 |
+
};
|
| 36 |
+
return () => ws.close();
|
| 37 |
+
}, []);
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<TelemetryContext.Provider value={{ data, stream }}>
|
| 41 |
+
{children}
|
| 42 |
+
</TelemetryContext.Provider>
|
| 43 |
+
);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export function useTelemetry() {
|
| 47 |
+
const context = useContext(TelemetryContext);
|
| 48 |
+
if (!context) {
|
| 49 |
+
throw new Error("useTelemetry must be used within a TelemetryProvider");
|
| 50 |
+
}
|
| 51 |
+
return context;
|
| 52 |
+
}
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2017",
|
| 4 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
| 5 |
+
"allowJs": true,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noEmit": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"module": "esnext",
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"jsx": "react-jsx",
|
| 15 |
+
"incremental": true,
|
| 16 |
+
"plugins": [
|
| 17 |
+
{
|
| 18 |
+
"name": "next"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": ["./src/*"]
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"include": [
|
| 26 |
+
"next-env.d.ts",
|
| 27 |
+
"**/*.ts",
|
| 28 |
+
"**/*.tsx",
|
| 29 |
+
".next/types/**/*.ts",
|
| 30 |
+
".next/dev/types/**/*.ts",
|
| 31 |
+
"**/*.mts"
|
| 32 |
+
],
|
| 33 |
+
"exclude": ["node_modules"]
|
| 34 |
+
}
|
model/classes.pkl
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 440
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:43241923778ab2ba4ee472ae79697c0730f7f325bf9d6607aea5d988fea6d7ab
|
| 3 |
size 440
|
model/knn.pkl
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:57cf9edd90be13d04cd8a419486598f4b2ca2f2d417a584aa600d9fd7ce6647e
|
| 3 |
+
size 5639846
|
model/label_encoder.pkl
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 543
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7d8b08e681c302b1d80cba57ab678d752a3608e7787fe7fcf82c4052c5eef91e
|
| 3 |
size 543
|