Build Granola
YESreplaces $14/mosaves $168/yrback to the verdict
A local meeting-notes app for your Mac: a menu bar button records system audio and your microphone, whisper.cpp transcribes on your machine, an LLM turns the transcript plus your rough notes into a summary with decisions and action items, and everything lands as Markdown files in a folder you own. Nothing leaves the machine except the optional LLM call.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- have readythe Mac you have
Why ScreenCaptureKit audio capture needs macOS 13+, and local transcription is only pleasant on Apple Silicon.
Get it Apple menu > About This Mac. Update through System Settings > General > Software Update if below 14.
Verify
sw_vers prints ProductVersion 14 or higher - installfree
Why Swift, SwiftUI and the macOS SDK come with it. The Command Line Tools alone are not enough for a signed menu bar app.
Get it Install from the Mac App Store (it is large; start the download first). Open it once to accept the licence and install components. open ↗
Verify
xcodebuild -version prints Xcode 16 or higher - installfree
Why cmake builds whisper.cpp; ffmpeg is the easiest way to inspect and convert audio while you debug Phase 1.
Get it Install Homebrew from brew.sh, then: brew install cmake ffmpeg open ↗
Verify
cmake --version and ffmpeg -version both print - installfree
Why The whisper-cli binary is what Phase 2 shells out to.
Get it git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp && cmake -B build && cmake --build build --config Release. The binary is build/bin/whisper-cli. open ↗
Verify
./build/bin/whisper-cli -h prints usage - have readyfree, 1.5 GB of disk
Why Without a model file there is no transcription. medium.en is the quality-speed balance for English meetings.
Get it Inside the whisper.cpp folder: ./models/download-ggml-model.sh medium.en. It saves to models/ggml-medium.en.bin. Use small.en (about 500 MB) if disk or speed is tight.
Verify
ls -lh models/ggml-medium.en.bin shows about 1.5G - API keypay per use; a one-hour meeting summary costs a few cents
Why Phase 3 sends the transcript to a model for the summary. Without a key the app still records and transcribes; it just skips the summary.
Get it Anthropic: console.anthropic.com > API Keys > Create Key. OpenAI: platform.openai.com/api-keys > Create new secret key. Copy it once; it is not shown again. Put it in .env, never in the source. open ↗
- API keypay per use; Groq has a free tier
Why An optional fast path when local transcription is too slow on your machine. Local stays the default.
Get it Groq: console.groq.com > API Keys. OpenAI: the same key as above works for the audio endpoint. open ↗
- decidefree
Why System audio arrives under the Screen Recording permission (no separate audio permission exists); the mic needs its own. Both are granted once per app build.
Get it macOS prompts on first use. If you dismiss the prompt, the app must be relaunched before it can ask again; otherwise System Settings > Privacy & Security > Screen Recording and > Microphone.
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
One meeting is one folder: `~/MeetingNotes/YYYY-MM-DD-HHMM-<slug>/` containing `audio.wav`, `transcript.txt`, and `notes.md`. `notes.md` carries YAML frontmatter (title, date, duration, participants, model used) so the folder stays greppable and portable with no app-specific index to corrupt.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
NOTES_DIR | required | ~/MeetingNotes | Where meeting folders are written. Any folder you sync or back up. |
WHISPER_BIN | required | /Users/you/whisper.cpp/build/bin/whisper-cli | Path to the binary you built. |
WHISPER_MODEL | required | /Users/you/whisper.cpp/models/ggml-medium.en.bin | Path to the model you downloaded. |
ANTHROPIC_API_KEYsecret | optional | sk-ant-... | Anthropic console. Leave empty to skip summaries. |
OPENAI_API_KEYsecret | optional | sk-... | OpenAI dashboard. Used for summaries if no Anthropic key, and for hosted Whisper if enabled. |
LLM_MODEL | required | claude-sonnet-5 | Model name for summaries. Anything the provider offers. |
GROQ_API_KEYsecret | optional | gsk_... | console.groq.com. Enables the hosted transcription fast path. |
TRANSCRIBE_MODE | required | local | local or hosted. Local is the default and works offline. |
The build, in order
Audio capture, proven as a CLI first
Before any UI exists, prove you can record system audio and the mic into a WAV that whisper-cli accepts. This is the phase that kills the project if skipped.
The CLI target is for this phase; the app target waits until Phase 5. Both share a Swift package with the capture code.
Files
MeetingNotes.xcodeprojSources/Capture/Build an SCStreamConfiguration with capturesAudio true. SCStream still requires a screen output to be added even for audio only: add one and set minimumFrameInterval to a very large value so no real frames are processed. On macOS 14.2+ you may instead use Core Audio process taps (AudioHardwareCreateProcessTap) for genuine audio-only capture; pick one and say which in a comment.
Do not use SCStreamConfiguration's microphone capture; it has known output-corruption reports. Tap the input node and collect buffers.
Resample explicitly with AVAudioConverter. Not 44.1 kHz, not float, not stereo. whisper-cli accepts 16-bit WAV only; every hour lost to this project is lost here.
- terminal
swift run capture-cli --seconds 30 --out /tmp/test.wav afinfo /tmp/test.wav /path/to/whisper-cli -m /path/to/ggml-medium.en.bin -f /tmp/test.wav
done when · tick each as it passeswatch out- Do not tell the user to install BlackHole or Loopback. ScreenCaptureKit delivers system audio under the screen-recording grant with no driver.
- The permission prompt appears once per launch. If it is dismissed the app must be relaunched; detect the denied state and say so.
Transcription
A transcript from a WAV, local by default, with progress you can see and a hosted fast path when a key exists.
Process with WHISPER_BIN, WHISPER_MODEL and the audio path. Read the .txt it writes. Capture stderr for progress lines.
terminalwhisper-cli -m $WHISPER_MODEL -f audio.wav -otxt -of transcript
A 45-minute meeting on the medium model is minutes of CPU. Parse whisper's progress output into a percentage; a UI that looks frozen will be assumed broken.
When hosted and a Groq or OpenAI key exists, post the WAV to the Whisper endpoint. Local remains the default: the recording should not leave the machine unless asked.
Write the transcript to a temp name and rename on completion. Killing the app mid-transcription must leave the audio intact and no half-written transcript.
done when · tick each as it passesSummarization
A fixed prompt turns the transcript and your jotted notes into a 5-bullet summary, decisions and action items with owners, and long meetings are chunked rather than truncated.
summarize(transcript, notes) returning structured text. Anthropic first; OpenAI as a second implementation selected by which key exists.
Split on paragraph boundaries into chunks under the model's limit, summarize each, then summarize the summaries. Never silently truncate: that produces confident notes about the first third of a meeting.
This is the actual Granola idea: your jottings steer what the summary emphasises.
done when · tick each as it passesNotes on disk
One folder per meeting, plain Markdown with frontmatter, never overwriting.
notes.md: YAML frontmatter (title, date, duration, participants, model), the summary, a divider, then the full transcript.
If the folder exists, suffix a counter. Two meetings in the same minute must produce two folders.
done when · tick each as it passesMenu bar app
Start and stop from the menu bar, jot notes while recording, see recent meetings, and get told exactly what to do when a permission is missing.
A recording indicator that is unmistakable. Wire the capture code from Phase 1.
Text typed during the meeting is passed to the summarizer in Phase 3.
Read NOTES_DIR; newest first.
If Screen Recording or Microphone is denied, show which System Settings pane to open and the relaunch caveat, instead of failing silently.
done when · tick each as it passesOffline, packaging, README
Works with the network off when the model is present, signed for your own machine, documented.
Transcript saved; summary skipped with an honest note in notes.md.
Sign with your development certificate for your own use. Right-click > Open the first time.
Permissions and where to grant them, model download size, disk cost per hour of audio, and a plain warning that recording a meeting can require consent from everyone in it depending on where they are.
Files
README.md
done when · tick each as it passesDistribute itproduct builder
Only for the product path: an app other people can install without terrifying Gatekeeper dialogs, that updates itself, and that tells you when it crashes.
Required to notarize. Xcode > Signing & Capabilities with the Developer ID Application certificate.
terminalxcrun notarytool submit MeetingNotes.zip --keychain-profile notary --wait xcrun stapler staple MeetingNotes.app
Host an appcast XML on your domain; sign updates with the EdDSA key Sparkle generates.
A local crash log the user can send you, or Sentry's macOS SDK behind an explicit opt-in toggle. Never on by default for a privacy-first app.
done when · tick each as it passes
That is the whole plan for Granola. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Sync across devices and a mobile app. That is most of the subscription.
- Automatic calendar joining and bot attendees.
- Someone else's servers and support when a recording is lost.
- sync across devices
- the mobile app
- automatic calendar joins
- someone else's servers & support
- Speaker diarization with a local model so action items get real owners
- Ollama as a local summarizer behind the same provider interface for a fully offline pipeline
Need the files? The project pack on the verdict page hands your agent the whole brief · more meeting notes.