An automated ML pipeline that uses an existing home CCTV camera to measure how long my 4-year-old daughter actually watches TV each day, and pings me on Telegram every afternoon with the number.
A weekend project that turned a TAPO C200 motion-clip stream into a daily one-number screen-time report — with a self-improving model and a human-in-the-loop correction channel built into the daily Telegram message.
TAPO C200 (living room) ──► TAPO Care Cloud
│ Lambda 1: downloader (11:30 IST)
▼
S3 clips/YYYY-MM-DD/...
│ S3 event per clip — fans out in parallel
▼
Lambda 2: processor (container)
ffmpeg → adult-filter heuristic →
MobileNetV3-Small @ 70% threshold
▼
S3 results/YYYY-MM-DD/...
│ Lambda 3: notifier (13:30 IST)
▼
Telegram: "📺 25m 30s · 175 clips"
+ 5 lowest-confidence frames for review
│ user replies "1, 3"
▼
Lambda 4: correction handler (API GW)
→ saves false positives to training/corrections/
│ Sunday 02:00 IST
▼
Processor re-runs as retrainer
→ fine-tunes model → uploads new weights to S3
| Layer | Tech |
|---|---|
| Camera | TP-Link TAPO C200 + TAPO Care cloud |
| Compute | AWS Lambda (4 functions, ap-south-1) |
| Heavy compute | Container Lambda (PyTorch + ffmpeg + OpenCV) |
| Model | MobileNetV3-Small, fine-tuned via transfer learning from ImageNet |
| Storage | S3 (clips, frames, results, model weights) |
| Secrets | SSM Parameter Store (TAPO creds, Telegram bot token) |
| Notifications | Telegram Bot API |
| Dashboard | Streamlit (operational metrics + confidence trends) |
| IaC | AWS SAM (CloudFormation) |
MobileNetV3-Small, ~2.5M parameters, fine-tuned from ImageNet weights on 603 frames extracted from 13 short clips (1 frame every 2 seconds, OpenCV). 80/20 train/val split, 20 epochs, weighted sampler for class balance, light augmentation (crop/flip/colour jitter). Live inference samples 1 frame every 10 seconds and only counts a frame as "watching" above a 0.70 confidence threshold. An adult-size heuristic skips frames where the largest moving object exceeds 40% of frame height — saves compute and avoids false positives from adults walking through.
→ Full plain-English walkthrough in ML_MODEL_EXPLAINER.md.
→ PM-framed problem/architecture/metrics doc in PM_INTERVIEW_DOC.md.
→ Substack narrative for general audience in SUBSTACK_POST.md.
The notifier doesn't just send a number — it also sends the 5 lowest-confidence "watching" frames of the day and asks the user to flag any that are wrong. Replies hit an API Gateway webhook → Lambda 4 saves the flagged frames as training/corrections/not_watching/. Every Sunday at 02:00 IST the processor re-runs as a retrainer: loads the corrections, samples an equal number of confirmed positives, fine-tunes for 10 epochs at LR 1e-4, uploads new weights to S3. Next cold start picks up the new model automatically.
.
├── lambdas/
│ ├── downloader/ # TAPO Care → S3 (zip Lambda)
│ ├── processor/ # ffmpeg + MobileNet inference (container Lambda)
│ ├── notifier/ # daily Telegram digest + review prompt
│ └── correction_handler/ # Telegram webhook → corrections store
├── model/
│ ├── train.py # cloud training entry point
│ ├── train_local.py # local training + per-class accuracy report
│ └── (mobilenet_v3.pt is gitignored — lives in S3)
├── infra/ # SAM templates (template.yaml + phase1/phase2)
├── scripts/
│ └── register_webhook.py # one-shot Telegram webhook registration
├── dashboard.py # Streamlit operational dashboard
└── tests/
| Component | Monthly |
|---|---|
| TAPO Care | ~₹150 |
| AWS Lambda (4 functions, ~175 clips/day) | ~₹0 (free tier) |
| AWS S3 | ~₹30 |
| Telegram Bot | Free |
| Total | ~₹180 |
- AWS account in
ap-south-1(Mumbai); TAPO server URL must match camera registration region (aps1for India, noteuw1). - TAPO username/password and Telegram bot token live as SSM SecureString parameters under
/tapo-tracker/...— never in source. - Local AWS credentials and
.streamlit/secrets.tomlare gitignored; production runs use Lambda execution roles, not access keys. infra/template.yamlis the primary SAM template;template-phase1.yamlandtemplate-phase2.yamlare the staged variants used during initial rollout.