Skip to content

v1.0.0 : stability release

Choose a tag to compare

@charle-com charle-com released this 20 Aug 13:16
· 1 commit to main since this release

Stability release. VoxPrompt now holds its macOS permissions across updates, never touches your audio output devices, and cannot leave the microphone open. This is the first version considered production ready.

Fixed

  • Permissions silently revoked (root cause). The app was signed with the hardened runtime but shipped no entitlements file. Verified on macOS 26.5.2 with three otherwise identical bundles: with the runtime flag and no entitlements, AVCaptureDevice.requestAccess returns false in 0.00 s and a real capture returns 57344 frames of pure zeroes (RMS 0.000000) with no error raised anywhere. The app believes it is recording and writes a silent WAV. An older TCC grant masks the problem until macOS re-evaluates it, which is why it looked like "updates break the permissions". VoxPrompt.entitlements now declares com.apple.security.device.audio-input and com.apple.security.automation.apple-events, and build.sh refuses to produce a bundle without them. The designated requirement is unchanged, so existing grants survive the update.
  • Bluetooth headphones degraded while VoxPrompt was merely running. AVAudioEngine attaches the default input AND output devices as soon as inputNode is touched, which pushed AirPods into hands-free profile (24 kHz) even when idle. Capture is now an input-only AudioUnit HAL bound to an explicitly resolved device, so the output side is never touched: verified with the speakers staying at running=0 throughout a recording.
  • Microphone could stay open forever. A missed key-up (locked screen, secure input field, another app swallowing the event, popover holding focus) left the recorder running with no way back. Fixed with a local event monitor alongside the global one, a 250 ms watchdog that reads the real physical key state, a hard 180 s cap, and forced release on session lock and system sleep.
  • Crash on the second recording attempt after a cold-start failure. A failed start left the audio tap installed; the next attempt hit an unrecoverable ObjC exception. The audio unit is now fully torn down on every error path.
  • Model could be corrupted on first launch. Warmup did not publish its loading task, so pressing the hotkey during startup began a second concurrent WhisperKit(config), and two downloads wrote the same incomplete file. All loading paths now share a single task: measured 6 concurrent calls resulting in exactly 1 model load.
  • A frozen decode permanently consumed a thread. Abandoned inference tasks stayed on Swift's cooperative pool, and after a few hangs the whole app stopped responding. Inference now runs off the cooperative pool, the watchdog timer is cancelled on success, and a timed-out pipeline is discarded rather than reused.
  • Wrong clipboard content pasted into slow apps. The 250 ms fixed delay before restoring the clipboard let slow targets paste the previous content. Restoration now waits for the target to consume the text, is skipped entirely if you copied something in the meantime, and never replays password-manager entries marked concealed or transient.
  • The paste fallback chain was unreachable. postCmdV always reported success, so AppleScript was never attempted and the HUD claimed "pasted" when nothing had been. Success is now determined from the actual target state.
  • A phrase could vanish during streaming. Two locks guarded the segment chain, so a natural cut and the final flush could run in parallel and one result was dropped without any error. The chain is now a single critical section and every task is awaited.
  • Legitimate speech mangled by the anti-repetition guard. "06 44 44 44 44" collapsed to "06 44" and "non non non" to "non". Numeric tokens are exempt, thresholds are raised, and the mid-decode abort only fires when the text stops progressing.
  • Glossary rewrote real words. "gel" became "GYL", "dandy" became "Gandy". Fuzzy matching now scales with term length and never touches a word the system dictionary recognises.
  • Two dictations could overlap and interleave their pastes. The in-flight guard was released by any call reaching the "not recording" early return, including the duplicate release synthesized by the key watchdog, which freed the lock held by a cycle still in flight. A hallucinated fragment from one dictation could then land in the middle of the next, and the clipboard restore gave up with "written by someone else". The lock is now only released by the cycle that owns it.
  • A key press too short to carry speech no longer produces a paste. A 0.02 s press was enough to run a full cycle, and Whisper reliably hallucinates a short phrase out of a few hundredths of a second of noise. Presses under 0.35 s are now discarded silently.
  • Streaming was pointless on the slow compute profile. Measured on a real dictation: 6.5 s of speech took 9.3 s because the forced cut sat at 12 s and no natural pause was long enough, so the entire clip was decoded after key release. The forced cut now adapts to the active profile, 5 s on cpu-only, which brought the same dictation down to 3.4 s with segments decoded while still speaking.
  • HUD appeared on the wrong display on multi-monitor setups, and reported a paste that had not happened.

Added

  • Permission dashboard in Preferences: live state of Microphone, Accessibility, and Automation, direct links to the right settings pane, and a repair action for a desynchronised TCC entry.
  • Accessibility prompt at launch when the permission is missing, with automatic reinstallation of the hotkey monitors the moment it is granted. Previously the app just sat there, silent.
  • Warning banner when the app runs from quarantine or outside /Applications, the two situations where macOS silently drops its permissions.
  • Microphone picker in Preferences, so a Bluetooth headset or a virtual device connecting mid-session cannot hijack the input.
  • Engine status surfaced in Preferences and in the HUD, including model download progress.
  • Optional update check, off by default. One request per day to the GitHub releases API, which only ever opens the release page. Nothing is downloaded or installed automatically.
  • install.sh, a one-line installer that verifies the bundle signature against the project certificate before copying, and avoids the Gatekeeper prompt entirely.
  • Single-instance guard: a second copy of the app now steps aside instead of double-pasting every dictation.
  • 22 unit tests covering the text cleanup logic, in a VoxPromptCore library so they can actually run under swift test.

Changed

  • build.sh verifies the signature, the entitlements and the designated requirement before finishing, and no longer falls back to ad-hoc signing, which was itself a cause of lost permissions. --install quits the running instance before replacing it, since replacing a running bundle invalidates its signature at runtime.
  • package-dmg.sh refuses to build a DMG from an unverified bundle and ships an installation note.
  • Forced streaming cuts now land on the quietest point of the last second instead of mid-word.
  • Compute profile cache is keyed by macOS build AND model, expires after 14 days, and requires two consecutive timeouts before persisting a downgrade.

Known limitation

  • Glossary terms cannot be injected as decoder prompt tokens. Measured on WhisperKit 0.18: beyond two forced prompt tokens the decoder immediately predicts end-of-transcript and returns an empty segment, regardless of content or decoding options. Glossary correction therefore runs after transcription, and prompt injection stays disabled with the measurement documented in the code.