Your screen
has a witness.

ApexCompanion looks at your screen every 10 seconds with Gemma 4, running on your own Mac. Tell it what you're working on, and it sits in your menu bar counting your focused minutes. Drift off task and it calls you out out loud, in the voice of a coach with no patience for you.

Built for watching yourself: no accounts, no remote reporting. Install it only on your own Mac.

A personal trainer who watches every rep and never posts the footage.

How a session looks

  1. Set a goal. Say what you're working on and pick 25, 50 or 90 minutes, or no limit.
  2. Focus. The menu bar shows ◉ 23m as your focused minutes add up. No praise out loud, so nothing interrupts you.
  3. Drift, get roasted. Two distracted checks in a row and your coach names what's on screen and what you said you'd be doing.
  4. Summary. When the time is up, the session ends and shows how you did.

Before you download

  • Your Mac: Apple silicon (M1 or later), macOS 14 or newer, 24 GB of memory recommended (it peaks at about 11.3 GB; 16 GB works but swaps with a browser and editor open; 8 GB is not recommended).
  • Space: about 8.3 GB one-time download. Keep about 12 GB of disk free.
  • First open: macOS will warn you because the app is not notarized. Go to System Settings → Privacy & Security and click Open Anyway.
  • No window: it lives in your menu bar. Pause, stop, or edit your sites from there.
  • Shared spaces: it speaks out loud and names what is on your screen. Choose Mute voice in the menu bar when others can hear; notifications still show.
  • Calls: while Zoom, Teams, FaceTime, Webex, Around, Tuple or a Meet tab is in front, it takes no screenshot and says nothing.

Developers: run from source

$ HF_HUB_OFFLINE=1 .venv/bin/python apex.py --persona closer
apex.py --persona closer
loaded gemma-4-e4b-it-8bit (mlx) · offline
check app=VS Code content="editing apex.py"
      category=coding → working
check app=Safari content="YouTube: cat compilation"
      category=entertainment_video → distracted (1/2)
check app=Safari content="YouTube: cat compilation"
      category=entertainment_video → distracted (2/2)
roast persona=closer · cooldown clear · speaking

Mock UI. The roast text is real output from the pipeline.

Watch it work

89 seconds, all real output: actual screenshots of public pages, actual verdicts from the classifier, actual roasts in each coach's voice.

The insight

Site blockers see URLs. Gemma sees context.

The same domain can be your best study tool or your worst habit. A blocklist keyed on URLs or window titles can't tell the difference. Gemma 4 looks at the actual pixels, so it can.

Blocklist What a blocker sees

It only sees a URL or a window title, so it matches patterns.

youtube.com/watch?v=…
Blocked matches "youtube.com"
youtube.com/watch?v=…
Blocked matches "youtube.com"
docs.python.org/3/library/asyncio.html
Allowed not on the list

Gemma 4 What ApexCompanion sees

The screen itself: app, content, intent.

YouTube: a Python asyncio tutorial, code on screen
educational_video Working
YouTube: a cat compilation, autoplay queue
entertainment_video Distracted
Python docs open beside the editor
docs_reading Working

The tutorial stays. The cats get called out. No allowlist to maintain.

How it works

How one check works

Gemma 4 describes what's on screen and picks a category. Ordinary Python maps that category to a verdict. The model never gets the final say on whether you're slacking.

Describe first, label second

Asking "working or distracted?" directly got a cat compilation labeled "working". Making the model first say what it sees, then pick a category, fixed that case.

Code owns the verdict

Categories map to verdicts in a lookup table you can read and edit.

codingdocs_readingnotes_writingeducational_videomessagingnews_readingother entertainment_videosocial_mediashoppinggamingfeed_browsingsports

Never roast on garbage

Malformed JSON or an unknown category counts as "working". One bad frame can't trigger a call-out, and two bad checks in a row are required anyway.

Offline after setup

Your screen is the most private thing you own.

A coach that watches your screen can only be trusted if the footage goes nowhere. That's why this has to run on-device, and why an open-weight model makes it possible.

  • No network calls while it runs. The model loads from local disk (HF_HUB_OFFLINE=1). Setup is the one time it goes online: the first run downloads uv, Python with about 850 MB of packages, and the 8.3 GB model.
  • Screenshots stay on the machine, then get deleted. Each capture is removed after its check. Inference, roast and voice all run locally on Apple silicon.
  • A small local log, readable only by you. App name, the site's host (not the full URL), and the screen description only when a roast fires. Delete it anytime.
  • Off during calls and pauses. With a call app or meeting tab in front, or while paused from the menu bar, it takes no screenshot and runs no model.
  • Keeps running with Wi-Fi off. This page makes no third-party requests either: no outside fonts, scripts or trackers, only its own video and poster.
Pick your coach

Pick your coach

Each roast is generated fresh by Gemma 4 and refers to what's actually on your screen and the goal you set. The app remembers your coach; switch any time.

Persona 01

The Closer

An arrogant corporate dealmaker. Bills by the minute and thinks you should too.

--persona closer
  • real output · on a cat compilationAre you actually watching cat videos when I'm trying to close a deal? Focus, or this conversation is over.
  • sample · on a shopping cartYou're comparing sneakers like it's a merger. Nobody's acquiring you at this rate. Close the tab.
  • sample · goal: lab report · on a box scoreYou said you'd be working on the lab report. That's a box score. Close it and get back to the deal.
Persona 02

The Whip

A cold Southern political operator. Soft voice, long memory, counts every vote and every minute.

--persona whip
  • sample · on a social feedFriend, I've ended careers over less than twenty minutes of scrolling. Put that feed down.
  • sample · on a game launcherNow I know you didn't open that game on my time. Close it, and we'll pretend I didn't see.
  • sample · goal: essay draft · on a video feedYou told me you'd be writing that essay, friend. I don't see an essay. I see a feed.

Lines marked "sample" were written for this page to show each voice; the line marked "real output" came from the pipeline. Live roasts are generated on-device and vary. Every spoken line passes a word filter: a line with a slur or injected instructions is replaced with a fixed safe line. Original archetypes, not impressions of any real person or character.

Measured, not promised

The numbers we actually measured.

M3 Max, mlx-community/gemma-4-e4b-it-8bit via mlx-vlm 0.7.10, offline. Anything not yet measured is marked as pending, not guessed.

Measured
0.87s
Vision check (classify only), warm median
The Gemma 4 vision call over the 28 tuning-set pages, model already loaded. p95 0.96 s. Excludes taking and resizing the screenshot.
Measured
0.6–0.8s
Roast generation
Measured 0.57 to 0.79 s. Gemma 4 text, persona voice, before it's spoken aloud.
Measured
3.4–3.7s
Model load
Paid once per session, from local disk, offline.
Measured
11.3GB
Peak memory
Peak footprint over a 306-check soak, steady state about 8.7 GB. A 24 GB Mac is recommended.
Blind test
15/16
Clear-cut pages right on a batch never used for tuning, 0 false roasts on clear-cut pages
23 public pages captured after prompt tuning finished. Wilson 95% interval about 72% to 99%. Distracted recall 6/7, exactly at the 0.85 gate. Labels hand-checked by the author (Oct 24), not an independent reviewer.
Tuning set
25/25
Clear-cut pages on the batch used while tuning the prompt
28 pages used while tuning, so not a fair test on its own. Both batches pooled: 40/41 clear-cut across 51 pages (95% interval lower bound about 87%). One false roast happened on a page labeled ambiguous, a lofi study stream.
A/B
9 vs 15
Asking Gemma "working or distracted?" vs letting code decide, on the 16 blind pages
Same model, same categories, temperature 0. Across all 41 clear-cut pages: 29/41 vs 40/41, and distractions caught 6/18 vs 17/18. The yes/no prompt called the Steam store page for Hades "working". Small set, web pages only.
Live run
624 checks
One 52-minute session on a real desktop, 0 errors, 0 unreadable model outputs
Author's own screen, Oct 24, 5 s interval. End to end per check: p50 1.29 s, p95 1.60 s. Measures reliability and speed, not accuracy: the checks are unlabeled.
10 scheck interval (setting)
2 checksin a row before a call-out (setting)
60 sminimum gap between roasts (setting)

Caveat, stated plainly: 16 blind clear-cut cases is a small set; one miss costs about 6 points. Only web pages captured in a headless browser were tested: no native apps (VS Code, the terminal, native Spotify, games) and no real desktop frames. The labeler is the author. Anything filed as "other", or misfiled into a working category, counts as working and hides the miss: the blind batch's one clear-cut miss was a YouTube Shorts clip filed as educational_video. The repo has the eval labels and results, but not the screenshots (they show third-party pages), so the eval can't be rerun from the repo alone. We checked the eval can fail: moving entertainment_video to the working side fails the recall gate, and moving coding to the distracted side gives 6 false roasts.