MouthGuard
May 19, 2026 · View on GitHub
A browser-based mouth closure tracker that uses your webcam and AI face detection to help you keep your mouth closed. Runs entirely client-side — no backend, no data leaves your device.
Features
- Real-time detection — MediaPipe Face Mesh runs on every webcam frame, measuring the gap between lip landmarks 13 and 14
- Smart alerts — configurable detection window: the mouth must stay open for the full delay before anything is registered, so brief openings never trigger false alerts
- Tab title blinking — flashes the browser tab title when your mouth has been open too long, visible even when you've switched to another tab
- Browser notifications — system notifications when served over
http://localhostorhttps:// - Audio alerts — five synthesized sounds (Soft beep, Chime, Double beep, Buzz, High ping) with adjustable volume
- Live lip gap chart — scrolling real-time plot of the lip gap in pixels with a threshold reference line
- Session stats — time open/closed, number of events, average open duration
- Session history — last 30 sessions persisted to
localStorage - Dark theme — clean, focused UI with Inter + JetBrains Mono fonts
Quick start
Docker image (recommended)
Pull and run the pre-built image:
docker run -p 8080:80 ghcr.io/sitolam/mouthguard:latest
Pin to a specific version:
docker run -p 8080:80 ghcr.io/sitolam/mouthguard:v1.1.0
Or with Docker Compose — create a docker-compose.yml:
services:
mouthguard:
image: ghcr.io/sitolam/mouthguard:latest
ports:
- "8080:80"
restart: unless-stopped
docker compose up -d
To update later:
docker compose pull && docker compose up -d
Open http://localhost:8080.
Python
python3 -m http.server 8080
NixOS
nix-shell -p python3 --run "python3 -m http.server 8080"
Note: Opening
index.htmldirectly as afile://URL works for most features, but browser notifications require anhttp://orhttps://origin. Tab title blinking works regardless.
Settings
| Setting | Default | Description |
|---|---|---|
| Sensitivity threshold | 5 | Lip gap (px) that counts as "open". Lower = more sensitive. |
| Alert delay | 1s | How long mouth must stay continuously open before it is detected and an alert fires. If the mouth closes before the delay expires, nothing is recorded. Range: 0 – 10 s in 0.1 s steps. |
| Sound type | Soft beep | Alert sound character |
| Volume | 85% | Alert sound loudness |
Tech stack
- Vanilla HTML, CSS, JavaScript — single
index.htmlfile, no build step - MediaPipe Face Mesh via CDN — client-side ML inference
- Chart.js via CDN — dashboard charts
- Nginx (Alpine) via Docker for serving
How it works
getUserMediacaptures the webcam stream- An inline Web Worker runs a
setIntervaltick loop — workers are not throttled in background tabs, keeping detection alive when you switch away - Each tick sends the current video frame to MediaPipe Face Mesh
- The vertical distance between landmarks 13 (upper lip inner) and 14 (lower lip inner) is measured in pixel space
- If the gap exceeds the threshold continuously for the full alert delay, the mouth is confirmed as open, an alert fires, and the detection window time is counted as open time. If the gap drops below the threshold before the delay expires, the event is discarded. Once confirmed open, the alert re-arms every alert delay until the mouth closes.
- On close, the continuous sound and tab blink stop immediately
Browser support
Tested in Chrome and Firefox. Requires:
getUserMedia(camera access)- Web Workers
- Web Audio API
NotificationAPI (optional, for system notifications)