IDKHowToCodeFr commited on
Commit
4cea933
·
verified ·
1 Parent(s): ed77767

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +25 -0
  3. ARCHITECTURE.md +98 -0
  4. DESIGN.md +55 -0
  5. Dockerfile +26 -8
  6. README.md +64 -7
  7. backend/Dockerfile +13 -3
  8. backend/alerts.log +20 -0
  9. backend/config.py +11 -0
  10. backend/database.py +84 -95
  11. backend/ensemble.py +6 -1
  12. backend/export.py +328 -0
  13. backend/inference.py +45 -0
  14. backend/main.py +122 -382
  15. backend/models.py +2 -2
  16. backend/patient_history.db +0 -0
  17. backend/preprocessing.py +15 -5
  18. backend/requirements.txt +12 -9
  19. backend/simulator.py +57 -0
  20. backend/tests/test_api.py +29 -0
  21. docker-compose.yml +15 -9
  22. frontend/Dockerfile +17 -8
  23. frontend/README.md +36 -0
  24. frontend/eslint.config.mjs +18 -0
  25. frontend/next.config.ts +11 -0
  26. frontend/package-lock.json +0 -0
  27. frontend/package.json +28 -0
  28. frontend/postcss.config.mjs +7 -0
  29. frontend/public/file.svg +1 -0
  30. frontend/public/globe.svg +1 -0
  31. frontend/public/next.svg +1 -0
  32. frontend/public/vercel.svg +1 -0
  33. frontend/public/window.svg +1 -0
  34. frontend/src/app/analytics/page.tsx +135 -0
  35. frontend/src/app/architecture/page.tsx +87 -0
  36. frontend/src/app/dashboard/page.tsx +218 -0
  37. frontend/src/app/edge/page.tsx +181 -0
  38. frontend/src/app/favicon.ico +0 -0
  39. frontend/src/app/globals.css +60 -0
  40. frontend/src/app/history/page.tsx +142 -0
  41. frontend/src/app/layout.tsx +34 -0
  42. frontend/src/app/mlops/page.tsx +118 -0
  43. frontend/src/app/page.tsx +115 -0
  44. frontend/src/app/simulator/page.tsx +246 -0
  45. frontend/src/components/ShapChart.tsx +101 -0
  46. frontend/src/context/TelemetryContext.tsx +52 -0
  47. frontend/tsconfig.json +34 -0
  48. model/classes.pkl +1 -1
  49. model/knn.pkl +2 -2
  50. 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.11-slim
2
- WORKDIR /app
3
- COPY requirements.txt .
4
- RUN pip install --no-cache-dir -r requirements.txt
5
- COPY . .
6
- EXPOSE 7860
7
- ENV PORT=7860
8
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- title: Tinyml Backend
3
- emoji: 🫀
4
- colorFrom: green
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
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
+ ![Next.js](https://img.shields.io/badge/Next.js-16+-black.svg?style=flat&logo=next.js)
6
+ ![React](https://img.shields.io/badge/React-19-61DAFB.svg?style=flat&logo=react)
7
+ ![FastAPI](https://img.shields.io/badge/FastAPI-0.111.0+-009688.svg?style=flat&logo=fastapi)
8
+ ![Scikit-Learn](https://img.shields.io/badge/scikit--learn-1.5.1+-F7931E.svg?style=flat&logo=scikit-learn)
9
+ ![License](https://img.shields.io/badge/license-MIT-green.svg)
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
- COPY backend/requirements.txt .
 
 
 
 
 
 
 
4
  RUN pip install --no-cache-dir -r requirements.txt
 
 
5
  COPY . .
6
- WORKDIR /app/backend
 
7
  EXPOSE 8000
8
- CMD sh -c "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}"
 
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 sqlite3
2
  import os
3
  import time
 
4
  from datetime import datetime
 
5
  from huggingface_hub import HfApi, hf_hub_download
 
6
 
7
- REPO_ID = "IDKHowToCodeFr/tinyml-logs"
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
- if not HF_TOKEN:
 
16
  print("No HF_TOKEN found. Skipping sync.")
17
  return
18
- try:
19
- # Download with cache-busting logic if possible or just fresh download
20
- print(f"Downloading {DB_NAME} from Hub...")
21
- path = hf_hub_download(
22
- repo_id=REPO_ID,
23
- filename=DB_NAME,
24
- repo_type="dataset",
25
- token=HF_TOKEN,
26
- force_download=True
27
- )
28
- import shutil
29
- shutil.copy(path, DB_PATH)
30
- print("Sync from Hub complete.")
31
- except Exception as e:
32
- print(f"Sync from Hub failed: {e}")
 
 
 
 
 
 
 
33
 
34
- def sync_to_hub():
35
- if not HF_TOKEN:
36
  return
37
  try:
38
- # Uploading back to Hub centralizes it for other instances
39
- api.upload_file(
40
- path_or_fileobj=DB_PATH,
41
- path_in_repo=DB_NAME,
42
- repo_id=REPO_ID,
43
- repo_type="dataset",
44
- token=HF_TOKEN
45
- )
 
46
  except Exception as e:
47
  print(f"Sync to Hub failed: {e}")
48
 
49
- def init_db():
50
- sync_from_hub()
51
- conn = sqlite3.connect(DB_PATH)
52
- cursor = conn.cursor()
53
- cursor.execute('''
54
- CREATE TABLE IF NOT EXISTS predictions (
55
- id INTEGER PRIMARY KEY AUTOINCREMENT,
56
- timestamp DATETIME,
57
- heart_rate REAL,
58
- spo2 REAL,
59
- sys_bp REAL,
60
- dia_bp REAL,
61
- temp REAL,
62
- fall_detection TEXT,
63
- prediction_label TEXT,
64
- confidence REAL
65
- )
66
- ''')
67
- conn.commit()
68
- conn.close()
69
 
70
- def log_prediction(data, prediction_label, confidence):
71
- # Pull latest before writing to avoid overwriting others' work
72
- sync_from_hub()
73
-
74
- conn = sqlite3.connect(DB_PATH)
75
- cursor = conn.cursor()
76
- cursor.execute('''
77
- INSERT INTO predictions (timestamp, heart_rate, spo2, sys_bp, dia_bp, temp, fall_detection, prediction_label, confidence)
78
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
79
- ''', (
80
- datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
81
- data.Heart_Rate,
82
- data.SpO2_Level,
83
- data.Systolic_BP,
84
- data.Diastolic_BP,
85
- data.Body_Temp,
86
- data.Fall_Detection,
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
- history = []
110
- for row in rows:
111
- history.append(dict(zip(col_names, row)))
112
- return history
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
- return final_pred, confidence, individual_preds, individual_probs, self.weights
 
 
 
 
 
 
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 m2cgen as m2c
9
  import shap
10
- from database import log_prediction, get_history
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
- df = pd.DataFrame([{
57
- 'Heart Rate (bpm)': data.Heart_Rate,
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
- X_proc, _ = await asyncio.to_thread(preprocess_data, df, False)
66
- final_pred, conf, ind_preds, ind_probs, weights = await asyncio.to_thread(eng.predict, X_proc)
67
-
68
- is_at_risk = 0 if final_pred == "Healthy" else 1
 
 
69
 
70
  # Critical Alert System — fault-tolerant
71
  if is_at_risk == 1 and float(conf) > 0.80:
72
- try:
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
- try:
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
- async def get_dataset():
104
- data_path = '/app/data/patient_dataset.csv' if os.path.exists('/app/data') else '../data/patient_dataset.csv' if os.path.exists('../data') else 'data/patient_dataset.csv'
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
- if not eng or model_name not in eng.models:
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
- elif model_name == "logreg" and hasattr(model, 'coef_'):
274
- coefs = model.coef_
275
- intercepts = model.intercept_
276
- n_classes = coefs.shape[0] if len(model.classes_) > 2 else 2
277
- n_features = coefs.shape[1]
278
- L.append(f"/* Logistic Regression with {n_classes} classes, {n_features} features */")
279
- L.append(f"#define N_FEATURES {n_features}")
280
- L.append(f"#define N_CLASSES {coefs.shape[0]}")
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
- return {"error": f"Export failed: {str(e)}"}
486
 
487
  @app.post("/retrain")
488
- async def retrain(file: UploadFile = File(...)):
489
- data_path = '/app/data/patient_dataset.csv' if os.path.exists('/app/data') else '../data/patient_dataset.csv' if os.path.exists('../data') else 'data/patient_dataset.csv'
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
- from models import train_models
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 retrained successfully!"}
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 = '/app/data/patient_dataset.csv' if os.path.exists('/app/data') else '../data/patient_dataset.csv' if os.path.exists('../data') else '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)
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
- # Resolve relative paths universally whether Docker or local execution
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==0.103.1
2
- uvicorn==0.23.2
3
- pydantic==2.4.2
4
- scikit-learn==1.3.0
5
- pandas==2.1.0
6
- numpy==1.25.2
7
- shap==0.42.1
8
- m2cgen==0.10.0
9
- sqlalchemy
 
 
 
 
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: backend/Dockerfile
8
  ports:
9
  - "8000:8000"
 
 
10
  volumes:
11
- - ./model:/app/model
12
- - ./data:/app/data
13
 
14
  frontend:
15
  build:
16
- context: .
17
- dockerfile: frontend/Dockerfile
18
  ports:
19
- - "8501:8501"
20
  environment:
21
- - API_URL=http://backend:8000
 
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 python:3.11-slim
2
  WORKDIR /app
 
 
3
 
4
- RUN apt-get update && apt-get install -y graphviz && rm -rf /var/lib/apt/lists/*
5
-
6
- COPY frontend/requirements.txt .
7
- RUN pip install --no-cache-dir -r requirements.txt
8
  COPY . .
9
- WORKDIR /app/frontend
10
- EXPOSE 8501
11
- CMD sh -c "streamlit run Home.py --server.port=${PORT:-8501} --server.address=0.0.0.0"
 
 
 
 
 
 
 
 
 
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 &gt;&gt;
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 &gt;&gt;
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:a72e398999fbcd2495f31ec5f28f07e8325e903d836d2f2fbef872d6da2785bb
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:0fd25eec3d294ae23ef7fcce62dad8c695f585a149935845bdcd4771b3271e61
3
- size 6440614
 
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:da67513db74567d59333258b4d1982743cfdc80e54a59feb67922e7ea1bb92a9
3
  size 543
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7d8b08e681c302b1d80cba57ab678d752a3608e7787fe7fcf82c4052c5eef91e
3
  size 543