call-copilot 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,331 @@
1
+ Metadata-Version: 2.4
2
+ Name: call-copilot
3
+ Version: 0.2.0
4
+ Summary: Call Copilot — silently record meeting audio in the background, then transcribe locally to clean markdown.
5
+ Author-email: Anisur Rahman <pshakilwizard@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Anisur Rahman
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/AnisurRahmann/call-copilot
29
+ Project-URL: Repository, https://github.com/AnisurRahmann/call-copilot
30
+ Project-URL: Issues, https://github.com/AnisurRahmann/call-copilot/issues
31
+ Project-URL: Changelog, https://github.com/AnisurRahmann/call-copilot/releases
32
+ Keywords: audio,transcription,whisper,meeting,recorder,macos,cli
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Environment :: MacOS X
35
+ Classifier: Intended Audience :: End Users/Desktop
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: MacOS :: MacOS X
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Capture/Recording
43
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
44
+ Requires-Python: >=3.11
45
+ Description-Content-Type: text/markdown
46
+ License-File: LICENSE
47
+ Requires-Dist: click>=8.1
48
+ Requires-Dist: audiotap>=0.0.2
49
+ Requires-Dist: soundfile>=0.12
50
+ Requires-Dist: faster-whisper>=1.0
51
+ Requires-Dist: rich>=13.0
52
+ Requires-Dist: pydantic>=2.0
53
+ Requires-Dist: numpy>=1.24
54
+ Provides-Extra: dev
55
+ Requires-Dist: pytest>=8.0; extra == "dev"
56
+ Requires-Dist: pytest-mock>=3.0; extra == "dev"
57
+ Requires-Dist: ruff>=0.6; extra == "dev"
58
+ Dynamic: license-file
59
+
60
+ # Call Copilot
61
+
62
+ > One thing. Done really well. Record meetings. Save transcripts. Don't break audio.
63
+
64
+ Call Copilot is a terminal command (`rec`) that records your meeting audio and, when you
65
+ stop it, transcribes locally and saves a clean markdown transcript. **Everything runs on
66
+ your machine** — no cloud, no API keys, no account. Audio and transcripts never leave the
67
+ computer.
68
+
69
+ ![Call Copilot demo](docs/demo.gif)
70
+
71
+ ```
72
+ $ rec setup # one-time: verify macOS + grant capture permission
73
+ $ rec start # starts recording, you keep working
74
+ $ rec stop # stops, transcribes, saves markdown
75
+ $ rec list # shows past recordings
76
+ ```
77
+
78
+ ## How it captures audio (no BlackHole, no Multi-Output Device)
79
+
80
+ By default, `rec` captures **both the microphone (your voice) AND system audio (other
81
+ participants / anything apps play)** — so a real meeting where *you* speak gets recorded,
82
+ not just the audio coming out of your speakers. Both sources are tapped directly via
83
+ Apple's Core Audio taps API (macOS 14.2+) using the [`audiotap`](https://pypi.org/project/audiotap/)
84
+ library. There is:
85
+
86
+ - **no virtual audio driver** to install (no BlackHole),
87
+ - **no Multi-Output Device** to create in Audio MIDI Setup,
88
+ - **no system output device** to switch and restore,
89
+ - and therefore **no silent-recording failure** that the old driver-based approaches
90
+ produce when routing breaks.
91
+
92
+ The mic and system streams are recorded as **two separate WAVs** (each at its own true
93
+ rate), transcribed separately, and merged into one transcript with `[Mic]` / `[System]`
94
+ labels so you can tell who said what. Want just one source? `rec start --mic-only` or
95
+ `--system-only`.
96
+
97
+ ### Grant capture permission (one-time, per terminal app)
98
+
99
+ macOS ties capture permission to the app you run `rec` from (Terminal, iTerm, **Warp**,
100
+ VS Code, …). You need **two** grants:
101
+
102
+ 1. **Microphone** (your voice): System Settings → Privacy & Security → **Microphone** →
103
+ enable your terminal app.
104
+ 2. **System audio** (grouped under Screen Recording on macOS 14.2+): System Settings →
105
+ Privacy & Security → **Screen Recording** → enable your terminal app, then **quit and
106
+ reopen it** (required for the change to take effect).
107
+
108
+ Until mic is granted, `rec start` records system audio only and skips the mic (it tells
109
+ you so). `rec setup` checks the current mic-permission status.
110
+
111
+ After `rec stop` (or Ctrl+C from `rec start`), the recording(s) are transcribed locally
112
+ with [faster-whisper](https://github.com/SYSTRAN/faster-whisper) (CPU, int8 — no API, no
113
+ data leaves your machine) and written to a markdown transcript.
114
+
115
+ ```
116
+ recording.wav ──> faster-whisper (local, free) ──> transcript.md
117
+ ```
118
+
119
+ ## Requirements
120
+
121
+ - **macOS 14.2 or later** (Sonoma — the Core Audio process taps API `rec` records through
122
+ landed in 14.2). Apple Silicon recommended. Intel works.
123
+ - **Python 3.11+** only needed for the `pipx` install; the Homebrew formula brings its own.
124
+
125
+ No virtual audio driver (no BlackHole), no Multi-Output Device, no manual Audio MIDI Setup.
126
+ The tap reads system output directly. `rec` checks the macOS version itself and refuses to
127
+ run below 14.2 with a one-line message — you don't have to guess.
128
+
129
+ ## Install
130
+
131
+ **Homebrew** (recommended — no Python to manage):
132
+
133
+ ```bash
134
+ brew install AnisurRahmann/tap/call-copilot
135
+ ```
136
+
137
+ **pipx** (if you prefer a plain Python install, no Homebrew):
138
+
139
+ ```bash
140
+ pipx install call-copilot
141
+ ```
142
+
143
+ Either way you get a `rec` command on your `PATH`. Then jump to
144
+ [One-time setup](#one-time-setup).
145
+
146
+ > Using `rec` on a machine below macOS 14.2 prints `Error: rec requires macOS 14.2 or
147
+ > later ...` and exits — no traceback, no mystery. See
148
+ > [docs/HOMEBREW.md](docs/HOMEBREW.md) if you're setting up the tap yourself.
149
+
150
+ ## Recording consent & privacy
151
+
152
+ Call Copilot records audio from your meetings, which may include **other participants**.
153
+ Recording laws vary by jurisdiction and many require the consent of everyone being
154
+ recorded (one-party vs. all-party consent). **Obtaining that consent is your
155
+ responsibility** — check your local laws and your organization's policy before recording.
156
+
157
+ On the technical side, your data stays put:
158
+
159
+ - Transcription runs **100% locally** with faster-whisper (CPU, int8) — no API, no cloud.
160
+ - **Audio and transcripts never leave your machine.** The only network access is the
161
+ first run of a given Whisper model, which downloads its weights from Hugging Face;
162
+ after that it's fully offline.
163
+ - Recordings live under `~/.local/share/rec/sessions/` (XDG data home), outside any repo.
164
+
165
+ ## One-time setup
166
+
167
+ ```bash
168
+ rec setup
169
+ ```
170
+
171
+ Verifies your macOS version, that the `audiotap` library + its bundled dylib load, saves
172
+ your config, and tells you about the one-time capture permission prompt.
173
+
174
+ ## Recording a meeting
175
+
176
+ ```bash
177
+ rec start # mic + system (default); Ctrl+C to stop & transcribe
178
+ rec start --system-only # just what apps play (not your voice)
179
+ rec start --mic-only # just your voice (not system audio)
180
+ rec list # browse past sessions
181
+ rec transcribe 2026-07-27_14-30-00 --model medium # re-transcribe at higher quality
182
+ rec diagnose 2026-07-27_14-30-00 # bundle debug info for an AI agent
183
+ ```
184
+
185
+ By default `rec start` records **both** your microphone and system audio; the transcript
186
+ labels each line `[Mic]` or `[System]`. Use `--system-only` / `--mic-only` to narrow.
187
+
188
+ **`rec start`** shows a live indicator while recording:
189
+
190
+ ```
191
+ ● REC 2026-07-28_14-30-00
192
+ elapsed 03:47 size 18.2 MB
193
+ press Ctrl+C to stop & transcribe
194
+ ```
195
+
196
+ Press **Ctrl+C** when your meeting ends — it stops the recording and transcribes in the
197
+ same command, then prints the transcript path. One command, start to finish.
198
+
199
+ Want the old background behavior instead? `rec start --detach` spawns the recorder and
200
+ exits immediately; stop it later from another terminal with `rec stop` (or check progress
201
+ with `rec status`).
202
+
203
+ > **Tip:** if a transcript comes back empty, the recording was silent — nothing was
204
+ > playing, or the capture permission was revoked. `rec start` (and `rec stop`) warn you
205
+ > about this immediately, and `rec diagnose <session>` bundles the audio levels + logs.
206
+
207
+ ## Configuration
208
+
209
+ Stored at `~/.config/rec/config.json` (XDG). Recordings live under
210
+ `~/.local/share/rec/sessions/{id}/`. Override the XDG roots with `XDG_CONFIG_HOME` /
211
+ `XDG_DATA_HOME` if needed.
212
+
213
+ ```json
214
+ {
215
+ "sample_rate": 16000,
216
+ "channels": 1,
217
+ "whisper_model": "base",
218
+ "capture": "system",
219
+ "sessions_dir": "~/.local/share/rec/sessions"
220
+ }
221
+ ```
222
+
223
+ 16 kHz mono float32 is Whisper's native input format — no resampling, smallest files
224
+ (~7.5 MB/min), best transcription accuracy.
225
+
226
+ ### A note on sample rate (why your recordings play at the right speed)
227
+
228
+ `audiotap`'s `sample_rate` parameter is **not honored** — the tap always delivers
229
+ audio at your output device's native rate (typically 48 kHz), regardless of what
230
+ we request. `rec` handles this transparently: the recorder **measures the true
231
+ capture rate** when the tap starts and writes the WAV at that rate (so playback
232
+ is the correct speed), and the transcriber **resamples to 16 kHz** before
233
+ feeding Whisper (so transcription is accurate). You don't need to do anything;
234
+ this section exists to explain the `capture_sample_rate` field in `session.json`
235
+ (which may differ from the `sample_rate` in your config).
236
+
237
+ ### A note on VAD (voice activity detection)
238
+
239
+ Transcription runs **without** faster-whisper's Silero VAD pre-filter by default.
240
+ The VAD is tuned for close-mic speech and aggressively rejects system-audio
241
+ capture (speakers/headphones via a tap, which has a different character) — we've
242
+ seen it discard 100% of a clearly-audible recording and return an empty
243
+ transcript. Whisper's own `no_speech_threshold` handles silence adequately
244
+ without that risk.
245
+
246
+ If you have clean close-mic input and want long silences skipped (faster,
247
+ cleaner output), enable it per run:
248
+
249
+ ```bash
250
+ rec stop --vad # VAD on for this transcription
251
+ rec transcribe <id> --vad # re-transcribe with VAD
252
+ ```
253
+
254
+ ## Stack
255
+
256
+ | Tool | Role |
257
+ |------|------|
258
+ | [`audiotap`](https://github.com/graphaelli/audiotap) | Core Audio taps → captures system audio directly (macOS 14.2+) |
259
+ | [`soundfile`](https://github.com/bastibe/python-soundfile) | Streams WAV chunks to disk (constant memory) |
260
+ | [`faster-whisper`](https://github.com/SYSTRAN/faster-whisper) | Local speech-to-text (CPU int8) |
261
+ | [`rich`](https://github.com/Textualize/rich) + [`click`](https://click.palletsprojects.com/) | Terminal UI + CLI |
262
+
263
+ Everything is free and open-source. Transcription runs 100% locally — no API cost, no
264
+ data leaves the machine. (The first run of a given whisper model downloads its weights
265
+ from Hugging Face; after that it's offline.)
266
+
267
+ ## Logging & debugging
268
+
269
+ Every activity — the audio tap lifecycle, chunk writes, transcription, formatting, each
270
+ CLI decision — is logged. Logs flow to three destinations:
271
+
272
+ | Destination | Path | Level | What it's for |
273
+ |---|---|---|---|
274
+ | **Console (stderr)** | your terminal | WARNING* | User-facing; clean by default |
275
+ | **Global log** | `~/.local/share/rec/logs/rec.log` | DEBUG | Monitor surface — `tail -f` it |
276
+ | **Session log** | `~/.local/share/rec/sessions/<id>/recorder.log` | DEBUG | Per-session post-mortem |
277
+
278
+ \* WARNING by default: `-v` → INFO, `-vv` → DEBUG, `--quiet` → CRITICAL, `REC_LOG_LEVEL=DEBUG`.
279
+
280
+ Every command failure is logged at `ERROR` with the reason + exit code; unexpected
281
+ crashes include the full traceback.
282
+
283
+ ### Hand a session to an AI agent to debug
284
+
285
+ ```bash
286
+ rec list # find the session id
287
+ rec diagnose 2026-07-27_14-30-00 # → writes sessions/<id>/diagnose.md
288
+ rec diagnose 2026-07-27_14-30-00 --stdout | <your-ai-tool> # pipe straight to an agent
289
+ ```
290
+
291
+ The bundle contains the session metadata, the daemon's `recorder.log`, the global-log
292
+ lines tagged with that session, the transcript if it exists, and the config — followed
293
+ by a debugging checklist. (`rec diagnose` accepts a unique prefix too, e.g. `2026-07-27`.)
294
+
295
+ > **Note on the recording loop:** the audio callback runs on Core Audio's real-time
296
+ > thread and deliberately logs nothing — it copies each chunk to a queue and a writer
297
+ > thread does the disk I/O. The daemon logs only at tap start/stop and on signals.
298
+
299
+ ## Development
300
+
301
+ You only need this if you're hacking on `rec` itself. End users should use the
302
+ [Install](#install) commands above.
303
+
304
+ ```bash
305
+ git clone https://github.com/AnisurRahmann/call-copilot.git && cd call-copilot
306
+ make install # creates .venv and installs `rec` in editable mode + dev deps
307
+ make test # unit tests (offline — no audio device or model download needed)
308
+ make run # rec start, using the dev venv's rec
309
+ ```
310
+
311
+ `make install` requires Python 3.11+ on your `PATH` (it pins `python3.11` for the
312
+ venv). For everything else — releasing, the Homebrew tap, code style — see
313
+ [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/HOMEBREW.md](docs/HOMEBREW.md).
314
+
315
+ ## Contributing
316
+
317
+ Issues and pull requests are welcome at
318
+ [github.com/AnisurRahmann/call-copilot](https://github.com/AnisurRahmann/call-copilot).
319
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a PR, and report security
320
+ issues privately per [SECURITY.md](SECURITY.md).
321
+
322
+ ## What this is NOT
323
+
324
+ - Not a real-time transcription tool (transcription happens after you stop).
325
+ - Not a meeting summarizer (just the transcript).
326
+ - Not a Zoom/Meet plugin (it captures system audio generically).
327
+ - Not cross-platform (macOS 14.2+ only).
328
+
329
+ ## License
330
+
331
+ MIT — see [LICENSE](LICENSE). © Anisur Rahman.
@@ -0,0 +1,16 @@
1
+ call_copilot-0.2.0.dist-info/licenses/LICENSE,sha256=GAQRXFSYf-N4Xte18w24Mm3rfj3IHt6SxG4201fCX6s,1070
2
+ rec/__init__.py,sha256=lP3lBk7eIk7Dkc74Yl4P8wfHAj_PLAi3sEVqL3cXkwM,312
3
+ rec/audio_check.py,sha256=Q5DwJT94_Rffjy1zz29Prx8aGqNTWjq2IWpAiQWdJJY,4905
4
+ rec/cli.py,sha256=u-Xi5yJR0Gq_QDAL5QGbDMYi0exP12Nr6Uu7AY_04ak,36277
5
+ rec/config.py,sha256=yuwEYmya3qYIgt8ynaNDWX0UokmRk4xnxoxwMoTN_jU,4834
6
+ rec/envcheck.py,sha256=ZWEEgGqePfYTIJNZajjw1dmK9i1DSDyXK83nMK9e8Cs,3694
7
+ rec/formatter.py,sha256=VYnKIqpAeq4XHIDgNpOASbzq_m7PGuKYDZpjsvXji0U,3719
8
+ rec/log.py,sha256=YddB8rWgtmEAEqnyWUDS4nE29K8GuIDrhhEUM0wLWpA,8942
9
+ rec/recorder.py,sha256=byz-5FpuKNllF6RrC30p78b3mPF3e4D2L3888E9hoIg,26922
10
+ rec/session.py,sha256=8bGu6LdsjbsoTlI-DjfQ2PL_xYAz1LBPjaZoTwrHiPM,5715
11
+ rec/transcriber.py,sha256=eh5SCrOM9ahvdIjE_XXGI3BKY0SBtjFbB1tnw6HIGq8,6809
12
+ call_copilot-0.2.0.dist-info/METADATA,sha256=7weV4mq-zBN5jdHZVMAEfbrwNERMnZCZtwTm513YOhE,14540
13
+ call_copilot-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
14
+ call_copilot-0.2.0.dist-info/entry_points.txt,sha256=DZ2FBOrZFejlVOEjlA6kNZOj-VNt4jC5h1ETW-l_qUk,37
15
+ call_copilot-0.2.0.dist-info/top_level.txt,sha256=aAhpQ5vNKbO1qVtzOviIHI79AYUz7beFknFX6PYLHUQ,4
16
+ call_copilot-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rec = rec.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anisur Rahman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ rec
rec/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Call Copilot — a terminal meeting recorder (command: `rec`).
2
+
3
+ Records system audio in the background via macOS Core Audio taps (the
4
+ `audiotap` library — zero degradation of what you hear, no virtual audio
5
+ driver) and transcribes locally with faster-whisper into clean markdown.
6
+ """
7
+
8
+ __version__ = "0.1.0"
rec/audio_check.py ADDED
@@ -0,0 +1,114 @@
1
+ """Audio level analysis — detect silent / near-silent recordings.
2
+
3
+ A silent recording is the #1 failure mode for system-audio capture: the tap
4
+ is running but no app is playing audio (or the permission was revoked mid-
5
+ stream), the recorder faithfully writes a file of zeros, and Whisper's VAD
6
+ strips everything → an empty transcript.
7
+
8
+ Catching this immediately after `rec stop` (before Whisper runs) saves the
9
+ user a full transcription cycle and points them at the real problem. Analysis
10
+ streams the file in chunks so it's constant-memory for multi-hour recordings.
11
+
12
+ The recorder writes 32-bit float WAV (audiotap delivers float32 PCM), so we
13
+ read as float64 for headroom in the sum-of-squares and compare peak against a
14
+ normalized threshold (float samples are in the range -1.0..1.0).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ from .log import get_logger
23
+
24
+ log = get_logger(__name__)
25
+
26
+ # A recording is "silent" if peak amplitude is below this. float samples are
27
+ # in [-1.0, 1.0]; real speech peaks well above 0.01 even when quiet. Below
28
+ # ~0.001 is effectively the digital noise floor.
29
+ SILENCE_PEAK_THRESHOLD = 0.001
30
+
31
+ # Chunk size for streaming analysis (frames). 1M frames ≈ 62s at 16kHz.
32
+ _ANALYSIS_CHUNK = 1_000_000
33
+
34
+
35
+ @dataclass
36
+ class AudioLevels:
37
+ """Streaming amplitude statistics over a WAV file."""
38
+
39
+ peak: float = 0.0 # max absolute sample value (0.0..1.0 for float)
40
+ rms: float = 0.0 # root-mean-square (overall loudness)
41
+ frames: int = 0 # total frames analyzed
42
+ sample_rate: int = 0
43
+ silent: bool = False # peak below SILENCE_PEAK_THRESHOLD
44
+
45
+ @property
46
+ def duration_seconds(self) -> float:
47
+ return self.frames / self.sample_rate if self.sample_rate else 0.0
48
+
49
+
50
+ def analyze_wav(wav_path: str | Path) -> AudioLevels | None:
51
+ """Stream a WAV and compute peak + rms. Returns None if unreadable/empty.
52
+
53
+ Reads as float64 (soundfile normalizes PCM/float to double for us). The
54
+ recorder writes float32, so values land in [-1.0, 1.0]. Streams in chunks
55
+ for constant memory on long recordings.
56
+ """
57
+ import numpy as np
58
+ import soundfile as sf
59
+
60
+ path = Path(wav_path)
61
+ if not path.exists():
62
+ log.warning("audio check: wav not found %s", path)
63
+ return None
64
+
65
+ try:
66
+ with sf.SoundFile(str(path), mode="r") as f:
67
+ sr = f.samplerate
68
+ peak = 0.0
69
+ sum_sq = 0.0
70
+ total = 0
71
+ for block in f.blocks(blocksize=_ANALYSIS_CHUNK, dtype="float64"):
72
+ arr = block.reshape(-1) if block.ndim > 1 else block
73
+ abs_arr = np.abs(arr)
74
+ peak = max(peak, float(abs_arr.max(initial=0.0)))
75
+ sum_sq += float((abs_arr ** 2).sum())
76
+ total += arr.size
77
+ except Exception as e: # pragma: no cover — defensive
78
+ log.warning("audio check: could not read %s (%r)", path, e)
79
+ return None
80
+
81
+ rms = (sum_sq / total) ** 0.5 if total else 0.0
82
+ silent = peak < SILENCE_PEAK_THRESHOLD
83
+ levels = AudioLevels(peak=peak, rms=rms, frames=total, sample_rate=sr, silent=silent)
84
+
85
+ if silent:
86
+ # Distinguish the two causes of silence. A tap that ran (frames > 0)
87
+ # but produced *literal* zeros (peak exactly 0.0) almost always means
88
+ # macOS is not honoring the capture permission for the running app:
89
+ # the tap is invoked with real frame counts but every sample is 0.
90
+ # A genuinely quiet-but-live source usually leaves some dither noise
91
+ # (peak > 0 but < threshold). Tailor the guidance accordingly.
92
+ if total > 0 and peak == 0.0:
93
+ log.error(
94
+ "RECORDING IS SILENT: peak=%.6f rms=%.6f over %.1fs (%d frames). "
95
+ "The tap ran but captured literally zero samples — this is the "
96
+ "macOS capture-permission signature, not a quiet source. Toggle "
97
+ "Screen Recording ON for your terminal app in System Settings > "
98
+ "Privacy & Security, then FULLY QUIT and reopen the app (a running "
99
+ "process keeps getting zero buffers until restarted). Re-check with "
100
+ "`rec setup`.",
101
+ peak, rms, levels.duration_seconds, total,
102
+ )
103
+ else:
104
+ log.error(
105
+ "RECORDING IS SILENT: peak=%.6f rms=%.6f over %.1fs — "
106
+ "the audio tap captured no usable signal. Either nothing was "
107
+ "playing, or macOS revoked the system-audio capture permission. "
108
+ "Run `rec setup` to re-check permissions, then record again while "
109
+ "audio is actually playing.",
110
+ peak, rms, levels.duration_seconds,
111
+ )
112
+ else:
113
+ log.info("audio levels OK: peak=%.4f rms=%.4f over %.1fs", peak, rms, levels.duration_seconds)
114
+ return levels