Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DeskWatch

A local, privacy-first activity tracker that watches your room through a webcam and tells you where your day actually went.

It works out what you're doing — at your desk, sleeping, praying, cooking, on your phone, watching TV — and shows you a timeline of your day, split into productive, neutral, and wasted. You decide which is which.

It runs entirely on your machine. It never writes a frame, an image, or a video to disk, and it makes no network calls at runtime.

┌─ Now ──────────────────────────────────────────────────────────┐
│  ● At desk            since 09:14 · 2 h 31 m       [ Pause ]    │
│  Why: seated in the desk zone · 90% sure · posture: sitting     │
│       · zone: Desk · sees: laptop, keyboard                     │
└────────────────────────────────────────────────────────────────┘

  Productive  1 h 15 m  ▓▓▓▓
  Neutral    15 h 59 m  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
  Wasted      4 h 15 m  ▓▓▓▓▓▓▓▓▓▓
  Unknown        30 m   (paused, dark, or unsure — never counted either way)

What makes it different: it refuses to lie to you. It tells you why it made every call, admits when it isn't sure, and never fills a gap with a guess.


The one thing you must understand

The camera can see your body. It cannot see your screen.

"Sitting at your desk writing code" and "sitting at your desk watching YouTube" are pixel-identical. No camera and no AI model can tell them apart, because the information simply isn't in the picture.

So DeskWatch does not pretend. Desk time counts as neutral by default, not productive. If you want it counted as productive, you say so in Settings — that's your call to make, not the camera's.

Everything else, it genuinely can see. Here's the honest accuracy picture:

Activity How it knows How much to trust it
Away nobody in frame Very high
Sleeping lying in the bed zone, still for 5+ min Very high
At desk sitting in the desk zone Very high
Praying kneeling/standing on the prayer-mat zone High
Cooking standing in the kitchen zone High — really means "you're in the kitchen"
In bed (awake) lying in bed, still moving High
Exercising standing + lots of motion Medium
Watching TV settled on the couch Medium — it's really "you're on the couch"
Eating bowl/cup/spoon near you Low–medium — the model often misses food
On phone phone detected near your wrist Low at first — see below

Phone detection is the weakest one. The model that spots phones was trained on close-up photos; a phone in your hand three metres from a webcam is small and often missed. Expect it to be wrong sometimes at first. Every time you correct it, that's recorded — the CSV export shows what the camera guessed versus what you said, so you can see for yourself how good it actually is on your camera, in your room.

Nothing is ever guessed. If DeskWatch isn't confident, it says Unknown and puts that time in its own bucket, where it counts neither for you nor against you. The same goes for time when it was paused, the room was dark, or the tracker was off.

And every single call it makes explains itself. The dashboard always shows why:

Why: lying still in bed for 7 min · 90% sure · posture: lying · zone: Bed


Install

Needs macOS, Python 3.12, and uv.

brew install uv                  # if you don't have it
git clone <this repo> && cd deskwatch

uv sync                          # NETWORK — downloads packages (~1-2 GB, incl. torch). Once.
uv run deskwatch setup           # NETWORK — downloads the two AI models (~12 MB).
                                 #   This is the LAST step that touches the internet.
uv run deskwatch calibrate       # No network. Pick a camera, draw your zones.
uv run deskwatch start           # No network. Runs the tracker + dashboard.

Then open http://127.0.0.1:8752.

macOS will ask for camera permission the first time. If no prompt appears, grant it yourself: System Settings → Privacy & Security → Camera, and enable it for your terminal.


Zones are the whole game

uv run deskwatch calibrate

You'll see a snapshot of your room in the browser. Click points around an area, name it, say what it is, and add it. Draw as many as you like.

Zones are what make activity detection honest. "Lying down in the bed zone" is a fact. "Asleep" with no bed zone would be a guess — so DeskWatch won't make it.

Draw this And DeskWatch can detect
Desk at desk
Bed sleeping, in bed awake, on phone in bed
Prayer mat praying
Kitchen cooking
Couch (+ TV) watching TV
Dining table eating
Exercise area exercising

Without zones, it can only detect away, moving about, and on phone. That's it. Draw your zones.

Point the camera so it can see as much of the room as possible — ideally from a high corner looking down, so it sees your body and not just your head. (A laptop webcam only sees your head and shoulders; DeskWatch handles that, but it can't see your bed from there.)


Using it

uv run deskwatch start       # tracker + dashboard
uv run deskwatch pause       # release the camera (for a video call)
uv run deskwatch resume
uv run deskwatch spike       # print what the camera sees, once a second — great for debugging
uv run deskwatch demo-data   # seed a fake day so you can see the dashboard immediately

Ctrl+Alt+P pauses and resumes from anywhere (needs Accessibility permission; if it's not granted, the dashboard button does the same job).

Covering the lens is the other pause. DeskWatch sees the covered lens and records Paused — never Away. Uncover it and it resumes in about three seconds.

Correcting it

Every segment has a "Not right?" button. One click, pick the real activity, done. Your answer always wins, and it's final.

The Today page opens with a "Check these" list at the top: the segments it was least sure about, and the activities it's known to be shaky at. That's your 30-second daily review. Everything else, leave alone.

Deciding what "wasted" means

Settings → What counts as what. Every activity maps to productive, neutral, or wasted, and it's yours to set. Defaults worth knowing:

  • At desk → neutral. The camera can't tell working from browsing. Set it to productive only if you mean it.
  • Sleeping → neutral. Sleep is not wasted time.
  • On phone / Watching TV → wasted. Change it if you disagree.

The mapping is applied when the page is drawn, so changing one recolours your whole history instantly. Decide gaming is fine now? One click, and every past day updates.

Start at login

./scripts/install_agent.sh      # launchd agent, restarts on failure
./scripts/uninstall_agent.sh    # remove it; your data is untouched

Privacy

  • No frames, video, or images are ever written to disk. Not in logs, not in error handlers, not as debug output. There is no cv2.imwrite and no cv2.VideoWriter anywhere in the codebase, and tests/test_no_image_writes.py parses the syntax tree of every source file on every test run to keep it that way.
  • Frames are processed in memory and thrown away. All that's ever saved: timestamped activity segments, your corrections, your settings, and text logs.
  • Zero network calls at runtime. Verified: the tracker process holds no TCP sockets at all. The dashboard listens on 127.0.0.1 only. htmx is bundled in the repo, not loaded from a CDN. Ultralytics telemetry is switched off in code, and models load from an absolute local file path (loading by name is what makes Ultralytics phone home).
  • The only step that uses the network is deskwatch setup.

The database is plain SQLite. That's fine for what it holds — no images, no content, just "you were at your desk from 09:00 to 11:30". If you want it encrypted later, swap SQLCipher in behind db.connect(); nothing else changes.

If other people share this room, tell them. DeskWatch has no face recognition and no per-person tracking — it just sees "a person" — but it is still a camera, and they deserve to know.


How it works

Once a second, every second:

  1. Grab the newest frame (buffer size 1, grab/retrieve — so the sample is fresh, not stale).
  2. Pose model finds people and their 17 body keypoints.
  3. Object model looks for a phone, laptop, keyboard, cup, bowl, book, TV.
  4. Posture is computed from keypoint geometry — standing / sitting / lying / kneeling. Pure maths, no model. (Lying = your torso is more horizontal than vertical. Kneeling = your shins are folded, so your ankles sit at knee height instead of below them — which is exactly what makes prayer posture detectable.)
  5. Zones: which of your polygons is the person standing in?
  6. The rules fuse it: bed zone + lying + still 5 min → sleeping. Every rule is one readable line, which is why the system can always tell you why.
  7. The state machine turns noisy per-second guesses into clean segments.

The state machine

fsm.py is a pure function — no I/O, no globals, it never even reads the clock. The same stream of observations always produces exactly the same segments, which is why it can be tested exhaustively without a camera.

  • A new activity commits only after winning ≥70% of the last 10 seconds. That kills flicker.
  • Away additionally needs a full minute of an empty room — walking out of shot for 20 seconds isn't leaving.
  • Commits are backdated to when you actually moved, not to when the machine made up its mind. Segments are contiguous: no gaps, no overlaps.
  • Segments under 30 s are absorbed into their neighbours, so a 20-second trip to the kitchen doesn't chop your morning into three pieces. Pauses and blackouts are exempt — a 5-second pause is deliberate, not noise.

Things it deliberately refuses to do

  • A dark room is not "asleep". It's Unobserved. Inferring sleep from darkness would be inventing data out of the absence of it.
  • A dead camera is not "away". If the camera fails, the state machine is fed nothing and heartbeats stop, so the gap renders as "tracker off". "The camera broke" is not "I left the room", and the report shouldn't claim otherwise.
  • A covered lens is not "away". It's Paused.

Reliability

  • Three processes: tracker, web, and a supervisor that restarts either if it dies (backoff 5 s → 60 s). SQLite (WAL mode) is the only channel between them.
  • A crash never corrupts data. Every write is a short transaction, and the open segment's start is saved every single tick. On restart, the orphaned segment is closed at the last heartbeat and the gap shows as "tracker off".
  • Camera unplugged? It reopens with exponential backoff and doesn't crash.
  • Times are stored as UTC and converted to local only when drawn. A day is a local calendar day; midnight-crossing segments are split; a DST day really is 23 or 25 hours long in the totals.
  • A single-instance lock stops two trackers fighting over the camera.

CPU

About 20–25% of one core (~3% of an 8-core Mac). Roughly half of that is just the cost of having a webcam open on macOS — it doesn't drop with lower resolution or a slower read rate, and it's the same whether or not we do any AI at all. The rest is the two models, which take ~120 ms per second.

Tests

uv run pytest

111 tests, none of which need a camera. They cover the state machine (flicker, backdating, minimum segments, both pause kinds, deterministic replay), posture geometry (including the head-and-shoulders webcam case, which was found by running against a real camera and finding it broke), the activity rules (every one of them, including that a phone on the desk is not a phone in your hand), rollups (DST days, midnight, tracker-off gaps, recolour-on-remap), crash recovery, and the privacy guarantee.


Not built, on purpose

No face recognition or per-person tracking. No screen or app tracking. No audio, ever. No alerts or nudges. No cloud, no accounts, no mobile app. No emotion or attention detection. No "focus score".

If you want to know whether desk time was work or browsing, a camera fundamentally cannot tell you. The honest options are: label it yourself, or add a local screen tracker like ActivityWatch — which sees the app and the tab, and is also free and local. DeskWatch is designed to sit alongside it: the camera knows you're at the desk, ActivityWatch knows what's on the screen.


If you run this yourself

Tell anyone who shares the room. DeskWatch has no face recognition and no per-person tracking — it only ever sees "a person" — but it is still a camera in a private space, and the people around you didn't consent to it. Point it at your own space, and use the pause (Ctrl+Alt+P, or just cover the lens) when others are around.

Check your local law before pointing a camera at shared areas. DeskWatch captures no audio, ever — which is also the thing most likely to cause you legal trouble.

License

AGPL-3.0 — see LICENSE.

This isn't a stylistic choice: DeskWatch uses Ultralytics YOLO for pose and object detection, which is AGPL-3.0, and that license reaches work built on top of it. If you want to build something closed-source from this, you'd need to replace the detector — MediaPipe (Apache-2.0) is the usual escape hatch.

About

Privacy-first webcam activity tracker. Detects what you're doing (desk, sleeping, praying, cooking, phone, TV), explains every call it makes, and never writes a frame to disk. Runs fully offline.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages