Skip to content

Native macOS app: laugh-log overhaul, audio robustness, TV de-confliction, app icon + release automation - #10

Merged
missingbulb merged 6 commits into
mainfrom
claude/laugh-detection-logging-lq957s
Jul 12, 2026
Merged

Native macOS app: laugh-log overhaul, audio robustness, TV de-confliction, app icon + release automation#10
missingbulb merged 6 commits into
mainfrom
claude/laugh-detection-logging-lq957s

Conversation

@missingbulb

Copy link
Copy Markdown
Owner

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

  • Fixed field order every line: start_iso, label, start, end, peak, duration, mean, source (+ type/context when present), via a small ordered-JSON renderer (JSONSerialization can't guarantee key order).
  • Real durations — episodes are now timed from 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.
  • Rounded confidencespeak/mean no longer logged at false float precision.
  • Richer data — logs the winning laugh subtype (type) and the top competing non-laugh classes (context), free from the same classifier result.
  • Operational log — new rotating laughcounter.log (separate from laughs.jsonl) records lifecycle events + errors, so "it stopped" is self-diagnosing. Openable from the menu.

Audio lifecycle robustness — Closes #6

  • One idempotent startListening() recovery path, triggered on launch, NSWorkspace.didWake, AVAudioEngineConfigurationChange, and a new "Resume listening" menu item; pauses cleanly on willSleep.
  • Fixes the cold-restart bug: the input format is now read after the engine is prepared and validated (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

  • Context-class gating (the approach chosen): an episode whose peak TV-context class (television/crowd/applause/cheering/music/…) out-scores its peak laughter is suppressed rather than counted, and the reason is written to the operational log. Threshold tvContextRatio is tunable. No new model.

source: voice vs button (part of #5)

  • The spoken command logs source:"voice"; the menu item / ⌘L logs source:"button" — previously both were voice.

App icon + DMG — Closes #8

  • Original 😄-motif icon generated from code (scripts/gen-icon.py, pure Python stdlib PNG encoder — reproducible & reviewable). Committed 1024² master → multi-resolution .icns at build time via sips/iconutil. Also used as the DMG volume icon.

Release automation + README — Closes #9

  • New release-macos-dmg.yml: pushing a v* tag builds, packages, and publishes a GitHub Release with LaughCounter.dmg attached (idempotent). build-macos-dmg.yml becomes the pure branch CI artifact builder.
  • READMEs now link the stable one-click download: releases/latest/download/LaughCounter.dmg.

Verification

  • Icon rendered and visually confirmed; generator is deterministic (byte-identical on re-run).
  • Workflow YAML parses; bash -n clean on the build/DMG scripts; ruff clean on the generator.
  • Swift changes build on the macOS CI runner (this dev env has no Swift toolchain). The Build macOS DMG workflow on this PR exercises the full app + icon + DMG path.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 6 commits July 12, 2026 21:28
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment