Local InferenceOpen SourceFreeActiveMachine-verified· intermediate · ~20 min setup

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.

by Shilpa Mitra· verified today· v1.0.0

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@20

Side 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. 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. 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 today
  • egress-okcontainstimeout 30s · max $0

    Expected: 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 fenced

  • clean-exitexit_codetimeout 30s · max $0

    Expected: 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

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).