Skip to content

LaughCounter: native macOS app (+ Python reference) that counts your laughs - #1

Merged
missingbulb merged 4 commits into
mainfrom
claude/home-laugh-counter-8unbu3
Jul 12, 2026
Merged

LaughCounter: native macOS app (+ Python reference) that counts your laughs#1
missingbulb merged 4 commits into
mainfrom
claude/home-laugh-counter-8unbu3

Conversation

@missingbulb

@missingbulb missingbulb commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Counts and logs every time you laugh at home, running on the living-room Mac mini. Ships in two forms that share one JSON log format.

🍎 Native macOS app — the clean, recommended way (mac/)

A tiny menu-bar app using only built-in macOS frameworks — no Python, no TensorFlow, no Homebrew, nothing left behind when you delete it.

  • Detects laughter with Apple's built-in Sound Analysis classifier (no model download), matching several laugh classes so it doesn't overfit one style.
  • Counts distinct laughs with the same hysteresis logic as the Python core, ported to Swift.
  • 😄 menu-bar icon shows today's count — the "it's running" indicator.
  • A soft blip confirms each logged laugh.
  • Hands-free feedback: an on-device speech recognizer listens for "I just laughed", logs the miss, and blips twice to confirm (nothing leaves the Mac); ⌘L / menu item as fallback.
  • Logs to ~/Library/Application Support/LaughCounter/laughs.jsonlno audio stored.

You never touch Xcode: GitHub Actions (.github/workflows/build-macos-dmg.yml) compiles the app on a macOS runner and produces a downloadable LaughCounter.dmg (build artifact, and attached to Releases on a tag). Install = drag to Applications, right-click→Open once, approve Microphone + Speech. See mac/README.md.

The Swift app is verified by the macOS CI build in this PR (it can't be compiled in the Linux dev environment where it was written).

🐍 Python reference / simulator (laughcounter/)

A fully-tested, cross-platform implementation — hysteresis counter, SQLite + JSONL storage, a phone-friendly web dashboard with a big "I just laughed" button and per-laugh corrections, stats (who/streaks/hours), clip-saving, speaker-attribution scaffolding, and an offline simulate mode. Core depends on only the Python standard library; ML/mic are optional lazily-imported extras.

python -m laughcounter simulate -n 30 --seed 1 && python -m laughcounter serve

📄 Design & tradeoffs

docs/DESIGN-AND-TRADEOFFS.md documents every decision: deployment options (native vs bundled Python vs one-folder vs Docker, and why Docker can't reach the Mac mic), how flagged misses become improvements (threshold tuning now → a personalised on-device Create ML model later), the who-laughed plan (me-vs-guest via Create ML, with honest limits), the voice-command tradeoffs, the roadmap, and privacy.

Testing

  • Python: 69 tests pass, zero third-party deps. Verified end-to-end (simulate → stats/log, mark/reject/who, live dashboard GET+POST).
  • Hardened via a 10-dimension adversarial review — all 22 confirmed findings fixed (incl. a real counting bug where a laugh resuming after a long gap could merge into the previous one), with regressions.
  • Native app: compiled + packaged by the macOS CI in this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KqdoJDEr32ACRuWGV8q12V

claude added 3 commits July 12, 2026 12:41
A privacy-first app that runs always-on on a Mac mini, listens through a
living-room mic, detects laughter, counts distinct laugh episodes, notes who
laughed, and logs each one — improving over time from one-tap feedback.

Core (stdlib-only, fully tested):
- Hysteresis state machine that collapses per-frame laughter scores into
  discrete laugh episodes (enter/exit thresholds, min-duration, merge-gap).
- SQLite + JSONL storage with speaker/clip/label fields and a migration.
- Feedback loop: "I just laughed" confirms a catch or logs a miss; relabel
  false positives and correct who laughed. CLI + phone-friendly web dashboard
  with a big tap button, live indication, and per-laugh controls.
- Short audio clip saved per laugh (to improve accuracy over time).
- Who-laughed attribution: cosine-similarity classifier over enrolled voice
  profiles (pure, tested); ECAPA embedding model optional.
- Stats: today/week/streaks, who-breakdown, detection health, hour histogram.
- launchd service generator for always-on operation; devices listing.

Optional extras (lazily imported, not needed for the core or tests):
- [yamnet]: real-time detection via Google's YAMNet + sounddevice mic capture.
- [speaker]: ECAPA-TDNN speaker embeddings.
- Experimental jrgillick robust-detector adapter.

63 tests pass with zero third-party dependencies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqdoJDEr32ACRuWGV8q12V
Correctness:
- counter: close a stale episode independently of the current frame's
  loudness, so a laugh resuming with a loud frame after a > merge_gap silence
  (or after dropped/batched frames during silence) is a NEW laugh, not merged
  into the previous one. Previously this silently undercounted. + regressions.
- live mic path: feed the counter its true per-buffer cadence (window_seconds)
  instead of the synthetic hop, so durations and the min_duration cutoff are
  correct when listening.
- simulate: validate inputs and reserve each laugh's full footprint per slot so
  synthetic timestamps stay monotonic; friendly errors for -n 0 and
  too-many-laughs-for-the-window (previously crashed and leaked the DB handle).

Robustness:
- listen: a clip-save/log I/O error or a backward wall-clock step (NTP) no
  longer kills the always-on listener; mic timestamps are clamped monotonic.
- storage: migration tolerates a concurrent ALTER (duplicate column) race.
- dashboard: require a JSON-object body (a literal null / non-dict no longer
  hangs the client or 500s), require the JSON content type (basic CSRF guard),
  and cap the request body.
- clips: resync the rolling buffer when wall-clock and sample-count diverge.
- speaker: tolerate a corrupt speakers.json and write it atomically; expand ~
  in the model cache path.
- listen --device: a numeric value now selects a device index, not a name.
- dashboard-recorded "missed" laughs now also land in the JSONL log.

Docs: correct the privacy note (one-time model download; clips only around
detected laughs), the stats/robust-detector/clip docstrings.

69 tests pass with zero third-party dependencies.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqdoJDEr32ACRuWGV8q12V
The clean, keep-the-Mac-tidy deployment: a tiny Swift menu-bar app that uses
only built-in macOS frameworks — no Python, TensorFlow, or Homebrew, nothing
left behind when deleted.

Native app (mac/, Swift Package, zero third-party deps):
- Laughter detection via Apple's built-in Sound Analysis classifier (no model
  download), matching several laugh classes so it doesn't overfit one style.
- The same hysteresis counting logic as the Python core, ported to Swift.
- Menu-bar 😄 icon showing today's count = the running indicator.
- A soft blip when a laugh is logged.
- Hands-free feedback: on-device Speech recognition listens for "I just
  laughed" and logs a miss, blipping twice to confirm; ⌘L / menu fallback.
- One shared mic tap fanned to both the detector and the recogniser.
- Logs to ~/Library/Application Support/LaughCounter/laughs.jsonl (no audio).

DMG build service (so nobody needs Xcode):
- build-app.sh / make-dmg.sh assemble and package the app.
- .github/workflows/build-macos-dmg.yml compiles on GitHub's macOS runners and
  uploads LaughCounter.dmg as an artifact (and attaches it to Releases on tag).

Docs:
- docs/DESIGN-AND-TRADEOFFS.md: deployment options (native vs bundled Python vs
  one-folder vs Docker), how missed laughs become improvements (threshold
  tuning now, a personalised Create ML model later), the who-laughed plan and
  its limits, the voice-command tradeoffs, roadmap, and privacy.
- mac/README.md: how to download the DMG, install, grant permissions, auto-start.
- Root README now presents the native app and the Python reference side by side.

Note: the Swift app is verified by the macOS CI build (it can't be compiled in
this Linux dev environment). Python reference unchanged — 69 tests still pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqdoJDEr32ACRuWGV8q12V
@missingbulb missingbulb changed the title LaughCounter: on-device laughter counter for the living room LaughCounter: native macOS app (+ Python reference) that counts your laughs Jul 12, 2026
Add a "Requirements (captured from the conversation)" table to
docs/DESIGN-AND-TRADEOFFS.md mapping every requirement — living-room scoping,
Google speaker untouched, Mac mini host, existing USB/webcam mic, feedback and
voice-mark, who-laughed, don't-overfit, keep-the-Mac-clean, DMG-by-service — to
where each is addressed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqdoJDEr32ACRuWGV8q12V
@missingbulb
missingbulb merged commit e0fa743 into main Jul 12, 2026
@missingbulb
missingbulb deleted the claude/home-laugh-counter-8unbu3 branch July 23, 2026 11:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants