From a5a5a2335aad7f1862a4a34943b709589289c893 Mon Sep 17 00:00:00 2001 From: Chris <20590073+what-name@users.noreply.github.com> Date: Tue, 19 May 2026 20:01:27 +0400 Subject: [PATCH 1/4] Add capture_mode config to skip source picker Adds `capture_mode` option to `[audio]` config section: - "picker" (default): show the SCContentSharingPicker to select a window, display, or app to capture - "all": capture all system audio directly via SCShareableContent, skipping the picker entirely The "all" mode uses the System Audio Recording Only permission tier instead of Screen & System Audio Recording, which is a better fit since ownscribe doesn't capture video. Config: `capture_mode = "all"` in [audio] section Flag: `--capture-mode-all` on the Swift binary Closes #18 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ownscribe/audio/coreaudio.py | 5 ++++- src/ownscribe/config.py | 2 ++ src/ownscribe/pipeline.py | 1 + swift/Sources/AudioCapture.swift | 38 ++++++++++++++++++++++---------- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/ownscribe/audio/coreaudio.py b/src/ownscribe/audio/coreaudio.py index 801bd2c..fabdbc1 100644 --- a/src/ownscribe/audio/coreaudio.py +++ b/src/ownscribe/audio/coreaudio.py @@ -78,9 +78,10 @@ def _find_binary() -> Path | None: class CoreAudioRecorder(AudioRecorder): """Records system audio using the ownscribe-audio Swift helper.""" - def __init__(self, mic: bool = False, mic_device: str = "", silence_timeout: int = 0) -> None: + def __init__(self, mic: bool = False, mic_device: str = "", capture_mode: str = "picker", silence_timeout: int = 0) -> None: self._mic = mic self._mic_device = mic_device + self._capture_mode = capture_mode self._silence_timeout = silence_timeout self._process: subprocess.Popen | None = None self._binary = _find_binary() @@ -96,6 +97,8 @@ def start(self, output_path: Path) -> None: raise RuntimeError("ownscribe-audio binary not found. Run: bash swift/build.sh") cmd = [str(self._binary), "capture", "--output", str(output_path)] + if self._capture_mode == "all": + cmd.append("--capture-mode-all") if self._mic or self._mic_device: cmd.append("--mic") if self._mic_device: diff --git a/src/ownscribe/config.py b/src/ownscribe/config.py index 7233fad..3364be2 100644 --- a/src/ownscribe/config.py +++ b/src/ownscribe/config.py @@ -16,6 +16,7 @@ device = "" # empty = system audio; or device name/index for sounddevice mic = false # also capture microphone input mic_device = "" # specific mic device name (empty = default) +capture_mode = "picker" # "picker" = show source picker; "all" = capture all system audio directly silence_timeout = 300 # seconds of silence before auto-stop; 0 = disabled [transcription] @@ -56,6 +57,7 @@ class AudioConfig: device: str = "" mic: bool = False mic_device: str = "" + capture_mode: str = "picker" # "picker" = show source picker; "all" = all system audio silence_timeout: int = 300 # seconds of silence before auto-stop; 0 = disabled diff --git a/src/ownscribe/pipeline.py b/src/ownscribe/pipeline.py index cd8a6d6..f0fe2e9 100644 --- a/src/ownscribe/pipeline.py +++ b/src/ownscribe/pipeline.py @@ -74,6 +74,7 @@ def _create_recorder(config: Config): recorder = CoreAudioRecorder( mic=config.audio.mic, mic_device=config.audio.mic_device, + capture_mode=config.audio.capture_mode, silence_timeout=config.audio.silence_timeout, ) if recorder.is_available(): diff --git a/swift/Sources/AudioCapture.swift b/swift/Sources/AudioCapture.swift index 9f9e6a4..6f4e9ec 100644 --- a/swift/Sources/AudioCapture.swift +++ b/swift/Sources/AudioCapture.swift @@ -225,19 +225,28 @@ class SystemAudioCapture: NSObject, SCStreamOutput, SCStreamDelegate, SCContentS super.init() } + var captureModeAll: Bool = false + func start() async throws { - // Configure and show the content sharing picker - let picker = SCContentSharingPicker.shared - var pickerConfig = SCContentSharingPickerConfiguration() - pickerConfig.allowedPickerModes = [.singleWindow, .singleDisplay, .singleApplication] - picker.defaultConfiguration = pickerConfig - picker.add(self) - picker.isActive = true - picker.present() - - // Suspend until the picker delegate fires - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - self.startContinuation = continuation + if captureModeAll { + let content = try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: false) + guard let display = content.displays.first else { + throw CaptureError.noDisplay + } + let filter = SCContentFilter(display: display, excludingApplications: [], exceptingWindows: []) + try await self.beginCapture(with: filter) + } else { + let picker = SCContentSharingPicker.shared + var pickerConfig = SCContentSharingPickerConfiguration() + pickerConfig.allowedPickerModes = [.singleWindow, .singleDisplay, .singleApplication] + picker.defaultConfiguration = pickerConfig + picker.add(self) + picker.isActive = true + picker.present() + + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.startContinuation = continuation + } } } @@ -726,6 +735,7 @@ func printUsage() { --output, -o FILE Output WAV file path (required for capture) --mic Also capture microphone input --mic-device NAME Use specific mic input device (implies --mic) + --capture-mode-all Capture all system audio without showing the source picker --silence-timeout N Auto-stop after N seconds of silence (0 = disabled) --help, -h Show this help @@ -761,6 +771,7 @@ func main() { var outputPath: String? var enableMic = false var micDeviceName: String? + var captureModeAll = false var silenceTimeout: TimeInterval = 0 var i = 2 @@ -773,6 +784,8 @@ func main() { exit(1) } outputPath = args[i] + case "--capture-mode-all": + captureModeAll = true case "--mic": enableMic = true case "--mic-device": @@ -813,6 +826,7 @@ func main() { let micPath = output + ".mic.tmp.wav" let capture = SystemAudioCapture(outputPath: systemPath) + capture.captureModeAll = captureModeAll capture.silenceTimeout = silenceTimeout var micCapture: MicCapture? From 7538621d6f1f9bf3590851e6a5001d2e96f6eb87 Mon Sep 17 00:00:00 2001 From: Chris <20590073+what-name@users.noreply.github.com> Date: Tue, 19 May 2026 20:16:01 +0400 Subject: [PATCH 2/4] Fix line length lint (E501) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ownscribe/audio/coreaudio.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ownscribe/audio/coreaudio.py b/src/ownscribe/audio/coreaudio.py index fabdbc1..53f10b7 100644 --- a/src/ownscribe/audio/coreaudio.py +++ b/src/ownscribe/audio/coreaudio.py @@ -78,7 +78,10 @@ def _find_binary() -> Path | None: class CoreAudioRecorder(AudioRecorder): """Records system audio using the ownscribe-audio Swift helper.""" - def __init__(self, mic: bool = False, mic_device: str = "", capture_mode: str = "picker", silence_timeout: int = 0) -> None: + def __init__( + self, mic: bool = False, mic_device: str = "", + capture_mode: str = "picker", silence_timeout: int = 0, + ) -> None: self._mic = mic self._mic_device = mic_device self._capture_mode = capture_mode From ce1ea746b1d9dbd4a4634f85e48f36fae09704d0 Mon Sep 17 00:00:00 2001 From: Chris <20590073+what-name@users.noreply.github.com> Date: Tue, 19 May 2026 20:18:46 +0400 Subject: [PATCH 3/4] Update test for capture_mode parameter, add capture_mode=all test Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_pipeline.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index b99c15e..6222eb4 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -60,7 +60,20 @@ def test_silence_timeout_passed_to_coreaudio(self): with mock.patch("ownscribe.audio.coreaudio.CoreAudioRecorder") as mock_cls: mock_cls.return_value.is_available.return_value = True _create_recorder(config) - mock_cls.assert_called_once_with(mic=False, mic_device="", silence_timeout=120) + mock_cls.assert_called_once_with(mic=False, mic_device="", capture_mode="picker", silence_timeout=120) + + def test_capture_mode_passed_to_coreaudio(self): + from ownscribe.pipeline import _create_recorder + + config = Config() + config.audio.backend = "coreaudio" + config.audio.device = "" + config.audio.capture_mode = "all" + + with mock.patch("ownscribe.audio.coreaudio.CoreAudioRecorder") as mock_cls: + mock_cls.return_value.is_available.return_value = True + _create_recorder(config) + mock_cls.assert_called_once_with(mic=False, mic_device="", capture_mode="all", silence_timeout=300) def test_silence_timeout_passed_to_sounddevice(self): from ownscribe.pipeline import _create_recorder From c0e3284503e79bcbab609cc35d12bf5d5050bc06 Mon Sep 17 00:00:00 2001 From: Pascal Berrang Date: Tue, 2 Jun 2026 12:05:58 +0200 Subject: [PATCH 4/4] Document capture_mode option in README Add the capture_mode = "all" config to the [audio] block and note that it skips the macOS source picker to record all system audio directly. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 52dd037..90d5730 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ This will: 3. Summarize with your local LLM 4. Save everything to `~/ownscribe/YYYY-MM-DD_HHMMSS/` +> **Note:** By default, macOS shows a source picker on each launch so you can choose what to capture. To skip it and always record all system audio, set `capture_mode = "all"` in the `[audio]` config section. + On first run, WhisperX / pyannote and the summarization model may download model files. ownscribe shows a `Preparing models` step and best-effort download progress in the TUI while this happens. Use `ownscribe warmup` to pre-download all models. ### Options @@ -187,6 +189,7 @@ backend = "coreaudio" # "coreaudio" or "sounddevice" device = "" # empty = system audio mic = false # also capture microphone input mic_device = "" # specific mic device name (empty = default) +capture_mode = "picker" # "picker" = show source picker; "all" = capture all system audio directly silence_timeout = 300 # seconds of silence before auto-stop; 0 = disabled [transcription]