stenograf
Accuracy-first, fully local meeting transcription for German and English (one language per meeting), with speaker labels. Audio is processed entirely in memory — nothing lands on disk but the transcript, unless you explicitly opt into keeping a recording.
Built for Apple Silicon (M-series) first; Linux and Windows support is designed in from the start.
Status: early release. On macOS and Linux the pipeline is complete end to end: live system-audio + microphone capture, live captions, and the high-accuracy speaker-labelled finalize pass, plus meeting notes. Windows support is in progress.
Why another transcription tool?
- No audio on disk. Live transcription of a meeting has far lighter
legal requirements than recording it. stenograf keeps the session's audio in
RAM only and writes nothing but text; keeping a WAV is a per-run opt-in
(
--record-audio). - Accuracy first. A two-pass design: fast live captions while the meeting runs, then a high-accuracy re-transcription of the full in-memory buffer the moment it ends. German is a first-class citizen, not an afterthought.
- Channel-aware speakers. Microphone and system audio are captured as separate streams, so local and remote voices never get confused; optional diarization separates further speakers within a channel (2–8 speakers).
- Speakers, not headphones. Remote voices leaving your laptop speakers and re-entering the mic are cancelled in the audio domain (WebRTC AEC3, with the system channel as the far-end reference), so they are never transcribed as a local speaker.
Install
One command sets up everything — uv (installed for you if missing), stenograf itself, the permission prompts, the model downloads, and a desktop launcher:
curl -fsSL https://raw.githubusercontent.com/daniel-om-weber/stenograf/main/install.sh | sh
That's the only command you have to type. Afterwards, double-click Stenograf on your Desktop (macOS) or start Stenograf from the application menu (Linux) to open the launcher — every workflow below is reachable there with the mouse. Re-running the command upgrades stenograf.
Works on macOS 14.4+ on Apple Silicon (the wheel ships the signed capture
helper — no toolchain needed) and Linux with PipeWire or PulseAudio (capture
uses parec, shipped with pipewire-pulse / pulseaudio-utils on every desktop
distro; ASR runs ONNX on CPU).
Manual install
With uv already installed:
uv tool install stenograf
steno doctor # environment checks
steno setup # one-time: permission prompts, desktop launcher, model downloads
macOS scopes the permission grant to the app the prompt came from, so run
steno setup once from each terminal app (or IDE) you'll start meetings from.
Pre-release channel: uv tool install git+https://github.com/daniel-om-weber/stenograf
installs the current main branch; building from the repository compiles the
capture helper on your machine, which needs the Xcode command-line tools
(xcode-select --install).
Windows
Same one command, run from PowerShell — installs uv if missing, then stenograf, then the launcher:
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/daniel-om-weber/stenograf/main/install.ps1 | iex"
Windows never prompts for the microphone, so capture stays silent until you flip the toggle under Settings > Privacy & security > Microphone. Windows Terminal is recommended for the live-caption TUI.
From a checkout
git clone https://github.com/daniel-om-weber/stenograf
cd stenograf
uv sync
sh native/helper/build.sh # builds + ad-hoc signs native/helper/stenocap
uv run steno doctor
uv run steno setup
Every command below is then uv run steno … from the repo.
The launcher
Double-click Stenograf (the Desktop icon on macOS, the application-menu
entry on Linux) — or run bare steno in a terminal — and a mouse-driven
launcher opens:
- Start meeting — capture this meeting with live captions; the speaker-labelled transcript replaces them the moment you stop.
- Transcribe a recording — turn an existing audio file into a transcript.
- Generate notes — summarize a finished meeting's transcript.
- Settings — show the active configuration.
- Check setup — verify models, permissions, and audio devices.
Everything the launcher does is also a plain CLI command — the rest of this document — so terminal users and scripts lose nothing.
The desktop app (preview)
The same five workflows also exist as a real native window, built on Qt Quick. It is opt-in while it settles; the terminal launcher above stays the default.
uv tool install --force 'stenograf[gui]' # adds Qt (~110 MB); one time
steno setup # points the desktop launcher at the app
steno --gui # …or just open the app
The launcher follows the extra on every platform: once Qt is installed,
steno setup rewrites the shortcut it already made so that double-clicking it
opens the app instead of a terminal window — on Linux the application-menu
entry, on Windows the Desktop Stenograf.cmd. Uninstall the extra, re-run
setup, and they turn back into the terminal launcher.
On macOS it goes further and replaces the Desktop shortcut with a real Stenograf.app — its own icon, Dock tile and Spotlight entry. It asks for microphone access once, under its own name rather than your terminal's, and keeps that permission across every later upgrade.
The app also lives in the menu bar (system tray on Windows and Linux), which is where it belongs for most of a meeting: the icon turns red while you are recording, and its menu can start, stop and finish a meeting without a window. Closing the window puts the app there rather than quitting it — a meeting in progress keeps running — and re-opening the app brings the window back. Start that way with:
steno --gui --tray # menu bar only, no window
Desktops with no tray host (stock GNOME, unless the AppIndicator extension is installed) simply get a window, as before.
It runs the same library the CLI does — same meeting folders, same settings.toml, same transcripts — so you can switch between the two freely, or never use it at all.
Usage
steno start # live captions, everything auto-detected
steno start --lang de --local 3 --remote 2 # hybrid meeting, German
steno transcribe recording.mov # batch-transcribe an existing file
steno start streams live captions while the meeting runs — a full-screen
TUI on a terminal, a plain line-by-line stream when piped — and replaces them
with the high-accuracy, speaker-labelled transcript the moment you stop
(Ctrl-C). The audio stays in RAM throughout; only the transcript is written.
Useful flags:
steno start --plain # plain caption stream instead of the TUI
steno start --no-live # skip live captions; just finalize on stop
steno start --title "Weekly sync" # name the meeting (notes + export use it)
steno start --flush-interval 60 # crash-checkpoint the captions every 60s
steno start --no-aec # disable echo cancellation (headphones)
steno start --diarization # separate speakers within each channel (off by default)
steno start --record-audio # opt in to keeping a WAV (off by default)
steno start --max-seconds 3600 # stop capture automatically after an hour
steno start --replay mic.wav # dev: drive the live pass from a file
Both start and transcribe accept --format md,json,txt,srt,vtt (default
md,json,txt — txt is the plain prose without speaker labels or timestamps),
--lang de|en, --diarization/--no-diarization to run or skip speaker
separation for one run (off by default: each channel is one speaker and the
diarizer model is never loaded — it costs minutes on some machines;
[speakers] diarization = true in the settings makes running it the
default), and --print to echo the transcript to stdout.
If you know how many people spoke in a recording, tell steno transcribe with
--speakers N — a count above 1 turns diarization on, and a known count is
the biggest diarization accuracy lever (omitted, the count is estimated
whenever diarization runs).
steno transcribe recognizes 2-channel recordings whose channels are separate
voice feeds — a --record-audio tee (mic left, system right) or a
dual-channel call recording — and transcribes them per channel through the
meeting pipeline (Local-N/Remote-N labels, per-channel diarization with
--local/--remote counts) instead of downmixing; ordinary stereo still
downmixes to mono. Force either way with --channels split|mix.
Where your files land
Every run writes its own date-named folder into Meetings inside your
documents folder — ~/Documents/Meetings, or your desktop's localised name for
it on Linux (~/Dokumente/Meetings on a German one):
~/Documents/Meetings/meeting-20260710-091500/
transcript.md / .json / .txt # the transcript (--format adds srt/vtt)
transcript.notes.md / .notes.json # if you generated notes
audio.wav # only with --record-audio
That's it — there is no separate library or index to manage. Browse with
Finder or ls, read with anything that opens markdown, delete with rm.
steno settings show prints the folder it resolved. Change the standing
location with [output] dir in settings.toml, or give one
run its own folder with --out DIR (files land directly in it; if DIR already
holds a transcript, stenograf refuses to replace it unless you add --force).
Audio is stored only when you passed --record-audio; without it a meeting
folder holds text alone.
Meeting notes (LLM summaries)
Turn any transcript into structured notes — summary, decisions, action items per owner, open questions — with the LLM of your choice:
steno notes --last # notes for the newest meeting
steno notes ~/Documents/Meetings/meeting-20260710-091500
steno notes path/to/transcript.json # …or any transcript file
steno start --notes # generate notes right after the meeting
Notes land as sibling transcript.notes.md/.notes.json files. On Apple
Silicon the default backend is mlx — a fully local in-process model, nothing
to set up. To use a different backend, configure it once in
~/Library/Application Support/stenograf/settings.toml:
[notes]
backend = "ollama" # fully local via `ollama serve`
model = "qwen3:8b"
or drive any CLI you already have (prompt on stdin, JSON out):
[notes]
backend = "command"
command = ["claude", "-p"]
[notes.export]
dir = "~/Documents/Obsidian/Meetings" # optional: also write one combined
# "YYYY-MM-DD – Title.md" note here
With [notes.export] set, every summarized meeting also produces a single
self-contained markdown note (frontmatter, summary, action items, collapsible
transcript) — drop the dir inside an Obsidian vault and meetings file
themselves. Three more levers in [notes]: instructions = "~/style.md"
appends your house style to the built-in prompt, thinking = false skips the
mlx model's reasoning pass (faster, less careful), and auto = true makes
notes the default for every meeting — steno start summarizes without
--notes, and the launcher's "Generate notes after the meeting" switch starts
on. Out of the box notes never run unless you ask (--notes, the launcher
switch, or steno notes afterwards); with auto = true, --no-notes still
skips them for one run. A notes failure never touches the transcript.
Naming speakers across meetings
Enroll a voice once and every later meeting relabels that speaker automatically (cross-meeting re-identification):
steno profiles enroll Daniel daniel-sample.wav # a short clip of one speaker
steno profiles enroll Daniel more.wav --reinforce # fold in another sample
steno profiles list # show enrolled voiceprints
steno profiles rename Daniel "Daniel W."
steno profiles remove Daniel
To name one person from a multi-speaker recording (e.g. a meeting saved with
--record-audio), diarize it and pick their cluster:
steno profiles enroll Anna meeting.wav --speakers 4 # lists the clusters
steno profiles enroll Anna meeting.wav --speakers 4 --speaker S2
Matching is on by default in steno start/transcribe and does nothing until
you enroll someone; disable it with --no-reid, or adjust the match strictness
with --reid-threshold (0–1, default 0.5). Voiceprints live in the platform data
dir (not the model cache) and are never uploaded.
Vocabulary
Domain terms and attendee names steer the transcript twice: they boost the decoder toward those spellings while it transcribes, and then correct the near-misses it still got wrong in the finalized text.
steno transcribe rec.mov --attendee "Anja Müller" --glossary Kubernetes,gRPC
steno transcribe rec.mov --glossary-file terms.txt
Write a term the way it appears in a sentence. A capitalized term (Kubernetes,
iOS) is imposed verbatim; an all-lowercase one asserts the spelling but leaves
the model's capitalization alone. In the correction pass a term and its
transcription must share a word count — gRPC can fix G R P C spoken as one
word, but not a term split across word boundaries.
An acronym you pronounce as a word needs both spellings. Biasing rewards the
exact token path you write, so NIRS only ever rewards N-I-R-S — a spelling
the acoustics never take when you say "nirs". List NIRS and Nirs: the
decoder can reach the second, and the correction pass snaps it back to the first,
since the two normalize to the same word. Only case variants of the same letters
work — a respelling (Ekmo for ECMO) is reachable but never snaps back.
Both layers are on whenever there are terms. [asr] boost scales the decoder
biasing (default 1.0; 0 turns it off, and much above ~3 it starts rewriting
words that were never in your list), and [vocab] glossary_threshold is the
similarity a word must reach before the correction pass touches it (default
0.95 — deliberately strict, because a loose threshold corrupts words the model
already had right).
Reach for the spellings before the knobs. On an 85-minute German meeting, adding
the pronounceable twins fixed three more terms for four extra changed words in
the rest of the transcript, where raising boost to 1.5 bought two and disturbed
forty-six — at 2.0 the correct compound "Medizintechnikgruppe" decayed into
"Medizinischechnikgruppe", pulled apart by a listed term it half-matched.
Settings
Standing preferences live in settings.toml in the platform data dir
(~/Library/Application Support/stenograf/ on macOS) so you stop re-typing
them. A flag always beats an environment variable (STENOGRAF_ASR_BACKEND,
STENOGRAF_NOTES_BACKEND, …), which beats the file, which beats the built-in
default — steno settings show labels where every value came from.
steno settings show # effective configuration + where each value comes from
steno settings edit # open in $EDITOR (template on first run), validate on save
The first steno settings edit writes a fully commented template documenting
every key. All keys are optional; the ones you're most likely to want:
[transcript]
formats = ["md", "json", "txt"] # default --format list (srt/vtt for subtitles)
[vocab] # standing vocabulary — MERGED with per-run
glossary_file = "~/steno/glossary.txt" # --glossary/--attendee flags
attendees = ["Anja Müller"]
glossary_threshold = 0.95
[output]
dir = "~/Documents/Meetings" # where meeting folders are created (default:
# Meetings/ in your documents folder)
[speakers]
diarization = true # separate speakers within a channel (off by
# default; a per-run flag or count also enables)
reid_threshold = 0.5 # cross-meeting voice match strictness (0–1)
profile_store = "~/steno/profiles.json"
[asr]
backend = "parakeet"
provider = "cpu" # ONNX execution provider for parakeet-onnx:
# cpu | dml (DX12 GPUs, Windows) | cuda | auto
[notes] # see "Meeting notes" above
backend = "mlx"
Typos fail loudly: an unknown table or key is an error, and steno doctor
validates the whole file.
Development
Requires uv and Python 3.12 or 3.13 (3.14 is not yet supported by the ASR stack).
uv sync
uv run pytest
uv run steno doctor
The test suite is label-free and runs without a meeting: model-gated and
real-audio tests self-skip when their assets are absent. The desktop app's
tests run headless (QT_QPA_PLATFORM=offscreen, no window is ever shown) and
skip entirely where PySide6 is not installed.
Three front-ends share one library: the CLI (stenograf/cli/), the terminal
launcher (stenograf/ui/) and the desktop app (stenograf/gui/). None of them
holds pipeline logic — the workflows they drive live in stenograf/flow.py, so
a behaviour change lands in all three at once.
See PLAN.md for the remaining roadmap;
native/README.md for the capture helper and its wire protocol; eval/README.md
for the model-evaluation and AEC-scoring harnesses.
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stenograf-0.4.0.tar.gz.
File metadata
- Download URL: stenograf-0.4.0.tar.gz
- Upload date:
- Size: 1.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b46f201d746591d7da2e01a13f58d950f71ee401970c4181fb0bf7973baac3e
|
|
| MD5 |
05aab94e6a2a4ce2a33bdfd6f9fffdde
|
|
| BLAKE2b-256 |
d5f45d4c21fe9473cae449f32e21ccd5a09bc07cef409bf343128dc0f30e06ac
|
Provenance
The following attestation bundles were made for stenograf-0.4.0.tar.gz:
Publisher:
release.yml on daniel-om-weber/stenograf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stenograf-0.4.0.tar.gz -
Subject digest:
0b46f201d746591d7da2e01a13f58d950f71ee401970c4181fb0bf7973baac3e - Sigstore transparency entry: 2255844112
- Sigstore integration time:
-
Permalink:
daniel-om-weber/stenograf@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/daniel-om-weber
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Trigger Event:
push
-
Statement type:
File details
Details for the file stenograf-0.4.0-py3-none-win_amd64.whl.
File metadata
- Download URL: stenograf-0.4.0-py3-none-win_amd64.whl
- Upload date:
- Size: 13.1 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4355702d6f0eefc3d81457d27e119f5fd1794e731cce8f29ac4b231ad0c60ee9
|
|
| MD5 |
71396b64682051991c9bd8ce6b662ee8
|
|
| BLAKE2b-256 |
9440316e8f16bdca3eba40311380f72a4244e592595df841a9dd61ab48679673
|
Provenance
The following attestation bundles were made for stenograf-0.4.0-py3-none-win_amd64.whl:
Publisher:
release.yml on daniel-om-weber/stenograf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stenograf-0.4.0-py3-none-win_amd64.whl -
Subject digest:
4355702d6f0eefc3d81457d27e119f5fd1794e731cce8f29ac4b231ad0c60ee9 - Sigstore transparency entry: 2255844132
- Sigstore integration time:
-
Permalink:
daniel-om-weber/stenograf@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/daniel-om-weber
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Trigger Event:
push
-
Statement type:
File details
Details for the file stenograf-0.4.0-py3-none-manylinux_2_39_x86_64.whl.
File metadata
- Download URL: stenograf-0.4.0-py3-none-manylinux_2_39_x86_64.whl
- Upload date:
- Size: 17.3 MB
- Tags: Python 3, manylinux: glibc 2.39+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bbfd872bd1554b52e99fa4dfcb1b07a23d37ac4ce731541ea8273169aad7146f
|
|
| MD5 |
f00c95e30c25a239ac22f5ec7aab322f
|
|
| BLAKE2b-256 |
448b5cc6ef28ba68e8a482949c7e71e410f3f698bf9e56f178b50b168547ea74
|
Provenance
The following attestation bundles were made for stenograf-0.4.0-py3-none-manylinux_2_39_x86_64.whl:
Publisher:
release.yml on daniel-om-weber/stenograf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stenograf-0.4.0-py3-none-manylinux_2_39_x86_64.whl -
Subject digest:
bbfd872bd1554b52e99fa4dfcb1b07a23d37ac4ce731541ea8273169aad7146f - Sigstore transparency entry: 2255844137
- Sigstore integration time:
-
Permalink:
daniel-om-weber/stenograf@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/daniel-om-weber
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Trigger Event:
push
-
Statement type:
File details
Details for the file stenograf-0.4.0-py3-none-macosx_14_0_arm64.whl.
File metadata
- Download URL: stenograf-0.4.0-py3-none-macosx_14_0_arm64.whl
- Upload date:
- Size: 10.2 MB
- Tags: Python 3, macOS 14.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07b110a23723b7f32b8ab00abc47a623a9c17913806aed80bf6ac5ee9a12fa35
|
|
| MD5 |
e0bfc1bc1ce9c43939ea712e5d819d90
|
|
| BLAKE2b-256 |
006af963cbf1793dc443539d71cd38bf088fb271bf8dc1fe80228243daf6e955
|
Provenance
The following attestation bundles were made for stenograf-0.4.0-py3-none-macosx_14_0_arm64.whl:
Publisher:
release.yml on daniel-om-weber/stenograf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stenograf-0.4.0-py3-none-macosx_14_0_arm64.whl -
Subject digest:
07b110a23723b7f32b8ab00abc47a623a9c17913806aed80bf6ac5ee9a12fa35 - Sigstore transparency entry: 2255844150
- Sigstore integration time:
-
Permalink:
daniel-om-weber/stenograf@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/daniel-om-weber
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Trigger Event:
push
-
Statement type:
File details
Details for the file stenograf-0.4.0-py3-none-any.whl.
File metadata
- Download URL: stenograf-0.4.0-py3-none-any.whl
- Upload date:
- Size: 1.1 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
68b2493205ee32bf5e26f4343fd90be1bb93d7e87a339823c20ed0d40e68c490
|
|
| MD5 |
710e5d151dde64c42bda311ad034c485
|
|
| BLAKE2b-256 |
9b181fe7142057926a59440e3b604d2858423e93840e73d98ddf31a0fbf90fd9
|
Provenance
The following attestation bundles were made for stenograf-0.4.0-py3-none-any.whl:
Publisher:
release.yml on daniel-om-weber/stenograf
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stenograf-0.4.0-py3-none-any.whl -
Subject digest:
68b2493205ee32bf5e26f4343fd90be1bb93d7e87a339823c20ed0d40e68c490 - Sigstore transparency entry: 2255844121
- Sigstore integration time:
-
Permalink:
daniel-om-weber/stenograf@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/daniel-om-weber
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9d72c126e1c7369147e916c5ca7e393cbd6b9903 -
Trigger Event:
push
-
Statement type: