diff --git a/README.md b/README.md index 3e05aea..76e8884 100644 --- a/README.md +++ b/README.md @@ -135,23 +135,23 @@ then call `ownscribe` directly. The examples in [Usage](#usage) use the bare ### Record, transcribe, and summarize a meeting ```bash -ownscribe # records system audio, Ctrl+C to stop +ownscribe # records system audio + mic, Ctrl+C to stop ``` This will: -1. Capture system audio until you press Ctrl+C (or auto-stop after 5 minutes of silence) +1. Capture system audio and your microphone until you press Ctrl+C (or auto-stop after 5 minutes of silence) 2. Transcribe with WhisperX 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. +> **Note:** By default, ownscribe records all system audio directly with no prompt. To show a macOS source picker on each launch instead, set `capture_mode = "picker"` 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 ```bash -ownscribe --mic # capture system audio + default mic (press 'm' to mute/unmute) +ownscribe --no-mic # capture system audio only (mic is on by default; press 'm' to mute/unmute) ownscribe --mic-device "MacBook Pro Microphone" # capture system audio + specific mic ownscribe --device "MacBook Pro Microphone" # use mic instead of system audio ownscribe --no-summarize # skip LLM summarization @@ -213,9 +213,9 @@ Config is stored at `~/.config/ownscribe/config.toml`. Run `ownscribe config` to [audio] backend = "coreaudio" # "coreaudio" or "sounddevice" device = "" # empty = system audio -mic = false # also capture microphone input +mic = true # 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 +capture_mode = "all" # "all" = capture all system audio directly; "picker" = show source picker silence_timeout = 300 # seconds of silence before auto-stop; 0 = disabled [transcription] diff --git a/src/ownscribe/cli.py b/src/ownscribe/cli.py index ad0dbf2..9ac623a 100644 --- a/src/ownscribe/cli.py +++ b/src/ownscribe/cli.py @@ -39,7 +39,11 @@ def _dir_size(path: str) -> str: @click.option("--language", default=None, help="Language code for transcription (e.g. en, de, fr).") @click.option("--initial-prompt", default=None, help="Context text to prime Whisper (vocab, speaker names, etc.)") @click.option("--hotwords", default=None, help="Comma-separated words to boost Whisper recognition.") -@click.option("--mic", is_flag=True, help="Also capture microphone input (mixed with system audio).") +@click.option( + "--mic/--no-mic", + default=None, + help="Also capture microphone input (mixed with system audio); on by default.", +) @click.option("--mic-device", default=None, help="Specific mic device name (implies --mic).") @click.option( "--keep-recording/--no-keep-recording", @@ -62,7 +66,7 @@ def cli( language: str | None, initial_prompt: str | None, hotwords: str | None, - mic: bool, + mic: bool | None, mic_device: str | None, keep_recording: bool | None, template: str | None, @@ -94,9 +98,12 @@ def cli( config.transcription.initial_prompt = initial_prompt if hotwords: config.transcription.hotwords = hotwords - if mic or mic_device: - config.audio.mic = True + if mic is False and mic_device: + raise click.UsageError("--no-mic and --mic-device cannot be used together.") + if mic is not None: + config.audio.mic = mic if mic_device: + config.audio.mic = True config.audio.mic_device = mic_device if keep_recording is not None: config.output.keep_recording = keep_recording diff --git a/src/ownscribe/config.py b/src/ownscribe/config.py index f5b0885..9d69144 100644 --- a/src/ownscribe/config.py +++ b/src/ownscribe/config.py @@ -14,9 +14,9 @@ [audio] backend = "coreaudio" # "coreaudio" (default) or "sounddevice" device = "" # empty = system audio; or device name/index for sounddevice -mic = false # also capture microphone input +mic = true # 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 +capture_mode = "all" # "all" = capture all system audio directly; "picker" = show source picker silence_timeout = 300 # seconds of silence before auto-stop; 0 = disabled [transcription] @@ -58,9 +58,9 @@ class AudioConfig: backend: str = "coreaudio" device: str = "" - mic: bool = False + mic: bool = True mic_device: str = "" - capture_mode: str = "picker" # "picker" = show source picker; "all" = all system audio + capture_mode: str = "all" # "all" = all system audio; "picker" = show source picker silence_timeout: int = 300 # seconds of silence before auto-stop; 0 = disabled diff --git a/tests/test_cli.py b/tests/test_cli.py index edecc7f..daafe2b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -30,13 +30,20 @@ def test_no_summarize_flag(self): config = mock_run.call_args[0][0] assert config.summarization.enabled is False - def test_mic_flag(self): + def test_no_mic_flag(self): runner = CliRunner() with _mock_config(), mock.patch("ownscribe.pipeline.run_pipeline") as mock_run: - result = runner.invoke(cli, ["--mic"]) + result = runner.invoke(cli, ["--no-mic"]) assert result.exit_code == 0 config = mock_run.call_args[0][0] - assert config.audio.mic is True + assert config.audio.mic is False + + def test_no_mic_with_mic_device_errors(self): + runner = CliRunner() + with _mock_config(): + result = runner.invoke(cli, ["--no-mic", "--mic-device", "USB Mic"]) + assert result.exit_code != 0 + assert "--no-mic and --mic-device cannot be used together" in result.output def test_device_flag(self): runner = CliRunner() diff --git a/tests/test_config.py b/tests/test_config.py index 977c00a..4e296f4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -29,9 +29,13 @@ def test_default_output_format(self): def test_default_mic_settings(self): cfg = Config() - assert cfg.audio.mic is False + assert cfg.audio.mic is True assert cfg.audio.mic_device == "" + def test_default_capture_mode(self): + cfg = Config() + assert cfg.audio.capture_mode == "all" + def test_default_diarization_telemetry_off(self): cfg = Config() diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 6222eb4..5e851c7 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -60,7 +60,7 @@ 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="", capture_mode="picker", silence_timeout=120) + mock_cls.assert_called_once_with(mic=True, mic_device="", capture_mode="all", silence_timeout=120) def test_capture_mode_passed_to_coreaudio(self): from ownscribe.pipeline import _create_recorder @@ -68,12 +68,12 @@ def test_capture_mode_passed_to_coreaudio(self): config = Config() config.audio.backend = "coreaudio" config.audio.device = "" - config.audio.capture_mode = "all" + config.audio.capture_mode = "picker" 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) + mock_cls.assert_called_once_with(mic=True, mic_device="", capture_mode="picker", silence_timeout=300) def test_silence_timeout_passed_to_sounddevice(self): from ownscribe.pipeline import _create_recorder