feat(audio): add opt-in local speech memory channel - #5
Conversation
A third channel beside screen and clipboard: WASAPI capture, WebRTC voice activity detection, and whisper.cpp in this process. Speech becomes `events` of kind `audio` in the same table, through the same writer, with no schema change at all. Off, and off in two independent ways. `audio` is a Cargo feature disabled by default, so the shipped binary has no capture path, no model loader, and nothing that can open a microphone - and building it needs neither CMake nor libclang. Even a build that HAS the feature records nothing until somebody runs `screenpipe audio run` or installs its own scheduled task; `screenpipe run` starts it never, and `screenpipe service install` installs it never. `audio service` is deliberately outside the feature gate, because a build that cannot record must still be able to remove a task an earlier build installed. Loopback is the default, not the microphone. Loopback hears what came out of the speakers; the microphone hears the room and everyone in it, which is a second decision the operator has to make explicitly, and every run that makes it says so at the top of the log. Silence decides the boundaries. Three voiced 20 ms frames open an utterance, 600 ms of silence closes it, the 200 ms before the trigger is kept so the first consonant survives, and the trailing silence is cut before the model sees it. Whisper is never asked where speech starts - that would cost several orders of magnitude more than a signal test built for the job. Each utterance is then its own event unless its transcript is identical to the open one. That is the clipboard's rule, and it is load-bearing here for a different reason: the writer REPLACES ocr_text on a merge, so merging two different transcripts would keep the second and lose the first. What it does merge is repetition, which is whisper's failure mode over a quiet room. What it refuses to record: any device name (the endpoint ROLE is stored instead - console or communications - because "Microphone (Realtek High Definition Audio)" identifies hardware in someone's house); audio under 250 ms; segments the model itself scores above 0.6 no-speech; and anything at all when nothing survives that filter. whisper.cpp's own stdout logging is routed into hooks that lead nowhere, because this process's stdout is a log file kept on disk and what whisper prints is somebody's speech. Three threads, because the capture thread must never block: the audio engine's buffer is finite and a stalled reader loses audio with no record that it happened. It hands closed utterances to the transcriber and drops them loudly if that queue is full. Dropping happens at the cheap end, never at the durable one. Two things found by running it rather than reasoning about it. A loopback stream delivers NOTHING while nothing is playing - not silence, nothing - so the first live run declared the stream stalled two seconds after opening on a perfectly healthy machine; a timed-out wait is now silence. And whisper-rs-sys's bundled bindings are glibc-generated and will not compile on MSVC, so libclang is genuinely required for this feature. Verified live: two spoken sentences captured over loopback, transcribed, and written to the disposable test database as kind='audio' rows with word-perfect transcripts, correct titles, and full merge_meta. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhXifh8P7AhWdAcBnSSW5b
Found by review on the design PR, and it was a real defect rather than a documentation one. The merger's idle-gap test measures `next.captured_at - open.ended_at`, and an utterance's timestamp is when the speech STARTED - so the "silence" between two turns silently included the length of the first one. A 30-second sentence followed by a 35-second pause read as a 65-second gap and split at a 60-second threshold that 35 seconds of silence never crossed. The claim in the design document that the interval between two audio samples IS the silence was simply wrong. `ObservationSample.observed_until` fixes it at the root: an observation may now occupy a span, `None` means it was an instant, and the merger takes the event's `ended_at` from it. Screen and clipboard samples are instants and nothing about their windows moves - there is a test for that. It also makes the durable window true. `ended_at - started_at` is now how long the speech actually ran, so `merge_meta.audio.duration_ms` - added an hour ago as a workaround for the window being an instant - is gone rather than left as a second source of truth that can disagree. Also from that review: `audio service install` is refused in a build without the feature. Uninstall and status stay open in every build, which was deliberate - a build that cannot record must still be able to turn one off - but install would have registered a task running `audio run` from a binary that refuses `audio run`, and the operator's only evidence would be a log they have no reason to read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhXifh8P7AhWdAcBnSSW5b
🤖 CodeAnt AI — Review Status
|
Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
@codex review |
There was a problem hiding this comment.
Pull request overview
Adds a Windows-only, opt-in audio capture/transcription “memory” channel that records transcripts as events(kind=audio) without persisting raw audio, while keeping default builds free of whisper.cpp/CMake/libclang requirements and adding dedicated CI coverage for the audio toolchain.
Changes:
- Introduces
screenpipe-audio(WASAPI capture + WebRTC VAD + local whisper.cpp transcription) and wires it into the CLI behind anaudiofeature flag. - Extends the merge contract (v6) to support utterance-window-scoped audio deduplication and span-based observations via
observed_until+merge_meta.audio. - Updates workspace pruning verification, README build/usage docs, and CI to keep audio opt-in while still testing it on Windows with pinned LLVM/Ninja.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| scripts/verify-pruned.ps1 | Verifies audio is a workspace member but not a default member, and keeps pruning constraints intact. |
| README.md | Documents opt-in audio semantics, build steps (incl. pinned Ninja/LLVM), and the durable audio event contract. |
| crates/screenpipe-screen/tests/merge_boundary_live.rs | Updates test sample construction for new ObservationSample fields. |
| crates/screenpipe-memory/tests/postgres_writer.rs | Adds persistence assertions for audio kind/title/merge_meta.audio and absence of audio key for non-audio rows. |
| crates/screenpipe-memory/tests/merge_contract.rs | Expands merge contract tests for audio utterance identity/dedup and span-based idle-gap behavior. |
| crates/screenpipe-memory/tests/debug_redaction.rs | Updates redaction test fixtures for new sample fields. |
| crates/screenpipe-memory/src/sample.rs | Adds observed_until + AudioMeta to ObservationSample and exposes observed_until() helper. |
| crates/screenpipe-memory/src/runner.rs | Updates runner test fixtures for new sample fields. |
| crates/screenpipe-memory/src/postgres.rs | Writes merge_meta.audio only when present (no "audio": null on other event kinds). |
| crates/screenpipe-memory/src/merge.rs | Adds EventKind::Audio, bumps merge contract version, and implements utterance-window-based audio merge hashing/splitting. |
| crates/screenpipe-memory/src/lib.rs | Re-exports AudioMeta for downstream (CLI/audio channel) use. |
| crates/screenpipe-cli/tests/cli_surface.rs | Adds CLI surface/consent assertions for audio help text and feature-gated behavior. |
| crates/screenpipe-cli/src/windows_source.rs | Updates screen sample construction for new sample fields. |
| crates/screenpipe-cli/src/service.rs | Splits service management into ServiceKind::{Screen,Audio} with separate task/binary/wrapper/log stems. |
| crates/screenpipe-cli/src/main.rs | Adds audio subcommands, feature-gated audio runtime loop/doctor, and routes audio service actions. |
| crates/screenpipe-cli/src/clipboard_source.rs | Updates clipboard sample construction for new sample fields. |
| crates/screenpipe-cli/src/audio_source.rs | New audio sample source implementing capture/transcribe/write pipeline and fixed diagnostic strings. |
| crates/screenpipe-cli/Cargo.toml | Introduces audio feature (off by default) with optional screenpipe-audio dependency. |
| crates/screenpipe-audio/src/vad.rs | New VAD segmentation (utterance boundaries, preroll, hangover, max length) with tests. |
| crates/screenpipe-audio/src/transcribe.rs | New local whisper.cpp transcription wrapper, model path guarding, and no-speech filtering. |
| crates/screenpipe-audio/src/lib.rs | New crate entrypoint and public API for capture/VAD/transcription/channel codes. |
| crates/screenpipe-audio/src/device.rs | New endpoint-role categorization logic avoiding device-name reads/persistence. |
| crates/screenpipe-audio/src/capture.rs | New WASAPI loopback/mic capture producing fixed-shape frames with timeout-as-silence handling. |
| crates/screenpipe-audio/Cargo.toml | Adds dependencies for WASAPI, WebRTC VAD, and whisper-rs (CPU-only defaults). |
| Cargo.toml | Adds audio crate to workspace members while keeping it out of default-members (opt-in build). |
| Cargo.lock | Records new dependency graph for audio + whisper-rs/wasapi/webrtc-vad. |
| .github/workflows/ci.yml | Splits non-audio CI from a dedicated Windows audio job with pinned LLVM/Ninja + env vars. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
PR Summary by Qodofeat(audio): opt-in local speech capture/transcription channel
AI Description
Diagram
High-Level Assessment
Files changed (27)
|
Code Review by Qodo
1.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 702c501c35
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 762b085feb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
User description
Summary
LIBCLANG_PATH, andCMAKE_GENERATOR=NinjaValidation
cargo test -p screenpipe-memory --test merge_contract: 38 passedcargo test -p screenpipe-audio: 20 passedcargo check -p screenpipe-cli --features audio: passedscripts/verify-pruned.ps1: passedEvidence boundaries
Documentation impact
Durable behavior changed, so README is updated in this PR for opt-in semantics, utterance-scoped deduplication, and the qualified audio build path.
CodeAnt-AI Description
Add an opt-in local audio channel for private speech transcription
What Changed
--microphoneoption for recording the roomaudio doctor, foreground recording, and a separate audio scheduled serviceImpact
✅ Private on-device transcription✅ Fewer silence and noise transcript entries✅ Separate control for room microphone capture💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.