An intelligent focus tracking system that uses computer vision and machine learning to analyse attention patterns during work or study sessions. FocusLens runs entirely on your local machine with no cloud, no video storage, no external dependencies.
FocusLens watches your face through your webcam, extracts behavioural signals in real time, and builds a picture of your cognitive state over time. It detects when you are focused, when you are distracted, and learns the natural rhythm of your attention so it can predict focus drops before they happen.
No raw video is ever stored. The system processes frames locally, extracts only numerical features, and discards the video immediately.
FocusLens is built as an edge-to-server system. The machine running the webcam handles all visual processing locally. Only a small JSON feature vector is sent to the server pipeline — never raw video frames.
Detects attention state from webcam feed at 10 frames per second using facial landmark analysis. Computes eye openness, head orientation, and gaze direction to classify each frame as focused or distracted.
Every frame produces a structured feature vector including eye aspect ratio (left and right independently), head pose in three axes (yaw, pitch, roll), iris position and gaze zone, blink detection, and a session identifier that ties all frames together.
Analyses the focus signal over time to detect natural attention cycles. Uses signal smoothing, peak detection, and frequency analysis to identify your personal productivity rhythm. Predicts upcoming focus drops before they occur.
Tracks iris position to determine where attention is directed — centre, left, right, up, or down. Feeds into distraction pattern analysis and future attention heatmap features.
Every session produces a summary report including overall focus score (0–100), distraction frequency, peak productivity windows, dominant attention cycle duration, and gaze zone distribution.
Experiment tracking via MLflow, model versioning, and an automated retraining pipeline. The rule-based focus classifier is designed to be replaced by a trained ONNX model with zero changes to the data pipeline.
| Layer | Technology |
|---|---|
| Edge capture | Python, OpenCV, MediaPipe FaceMesh |
| Transport | WebSocket, JSON feature vectors |
| Message bus | Apache Kafka (via apache/kafka:3.7.0) |
| Ingestion | Python, FastAPI |
| Event processing | Python, FastAPI, psycopg2 |
| Analytics | Python, FastAPI, Pandas, SciPy |
| Time-series DB | PostgreSQL + TimescaleDB extension |
| Cache | Redis |
| ML tracking | MLflow |
| Model serving | ONNX Runtime |
| Frontend | React, Vite |
| Container runtime | Docker, Docker Compose |
| Orchestration | Kubernetes (minikube) |
| CI/CD | GitHub Actions (self-hosted runner) |
| Observability | Prometheus, Grafana, Loki |
focuslens-ai/
├── edge/ # edge agent — runs on webcam machine
│ ├── main.py # orchestrator, camera loop
│ ├── landmark_extractor.py # MediaPipe feature extraction
│ ├── visualizer.py # optional visual overlay (SHOW_WINDOW flag)
│ └── requirements.txt
│
├── services/
│ ├── ingestion/ # WebSocket receiver, Kafka producer
│ │ ├── main.py
│ │ ├── init_db.py # creates database schema
│ │ ├── requirements.txt
│ │ └── Dockerfile
│ ├── event/ # Kafka consumer, PostgreSQL writer
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ └── Dockerfile
│ ├── analytics/ # rhythm engine, session scoring, REST API
│ │ ├── main.py
│ │ ├── db.py
│ │ ├── rhythm_engine.py
│ │ ├── requirements.txt
│ │ └── Dockerfile
│ └── backend/ # BFF — aggregates data for dashboard
│ ├── main.py
│ ├── requirements.txt
│ └── Dockerfile
│
├── frontend/ # React dashboard
│ ├── index.html
│ ├── package.json
│ └── src/
│ ├── api.js # API calls to backend
│ └── main.js # dashboard UI
│
├── k8s/ # Kubernetes manifests
│ ├── configmap/
│ │ └── config.yaml
│ ├── secrets/
│ │ └── secrets.yaml
│ ├── infra/
│ │ ├── redpanda.yaml # Kafka (apache/kafka:3.7.0)
│ │ ├── postgres.yaml # TimescaleDB
│ │ └── redis.yaml
│ ├── services/
│ │ ├── ingestion.yaml
│ │ ├── event.yaml
│ │ ├── analytics.yaml
│ │ └── backend.yaml
│ └── ingress.yaml
│
├── docker-compose.yml # local development (no K8s)
└── .gitignore
Each webcam frame produces a feature vector:
{
"session_id": "746014e1-5464-4931-8dea-e446a2b43e7d",
"frame_id": 1042,
"ts": 1742600000000,
"eye": {
"ear_left": 0.32,
"ear_right": 0.30,
"ear_avg": 0.31,
"blink_detected": false
},
"head_pose": {
"yaw": -3.2,
"pitch": 2.1,
"roll": 0.5
},
"gaze": {
"iris_left_x": 0.48,
"iris_left_y": 0.51,
"iris_right_x": 0.49,
"iris_right_y": 0.50,
"gaze_zone": "center"
},
"focus": {
"rule_based": true,
"score": null,
"model_version": null
},
"face": {
"detected": true,
"confidence": 0.97
}
}Before starting, install the following on your machine:
- Python 3.11 via miniconda
- Docker Desktop — must be running before any step
- Node.js 22 via nvm
- minikube — local Kubernetes
- kubectl — Kubernetes CLI
- helm — Kubernetes package manager
# install minikube
brew install minikube
# install kubectl
brew install kubectl
# install helm
brew install helm
# install nvm then node 22
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# restart terminal then:
nvm install 22
nvm use 22This is the fastest way to get running. No Kubernetes required.
conda create -n focus python=3.11
conda activate focusdocker compose up -dStarts Redpanda, PostgreSQL + TimescaleDB, and Redis.
Verify:
docker compose ps
# should show 3 containers running: redpanda, postgres, rediscd services/ingestion
python -m pip install psycopg2-binary
python init_db.py
# output: [db] Tables created successfullyRun from the repo root:
# edge agent
cd edge && python -m pip install -r requirements.txt && cd ..
# services
cd services/ingestion && python -m pip install -r requirements.txt && cd ../..
cd services/event && python -m pip install -r requirements.txt && cd ../..
cd services/analytics && python -m pip install -r requirements.txt && cd ../..
cd services/backend && python -m pip install -r requirements.txt && cd ../..
# frontend
cd frontend && npm install && cd ..Open 7 terminals. Activate conda activate focus in each Python terminal.
# Terminal 1 — infrastructure (already running)
docker compose up -d
# Terminal 2 — ingestion service
cd services/ingestion
uvicorn main:app --host 0.0.0.0 --port 8001 --reload
# Terminal 3 — event service
cd services/event
python main.py
# Terminal 4 — analytics service
cd services/analytics
uvicorn main:app --host 0.0.0.0 --port 8004 --reload
# Terminal 5 — backend API
cd services/backend
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
# Terminal 6 — frontend dashboard
cd frontend
npm run dev
# Terminal 7 — edge agent (starts webcam)
cd edge
python main.pyhttp://localhost:3000
docker exec -it focuslens-ai-postgres-1 psql -U fl_user -d focuslens \
-c "SELECT session_id, ts, ear_avg, focused, gaze_zone FROM focus_events ORDER BY ts DESC LIMIT 5;"Make sure Docker Desktop is running first.
minikube start --cpus=4 --memory=7000mb --disk-size=30g --driver=dockerVerify:
minikube status
kubectl get nodes
# should show: minikube Ready control-planeminikube addons enable ingress
minikube addons enable metrics-serverkubectl create namespace focuslenskubectl apply -f k8s/configmap/config.yaml
kubectl apply -f k8s/secrets/secrets.yaml
# verify
kubectl get configmap -n focuslens
kubectl get secret -n focuslenskubectl apply -f k8s/infra/postgres.yaml
kubectl apply -f k8s/infra/redis.yaml
kubectl apply -f k8s/infra/redpanda.yamlWait for all three to be Running (takes 1-2 minutes for image pulls):
kubectl get pods -n focuslens -w
# wait until postgres-0, redis-0, redpanda-0 all show Runningkubectl exec -it postgres-0 -n focuslens -- psql -U fl_user -d focuslens -c "
CREATE EXTENSION IF NOT EXISTS timescaledb;
CREATE TABLE IF NOT EXISTS focus_events (
id BIGSERIAL,
session_id TEXT NOT NULL,
frame_id INTEGER NOT NULL,
ts TIMESTAMPTZ NOT NULL,
ear_left FLOAT,
ear_right FLOAT,
ear_avg FLOAT,
blink BOOLEAN,
yaw FLOAT,
pitch FLOAT,
roll FLOAT,
gaze_zone TEXT,
iris_left_x FLOAT,
iris_left_y FLOAT,
focused BOOLEAN,
focus_score FLOAT,
PRIMARY KEY (id, ts)
);
SELECT create_hypertable('focus_events', 'ts', if_not_exists => TRUE);
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
started_at TIMESTAMPTZ NOT NULL,
ended_at TIMESTAMPTZ,
focus_score FLOAT,
total_frames INTEGER DEFAULT 0
);
"This points your Docker CLI at minikube's internal Docker daemon so images are available to K8s without pushing to a registry:
eval $(minikube docker-env)
docker build -t focuslens/ingestion:latest services/ingestion/
docker build -t focuslens/event:latest services/event/
docker build -t focuslens/analytics:latest services/analytics/
docker build -t focuslens/backend:latest services/backend/Verify images are available:
docker images | grep focuslenskubectl apply -f k8s/services/ingestion.yaml
kubectl apply -f k8s/services/event.yaml
kubectl apply -f k8s/services/analytics.yaml
kubectl apply -f k8s/services/backend.yamlWatch all pods come up:
kubectl get pods -n focuslens -wAll 7 pods should show Running:
analytics Running
backend Running
event Running
ingestion Running
postgres-0 Running
redis-0 Running
redpanda-0 Running
Since we are running minikube locally with the Docker driver, use port-forward to access services from your machine:
# expose backend API
kubectl port-forward svc/backend -n focuslens 8000:8000 &
# expose ingestion WebSocket
kubectl port-forward svc/ingestion -n focuslens 8001:8001 &curl http://localhost:8000/health
# {"status": "ok"}Open a new terminal with the focus conda environment:
conda activate focus
cd edge
python main.pyThe edge agent opens your webcam and starts sending landmark data to the K8s ingestion service.
kubectl logs -f deployment/event -n focuslens
# should show: [event] Saved frame 1 session xxxx focused=True# get your session id
curl http://localhost:8000/api/latest-session
# get full session report
curl http://localhost:8000/api/sessions/{session_id} | python -m json.toolcd frontend
nvm use 22
npm run devOpen http://localhost:3000
When you change any service code, rebuild and redeploy:
eval $(minikube docker-env)
docker build -t focuslens/{service}:latest services/{service}/
kubectl rollout restart deployment {service} -n focuslens
# example — rebuild backend
docker build -t focuslens/backend:latest services/backend/
kubectl rollout restart deployment backend -n focuslens| Service | Port | Notes |
|---|---|---|
| Frontend dashboard | 3000 | npm run dev |
| Backend API | 8000 | port-forwarded from K8s |
| Ingestion service | 8001 | port-forwarded from K8s |
| Analytics service | 8004 | internal K8s only |
| PostgreSQL | 5432 | internal K8s only |
| Redpanda (Kafka) | 9092 | internal K8s only |
| Redis | 6379 | internal K8s only |
# health check
GET http://localhost:8000/health
# latest session id
GET http://localhost:8000/api/latest-session
# full session analytics report
GET http://localhost:8000/api/sessions/{session_id}
# list all sessions
GET http://localhost:8000/api/sessions
# live feed — last 60 frames
GET http://localhost:8000/api/live/{session_id}# see all pods
kubectl get pods -n focuslens
# see all pods across all namespaces
kubectl get pods -A
# logs for a service
kubectl logs -f deployment/backend -n focuslens
# restart a deployment
kubectl rollout restart deployment backend -n focuslens
# describe a pod (for debugging)
kubectl describe pod {pod-name} -n focuslens
# exec into a pod
kubectl exec -it {pod-name} -n focuslens -- /bin/bash
# exec into postgres
kubectl exec -it postgres-0 -n focuslens -- psql -U fl_user -d focuslens
# check recent focus events
kubectl exec -it postgres-0 -n focuslens -- psql -U fl_user -d focuslens \
-c "SELECT session_id, ts, ear_avg, focused, gaze_zone FROM focus_events ORDER BY ts DESC LIMIT 10;"After you have set up Kubernetes and deployed your services, follow these steps to get everything running locally.
minikube start # ~30 secondsThis allows you to access them locally on your machine:
kubectl port-forward svc/backend -n focuslens 8000:8000 &
kubectl port-forward svc/ingestion -n focuslens 8001:8001 &cd frontend
npm run devpython edge/main.pyYou can run a start_all.sh script to automate all steps:
#!/bin/bash
# Start Minikube
minikube start
# Port-forward services
kubectl port-forward svc/backend -n focuslens 8000:8000 &
kubectl port-forward svc/ingestion -n focuslens 8001:8001 &
# Start frontend
(cd frontend && npm run dev) &
# Start edge service
python edge/main.pyMake it executable and run:
chmod +x start_all.sh
./start_all.shThis will start Minikube, port-forward services, launch the frontend, and run edge/main.py all at once.
| Phase | Approach | Status |
|---|---|---|
| 1 | Rule-based classifier (EAR + head pose thresholds) | Done |
| 2 | CNN on face crops — MobileNetV3 fine-tuned | Planned |
| 3 | LSTM over 30-frame windows for temporal smoothing | Planned |
| 4 | Multimodal — vision + time-series fusion | Planned |
- Raw video frames are never stored anywhere
- MediaPipe runs locally on the edge device
- Only numerical feature vectors leave the edge process
- All data stays on your local machine
- No external API calls, no cloud storage
