meetingscribe

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 12, 2026 License: MIT Imports: 3 Imported by: 0

README

meeting-scribe

English · Русский

Meeting transcripts separated by speaker, plus a detailed summary — on your own Mac.

macOS Go License

Point it at a recording of a meeting. It splits the audio by voice, writes out who said what, and produces a follow-up: the meeting's topics, and under each who does what and by when.

Speech recognition and speaker separation run locally. Only the transcript text leaves your machine, and only for two steps — polishing and summarising — through the Claude or ChatGPT subscription you already have. No API keys, no per-minute billing.

What leaves your machine

The honest version, including the exception:

Step Where it runs Leaves the machine?
Audio decoding (ffmpeg) local no
Speech recognition (whisper.cpp) local no
Speaker separation (sherpa-onnx) local no
Transcript polishing your Claude/ChatGPT subscription yes — transcript text
Summary your Claude/ChatGPT subscription yes — transcript text
Results your disk no

The audio file itself is never uploaded anywhere. Transcription and speaker separation work with the network off — unplug and try it. What does go out is the transcript text, sent by the CLI agent already installed under your own account.

Requirements

  • macOS on Apple Silicon (M1/M2/M3…).
  • A Claude or ChatGPT subscription — needed only for polishing and summarising. Transcription and speaker separation work without one.
Provider CLI Minimum plan
Claude claude Pro — Claude Code is not available on the free tier
ChatGPT codex any, including free — only the usage limits differ
  • ~4–6 GB of disk for the speech models, downloaded once on first install.

You log in to the CLI yourself; meeting-scribe runs it as a subprocess under your account and never handles an API key.

Install

curl -fsSL https://raw.githubusercontent.com/krischandran/meeting-scribe/main/scripts/install.sh | bash

The script installs only what is missing (Xcode CLT, Homebrew, Go, ffmpeg, whisper.cpp), downloads the speech models with sha256 verification, writes a config and creates a meeting-scribe app in your Applications folder. Budget 15–40 minutes on a clean machine; later starts are fast.

Then log in to your provider once — run claude or codex in a terminal — and double-click the app icon.

When a new release is out the app says so in the sidebar and offers to install it. It installs the release and runs the setup that goes with it, then asks you to restart the app; it does not restart itself, because while the old process is alive the system hands that one back.

Using it

The window has four screens.

Run is where the work happens. Drag a recording onto the window, or press Choose file. You can leave the output folder alone — without a choice the result goes to the default folder. Press Run and the stages tick past as they finish; when the last one is done the transcript and the summary appear on the same page. Supported inputs are m4a, mp3, wav, mov and mp4.

Two controls decide how speakers are handled, and they are independent. The separate by speaker checkbox decides whether that step runs at all; unticked, it is skipped entirely and the transcript comes out as plain paragraphs with no labels. When it is ticked, how many speakers takes a number from 1 to 30 and the separator is told exactly that. One speaker skips the step too — there is nothing to separate — so it is the same as unticking the box, said the other way round.

Give the real number. Left to work it out alone, the separator is dependable on short recordings and degrades badly on long ones: on an 86-minute meeting between two people it reported fifty, and it reported it without any sign that anything had gone wrong.

History lists every past meeting, newest first. Clicking a row reopens that meeting's transcript and summary, and a summary can be built or rebuilt from there — including for a meeting recorded weeks ago, or after the app has been restarted.

Status shows every dependency and whether it is present. If something reads as missing, press install what's missing.

Provider picks which subscription does the polishing and summarising, Claude or ChatGPT.

Language is a switch in the sidebar under the screen list, labelled Language in both languages on purpose: a switch labelled in the language you cannot read is no use to the person looking for it. One setting drives all of it — recognition, the summary text, its section headings and the words in the window.

A long run needs no watching. The two slow stages report a percentage as they go, the Mac is held awake for as long as there is work to lose, and the screen is rebuilt from the server every time you come back to it: switch tabs, reload the page, close and reopen the window, and the run is still there at the stage and percentage it has reached. A finished run stays on screen too, with a button that opens it in History instead of printing a path at you.

If a run did not finish — the lid was closed, the app was quit — the Run screen says so on the next visit, names the stage it stopped at and offers to carry on. Carrying on reuses whatever was already computed, so it costs less than starting over. If the recording itself is gone there is no button: drop the file in again, and the stages that did finish still come back from the cache.

Closing: the shut down server button at the bottom left, or Cmd+Q. The red close button only hides the window — the program keeps running and clicking the icon brings it back.

Everything the window does is also a command:

meeting-scribe app                       # the native window (same as the icon)
meeting-scribe serve                     # same UI, as a browser tab instead
meeting-scribe run meeting.m4a --title "Release planning"
meeting-scribe run meeting.m4a --speakers 3          # exactly N speakers (1..30)
meeting-scribe run meeting.m4a --no-diarize          # skip separation: faster, no labels
meeting-scribe run meeting.m4a --stage transcribe    # extract|transcribe|diarize|clean|save|all
meeting-scribe summarize out/2026-03-14-release-planning   # idempotent; --force rebuilds
meeting-scribe history                   # saved meetings, newest first
meeting-scribe doctor                    # dependency status, changes nothing
meeting-scribe cache                     # what the engine cache holds, and how much
meeting-scribe cache --prune             # apply the size ceiling now
meeting-scribe version

run, doctor, history and cache take --json so they can be scripted:

DIR=$(meeting-scribe run meeting.m4a)                 # the path, one line
meeting-scribe doctor --json | jq -r '.[] | select(.present | not) | .name'

What you end up with

Each meeting becomes its own folder under out/, named by date and title, holding plain files you can open in any editor.

The transcript is the readable one: the meeting written out turn by turn, each turn marked with the speaker who said it. Speakers are separated by voice, not identified by name — they are labelled SPEAKER_00, SPEAKER_01 and so on, in order of first appearance, and SPEAKER_? where the separation was not confident enough to commit. Matching a label to a person is left to you.

The summary is a follow-up: the thing you paste into a work chat after the call. A title with the date, then the meeting's own topics as headings, each carrying short lines that fuse state, owner and deadline. The owner is named inside the line; where a speaker was never matched to a person the label stays as it is (SPEAKER_00), so two unknowns do not collapse into one.

A deadline appears only if one was actually said — a missing one is not spelled out. An open question is the last line of the topic it belongs to rather than a list of its own. The number of topics and lines is capped: a summary that grew with the meeting would stop being a summary.

Alongside them the folder keeps the normalised audio and two machine-readable files: the raw recognition result, which is never modified and which every other file is derived from, and a provenance record noting the source hash, engine, model, duration and which stages came from cache. Because the raw result is immutable, any later stage can be re-run without redoing the recognition.

How it works

Five stages, each independently restartable:

ffmpeg          whisper.cpp        sherpa-onnx        LLM              disk
extract    →    transcribe    →    diarize       →    clean       →    save
audio→WAV       word timings       speaker turns      dictionary       out/<date>-<slug>/
                                   (optional)         + polish

Speaker labels come from the diarizer, which hears voices but not meaning. The clean stage lets the LLM correct boundaries the diarizer could not hear, applies your dictionary, and polishes wording without changing what was said.

summarize is a separate idempotent command, so a summary can be redone — with a different meeting type, for instance — without re-running transcription.

Speaker separation is the expensive stage by a wide margin. On an 86-minute recording transcription took 5 minutes and separation 15, and that is with the threads it now gets: single-threaded the same run took 38. Skipping it is the difference between minutes and most of an hour.

Limitations

  • macOS on Apple Silicon only. No Intel Mac, Linux or Windows build.
  • Speakers are labelled, not named. You get SPEAKER_00 and SPEAKER_01, not Anna and Mark.
  • Say how many speakers there are. Automatic detection is dependable on short recordings and unreliable on long ones, and it fails quietly.
  • Batch, not live. It processes a finished recording; there is no real-time transcription.
  • Two languages, Russian and English. Anything else is not translated: the interface falls back to Russian and the summary keeps Russian headings, while recognition still works — whisper takes many more codes than this app has words for.
  • Polishing and summarising need a subscription and stop at your plan's usage limits. The transcript is saved either way, and summarize can be re-run later.
  • Overlapping speech is hard. Speaker separation degrades when people talk over each other or the microphone is far away.

Configuration

Copy config.yaml.example to config.yaml and point it at your binaries and models. Environment variables override the file: MSCRIBE_OUT_DIR, MSCRIBE_CACHE_DIR, MSCRIBE_CACHE_MAX_SIZE, MSCRIBE_LLM_PROVIDER, MSCRIBE_WHISPER_MODEL, MSCRIBE_WHISPER_LANGUAGE, MSCRIBE_CONFIG. Lookup order is MSCRIBE_CONFIG → ./config.yaml → ~/Library/Application Support/meeting-scribe/config.yaml.

Three settings are worth knowing about:

  • sherpa.num_threads (default 8) — threads for speaker separation. Measured 2.48x faster than single-threaded on an 86-minute recording, with byte-identical output.
  • timeouts.diarize (default 120m) — separation runs at roughly 0.18x realtime with those threads, so this covers about four hours of audio. A stage that runs out of budget says so and names the setting to raise.
  • cache_max_size (default 2GB) — the ceiling on decoded audio and engine results. Over it, the least recently used entries go, and an entry a run is reading right now is never removed. meeting-scribe cache shows the state and --prune applies the ceiling on demand.

Three more config files are written into ~/Library/Application Support/meeting-scribe/configs/ by setup:

  • dictionary.yaml — ordered term replacements applied before the LLM. Ships empty on purpose: a rule fires on every meeting, so a rule that is wrong for one corrupts it silently. Add only proper nouns the model cannot guess — company, product and people names from your own work.
  • summary.yaml — default meeting type and chunk budget. A language key here is ignored and the app says so at startup: the language is the sidebar switch, one setting for everything.
  • clean.yaml — polish prompt and chunk budget.

The app produces the follow-up and offers no other choice. The older category shape — a gist, decisions with their reasoning, tasks, discussion by topic, risks and next steps — is still in the build behind the meeting-type presets: standup, one_on_one, client_sales, retro, decision, board, plus seven analyst presets (requirements, integration, architecture, design_review, refinement, planning, project_status) that additionally fill a requirements block. Asking for one of those switches the summary to that shape. They are reachable from summary.yaml and from meeting-scribe summarize --meeting-type, not from the dropdown.

Development

go build ./...
make verify   # vet + gofmt + golangci-lint + tests + architecture check

Clean/hexagonal, enforced by an import matrix:

domain (stdlib only) ← port ← usecase ← adapter (cli/web/window) / infrastructure

Concrete engines are wired only in cmd/meeting-scribe/main.go. Each sits behind a port (AudioExtractor, Transcriber, Diarizer, LLM, TranscriptStore), so a different ASR engine or a new surface drops in without touching usecase code. The contract is checked by go-arch-lint, or offline by internal/archtest.

No CGO anywhere, including the native window (purego/objc over NSWindow + WKWebView). Building and unit-testing needs none of the runtime binaries: subprocess adapters are tested against a fake runner and golden fixtures, and integration paths skip when a binary is absent. The embedded frontend is plain HTML/CSS/JS with no build step and no CDN, so there is no node toolchain either; internal/adapter/web/static_drift_test.go is what holds it to its contract.

License

MIT — see LICENSE.

Documentation

Overview

Package meetingscribe is the module-root package. It exists only to embed the default config assets into the binary so an installed `meeting-scribe` is self-contained — `go install` does not leave the source tree. Only the composition root imports it; no internal layer does.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ConfigAsset

func ConfigAsset(name string) ([]byte, error)

ConfigAsset returns the embedded content of configs/<name> (e.g. "dictionary.yaml").

func ConfigAssets

func ConfigAssets() (map[string][]byte, error)

ConfigAssets returns every embedded config asset as name -> content, excluding the deps-manifest.

func DepsManifestBytes

func DepsManifestBytes() ([]byte, error)

DepsManifestBytes returns the embedded deps-manifest.yaml content.

Types

This section is empty.

Directories

Path Synopsis
cmd
meeting-scribe command
Command meeting-scribe is the composition root: it wires config, the resolved LLM provider and the concrete infrastructure adapters into the usecases.
Command meeting-scribe is the composition root: it wires config, the resolved LLM provider and the concrete infrastructure adapters into the usecases.
internal
adapter/cli
Package cli holds the thin cobra controllers over the usecases.
Package cli holds the thin cobra controllers over the usecases.
adapter/web
Package web is the HTTP adapter (a browser UI on 127.0.0.1) over the same usecases the cli adapter drives.
Package web is the HTTP adapter (a browser UI on 127.0.0.1) over the same usecases the cli adapter drives.
adapter/window
Package window is the native-macOS UI adapter: an NSWindow + WKWebView shell over the same HTTP server the web adapter serves.
Package window is the native-macOS UI adapter: an NSWindow + WKWebView shell over the same HTTP server the web adapter serves.
config
Package config loads and validates meeting-scribe configuration from a YAML file with environment overrides.
Package config loads and validates meeting-scribe configuration from a YAML file with environment overrides.
infrastructure/agentcli
Package agentcli implements port.LLM by running the user's subscription agent CLI (claude/codex) as a subprocess — no API key.
Package agentcli implements port.LLM by running the user's subscription agent CLI (claude/codex) as a subprocess — no API key.
infrastructure/appbundle
Package appbundle implements port.AppBundler: generate a double-clickable macOS .app whose executable is a symlink to the installed binary, which detects the bundle launch via argv[0].
Package appbundle implements port.AppBundler: generate a double-clickable macOS .app whose executable is a symlink to the installed binary, which detects the bundle launch via argv[0].
infrastructure/brew
Package brew implements port.PackageManager over the Homebrew CLI.
Package brew implements port.PackageManager over the Homebrew CLI.
infrastructure/buildinfo
Package buildinfo resolves this binary's version from the build info the Go toolchain stamps in.
Package buildinfo resolves this binary's version from the build info the Go toolchain stamps in.
infrastructure/codesign
Package codesign implements port.CodeSigner over the system xattr and codesign tools.
Package codesign implements port.CodeSigner over the system xattr and codesign tools.
infrastructure/configwriter
Package configwriter implements port.ConfigWriter over the local filesystem.
Package configwriter implements port.ConfigWriter over the local filesystem.
infrastructure/download
Package download implements port.Downloader over net/http with on-the-fly sha256.
Package download implements port.Downloader over net/http with on-the-fly sha256.
infrastructure/extract
Package extract implements port.Archiver over the system tar (bsdtar auto-detects bzip2/gzip).
Package extract implements port.Archiver over the system tar (bsdtar auto-detects bzip2/gzip).
infrastructure/ffmpeg
Package ffmpeg implements port.AudioExtractor by shelling out to ffmpeg for the canonical 16 kHz mono pcm_s16le WAV shared by the transcriber and the diarizer.
Package ffmpeg implements port.AudioExtractor by shelling out to ffmpeg for the canonical 16 kHz mono pcm_s16le WAV shared by the transcriber and the diarizer.
infrastructure/filestore
Package filestore implements port.TranscriptStore by writing each meeting into a date-named directory (out/YYYY-MM-DD-<slug>/), confined under the output root via os.OpenRoot.
Package filestore implements port.TranscriptStore by writing each meeting into a date-named directory (out/YYYY-MM-DD-<slug>/), confined under the output root via os.OpenRoot.
infrastructure/power
Package power holds a macOS idle-sleep assertion for as long as a job runs.
Package power holds a macOS idle-sleep assertion for as long as a job runs.
infrastructure/release
Package release answers one question: has a newer version been published than the one running?
Package release answers one question: has a newer version been published than the one running?
infrastructure/runner
Package runner provides the subprocess-execution seam shared by every engine adapter.
Package runner provides the subprocess-execution seam shared by every engine adapter.
infrastructure/sherpa
Package sherpa implements port.Diarizer by shelling out to the sherpa-onnx offline speaker-diarization CLI and parsing its speaker segments.
Package sherpa implements port.Diarizer by shelling out to the sherpa-onnx offline speaker-diarization CLI and parsing its speaker segments.
infrastructure/whispercpp
Package whispercpp implements port.Transcriber by shelling out to the whisper.cpp CLI (`whisper-cli -ojf`), mapping its JSON — or, defensively, its stdout — into a Transcript.
Package whispercpp implements port.Transcriber by shelling out to the whisper.cpp CLI (`whisper-cli -ojf`), mapping its JSON — or, defensively, its stdout — into a Transcript.
usecase
Single-instance guard for `serve`: the serve command calls it BEFORE web.Server.Run.
Single-instance guard for `serve`: the serve command calls it BEFORE web.Server.Run.
usecase/port
Package port declares the ports the usecase layer depends on.
Package port declares the ports the usecase layer depends on.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL