Skip to content

macOS app: you-vs-TV attribution, mic-restart robustness, real-window durations - #15

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

macOS app: you-vs-TV attribution, mic-restart robustness, real-window durations#15
missingbulb merged 4 commits into
mainfrom
claude/laugh-detection-logging-lq957s

Conversation

@missingbulb

@missingbulb missingbulb commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Field-driven follow-ups after v0.2.0. The reported "no laughs detected" turned out to be a wedged USB webcam mic (a physical reconnect fixed it), not a code bug — but the investigation surfaced several real improvements, and a better direction for TV handling.

You-vs-TV attribution + two counters (#7 rethought). Context-class suppression couldn't separate TV laughter from the owner's — both are the laughter class. Replaced with a transparent hypothesis: the strongest produced-audio context class (music / speech / audience / instruments) becomes tv_signal; ≥ 0.3origin: "tv", else "me". On the sample data this cleanly split a 40s music/speech/crowd run (TV) from clean high-confidence laughter+giggling+breathing (owner). Menu bar shows 😄 N 📺 M; only your laughs blip; nothing is discarded — every laugh is logged with origin / tv_signal / origin_reason. It's a guess (a real laugh over TV music will misfire); loudness/proximity and eventual laugh-enrollment would sharpen it.

Mic-restart robustness (#6). The overhaul added engine stop/start cycles the original never did (wake / config-change / manual resume); aggressive AVAudioEngine restarts can wedge a USB mic. All triggers now go through one single-flight, rate-limited path with a settle delay between teardown and re-acquire — no overlap-restart, no rapid-cycle. Plus a guard so our own restart's AVAudioEngineConfigurationChange can't self-trigger a loop, and the analyzer is configured before audio starts.

Real-window durations, no double-count. SoundAnalysis emits ~3s windows every ~1.5s; mergeGap (was 1.0, below the hop) made each overlapping window its own laugh — double-counting and pinning duration at ~3s. Raised to 2.0 so one laugh is one episode with a varied (if coarse ~3s) duration; documented the coarseness honestly.

Detection quality + logging. Laugh keywords are now stems (giggl, chuckl, …) so the classifier's giggling counts. enterThreshold lowered (0.3 → 0.15) so near-misses log as candidates — later "I laughed" feedback can align to a nearby event. Trimmed the diagnostic heartbeat to one "first analysis result received" per stream.

Fixes #14. Refs #6, #7.

🤖 Generated with Claude Code

claude added 2 commits July 12, 2026 22:02
The audioConfigChanged handler restarted the engine on every
.AVAudioEngineConfigurationChange, but (re)starting AVAudioEngine posts that
same notification — so it re-triggered itself in a loop, and the engine never
ran long enough for SoundAnalysis to detect anything (no laughs, no log entries,
while the mic looked live).

Guard against it: set suppressConfigChange around our own start/stop and ignore
config-change notifications while it's set (released ~1s after the engine
settles). Genuine later device changes are still handled once the guard clears.
The handler now also touches state only on the main thread.

Fixes #14

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
The activity log ruled out the restart-loop theory (only manual restarts), so the
detection path itself produces nothing. Two changes:

- Configure the SoundAnalysis analyzer BEFORE starting the audio engine (split
  AudioHub into prepareFormat() + start(format:)), matching the original working
  order — the analyzer now exists before any buffer arrives, so no early buffers
  are dropped and analysis reliably starts. Keeps the post-prepare format
  validation that fixed the cold-start case.
- Add a detection heartbeat to the operational log: "first analysis result
  received" once per stream, then "detection heartbeat: N windows, max laugh
  score X" every 60 windows. This makes a "no laughs detected" report answerable
  from the log alone — it distinguishes no-results (analyzer silent) from
  results-with-low-scores (classifier not hearing laughter) from
  scores-but-no-episodes (counter issue).

Refs #14

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
@missingbulb missingbulb changed the title Fix restart loop that stopped laugh detection Audio restart robustness + detection heartbeat diagnostics Jul 12, 2026
claude added 2 commits July 12, 2026 22:25
Field-informed fixes after the v0.2.0 report (the "no laughs" turned out to be a
wedged USB mic, not the app):

- Restart hardening: all (re)start triggers (launch, wake, config-change, manual
  resume) now go through a single-flight, rate-limited path with a brief settle
  between teardown and re-acquire — the engine can't overlap-restart or
  rapid-cycle, which is what can wedge a USB mic.
- Merge overlapping analysis windows: the built-in classifier emits ~3s windows
  every ~1.5s, so mergeGap (was 1.0) sat below the hop — every window became its
  own laugh, double-counting one laugh and pinning duration at a constant ~3s.
  Raised to 2.0 so a laugh spanning windows is one episode with a varied (if
  coarse) duration. Documented duration as coarse in the README.
- Match gerund laugh classes: the classifier emits "giggling", but the keyword
  was "giggle" (no substring match), so giggles were shunted to context instead
  of counted. Keywords are now stems ("giggl", "chuckl", …).
- Trim the detection heartbeat to one "first analysis result received" line per
  stream (dropped the noisy periodic beat now that detection is confirmed).

Refs #14

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
Field data showed context-class *suppression* couldn't separate TV laughter from
the owner's (both are the "laughter" class). Replace it with a transparent
you-vs-TV hypothesis and keep every laugh:

- Attribution: the strongest "produced-audio" context class — music, speech,
  audience, instruments (tvKeywords expanded to cover them) — becomes the
  episode's tv_signal. tv_signal >= tvThreshold (0.3) → origin "tv", else "me".
  In the sample data this cleanly split a 40s music/speech/crowd run (TV) from
  clean high-confidence laughter+giggling+breathing (you). It's a guess, logged
  with its reason; nothing is discarded.
- Two menu-bar counters: "😄 N  📺 M" (you vs TV today). todayCount(origin:)
  splits the buckets; a reported miss counts as you. Only your laughs blip.
- Denser low-confidence logging: enterThreshold 0.3 -> 0.15 (exit 0.2 -> 0.1) so
  near-misses are logged as candidates too — later "I laughed" feedback can align
  to a nearby event instead of nothing.
- Log origin / tv_signal / origin_reason in the JSONL and the activity log;
  document the two counters and hypothesis in the README.

Removed the old onSuppressed/tvContextRatio gate (superseded by attribution).

Refs #7

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
@missingbulb missingbulb changed the title Audio restart robustness + detection heartbeat diagnostics macOS app: you-vs-TV attribution, mic-restart robustness, real-window durations Jul 12, 2026
@missingbulb
missingbulb merged commit 29b86be into main Jul 12, 2026
1 check passed
missingbulb pushed a commit that referenced this pull request Jul 12, 2026
CFBundleShortVersionString 0.2.0 -> 0.2.1 (build 3) to match the release tag,
following the #15 merge (attribution, mic-restart robustness, duration fix).

Refs #16

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ
missingbulb added a commit that referenced this pull request Jul 12, 2026
CFBundleShortVersionString 0.2.0 -> 0.2.1 (build 3) to match the release tag,
following the #15 merge (attribution, mic-restart robustness, duration fix).

Refs #16


Claude-Session: https://claude.ai/code/session_01ATTAHuLKSR8K7ZFawgbncJ

Co-authored-by: Claude <noreply@anthropic.com>
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.

Regression: no laughs detected after v0.2.0 — audio-config-change handler restart loop

2 participants