Everything the UI does, headlessly. Useful for batch-transcribing recordings and for running a capture session from a terminal without the window in the way.
All commands write into the same organised notes tree the UI reads, so a session transcribed from a recording shows up in the UI alongside live ones.
Lists the audio endpoints you can capture, with the ids to pass to --channel.
$ whispernotes devices
LOOPBACK (system audio — capture what you HEAR, e.g. Teams)
* speakers-realtek-hd Speakers (Realtek High Definition Audio) 48000 Hz / 2ch [default]
vb-cable-in CABLE Input (VB-Audio Virtual Cable) 48000 Hz / 2ch
MICROPHONE (capture what you SAY)
* headset-mic Headset Microphone (Jabra Evolve 65) 16000 Hz / 1ch [default]
APPLICATIONS (capture ONE app's audio — nothing else on the machine)
! Per-application capture needs Windows build 20348 or later (Windows 11 / Server 2022). This machine reports build 19045, so application inputs record system audio instead.
brave brave 44100 Hz / 2ch
discord Discord 44100 Hz / 2ch
outlook Inbox - you@example.com - Outlook 44100 Hz / 2ch
ms-teams ms-teams 44100 Hz / 2ch
spotify Spotify Premium 44100 Hz / 2ch
* = system default for that role
To transcribe a Teams call you want a LOOPBACK entry — the render endpoint Teams is playing through. See "Isolating Teams audio" in the README.
The short id in the first column is derived from the endpoint's friendly name, because the raw
WASAPI endpoint id looks like {0.0.0.00000000}.{ab116aac-...} and nobody is retyping that.
--channel accepts either form, plus the exact friendly name; ids are matched
case-insensitively. The short id stays stable when you change your default device, and a numeric
suffix (-2, -3) disambiguates endpoints whose names collide.
APPLICATIONS lists running processes instead of device endpoints, so you can capture, say,
Outlook's audio without also picking up Spotify. Its slug is derived from the executable stem
(outlook, not the window title), and the raw id is app:outlook.exe — keyed on the executable
name rather than the process id, so it keeps working after the app (or WhisperNotes) restarts.
--channel accepts any of the slug, the raw id, or the bare executable name (outlook.exe).
Applications are never chosen implicitly: listen with no --channel always falls back to device
loopback, never to an app, since guessing which running process to isolate would silently narrow a
recording to whatever happened to have focus.
Per-application capture is built on WASAPI process loopback, which Windows only exposes from build
20348 onward — in practice that means Windows 11 or Windows Server 2022; Microsoft's own docs call
the floor "Windows 10 Build 20348", which is misleading, since 20348 is the Server 2022 RTM build
and retail Windows 10 tops out at 19045. Below that build, an application input does not fail —
it falls back to recording the whole machine's audio, same as a LOOPBACK entry, and every surface
(the ! line above, listen's channel line, the Inputs page, and the capture bar) says so rather
than quietly scoping the note to more than it claims.
Add --verbose to also print each endpoint's raw id; for an application this line also carries
its live process id, since that's the one place a pid is shown — it isn't part of the id itself
because pids get recycled between runs.
Live capture and dictation. Runs until Ctrl+C, then finalizes the session and writes notes.md.
| Option | Default | Meaning |
|---|---|---|
-c, --channel <id> |
last used, else default render loopback | Endpoint or application from whispernotes devices. |
-t, --title <text> |
local timestamp | Session title; also the folder name. |
-p, --project <name> |
from settings | Groups the session into a project folder. |
-m, --model <size> |
base |
tiny|base|small|medium|large-v3|large-v3-turbo |
-l, --language <code> |
auto |
ISO code, or auto to detect. |
--tag <tag> |
none | Repeatable. |
--prompt <text> |
from settings | Vocabulary hint — client names, acronyms. |
--keep-audio |
off | Also save the captured WAV next to the notes. |
--threads <n> |
CPU count, capped | Decoder threads. |
--diarize |
from settings (on) | Label each line with the speaker who said it. |
--no-diarize |
— | Skip speaker attribution, even if settings enable it. |
--max-speakers <n> |
8 |
Ceiling on how many voices may be reported. |
--quiet |
off | Suppress the live transcript echo; only print the final path. |
$ whispernotes listen --channel speakers-realtek-hd --project "Acme Corp" --title "Sprint review" --model small
model small (already downloaded)
channel Speakers (Realtek High Definition Audio) [loopback]
session ~/Documents/WhisperNotes/Acme Corp/2026/2026-07-25/143022-sprint-review
Ctrl+C to stop.
[00:00:04] Right, let's get started. Dan, where are we on the firewall change?
[00:00:11] Still blocked. I've chased the network team twice this week.
^C
finalized 1 min 12 sec, 18 entries
notes ~/Documents/WhisperNotes/Acme Corp/2026/2026-07-25/143022-sprint-review/notes.md
Ctrl+C is the normal way to end a session, not an abort: the first press stops the capture, lets
the decoder flush the audio still buffered, appends those last segments, finalizes and renders
notes.md, then exits 130. A second Ctrl+C terminates immediately, in case a decode wedges.
The endpoint actually used is written back to LastChannelId, which is what "last used" in the
table above reads on the next run. Nothing else about the invocation is persisted — a one-off
--notes-root or --models-root never ends up in the settings file.
--channel targeting an application prints an [application] kind on the channel line instead of
[loopback] or [microphone] — or [application → system audio fallback] on a Windows build
below 20348, where the session is honestly labelled as system audio rather than pretending it
isolated the app. Since applications are excluded from the "last used, else default" fallback in
the table above, an application --channel always has to be named explicitly.
With --keep-audio the captured WAV is written to <session>/audio/session.wav as 16 kHz mono
16-bit PCM, which is the format transcribe can re-read directly.
Speakers. Diarization works out who spoke when. It runs offline, from a small voice-embedding
model that turns each stretch of speech into a vector and groups the stretches that sound like the
same person. New voices come out as Speaker 1, Speaker 2 and so on. Names are supplied by a
human in the desktop app, then retained in the local speaker-profile catalog so CLI and desktop
recordings can recognize them later. Several separate voice profiles may share one name, which
repairs false splits without discarding either voiceprint. --max-speakers caps how many clusters
may be reported, and a recording with only one unknown voice is left unlabelled. Tune within-session
clustering with Diarization.MergeThreshold and cross-session matching with
Diarization.ProfileMatchThreshold.
This is the --video flag. Converts a video (or any media file) to a 16 kHz mono audio
channel with ffmpeg, then runs the same local Whisper over it.
| Option | Default | Meaning |
|---|---|---|
-v, --video <path> |
required | Input media. Any container ffmpeg reads. --input/-i are accepted aliases. |
--stream <n> |
first audio stream | Which audio stream to take, from --list-streams. |
--list-streams |
off | Print the file's audio streams and exit without transcribing. |
-t, --title <text> |
input filename | Session title. |
-p, --project <name> |
from settings | Project folder. |
-m, --model <size> |
base |
As above. |
-l, --language <code> |
auto |
As above. |
--tag <tag> |
none | Repeatable. |
--prompt <text> |
from settings | Vocabulary hint. |
--keep-audio |
off | Keep the extracted WAV instead of deleting it. |
--threads <n> |
CPU count, capped | Decoder threads. |
--diarize |
from settings (on) | As above. |
--no-diarize |
— | As above. |
--max-speakers <n> |
8 |
As above. |
-o, --output <dir> |
notes root | Override where this session is written. |
$ whispernotes transcribe --video "Sprint review-20260725.mp4" --list-streams
#1 aac 2ch 48000Hz [eng] "Mixed audio"
#2 aac 1ch 48000Hz [und] "Dan Whitfield"
$ whispernotes transcribe --video "Sprint review-20260725.mp4" --stream 2 --project "Acme Corp" --model small
model small (already downloaded)
session ~/Documents/WhisperNotes/Acme Corp/2026/2026-07-25/143022-sprint-review-20260725
extracting ██████████████████████ 100% (ffmpeg, stream #2 -> 16 kHz mono)
transcribing ████████████░░░░░░░░░░ 58% 00:12:31 / 00:21:40
notes ~/Documents/WhisperNotes/Acme Corp/2026/2026-07-25/143022-sprint-review-20260725/notes.md
--stream takes the ffmpeg stream index printed by --list-streams, which counts every stream in
the container — in a video file the first audio stream is usually #1, not #0.
Teams meeting recordings download as .mp4, so this is the recovery path for the case that
prompted this tool: Teams' own transcription didn't run, but you have the recording.
The extracted WAV goes to a temp file and is deleted when the run finishes; --keep-audio writes
it to <session>/audio/session.wav instead and leaves it there. -o/--output redirects just this
session's tree, overriding the global --notes-root.
Chunks that hold nothing but digital silence are not sent to the decoder. Whisper reliably invents captions for silence, and an invented line in a billable record is worse than a missing one.
Lists past sessions from the notes tree.
| Option | Meaning |
|---|---|
-p, --project <name> |
Filter to one project. |
--since <date> / --until <date> |
Date range, yyyy-MM-dd or relative like 7d. |
-s, --search <text> |
Match against title and transcript text. |
--json |
Machine-readable output. |
Relative bounds take a h (hours), d (days) or w (weeks) suffix and count back from now.
--until on a plain yyyy-MM-dd includes that whole day.
Each session prints one summary line — start, project, title, duration, entry count — followed by
the path to its notes.md. --json emits the same records with the session id, tags, model,
source description and the transcript path as well.
Exports authored notes from the Notes page. Markdown, HTML, and PDF export one note selected by its id or exact title. Obsidian exports the whole authored-note library as a ZIP of UTF-8 Markdown files grouped by project.
# One note. The extension and a safe filename are chosen automatically.
whispernotes export markdown --document "Sprint review"
whispernotes export html --document "Sprint review" --output .\exports
whispernotes export pdf --document "Sprint review" --output .\exports\sprint-review.pdf
# Every authored note, ready to extract and open/copy into an Obsidian vault.
whispernotes export obsidian --output .\exports\whispernotes-obsidian.zip| Argument / option | Meaning |
|---|---|
format |
markdown, html, pdf, or obsidian. |
-d, --document <id-or-title> |
Required for the single-note formats; exact titles are case-insensitive. |
-o, --output <path> |
Destination file or an existing directory; defaults to the current directory. |
--overwrite |
Explicitly replace an existing destination. Without it, export refuses to clobber a file. |
If more than one note has the requested title, the command prints the ids to choose from. An Obsidian export ignores the current UI search and always includes every authored note. Extract the ZIP, then open that folder as a vault or move its project folders into an existing vault.
| Subcommand | Meaning |
|---|---|
models list |
Show each model, its size on disk, and whether it's downloaded. |
models download <size> |
Pre-fetch weights with a progress bar. |
models path |
Print the models directory. |
Pre-downloading matters: the first listen on an un-fetched medium would otherwise stall for
a 1.5 GB download at the exact moment your meeting starts.
Answers "is this actually using my graphics card?". Whisper.net picks a native runtime when the
first model loads and falls back to the CPU without saying so, and a CPU fallback looks exactly
like a GPU run except roughly 40x slower. doctor loads a model and reports what came back.
$ whispernotes doctor
model large-v3-turbo
requested gpu, device 0
backend vulkan — gpu accelerated
devices 0 = NVIDIA GeForce RTX 3080 (NVIDIA) | uma: 0 | fp16: 1 | bf16: 1 | ...
1 = Intel(R) UHD Graphics 770 (Intel Corporation) | uma: 1 | fp16: 1 | ...
More than one adapter is visible and device 0 is in use. Try the others with --gpu-device <n>,
and make the winner stick by setting Gpu.Device in the settings file.
load 1.7 s
It needs at least one model on disk — it has to load something to find out what that load resolves
to. Any downloaded model will do; if the configured one is missing it uses whichever is present,
and tells you to models download tiny if none are.
backend: cpu when you did ask for the GPU is the failure case, and doctor says so and why. The
Vulkan runtime needs vulkan-1.dll, which current NVIDIA, AMD and Intel drivers all install, so
updating the graphics driver is the usual fix. --verbose prints every path the loader tried.
| Subcommand | Meaning |
|---|---|
config show |
Print effective settings and the settings file path. |
config set <key> <value> |
Set one value, e.g. config set NotesRoot "D:\Notes". |
config path |
Print the settings file path. |
Keys, matched case-insensitively: NotesRoot, ModelsRoot, Model, Language, Threads,
Gpu.Enabled, Gpu.Device, LastChannelId, DefaultProject, InitialPrompt, KeepSessionAudio, FfmpegPath,
Chunking.MinChunkSeconds, Chunking.MaxChunkSeconds, Chunking.SilenceMilliseconds,
Chunking.SilenceThreshold, Diarization.Enabled, Diarization.MaxSpeakers,
Diarization.MergeThreshold, Diarization.ProfileMatchThreshold, Diarization.MinObservationSeconds,
Diarization.MaxObservationSeconds. Passing an empty value clears an optional setting back to its
default.
config show prints the effective settings — the file merged with this invocation's global
options. config set always writes the file itself, never the merged view.
| Option | Meaning |
|---|---|
--notes-root <dir> |
Override the notes root for this invocation. |
--models-root <dir> |
Override the models directory. |
--ffmpeg <path> |
Explicit ffmpeg location if it isn't on PATH. |
--verbose |
Diagnostic logging, including resolved binary paths. |
--no-gpu |
Decode on the CPU. Roughly 40x slower — for working around a bad driver. |
--gpu-device <n> |
Which adapter to decode on, indexed as whispernotes doctor lists them. |
--no-gpu and --gpu-device are per-invocation, like every other global: they never touch the
settings file. Make a choice permanent with config set Gpu.Enabled / config set Gpu.Device.
They apply to doctor too, which is the point — try an adapter, see what it resolved to, keep the
one that won.
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Bad usage / invalid argument. Includes an unparseable command line, an unknown --model, a --stream the file doesn't have, and a --video path that does not exist. |
| 2 | Requested audio device not found — or the device the session was capturing died mid-run, in which case the notes are still finalized first. |
| 3 | ffmpeg or ffprobe missing, or the probe/conversion failed (including a file ffmpeg cannot read). |
| 4 | Model missing and could not be downloaded. |
| 130 | Interrupted with Ctrl+C. For listen this is the NORMAL exit and notes are still finalized. |
Every command reports failures as one sentence on stderr and one of these codes. Nothing prints a
stack trace; --verbose adds the full exception underneath if you need it.
Progress bars redraw in place on a real console and degrade to periodic complete lines when stdout is redirected to a file or a pipe, so piping a run into a log never produces a wall of overwritten bars.