TL;DR
First stable release since v0.4.1 (4 weeks, 372 commits). Headline changes:
- Teams 2.x silent recordings fixed — multi-PID audio tap captures all Electron helper processes, not just the shell PID.
- Broken-mic detection — proactive zero-signal probe at launch and on app activation; red badge appears before you'd ever hit "Record" with a dead mic.
- Asymmetric-silence indicator — menu-bar icon tints red on the side (mic top half / app bottom half) that stopped delivering audio mid-recording.
- Speaker-naming dialog hardening — 750 ms keyboard-grace period prevents a stray Enter/Esc right after the meeting from auto-confirming "Speaker 1 / Speaker 2" placeholders. Hit ~19 % of historical sessions.
- Off-main pipeline I/O — recoverOrphans, processed-recordings migration, and snapshot writes moved off the main actor; no more UI stalls on long log directories.
- macOS notifications + verbose diagnostics — per-PID tap targets, output-device changes, and 5-second RMS streams for both channels are now logged under
com.meetingtranscriber.audiotap.
Plus a refreshed README with an animated end-to-end pipeline hero, a Mermaid "How it works" diagram, and a Homebrew one-line install above the fold.
MeetingTranscriber 0.5.1
Installation
Via Homebrew (recommended):
brew tap pasrom/meeting-transcriber
brew install --cask meeting-transcriberManual:
- Download the DMG below
- Mount it and drag MeetingTranscriber to Applications
SHA256
2b20a85ad51a7ac0bf5b5087e624516d5fc5137f3bd646c2552af30954817935
What's Changed
🚀 Features
- feat: add App Store build variant with APPSTORE flag by @pasrom in #29
- feat: use PowerAssertionDetector for meeting detection by @pasrom in #30
- feat(app): add custom protocol output directory by @pasrom in #34
- feat(app): add elapsed time display to pipeline stages by @pasrom in #36
- feat: add NVIDIA Parakeet TDT v3 as alternative ASR engine by @pasrom in #51
- feat: add Qwen3-ASR as third transcription engine (macOS 15+) by @pasrom in #52
- feat(app): pipeline warnings — surface non-fatal issues in menu bar by @pasrom in #60
- feat(app): merge consecutive same-speaker segments + pipeline warnings integration by @pasrom in #61
- feat(app): configurable protocol output language by @pasrom in #62
- feat(app): None LLM provider + graceful protocol generation fallback by @pasrom in #63
- feat(app): add VAD preprocessing via FluidAudio Silero v6 by @pasrom in #76
- feat(app): add custom vocabulary support for Parakeet CTC boosting by @pasrom in #77
- feat(app): add Sortformer diarizer mode for overlap-aware speaker diarization by @pasrom in #78
- feat(app): permission health check — detect broken TCC entries by @pasrom in #90
- feat(build): add Homebrew beta cask for pre-releases by @pasrom in #94
- feat(app): add audioDebugLogging setting for forensic capture diagnostics by @pasrom in #117
- feat(audiotap): expand audioDebugLogging with mic + device-detail + tap-format by @pasrom in #118
- feat(app): surface known speaker names as quick-pick chips in naming dialog by @pasrom in #123
- feat(app): track speaker recency for picker ranking by @pasrom in #124
- feat(app): centroid-based speaker matching + embedding quality filter by @pasrom in #125
- feat(app): log speaker recognition outcomes to JSONL by @pasrom in #126
- feat(app): record top-3 match candidates per recognition event by @pasrom in #129
- feat(app): show recognition stats in Settings by @pasrom in #130
- feat(app): manage known voices via Settings by @pasrom in #131
- feat(app): live-filter chip rows in speaker naming dialog by @pasrom in #132
- feat(app): enroll speakers from existing recordings by @pasrom in #133
- feat: allow re-opening speaker naming dialog after dismissal by @pasrom in #108
- feat(app): debug RPC server skeleton with /state endpoint by @pasrom in #136
- feat: record-only mode by @pasrom in #144
- feat(app): persistent toggle for debug RPC server in Settings → Advanced by @pasrom in #147
- feat: diagnostic logging end-to-end (toggle + export + redaction) by @pasrom in #152
- feat(app): persistent file-based diagnostic logs (~30 day retention) by @pasrom in #154
- feat(app): RPC routes for speaker DB testing by @pasrom in #161
- feat(app): expose engine state in /state RPC for live observability by @pasrom in #207
- feat(test): add WER/DER quality regression suite scaffolding by @pasrom in #209
- feat(app): expose lastJob in RPC state snapshot by @pasrom in #217
- feat(e2e): add live-recording E2E (deployed app + RPC driver) by @pasrom in #216
- feat(rpc): /action/enqueueFile + chained reimport E2E by @pasrom in #248
- feat(app): expose Parakeet language picker by @pasrom in #272
- feat(app): import paired _app+_mic recordings as one dual-track job by @pasrom in #274
- feat(app): per-channel signal indicator by @pasrom in #286
- feat(app): detect symmetric-silence recordings (PR #286 follow-up) by @pasrom in #295
- feat(app): tap whole bundle PID tree for Electron meeting apps by @pasrom in #307
- feat(app): keyboard-grace period prevents accidental speaker-name confirms by @pasrom in #310
🐛 Bug Fixes
- fix(build): apply entitlements for App Store builds without notarization by @pasrom in #33
- fix(app): update Accessibility permission description by @pasrom in #35
- fix(app): fix menu bar icon staying inactive during file processing by @pasrom in #43
- fix(app): exclude manual recording from isWatching state by @pasrom in #44
- fix(app): show error message in job row by @pasrom in #45
- fix(app): mark recording as processed when job fails by @pasrom in #46
- fix(app): improve mic device change handling with testable restart policy by @pasrom in #58
- fix(app): hardening — crash prevention, thread safety, filename sanitization by @pasrom in #59
- fix: detect actual channel count to prevent Mickey Mouse with mono USB devices by @pasrom in #67
- fix(app): resolve SwiftFormat 0.60.1 lint violations by @pasrom in #75
- fix: harden USB audio sample rate detection by @pasrom in #65
- fix: correct sample rate detection for Bluetooth devices by @pasrom in #85
- fix(ci): scope RC changelog to previous RC instead of last stable release by @pasrom in #93
- fix: security hardening (path traversal, file permissions, shell quoting) by @pasrom in #97
- fix(app): add KMeans crash recovery for FluidDiarizer by @pasrom in #98
- fix(build): staple notarization ticket to .app and DMG by @pasrom in #91
- fix(app): prevent mix.wav 2x duration from device restart mid-recording by @pasrom in #101
- fix(app): use resampled audio for mix fallback when mic is unavailable by @pasrom in #105
- fix(app): guard against missing mic hardware by @pasrom in #107
- fix(audiotap+app): three teardown-safety bugs found in architecture review by @pasrom in #119
- fix(app): keep naming sidecar when window closes without confirm by @pasrom in #134
- fix(test): inject UserDefaults into AppSettings (parallel-safe) by @pasrom in #143
- fix(app): warn about missing speaker recognition in Sortformer mode by @pasrom in #146
- fix(app): debug RPC edge cases (env-var override + test pollution) by @pasrom in #149
- fix(app): keep Output Folder picker interactive in record-only mode by @pasrom in #151
- fix(app): SpeakerNamingView Confirm/Skip/Re-run unresponsive after multi-job switch by @pasrom in #156
- fix(app): cache knownSpeakerNames on PipelineQueue (closes #155) by @pasrom in #158
- fix(app): refresh PipelineQueue cache on speakers DB mutations by @pasrom in #159
- fix(app): reset Re-run guard when dialog re-presents after diarization by @pasrom in #162
- fix(app): also reset stepper + names when naming dialog re-presents by @pasrom in #163
- fix(app): match actual transcript format when re-applying speaker names by @pasrom in #164
- fix(app): lazy SpeakerMatcher init in Known Voices sheet by @pasrom in #167
- fix(app): hygiene batch — dead branch, lazy disk reads, migration cleanup, ID helper by @pasrom in #177
- fix(test): preserve fixture in ResamplingIntegrationTests pipeline tests by @pasrom in #176
- fix(app): preserve fractional precision in speaker-snippet sample range by @pasrom in #178
- fix(app): rotate persistent diagnostic log across day boundaries by @pasrom in #179
- fix(app): re-arm Re-run button after each late diarization cycle by @pasrom in #181
- fix(app): make speakers.json mutations race-free and gate RPC seeds by @pasrom in #182
- fix: RPC quick-wins — constant-time auth, Host allowlist, mt-cli timeouts by @pasrom in #184
- fix: small RPC + Settings hardening (PII-safe screenshots, off-main diagnostics, unique meeting slugs) by @pasrom in #185
- fix(app): split statRow to bring type-checking under 300ms threshold by @pasrom in #198
- fix(app): split statRow further to bring type-checking under 300ms by @pasrom in #200
- fix(app): share RecognitionStatsLog actor between pipeline and SettingsView by @pasrom in #201
- fix(app): open security scope on bookmark URL, not record-only child by @pasrom in #203
- fix(app): rotate RPC bearer token on settings toggle off → on by @pasrom in #202
- fix(app): auto-restart persistent log streamer on unexpected exit by @pasrom in #204
- fix(app): propagate runtime AppSettings changes to engine instances by @pasrom in #206
- fix(app): give up PersistentDiagnosticLog on instant-fail spawn (200 % CPU on macOS 26) by @pasrom in #218
- fix(app): fall back to app-only diarization when mic track has no speakers by @pasrom in #219
- fix(app): detach diagnostic-log readabilityHandler on subprocess EOF by @pasrom in #220
- fix(e2e): respect E2E_ENABLED flag so e2e.yml actually runs engine tests by @pasrom in #221
- fix(ci): restore keychain search list + dedup e2e runs by SHA by @pasrom in #238
- fix(ci): release.yml stops swallowing Homebrew tap update failures by @pasrom in #242
- fix(ci): install-developer-id prepends to keychain search list (don't drop mt-ci.keychain-db) by @pasrom in #241
- fix(app): add 22 languages to WhisperKit picker by @pasrom in #263
- fix(test): scope keychain delete to the one test that needs it by @pasrom in #282
- fix(test): drop racy isRunning precondition in PersistentDiagnosticLog test by @pasrom in #289
- fix(ci): align meeting-simulator binary path with e2e-app.sh (release) by @pasrom in #299
- fix(tools): point meeting-simulator's findFixture() at the real fixture path by @pasrom in #298
- fix(scripts): surface real failures in e2e-silent-recording instead of masking them by @pasrom in #301
- fix(scripts): detect dev .app crash in e2e-app.sh polling loop by @pasrom in #302
- fix(test): split testResamplePreservesSignalEnergy expression for stable type-check by @pasrom in #306
- fix(app): detect zero-signal mic callbacks as broken microphone by @pasrom in #308
🧹 Maintenance
- build(app): remove unnecessary entitlements by @pasrom in #26
- refactor(app): use sandbox-compatible paths in AppPaths by @pasrom in #28
- refactor(app): use macOS Keychain for secret storage by @pasrom in #27
- ci: add App Store build to release workflow by @pasrom in #31
- ci: use distinct DMG names for App Store build by @pasrom in #32
- perf(app): cache MenuBarIcon animation frames at startup by @pasrom in #38
- build(deps): bump actions/github-script from 7 to 8 by @dependabot[bot] in #39
- build(deps): bump github.com/fluidinference/fluidaudio from 0.12.3 to 0.12.4 in /app/MeetingTranscriber by @dependabot[bot] in #40
- build(deps): bump github.com/argmaxinc/whisperkit from 0.16.0 to 0.17.0 in /app/MeetingTranscriber by @dependabot[bot] in #41
- refactor(app): extract AppState ViewModel + BadgeKind.compute pure function by @pasrom in #49
- refactor(app): normalize all recorded audio to 16kHz at capture time by @pasrom in #50
- test(app): comprehensive test coverage improvements by @pasrom in #56
- test(app): add workflow integration tests for pipeline by @pasrom in #55
- test(app): add FFT-based frequency preservation tests for AudioMixer by @pasrom in #57
- chore(app): update dependencies to latest compatible versions by @pasrom in #64
- chore(app): update FluidAudio to 0.13.4 by @pasrom in #74
- build(deps): bump github.com/pointfreeco/swift-snapshot-testing from 1.19.1 to 1.19.2 in /app/MeetingTranscriber by @dependabot[bot] in #81
- build(deps): bump github.com/argmaxinc/whisperkit from 0.17.0 to 0.18.0 in /app/MeetingTranscriber by @dependabot[bot] in #80
- test: close critical test coverage gaps (+72 tests) by @pasrom in #86
- test(app): add missing SwiftUI view tests by @pasrom in #87
- test(app): add minor view test gap coverage by @pasrom in #88
- test(app): add E2E integration tests by @pasrom in #89
- build(deps): bump github.com/fluidinference/fluidaudio from 0.13.4 to 0.13.6 in /app/MeetingTranscriber by @dependabot[bot] in #92
- ci: harden Dependabot pipeline against signing-secret absence by @pasrom in #111
- build(deps): bump actions/github-script from 8 to 9 by @dependabot[bot] in #110
- build(deps): bump dependabot/fetch-metadata from 2 to 3 by @dependabot[bot] in #114
- refactor(audiotap): extract OutputDeviceChangeCoordinator as pure state machine by @pasrom in #120
- build(deps): bump github.com/fluidinference/fluidaudio from 0.13.6 to 0.14.3 in /app/MeetingTranscriber by @dependabot[bot] in #121
- chore(test): anonymize fixture names + drop unused q2 var by @pasrom in #135
- chore(docs): drop stale swift-architecture.md by @pasrom in #137
- test(snapshot): regenerate MenuBarIcon reference PNGs by @pasrom in #139
- refactor(app): split SettingsView into 6 topic-grouped tabs by @pasrom in #141
- test: harden two test-isolation bugs surfaced by --parallel by @pasrom in #140
- ci: parallel Swift tests with cached ML models by @pasrom in #145
- ci: skip macOS jobs and DMG build on docs-only PRs by @pasrom in #148
- ci(release): mention beta channel in RC release notes by @pasrom in #150
- test(app): regression tests for Skip and Re-run multi-job switching by @pasrom in #157
- perf(app): cache subview sizes in ChipFlowLayout by @pasrom in #168
- ci: cancel run when any job fails by @pasrom in #172
- chore: gitignore docs/plans/.local for personal scratch by @pasrom in #173
- ci: set predicate-quantifier=every for paths-filter by @pasrom in #175
- perf(app): scope menu-bar timer to sub-view + skip ticks for static badges by @pasrom in #169
- build(deps): bump github.com/fluidinference/fluidaudio from 0.14.3 to 0.14.4 in /app/MeetingTranscriber by @dependabot[bot] in #170
- build(deps): bump github.com/argmaxinc/whisperkit from 0.18.0 to 1.0.0 in /app/MeetingTranscriber by @dependabot[bot] in #171
- build: zero compiler warnings + warnings-as-errors gate by @pasrom in #187
- ci(lint): close lint-coverage gap on mt-cli + audiotap tests by @pasrom in #188
- test: integration coverage for RPC Host allowlist, slug uniqueness, mt-cli timeout by @pasrom in #186
- build: warn on slow type-check sites (>300ms) by @pasrom in #190
- build: full Swift 6 language mode (sources strict, tests v5-pinned) by @pasrom in #191
- build(ci): pre-push release-parity check by @pasrom in #194
- build(app): compile-perf threshold 500ms → 300ms by @pasrom in #193
- test(app): factor out tmpDir setUp/tearDown into helper by @pasrom in #195
- test(app): add makeTempFile + fixtureURL helpers, sweep ~30 sites by @pasrom in #196
- ci: weekly build-performance tracking workflow by @pasrom in #197
- ci: enable Thread- + AddressSanitizer in CI + fix race TSan caught by @pasrom in #208
- ci(workflows): add quality-baseline job (push/cron/dispatch only) by @pasrom in #211
- ci(workflows): extract Swift-test setup into composite action by @pasrom in #212
- ci(workflows): split background QA into separate workflow by @pasrom in #213
- ci(workflows): hygiene sweep on test matrices and pre-warm by @pasrom in #214
- ci(e2e): pin DEVELOPER_DIR for self-hosted Mac runners by @pasrom in #215
- test(quality): add diarization DER regression suite by @pasrom in #223
- test(e2e): expand multi-format ingestion to all 7 fixture formats by @pasrom in #224
- test(e2e): cover record-only path from handleMeeting with real fixture WAV by @pasrom in #225
- test(e2e): VAD trim + engine + remap chain coverage by @pasrom in #226
- ci(appstore): smoke-launch the App Store variant on push + nightly by @pasrom in #227
- ci(e2e): --two-meetings flag for cooldown + state-reset coverage by @pasrom in #228
- test(quality): add Parakeet WER coverage + hoist shared fixture helper by @pasrom in #229
- ci: extract 4 composite actions + centralize paths-filter by @pasrom in #231
- chore(ci): align actions/upload-artifact on v7 by @pasrom in #232
- ci(e2e-app): pin live-recording job to audio-labeled runner by @pasrom in #233
- ci(qa): move TSan + ASan matrix to self-hosted Mac mini by @pasrom in #234
- ci(e2e): trigger on push to main so tag gate has data by @pasrom in #236
- ci: declare stable-tag protection ruleset by @pasrom in #237
- ci(e2e-app): drop paths-filter so push-event check-runs land on every main SHA by @pasrom in #240
- chore(ci): drop redundant tags trigger from e2e.yml by @pasrom in #243
- ci(install-developer-id): fail fast on keychain with no valid signing identity by @pasrom in #244
- ci(setup-swift-test): pre-warm fails fast on rename or swift-test crash by @pasrom in #245
- ci(security): wipe imported .p12 + SHA-pin ncipollo/release-action by @pasrom in #246
- test(e2e-app): cover record-only mode with sidecar+WAV assertions by @pasrom in #247
- ci: tighten job timeouts + step-level limits for timed_out visibility by @pasrom in #249
- ci(e2e-app): fail fast with actionable error on Fast User Switching by @pasrom in #252
- ci: add code coverage reporting via Codecov by @pasrom in #251
- ci(codecov): tighten ignores + upload audiotap as separate flag by @pasrom in #255
- chore(deps): bump github.com/fluidinference/fluidaudio from 0.14.4 to 0.14.5 in /app/MeetingTranscriber by @dependabot[bot] in #257
- ci(quality): disable mt-ci keychain auto-lock during sanitizer runs by @pasrom in #259
- test(app): cover ClaudeCLIProtocolGenerator parsing + path resolution by @pasrom in #261
- ci(quality): bump SPM cache-key to invalidate corrupted cache by @pasrom in #262
- test(app): cover VoiceEnrollmentView (0%→92%) + extract pure logic by @pasrom in #260
- test(app): expand SpeakerNamingView coverage from 56% to 75% by @pasrom in #265
- test(app): cover makeSpeakerDBActions closures via temp-path SpeakerMatcher by @pasrom in #264
- test(app): expand KnownVoicesView coverage from 53% to 57% by @pasrom in #266
- ci(mini): atomic keychain-prepend helper + migrate all call sites by @pasrom in #268
- test(app): extract longestSegment helper + cover infinite-container layout by @pasrom in #267
- test(app): extract 3 pure helpers from KnownVoicesView + cover them by @pasrom in #269
- test(app): extract OutputSettings display helpers + fix path-prefix bug by @pasrom in #270
- ci(e2e-app): chain record-only + reimport via --reimport-latest by @pasrom in #273
- refactor(app): extract WatchLoopEndPolicy as pure decision function by @pasrom in #275
- refactor(app): inject Clock into WatchLoop for deterministic async tests by @pasrom in #276
- refactor(app): introduce WatchLoopState snapshot value type by @pasrom in #277
- refactor(app): funnel WatchLoop mutations through update(_:) by @pasrom in #278
- refactor(app): extract ManualRecordingMonitorPolicy as pure decision function by @pasrom in #279
- ci(mini): exclude ~/Library/Keychains from Spotlight via .metadata_never_index by @pasrom in #280
- refactor(app): extract PipelineSnapshot as pure I/O helper by @pasrom in #281
- test(app): isolate AppStateTests pipeline-queue snapshot per test method by @pasrom in #283
- perf(app): dispatch PipelineQueue.saveSnapshot off the main actor by @pasrom in #284
- perf(app): move recoverOrphanedRecordings dir scan off the main actor by @pasrom in #285
- ci(e2e-app): add codesign pre-flight smoke test to catch trustd drift early by @pasrom in #287
- perf(app): move migrateProcessedRecordings off the main actor by @pasrom in #288
- test(app): cover ClaudeCLI subprocess builders via three pure helpers by @pasrom in #290
- refactor(app): extract ProtocolGenerator.buildSystemPrompt shared by both LLM generators by @pasrom in #291
- test(app): cover AppState.makeProtocolGenerator .none provider branch by @pasrom in #292
- refactor(scripts): extract shared e2e helpers into scripts/lib/ by @pasrom in #296
- ci(e2e-app): run silent-recording detector e2e as additional lane by @pasrom in #297
- perf(scripts): collapse 3× jq invocations per polling tick into one @TSV pass by @pasrom in #304
- refactor(scripts): add snapshot_default + restore_float_default helpers by @pasrom in #305
- test(audiotap): cover Helpers.swift end-to-end (9% → ~98%) by @pasrom in #309
- test(app): extract + cover three ClaudeCLI pure helpers by @pasrom in #311
- chore(deps): bump github.com/fluidinference/fluidaudio from 0.14.5 to 0.14.7 in /app/MeetingTranscriber by @dependabot[bot] in #312
- test(app): extract + cover three FFmpegHelper pure helpers by @pasrom in #313
- test(app): extract Parakeet token-grouping + Qwen3 chunking into testable sibling types by @pasrom in #314
- test: cover DebugRMSReporter.tick() + PIDTranslation + NotificationManager.notificationContent by @pasrom in #315
📖 Documentation
- docs(docs): add branch protection rules to CLAUDE.md by @pasrom in #25
- docs: update WhisperKit dual-source API references by @pasrom in #37
- docs: complete architecture docs for Parakeet + Qwen3 engines by @pasrom in #53
- docs: update README for Parakeet and Qwen3 transcription engines by @pasrom in #54
- docs: update documentation to match current codebase by @pasrom in #68
- docs: update CLAUDE.md to match current codebase by @pasrom in #71
- docs: remove non-existent generate_test_audio_10speakers.sh from CLAUDE.md by @pasrom in #72
- docs: update documentation to match current codebase by @pasrom in #73
- docs: permission problem badge + ignore .worktrees by @pasrom in #95
- docs: document Homebrew beta cask by @pasrom in #96
- docs: add PermissionHealthCheck.swift to project structure and architecture notes by @pasrom in #102
- docs: update documentation to match current codebase by @pasrom in #103
- docs(rpc): document debug RPC + restore pipeline diagram by @pasrom in #138
- docs: update documentation to match current codebase by @pasrom in #142
- docs(arch): no expensive work in SwiftUI hot paths by @pasrom in #160
- docs(claude): document docs/plans/.local convention by @pasrom in #174
- docs(claude): forbid referencing .local/ content in shared artifacts by @pasrom in #210
- docs(readme): add Testing & CI section highlighting on-device E2E by @pasrom in #222
- docs(readme): add status badges for E2E + quality + App Store workflows by @pasrom in #250
- docs: update documentation to match current codebase by @pasrom in #153
- docs(arch): document menu-bar state animations + overlay precedence by @pasrom in #293
- docs(readme): document per-channel asymmetric-silence indicator with GIFs by @pasrom in #294
- docs(readme): refresh hero with end-to-end pipeline GIF by @pasrom in #316
- docs(readme): render "How it works" pipeline as Mermaid by @pasrom in #317
Other Changes
- concurrency(test): migrate test target to Swift 6 by @pasrom in #192
- concurrency(app): preemptively guard remaining AV/AX imports by @pasrom in #199
- Revert: ci(e2e) concurrency group back to github.ref by @pasrom in #239
Full Changelog: v0.4.1...v0.5.1