What's new in v2.4.0
- Download improvements. We revamped how the speech and polish models download, and where they are stored on disk. Downloads are faster, resume on their own if your connection drops, and can be cancelled if they stall.
- Proper Bluetooth and USB audio support. EnviousWispr now properly supports Bluetooth and USB microphones, including on Mac mini. We also rebuilt the entire infrastructure around our sound capture engine to be far more robust, so recording no longer fails quietly when a microphone misbehaves.
- Customizable sound cues. Optional chimes when recording starts and stops. Twelve to choose from, soft to loud. Off by default.
- Choose your recording pill location. Move the recording pill to the top or bottom of your screen and it remembers.
- Larger menu bar icon. The menu bar icon is now larger and easier to spot.
- Open Source Licenses. A new screen in Settings so the open source licenses EnviousWispr is built on are always available to read in the app.
- All OpenAI models now properly supported. OpenAI changed how its API calls work. We fixed them so their full library of models can be used for cloud polishing. This also includes smaller fixes, such as making sure you are notified if you forget to enter your API key.
- Timer bug addressed. The timer no longer resets when you swap between screens.
Full technical changelog
What's Changed
- docs(readme): clarify EG-1 model is not open source by @saurabhav88 in #1369
- feat(release): auto-trigger Homebrew cask bump on release (#1368) by @saurabhav88 in #1370
- refactor(llm): converge EG-1 polish delivery onto the shared engine (#1363) by @saurabhav88 in #1384
- feat(audio): observe selected-vs-effective mic transport per recording (#1376) by @saurabhav88 in #1387
- fix(build): 302-redirect model downloads to the edge-cached R2 path (#1405) by @saurabhav88 in #1406
- feat(audio): capture-backend bake-off control plane + candidates A/C (#1377) by @saurabhav88 in #1407
- feat(asr): same-source download retry + wedge-cancel for model delivery (#1405 Phase 2, #1371) by @saurabhav88 in #1410
- feat(audio): reinstate capture-backend bake-off slice 2b — candidate D (#1377) by @saurabhav88 in #1418
- feat(llm): teach EG-1's delivery manifest to support sharded files (#1417) by @saurabhav88 in #1420
- feat(llm): ship EG-1's real sharded delivery manifest (#1417) by @saurabhav88 in #1425
- Honor system default input for auto capture by @saurabhav88 in #1427
- fix(pipeline): route confident non-text paste refusals to breadcrumb, not Sentry alert by @saurabhav88 in #1436
- feat(reports): add daily performance report worker by @saurabhav88 in #1437
- fix(pipeline): route AFM polish cancellation to a breadcrumb, not a Sentry alert by @saurabhav88 in #1443
- fix(audio): AirPods degraded-capture fix — real format stabilization, degraded-lead salvage, fleet telemetry by @saurabhav88 in #1444
- fix(asr): remove install-phase false wedge, fix load cancel/death contract, add install Cancel (#1388) by @saurabhav88 in #1456
- chore(dev-tooling): point validate-pr.sh Codex stub text at codex-5.6-sol by @saurabhav88 in #1459
- fix(pipeline): alert only on our own polish bugs, count the rest (#1446) by @saurabhav88 in #1460
- fix(audio): deliver the dictation immediately when the microphone dies mid-recording by @saurabhav88 in #1465
- fix(audio): install input tap with hardware format, not stale node output format (#1353) by @saurabhav88 in #1473
- fix(observability): worker becomes a post/suppress notifier, TIK is sole ticket writer (#1470) by @saurabhav88 in #1476
- fix(asr): honor prior-text flag in S5 benchmark by @saurabhav88 in #1474
- feat(observability): show Dev vs Release build type on every Sentry alert (#1470) by @saurabhav88 in #1477
- docs(llm): correct PolishMode routing comment for #1255 cloud-fixed path by @saurabhav88 in #1478
- fix(worker): clean version + real OS/device in Sentry Discord alerts by @saurabhav88 in #1479
- feat(release): branded DMG install window; relocate GPL/notice files into app bundle (#1486) by @saurabhav88 in #1488
- fix(audio): harden Bluetooth settle check — validity guard + post-rebuild re-verify (#1445) by @saurabhav88 in #1489
- docs(readme): align EG-1 training restriction with license by @saurabhav88 in #1469
- content(website): improve trust and attribution by @saurabhav88 in #1491
- fix(update): one-click recovery from App Translocation (#1451) by @saurabhav88 in #1490
- feat(ui): lead each What's New item with its specific improvement by @saurabhav88 in #1494
- fix(pipeline): end filler-only captures quietly instead of a failed pill (#1358) by @saurabhav88 in #1496
- feat(ui): Bluetooth cold-start awareness card + Microphone settings guide (#1480) by @saurabhav88 in #1495
- test(pipeline): gate DEBUG-hook #1358 recovery tests for the release lane by @saurabhav88 in #1498
- feat(llm): retire the shipped EG-1 monolith and rehome fresh installs (#1386 PR-1) by @saurabhav88 in #1500
- Fix flaky post-merge Release tests by @saurabhav88 in #1502
- fix(telemetry): stage-classify Sparkle update failures so install_failed stops over-firing by @saurabhav88 in #1503
- fix(llm): per-model OpenAI request shape + self-healing param strip (#1330) by @saurabhav88 in #1505
- test(audio): dead-mic proof bench (Part of #1317) by @saurabhav88 in #1504
- fix(ui): Settings survives Check for Updates; recording timer stops resetting mid-session by @saurabhav88 in #1507
- refactor(audio): move dead-air classifier to Core, add stall failure-mode field by @saurabhav88 in #1508
- fix(llm): dedupe output_classifier_load_failed Sentry alerts to at most once per process by @saurabhav88 in #1509
- fix(vad): bundle Silero VAD model instead of downloading it at record-start by @saurabhav88 in #1510
- feat(audio): detect the mic-harness all-zero glitch and end the take honestly by @saurabhav88 in #1512
- feat(audio): auto-heal the mic-harness all-zero glitch, and say so (Closes #1317) by @saurabhav88 in #1518
- test(uat): move the #1317 dead-mic proof bench offline (Part of #1317) by @saurabhav88 in #1519
- refactor(audio): delete the dead capture-session backend, and pin the Sentry identity that made it unsafe (Closes #1524) by @saurabhav88 in #1527
- test(audio): fix flaky ResolvedRouteTransportsGridTests via injectable device seam by @saurabhav88 in #1531
- feat(audio): measure the bound mic's native channel count as fleet telemetry by @saurabhav88 in #1532
- refactor(core): move StableSentryErrorIdentity to Core (Part of #1525) by @saurabhav88 in #1534
- refactor(services): pin ModelLoadWatchdog.WedgeError's Sentry identity (Part of #1525) by @saurabhav88 in #1535
- refactor(services): pin RecoveryReplayError + RecoveryArmError's Sentry identity by @saurabhav88 in #1537
- refactor(audio): route every device to the HAL backend, delete AVAudioEngineSource (Part of #1511) by @saurabhav88 in #1536
- test(audio): gate DEBUG-only proxy route-shape test under #if DEBUG (Part of #1511) by @saurabhav88 in #1538
- ci(workflow): compile Release test targets on the PR gate, not just post-merge by @saurabhav88 in #1541
- fix(audio): delete bufferSeenThisSession race + add HAL teardown quiescence tripwire by @saurabhav88 in #1542
- refactor(audio): collapse the audio-capture XPC boundary in-process (Closes #1543) by @saurabhav88 in #1546
- refactor(services): pin OutputClassifierError's Sentry identity (Part of #1525) by @saurabhav88 in #1547
- refactor(services): pin LLMError's Sentry identity (Part of #1525) by @saurabhav88 in #1549
- refactor(services): pin KeyStoreError's Sentry identity (Part of #1525) by @saurabhav88 in #1550
- refactor(services): pin the ASR/XPC error cluster's Sentry identity (Part of #1525) by @saurabhav88 in #1551
- feat(audio): acknowledge the press immediately — remove the first-buffer transport gate (Part of #1548) by @saurabhav88 in #1552
- fix(sentry-triage): stop TIK from reopening closed issues on evidence it already explained by @saurabhav88 in #1554
- fix(release): make appcast delivery recoverable by @saurabhav88 in #1553
- fix(audio): retire a dead microphone source on a zero-signal take (Part of #1511) (Part of #1520) by @saurabhav88 in #1555
- chore(telemetry): retire duplicate metric.pipeline.e2e_seconds event by @saurabhav88 in #1560
- feat(ui): add in-app Open Source Licenses screen + download-page source link by @saurabhav88 in #1561
- fix(paste): distinguish confirmed-absent from unreadable menu-bar/enabled-state AX reads by @saurabhav88 in #1559
- refactor(ui): one narrator for terminal error + interruption pill copy (Part of #1511) by @saurabhav88 in #1563
- feat(pipeline): consolidate polish-skip telemetry, silence LLMPolishStep during recovery by @saurabhav88 in #1565
- refactor(ui): one narrator for the processing/phase-label family (Part of #1511) by @saurabhav88 in #1566
- refactor(ui): one narrator for the post-completion + advisory warning family (Part of #1511) by @saurabhav88 in #1568
- refactor(ui): one narrator authors every recording-lifecycle status word (Closes #1569) (Part of #1511) by @saurabhav88 in #1571
- feat(worker): Phase 10 product-health telemetry alerts (#1179) by @saurabhav88 in #1573
- feat(settings): warn proactively when a cloud provider has no working key (#1455) by @saurabhav88 in #1574
- fix(ui): bigger menu bar icon, recenter it, drop audio-reactive shrink by @saurabhav88 in #1577
- refactor(recovery): one deletion owner + root-cause telemetry (Part of #1464) by @saurabhav88 in #1575
- feat(settings): remembered top/bottom position for the recording pill by @saurabhav88 in #1583
- refactor(delivery): extract the legacy-retirement mechanism EG-1 and WhisperKit both need (Part of #1386) by @saurabhav88 in #1584
- fix(worker): stop clearing the sentry-triage throttle on resolved/archived by @saurabhav88 in #1586
- fix(telemetry): stop hand-spelling asr_backend, read it from the enum by @saurabhav88 in #1587
- fix(worker): cap daily-report PostHog concurrency, add per-query retry by @saurabhav88 in #1590
- test(hotkey): make suspend/resume idempotence test assert real state by @saurabhav88 in #1605
- test(pipeline): delete 4 KernelDictationDriverSurfaceTests that asserted nothing by @saurabhav88 in #1608
- feat(settings): optional recording start/stop sound cues (#1342) by @saurabhav88 in #1609
- feat(asr): converge the multilingual engine onto the owned delivery engine via retire-and-refetch (Part of #1386) by @saurabhav88 in #1606
- test(pipeline): strengthen cancelPendingUnloadIdempotent to prove real cancellation by @saurabhav88 in #1611
- fix(llm): make .silent telemetry provably discard instead of untestable by @saurabhav88 in #1610
- fix(llm): extract platform-independent preflight decision for Apple Intelligence by @saurabhav88 in #1612
- test(settings): assert exact provider monogram values, not just non-empty by @saurabhav88 in #1614
- test(asr): assert the exact language-flip event, not a loose count + partial check by @saurabhav88 in #1615
- test(app): assert engine.switch_superseded's exact from/to payload by @saurabhav88 in #1616
- ci: measure and report code coverage on the debug lane by @saurabhav88 in #1617
- test(app): assert engine.switch_failed and engine.warm exact payloads (#1622) by @saurabhav88 in #1630
- test(pipeline): match each identity-stamp consumer separately, not a raw text count (#1623) by @saurabhav88 in #1632
- test(pipeline): drive real OR + fallback-metrics-gate paths instead of recomputing them (#1624) by @saurabhav88 in #1633
- test(pipeline): assert exactly-once read + exact LID diagnostic values, not weak proxies (#1626) by @saurabhav88 in #1636
- test(pipeline): prove the Ollama polisher was invoked, not just constructed (#1627) by @saurabhav88 in #1638
- feat(asr): the Remove button — refuse during dictation, then unload and delete in L1 order by @saurabhav88 in #1637
- test(app): assert exact payload on all 4 Sparkle update-stage events, not just the first (#1628) by @saurabhav88 in #1639
- test(pipeline): check assertAttached() runs before bootstrapper forwarding, per callback (#1625) by @saurabhav88 in #1634
- test: assert exact values across 4 low-severity weak-proxy checks (#1629) by @saurabhav88 in #1641
- feat(settings): expand recording sound cues to 12 options (#1618) by @saurabhav88 in #1642
- feat(asr): bundle the WhisperKit tokenizer into the app, no runtime fetch by @saurabhav88 in #1644
- feat(observability): pin Sentry identity for the 4 remaining leftovers (Part of #1525) by @saurabhav88 in #1645
- fix(user): fail closed when the custom-words file exists but cannot be read (#1646) by @saurabhav88 in #1647
- feat(observability): pin Sentry identity for the kernel fallback enum + AudioError by @saurabhav88 in #1648
- fix(ui): Top-position recording pill no longer jumps when it switches to Transcribing/Polishing by @saurabhav88 in #1651
- refactor(observability): shared test fixture + injectable keychain telemetry factory (Part of #1525) by @saurabhav88 in #1652
- feat(observability): normalize AFM/WhisperKit/Parakeet/XPC raw errors (Part of #1525) by @saurabhav88 in #1653
- fix(worker): drop tier_a's redundant dev-exclusion subquery, degrade instead of failing the report by @saurabhav88 in #1656
- feat(user): Custom Words import sheet shell with DEBUG-only entry (PR-F1, #1657) by @saurabhav88 in #1659
- docs(worker): correct daily-report deploy + trigger-secret source by @saurabhav88 in #1660
- chore(website): remove blog comment section, decommission comments backend by @saurabhav88 in #1662
- feat(user): Custom Words import contract + compare engine (PR-F2a, #1661) by @saurabhav88 in #1664
- feat(services): compile-time guard narrows Sentry capture to conforming errors by @saurabhav88 in #1666
- feat(user): atomic commit for a reviewed Custom Words import (PR-F2b, #1665) by @saurabhav88 in #1668
- feat(pipeline): model-load boundary miss keeps its own bridged Sentry identity (#1658) by @saurabhav88 in #1673
- chore(release): bump version to 2.4.0 by @saurabhav88 in #1674
Full Changelog: v2.3.1...v2.4.0