The problem
Sitting at a desk all day, it’s easy to forget to drink water. So I built jalLijiye (Hindi for “please have some water”): a macOS menu bar app that reminds me, then uses the webcam to check that my water bottle actually shows up. No bottle, no peace.
v0
The first version was the obvious one. It watched me all day. The webcam ran at 5 FPS, a person model checked I was at my desk, a custom bottle detector looked for my bottle, and a small heuristic guessed whether the bottle was near my mouth. A state machine turned all that into “drank” or “didn’t drink”.
It worked, and it was a bad citizen. On my M1 MacBook Air, Activity Monitor showed the Python process at about 149% of a core on CPU, and about 70% on the Apple GPU path (MPS). That is a lot of resource taken for a reminder. A water reminder should not be the loudest thing on the machine.
The idea: stay asleep until it matters
The fix was a design change, not a faster model. Most of the day the app has nothing to do, so it shouldn’t do anything.
The new flow is shown below. The app waits for the reminder interval before it opens the camera or loads the ML runtime. When a reminder is due, it shows a red “Drink water” prompt in the menu bar and runs only the bottle detector for up to 60 seconds. If the bottle shows up in at least 5 of the last 10 sampled frames (at 1FPS), the reminder has done its work and it goes back to sleep. If not, it stops, keeps the prompt visible and retries after a 10 minute snooze. It also restarts the check window after the Mac wakes, and it only runs on my work-from-home days.
I also dropped the person and face models. The bottle is the only thing the app looks for now.
Part 1: train a bottle detector that knows my bottle
A generic detector isn’t great at one specific checkered bottle on my desk. So I recorded short webcam clips, pulled one frame per second, and had MLX SAM 3 (Native Apple Silicon Implemetation of SAM 3) draft the labels. I reviewed every label preview by eye and fixed the misses. That gave a small dataset of 92 frames, split in time order (60 train, 16 validation, 16 test) so near-identical neighbouring frames wouldn’t leak across splits.
I trained YOLOv8n and YOLO26n on it. YOLOv8n found the bottle in all 14 positive test frames. The first YOLO26n run stopped early and found none. Retrained for the full 30 epochs it recovered to 11 of 14. YOLOv8n stayed in the app.
Part 2: PyTorch on MPS vs Core ML
Next I exported the model to Core ML (FP16) and compared it with PyTorch on MPS, using frames replayed from a recorded clip. This was done to evaluate the consequence of the power efficiency and ANE (Appled Neural Engine) utilization by CoreML. This was an isolated inference test, not the full app.
- Mean inference: 24.8 ms per frame on PyTorch MPS, 21.5 ms on Core ML.
- Memory after the run: 492 MB vs 346 MB, so Core ML used about 30% less.
- The two backends agreed on whether the bottle was present in 131 of 132 frames.
- One thing that didn’t improve: Core ML used more CPU time over the run (3.02 s vs 2.26 s).
So Core ML was a little faster and lighter on memory, but not a free win on CPU. I’m sharing that on purpose.
Part 3: just check less often
The biggest saver wasn’t the model. It was the schedule. With the app idle between reminders and checking at 1 FPS, my Activity Monitor readings during checks were roughly 0.2 to 12% of one core, with GPU use near zero. Moving to 5 FPS pushed it to about 4 to 15%. I also tried an INT8 model and a 320x320 FP16 model. They didn’t show a clear improvement in what I measured, and I haven’t measured the 320 model’s accuracy yet.
What I’d tell you before you trust these numbers
- The CPU figures are Activity Monitor readings from my own M1 MacBook Air, not a controlled benchmark. Treat them as ranges.
- The model comparison is 16 test frames from one short clip, in one room, with one bottle and only two negative frames. It says nothing about other bottles, backgrounds or lighting.
- The Core ML vs MPS numbers measure inference alone, not the whole app.
The conclusion
The thing that made jalLijiye cheap (~6% CPU, ~1% GPU) wasn’t a clever model. It was deciding when not to run one. Going from an always-on person and bottle pipeline at well over 100% of a core to a reminder that sleeps, wakes, looks for one bottle and goes back to sleep brought it down to a low single-digit to low double-digit percent of a core while it’s checking, and idle the rest of the time.
The app is now live in my MacBook Air M1 24x7. I plan to include a minor dashboard where I can monitor my last week’s hydration activity.
If you’re building something that watches the camera, ask first whether it needs to watch all the time.
The code, training workflow and every measurement above are in the repo: github.com/prateek-mehra/jalLijiye.