Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Full flow:

Key files:

- `AudioEngine.swift` wraps `AVAudioEngine`. It follows System Default route changes with bounded recovery and binds explicit microphone selections directly to the capture input audio unit.
- `AudioEngine.swift` captures the microphone through `HALInputCapture` (a Core Audio input callback on the chosen device), not `AVAudioEngine`. `AVAudioEngine` couples input and output in one I/O unit, so a Bluetooth headset switching profile stopped the capture and a built-in microphone selected while a headset was the output delivered no samples. The capture follows the System Default at start and, with an explicit selection, uses that device; a sample-rate change does not interrupt it, and only a stopped capture is rebuilt, with bounded retries.
- `GrammarEnhancer.swift` spawns the `copilot` CLI as a subprocess; `EnhancedModel` holds the evaluated cleanup models and loads model-specific prompt profiles.
- `OverlayPanel.swift` is a borderless `NSPanel` with one waveform design shared by `.listening`, `.transcribing`, `.enhancing`, `.done`, and `.error`.
- `UpdateManager.swift` wraps [AppUpdater](https://github.com/mxcl/AppUpdater) to check GitHub Releases, validate Developer ID signatures, and install and relaunch in place.
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to OpenWritr will be documented in this file.
Each release's notes on GitHub are generated from its section here, and the
broker publication handoff fails when the section for a tag is missing or empty.

## [1.7.4] — 2026-10-13

### Fixed
- Dictation works with Bluetooth headsets such as AirPods Pro and AirPods Max. The microphone is now captured through an input-only Core Audio callback instead of `AVAudioEngine`, which tied the capture to the output device: a headset switching to its microphone profile stopped the engine and lost the start of the recording, and selecting the built-in microphone while a headset was the output could deliver no audio at all
- Recording starts faster (about 70 ms instead of about 250 ms on the built-in microphone)
- The start tone and the Listening overlay now wait for the first audio, so with a Bluetooth headset they appear when speech is actually being captured. Before, the tone played as soon as the capture was requested, while the headset was still switching its output profile, and was not heard. The capture records the whole time, so nothing is lost while waiting

### Added
- **Keep Last Dictation** in the menu bar menu. When it is on, OpenWritr keeps the text of the last dictation in memory only, never on disk or in the logs, and **Paste Last Dictation** pastes it again into the app in front. Use it when a long dictation could not be pasted because the target window was gone. It follows the same rules as a fresh dictation (typing in Windows App when that setting is on), pastes even with Auto-Paste off, and applies to clipboard-only recordings too. Turning the setting off or quitting discards the text. Off by default

### Changed
- Microphone capture no longer depends on the output device

## [1.7.3] — 2026-10-13

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
<key>CFBundleIdentifier</key>
<string>com.openwritr.app</string>
<key>CFBundleVersion</key>
<string>1.7.3</string>
<string>1.7.4</string>
<key>CFBundleShortVersionString</key>
<string>1.7.3</string>
<string>1.7.4</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Torsten Mahr. Released under the MIT License.</string>
<key>OpenWritrDescription</key>
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ Clipboard-only works even with Auto-Paste off. Successful copying intentionally

Normal Auto-Paste restores the original clipboard, including an empty clipboard, unless another application or the user has changed it. Output errors are shown explicitly; delayed restoration errors appear as a dismissible menu warning. With Auto-Paste off, standard recordings remain inside OpenWritr and are not copied automatically.

### Keep Last Dictation

If a long dictation could not be pasted because the target window was gone, turn on **Keep Last Dictation** in the menu bar menu. OpenWritr then keeps the text of the last dictation in memory only: it is never written to disk or the logs, turning the setting off or quitting discards it, and it is kept even when pasting failed. **Paste Last Dictation** (shown while the setting is on) pastes it again into the app in front, with the same routing as a fresh dictation, including typing in Windows App when that setting is on. It pastes even with Auto-Paste off, and it also covers clipboard-only recordings. The setting is off by default.

### Remote desktop apps (experimental)

Windows App copies the Mac clipboard to the remote PC only after you leave its window, even with clipboard redirection set to Bidirectional. A paste into the remote session can therefore insert old text. Turn on **Type Text in Remote Desktop Apps (Experimental)** in Settings: while Windows App is frontmost, Auto-Paste then types the text key by key instead of pasting, and your clipboard is not touched. It applies only to Windows App (`com.microsoft.rdc.macos`), only with Auto-Paste on, and not to clipboard-only recordings (Option), which still copy.
Expand Down Expand Up @@ -310,7 +314,8 @@ Sources/OpenWritr/
├── OpenWritrApp.swift # App entry, presence modes, state machine
├── MenuBarView.swift # Menu bar dropdown UI
├── SettingsView.swift # Dedicated settings window
├── AudioEngine.swift # AVAudioEngine, 16kHz capture, realtime-safe
├── AudioEngine.swift # Capture lifecycle, 16kHz conversion, device selection
├── HALInputCapture.swift # Input-only Core Audio capture (no AVAudioEngine)
├── TranscriptionManager.swift # FluidAudio model loading + transcription
├── GrammarEnhancer.swift # Cleanup provider routing and remote providers
├── AppleIntelligenceEnhancer.swift # macOS 26+ on-device cleanup
Expand All @@ -331,7 +336,7 @@ Sources/OpenWritr/
- **FluidAudio** — CoreML-optimized ASR framework
- **NVIDIA Parakeet TDT 0.6B v3** — non-autoregressive transducer, 25 languages
- **Apple Neural Engine** — hardware-accelerated inference via CoreML
- **AVAudioEngine** — low-latency microphone capture at 16kHz
- **Core Audio** — low-latency, input-only microphone capture, converted to 16kHz
- **CGEventTap** — global Fn key detection (requires Accessibility permission)
- **AppUpdater** — signed, in-app update checks against GitHub Releases

Expand Down
7 changes: 7 additions & 0 deletions Sources/OpenWritr/AudioDiagnostics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ struct AudioDiagnostics: Equatable, Sendable {
)
}

/// Transport, channels and sample rate of one device, for logs. Never includes its name or UID.
static func deviceFormatSummary(for deviceID: AudioDeviceID) -> String {
let rate = (property(of: deviceID, selector: kAudioDevicePropertyNominalSampleRate) as Float64?)
.map { String(Int($0.rounded())) } ?? "unknown"
return "transport=\(transportName(of: deviceID)) channels=\(inputChannelCount(of: deviceID)) rate=\(rate)"
}

static func transportName(forCode code: UInt32) -> String {
switch code {
case kAudioDeviceTransportTypeBuiltIn: return "built-in"
Expand Down
Loading
Loading