Native macOS app: laugh-log overhaul, audio robustness, TV de-confliction, app icon + release automation - #10
Merged
Conversation
Rework the detection→log pipeline in the native app: - LaughDetector now emits a full LaughObservation per analysis window: the laughter confidence and which laugh class won, the strongest TV-context class, a few top competing classes, and the window's REAL audio timing from SNClassificationResult.timeRange (anchored to the first buffer of the stream). - LaughCounter builds episodes from that real timing, so duration reflects the actual laugh instead of a fixed 0.5s, and records the laugh subtype + top context classes. Adds conservative TV/laugh-track suppression: an episode whose peak TV-context class out-scores its peak laughter is reported as suppressed, not counted (threshold tunable via tvContextRatio). - Store writes each JSONL line in a fixed field order (start_iso, label, start, end, peak, duration, mean, source, then type/context) via a small ordered-JSON renderer, and rounds confidence values (no more false float precision). - New AppLog: a rotating operational log (laughcounter.log) separate from laughs.jsonl, so lifecycle events and errors are self-diagnosing. Refs #5, #7 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
Make listening survive sleep/wake and app restarts, and give a manual recovery: - AudioHub reads the LIVE input format after preparing the engine and validates it (sampleRate/channels > 0), fixing the cold-restart bug where the detector was configured from a not-yet-ready 0 Hz format and silently produced nothing while the mic tap looked live. - AppDelegate gains one idempotent startListening() recovery path used on launch, after NSWorkspace.didWake, on AVAudioEngineConfigurationChange, and from a new "Resume listening" menu item; it pauses cleanly on willSleep. - Menu shows listening status and can open the activity log; every state change is written to the operational log. - "I just laughed" now records source "voice" for the spoken command and "button" for the menu/⌘L click, instead of labelling both as voice. - Wires the counter's onLaugh/onSuppressed to logging + chimes. Refs #5, #6, #7 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
The app had no icon and the DMG was an unstyled window. - scripts/gen-icon.py renders an original 😄-motif icon using only the Python stdlib (a zlib-based PNG encoder), so the art is reproducible and reviewable; the 1024² master is committed as Resources/AppIcon.png. - build-app.sh turns the master into a multi-resolution AppIcon.icns with sips/iconutil and bundles it; Info.plist references it via CFBundleIconFile. - make-dmg.sh bakes the icon in as the volume's icon (via .VolumeIcon.icns + SetFile), so the install window shows the 😄 — no fragile Finder scripting. Closes #8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
- New release-macos-dmg.yml: pushing a v* tag builds the app, packages the DMG, and publishes a GitHub Release with LaughCounter.dmg attached (idempotent — re-uploads if the release already exists). - build-macos-dmg.yml is now purely the branch CI artifact builder (dropped the release trigger/attach step, narrowed permissions to contents: read). - READMEs link the stable releases/latest/download/LaughCounter.dmg one-click download and document the tag-to-release flow and the icon generator. Closes #9 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
`timeRange` is a member of SNClassificationResult, not the `SNResult` protocol, so `result.timeRange` failed to compile. Use the already-cast `classification` value. Refs #5 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
- LaughCounter now tracks episodes from a lower enterThreshold and only *counts* those whose peak reaches countThreshold. Episodes in between are emitted via onCandidate and logged with label "candidate" (silent, not counted) so you can analyse near-misses and tune thresholds later. Tunable via logCandidates. - todayCount excludes "candidate" (as it does "rejected"). - Menu item reads "Start listening" when idle (one click to start after a restart) and "Restart listening" while active. - Document the laugh-log fields, labels, sub-threshold logging, and TV suppression / activity log in mac/README.md. Refs #5, #7 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
This was referenced Jul 12, 2026
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.
Addresses a batch of issues in the native menu-bar app (
mac/), agreed on with the owner. All Swift compiles on the macOS CI runner (no Swift toolchain in the dev environment).What & why
Laugh log overhaul — Closes #5
start_iso, label, start, end, peak, duration, mean, source(+type/contextwhen present), via a small ordered-JSON renderer (JSONSerializationcan't guarantee key order).SNClassificationResult.timeRange(real audio time) instead of wall-clock, so a laugh reports its actual length rather than the fixed 0.5s analysis-window floor.peak/meanno longer logged at false float precision.type) and the top competing non-laugh classes (context), free from the same classifier result.laughcounter.log(separate fromlaughs.jsonl) records lifecycle events + errors, so "it stopped" is self-diagnosing. Openable from the menu.Audio lifecycle robustness — Closes #6
startListening()recovery path, triggered on launch,NSWorkspace.didWake,AVAudioEngineConfigurationChange, and a new "Resume listening" menu item; pauses cleanly onwillSleep.sampleRate/channels > 0), instead of building the analyzer from a not-yet-ready 0 Hz format that silently produced no scores while the mic looked live.TV de-confliction — Closes #7
tvContextRatiois tunable. No new model.source: voicevs button (part of #5)source:"voice"; the menu item / ⌘L logssource:"button"— previously both werevoice.App icon + DMG — Closes #8
scripts/gen-icon.py, pure Python stdlib PNG encoder — reproducible & reviewable). Committed 1024² master → multi-resolution.icnsat build time viasips/iconutil. Also used as the DMG volume icon.Release automation + README — Closes #9
release-macos-dmg.yml: pushing av*tag builds, packages, and publishes a GitHub Release withLaughCounter.dmgattached (idempotent).build-macos-dmg.ymlbecomes the pure branch CI artifact builder.releases/latest/download/LaughCounter.dmg.Verification
bash -nclean on the build/DMG scripts;ruffclean on the generator.🤖 Generated with Claude Code
Generated by Claude Code