Prove your meeting-notes pipeline never phones home (and gates on consent)
Run capture -> whisper.cpp transcription -> Ollama summary fully on your machine, with a CI check that every endpoint is loopback, no cloud host or API key appears anywhere in the config, and recording is gated on a consent acknowledgment.
Run this workflow
CI-verified, 2/2 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
Anyone building a local meeting-notes pipeline who needs proof it never phones home. CI parses the pipeline config and asserts every URL is loopback (localhost/127.0.0.1), no known cloud host (Anthropic, OpenAI, Groq, OpenRouter, Google) appears anywhere in it, no API key is set, transcription is marked offline with a local model file, and the consent gate is ON. No audio, no model call, no key. Transcript accuracy, summary quality, and legal compliance are fenced.
Not for
- Believing local makes recording legal, consent law does not care where the bytes are processed; the gate here forces the acknowledgment, it cannot obtain the consent for you
- Trusting the local summary blindly, a small model can invent an action item nobody agreed to; check the summary against the transcript, never the other way around
- Perfect speaker labels, diarization tooling (WhisperX/pyannote) says itself it is far from perfect on crosstalk, and Meetily's free edition does not ship diarization at all (it is a planned PRO feature)
The Stack
Tested Against
github.com/ggml-org/whisper.cpp (2026-07)github.com/ollama/ollama (2026-07)node@20Side effects & data flow
- Network
- none, local only
- Writes
- ./notes-pipeline.json
- Credentials
- none required
Prerequisites
- whisper.cpp built locally with a ggml model file (base or small on a modest laptop)
- Ollama installed with a small instruct model pulled
Steps
- 1
Scaffold the pipeline config and audit it for egress + consent
The config declares all three stages. The audit walks every value: any URL must be loopback, a denylist of known cloud AI hosts must not appear anywhere, no api_key/token field may carry a value, transcription must be offline with a local model file, and capture.require_consent_ack must be true. Flip the summarizer to a cloud provider and this fails loudly, which is exactly the dropdown mistake it exists to catch.
cat > notes-pipeline.json <<'JSON' { "capture": { "mic": true, "system_audio": true, "require_consent_ack": true }, "transcribe": { "engine": "whisper.cpp", "model_file": "./models/ggml-base.en.bin", "offline": true }, "summarize": { "provider": "ollama", "base_url": "http://127.0.0.1:11434", "model": "llama3.2:3b" } } JSON node -e ' const fs = require("fs"); const raw = fs.readFileSync("notes-pipeline.json", "utf8"); const c = JSON.parse(raw); function bad(m) { console.error("BAD: " + m); process.exit(1); } const loopback = ["localhost", "127.0.0.1", "::1"]; const cloudHosts = ["api.anthropic.com", "api.openai.com", "api.groq.com", "openrouter.ai", "generativelanguage.googleapis.com"]; const keyNames = ["api_key", "apikey", "apiKey", "token", "secret"]; let urls = 0; function walk(node, path) { if (node && typeof node === "object") { for (const k of Object.keys(node)) { if (keyNames.indexOf(k) >= 0 && node[k]) bad("credential field " + path + "." + k + " is set; a local pipeline needs no key"); walk(node[k], path + "." + k); } } else if (typeof node === "string" && node.indexOf("http") === 0) { urls++; const host = new URL(node).hostname; if (loopback.indexOf(host) < 0) bad("endpoint " + node + " is not loopback; this pipeline phones home"); } } walk(c, "config"); for (const h of cloudHosts) { if (raw.indexOf(h) >= 0) bad("known cloud AI host " + h + " found in config"); } if (c.transcribe.engine !== "whisper.cpp" || c.transcribe.offline !== true) bad("transcription must be whisper.cpp with offline: true"); if (!c.transcribe.model_file || c.transcribe.model_file.indexOf("./") !== 0) bad("transcription model must be a local file path"); if (c.capture.require_consent_ack !== true) bad("consent gate is OFF; local does not make recording legal, the pipeline must refuse to arm without an acknowledgment"); console.log("pipeline OK: " + urls + " endpoint(s) all loopback; 0 cloud hosts; no API keys; transcription offline (whisper.cpp, local model file); consent gate ON. Summary quality and the law itself are fenced"); ' - 2
Run the pipeline for real (the model steps, not checked by CI)
Record with consent acknowledged, transcribe (./main -m models/ggml-base.en.bin -f meeting.wav in whisper.cpp, or WhisperX for word timestamps and speaker labels), then pipe the transcript to the local summarizer: ollama run llama3.2:3b with a prompt asking for decisions and action items. Transcript accuracy on crosstalk, summary quality (a small model can invent an action item), and whether your consent practice satisfies your jurisdiction are all fenced, no green check can promise any of them.
Eval, 2 fixtures
Last passed: verified todayegress-okcontainstimeout 30s · max $0Expected:
pipeline OK: 1 endpoint(s) all loopback; 0 cloud hosts; no API keys; transcription offline (whisper.cpp, local model file); consent gate ON. Summary quality and the law itself are fencedclean-exitexit_codetimeout 30s · max $0Expected:
0
Results
A private Otter replacement has three stages: capture the audio, transcribe locally (whisper.cpp), summarize with a local model (Ollama). The stage that silently un-privates it is the summary: friendly apps like Vibe and Meetily offer cloud providers (Claude, Groq, OpenRouter) right next to the Ollama option, so one dropdown ships your whole meeting to a server. This recipe makes 'stays local' a checkable property of the pipeline config instead of a hope. And the rule the cloud lawsuits taught: local processing does not make recording legal, so the config refuses to arm capture until a consent acknowledgment is set.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Related workflows
- Unsloth: write parametric memory in with a fine-tune config
- Validate an Apple Core AI export entry and skill plugin before you touch a Mac
- Serve NVIDIA Nemotron 3 Ultra yourself for high-throughput agents (vLLM)
- Serve GLM-5.1 yourself for long-horizon agentic coding (vLLM)
- Serve MiniMax M3 yourself for agentic coding (vLLM)
- Local model chore: read a photo with a vision model, on-device
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).