LaughCounter: native macOS app (+ Python reference) that counts your laughs - #1
Merged
Merged
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
~/Library/Application Support/LaughCounter/laughs.jsonl— no audio stored.You never touch Xcode: GitHub Actions (
.github/workflows/build-macos-dmg.yml) compiles the app on a macOS runner and produces a downloadableLaughCounter.dmg(build artifact, and attached to Releases on a tag). Install = drag to Applications, right-click→Open once, approve Microphone + Speech. Seemac/README.md.🐍 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
simulatemode. 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.mddocuments 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
🤖 Generated with Claude Code
https://claude.ai/code/session_01KqdoJDEr32ACRuWGV8q12V