Millet is a meeting transcription, summarization, and PDF output tool. It's named after the Ottoman millet system — the legal framework of communal autonomy that, in 1493, made it possible for two Sephardic Jewish brothers to establish Istanbul's first printing press, just one year after their expulsion from Spain. Like the millets it's named after, this tool operates under its own rules, on your own machine, within the broader vezir ecosystem.
Formerly known as meetscribe. PyPI distribution name:
millet-pipeline (the bare millet slot on PyPI is held by an
unrelated dormant 2021 package). See
CHANGELOG.md for the rename details and full release
history.
Meeting transcription with speaker diarization, AI-generated summaries, and professional PDF output.
Records dual-channel audio (your mic + system audio) from any meeting
app and produces diarized transcripts using WhisperX + pyannote-audio.
Works fully offline with local models, or optionally use cloud APIs
(OpenRouter, Claude Max) for higher-quality summaries. A
summarization preset selector picks one of three backends per run:
high-quality (Sonnet 4.6), confidential (GLM-5.2 inside a
hardware-attested Tinfoil TEE — the prompts never leave the secure
enclave, and the resulting PDF carries a red CONFIDENTIAL watermark on
every page), or alternative (Kimi K2.6 via OpenRouter).
Because millet captures system audio at the OS level, it works with every voice/video call application:
Any app that plays audio through your system speakers will work -- including browser-based meetings and standalone desktop clients.
openai/whisper-large-v3-turbo, word-level timestamps via wav2vec2 alignment--summary-preset high-quality | confidential | alternative resolves to a (backend, model) pair;
the confidential preset routes to a Tinfoil TEE-attested GLM-5.2
so prompts cannot be seen by the model provider or cloud
operatortinfoil backend get a red CONFIDENTIAL header + footer on every
page (auto-detected from summary.backend, survives relabeling).txt, .srt, .json, .summary.md, .pdf.summary.md carries a typed
schema (action items, decisions, participants, topics, language,
duration) plus a matching .frontmatter.json sidecar, ready for
indexers and downstream tooling like
vezirmillet record, millet transcribe, millet run, millet gui,
millet label, millet enroll, millet sync, millet ingest,
millet download, millet translate, millet devices, millet check# Install from PyPI
pip install millet-pipeline
# Set your HuggingFace token (required for speaker diarization)
export HF_TOKEN=hf_your_token_here
# Record a meeting, then auto-transcribe + summarize when you stop
millet run
# Press Ctrl+C when the meeting ends
millet runs in two configurations:
Linux desktop (full pipeline: record + transcribe + label + sync)
macOS Apple Silicon (post-capture pipeline: transcribe + label + sync)
pip install 'millet-pipeline[mlx]' to auto-select MLX Whisper for ASRmillet record / millet run (audio capture) require Linux. On a Mac,
feed in audio captured elsewhere via millet transcribe <file.wav>, or use
vezir to run a Mac as a server with
Linux/Android thin clients providing the recordings.See REQUIREMENTS.md for full hardware/software details.
# Ubuntu / Pop!_OS / Debian
sudo apt install ffmpeg pulseaudio-utils
# Fedora
sudo dnf install ffmpeg pulseaudio-utils
# From PyPI (recommended)
pip install millet-pipeline
# Optional: pull the Tinfoil TEE SDK to enable the Confidential preset
pip install 'millet-pipeline[tee]'
# From source
git clone https://github.com/pretyflaco/millet
cd millet
pip install -e .
This creates the millet command in your PATH (the older meet command is
kept as a deprecated alias). The [tee] extra adds the tinfoil Python SDK
(≈ 2 MB). Set TINFOIL_API_KEY to use the --summary-preset confidential
route; see Summarization presets below.
export HF_TOKEN=hf_your_token_here
# Add to ~/.bashrc for persistence:
echo 'export HF_TOKEN=hf_your_token_here' >> ~/.bashrc
Install from https://ollama.com, then pull the default summary model:
ollama pull qwen3.5:9b
millet check
millet devices
Start recording before or during your meeting:
millet record
Press Ctrl+C when the meeting ends. A 10-second drain buffer ensures all audio
is captured. Recordings are saved to ~/meet-recordings/.
Options:
-o /path -- save recordings elsewhere--virtual-sink -- create isolated virtual sink (avoids capturing notification sounds)--mic <source> -- specify mic source (use millet devices to find names)--monitor <source> -- specify monitor sourcemillet transcribe ~/meet-recordings/meeting-20260312-140000/meeting-20260312-140000.wav
Options:
-m large-v3-turbo -- Whisper model (default: large-v3-turbo; also: base, medium, large-v2)-l auto -- language code or auto to auto-detect (default: auto; e.g. en, de, tr, fa)--asr-backend auto -- ASR backend: auto, whisperx, mlx, or
parakeet. On Apple Silicon with mlx-whisper installed, auto uses MLX
Whisper for ASR. MLX only replaces the transcription step; millet still
requires WhisperX for audio loading, alignment, and diarization. parakeet
uses NVIDIA Parakeet ONNX (English; install with pip install 'millet-pipeline[parakeet]'); see --parakeet-model /
--parakeet-keep-alignment.--mlx-model <repo-or-path> -- MLX Whisper model path/repo (default: maps
large-v3-turbo to mlx-community/whisper-large-v3-turbo)--device cuda -- cuda or cpu. Default: auto-detected — cpu on
Apple Silicon (since macOS has no CUDA), cuda elsewhere.--torch-device mps -- optional PyTorch device for alignment/diarization;
useful with MLX ASR or CPU ASR on Apple Silicon.--compute-type float16 -- float16 or int8 for lower VRAM (default: float16)-b 16 -- batch size, reduce if running low on VRAM (default: 16)--min-speakers 2 / --max-speakers 6 -- hint for number of speakers--no-diarize -- skip speaker diarization--no-summarize -- skip AI summary generation--summary-backend openrouter -- summary backend (ollama, openrouter, claudemax, openai, tinfoil)--summary-model <model> -- model for summary (default: per-backend)--skip-alignment -- skip word-level alignment (useful if alignment model is unavailable)--mixdown mono|dual|dual-diarize -- stereo mixdown mode (default:
dual-diarize). See Dual-channel modes below.--default-language <code> -- team/operator default language that biases
low-confidence auto-detection (e.g. en); pairs with
--language-detection-segments N (default 6).--channel-correct/--no-channel-correct (+
--channel-correct-margin, default 0.30) for mic-bleed/echo correction,
--consolidate-remote-clusters/--no-... to merge phantom remote speakers,
and --single-source-fallback/--no-... for in-room (non-headphone)
recordings detected as single-source.Stereo recordings carry your mic on the left channel and system audio on the
right. transcribe defaults to --mixdown dual-diarize, which
transcribes each channel independently for best accuracy and then diarizes
the remote (system) channel to separate multiple remote participants. It
labels the local speaker as YOU (mic) and diarized remotes as REMOTE_1,
REMOTE_2, … The three modes:
dual-diarize (default) -- per-channel transcription + diarization of
remotes. Best accuracy for headphone setups where mic and system audio
don't bleed into each other.dual -- per-channel transcription with no remote diarization (channel
identity = speaker identity: YOU vs. REMOTE).mono (legacy) -- mix both channels to mono, then diarize. Use it when
your speakers play into the room and both voices appear on both channels.millet transcribe --mixdown dual-diarize ~/meet-recordings/meeting-20260312-140000/
Note: millet run still defaults to --mixdown mono and accepts mono or
dual; pass --mixdown dual there for headphone recordings.
millet run
Records until Ctrl+C, then automatically transcribes, generates a summary,
and produces a PDF. Takes options from both record and transcribe. Note
run's --mixdown defaults to mono and accepts only mono or dual
(unlike transcribe, which defaults to dual-diarize); use millet transcribe on the saved recording if you need dual-diarize.
millet gui
A small always-on-top window with:
When 2 or more speakers are detected, a speaker labeling dialog appears before the results are saved. Each speaker is shown with their channel and a sample line of text. If voice profiles exist, confident matches are shown automatically. Enter a real name or leave blank to keep the auto-assigned label (YOU, REMOTE_1, etc.).
If meeting sync is configured and the recording matches a scheduled meeting, a sync confirmation prompt appears with Push / Skip buttons.

millet label ~/meet-recordings/meeting-20260313-214133
For each speaker in the recording, millet label:
ffplay).txt, .srt, .json, .summary.md, .pdf) with the new namesWith --auto, voice profiles are used to automatically identify known speakers.
Confident matches are applied without prompting; only unrecognized speakers get
the interactive prompt:
millet label --auto ~/meet-recordings/meeting-20260313-214133
Options:
--auto -- auto-label using voice profiles (see Voiceprint speaker recognition)--no-audio -- skip audio playback, just show text samples--no-summary -- use find-and-replace instead of re-running Ollama--summary-backend / --summary-model -- override summary backend and model for regeneration--apply-json FILE (or - for stdin) -- non-interactive labeling: apply a
{"OLD_ID": "Name", ...} map (or {"labels": {...}} envelope) and exit--update-profiles -- with --apply-json, update voiceprint profiles from
the confirmed labels--summary-language <code> -- also emit a translated .summary.<lang>.md--team <name> -- use a team-scoped profile database (see below)When run without a TTY (e.g. from a worker), --auto applies confident
matches and skips the interactive prompt automatically, writing an
.autoid.json sidecar with per-speaker match confidence.
Each recording gets its own session directory:
~/meet-recordings/meeting-20260312-140000/
meeting-20260312-140000.wav # Stereo audio (16kHz)
meeting-20260312-140000.session.json # Recording metadata
meeting-20260312-140000.ffmpeg.log # ffmpeg capture log
meeting-20260312-140000.txt # Plain text transcript
meeting-20260312-140000.srt # Subtitle format
meeting-20260312-140000.json # Full detail (word-level timestamps)
meeting-20260312-140000.summary.md # AI meeting summary with YAML frontmatter
meeting-20260312-140000.summary.meta.json # Summary backend/model + timing metadata
meeting-20260312-140000.frontmatter.json # Structured frontmatter (schema_version 1)
meeting-20260312-140000.pdf # Professional PDF (summary + transcript)
Example .txt output:
[00:00:12 --> 00:00:18] YOU: So the main issue we're seeing is with the API rate limiting.
[00:00:19 --> 00:00:25] REMOTE_1: Right, I think we should implement exponential backoff.
[00:00:26 --> 00:00:31] YOU: Agreed. Can you also look at caching the responses?
Every .summary.md ships with a typed YAML frontmatter block plus a
matching .frontmatter.json sidecar. The schema is intentionally
small in v1 so downstream consumers can rely on it:
---
schema_version: 1
type: meeting
title: Q2 Pricing Discussion
date: "2026-03-17T14:00:00+00:00"
duration: PT42M17S
language: en
participants:
- name: YOU
role: null
channel: mic
- name: Alice
role: null
channel: system
topics:
- pricing
- onboarding
action_items:
- assignee: Alice
task: Send pricing doc
due: Friday
status: open
decisions:
- text: Run pricing experiment at $99/mo
topic: pricing
source:
session_id: meeting-20260312-140000
audio_sha256: null
---
## Meeting Overview
...
## Key Topics Discussed
...
schema_version: 1 is what every consumer should pin against.
The JSON sidecar contains the exact same dict for tools that don't
want to parse YAML. [vezir](https://github.com/pretyflaco/vezir) 0.2.0+ reads these files directly to build a queryable index over
your meetings.
Sessions recorded before meetscribe 0.7.0 / millet-pipeline 0.9.0 don't carry frontmatter. Re-extract it for one or more sessions with:
# Re-run the LLM to produce frontmatter; idempotent (skips sessions
# whose .summary.meta.json already records data_extracted=true).
millet ingest ~/meet-recordings/meeting-2026*
# Force re-extraction even when frontmatter is already present:
millet ingest --force ~/meet-recordings/meeting-20260312-140000
# Preview without invoking the LLM:
millet ingest --dry-run ~/meet-recordings/meeting-2026*
millet ingest accepts the same --summary-backend /
--summary-model / --ollama-singlepass flags as
millet transcribe and regenerates the PDF by default
(--no-pdf to skip).
millet generates a structured meeting summary with:
| Model | Size | Speed | Notes |
|---|---|---|---|
qwen3.5:9b | 6.6 GB | ~18-35s | Default -- best balance of quality and speed |
gemma3:12b | 8.1 GB | ~15s | Fastest |
qwen3:14b | 9.3 GB | ~39s | Good quality |
glm-4.7-flash | 19 GB | ~37s | Must use thinking-off mode (handled automatically) |
Change the model:
millet run --summary-model gemma3:12b
Disable summaries:
millet run --no-summarize
millet supports five backends with automatic fallback:
| Backend | Setup | Cost | Quality | Privacy |
|---|---|---|---|---|
ollama (default) | ollama serve + ollama pull qwen3.5:9b | Free | Good | Fully local |
openrouter | Set OPENROUTER_API_KEY | Pay-per-use | Excellent | Cloud (model-provider-visible) |
claudemax | Run claude-max-api-proxy on localhost:3457 | Claude Max subscription | Excellent | Cloud (Anthropic-visible) |
tinfoil | pip install 'millet-pipeline[tee]', set TINFOIL_API_KEY (or drop a key file at ~/models/tinfoil/tinfoil.txt) | ~$0.009/meeting | Excellent (GLM-5.2) | Hardware-attested TEE — prompts not visible to provider/operator |
openai | Set MILLET_OPENAI_BASE_URL | Varies | Varies | Depends on endpoint |
The openai backend works with any OpenAI-compatible API — Lemonade, LiteLLM,
vLLM, text-generation-webui, LocalAI, or any self-hosted endpoint.
The tinfoil backend runs inference inside a hardware-attested TEE (AMD
SEV-SNP or Intel TDX, depending on the model). The model provider can't
see the prompts, the cloud operator can't see the prompts, and the
integrity is checked against an attestation report on every request.
~$0.009 per meeting; latency ~66 s for a 30-min recording on GLM-5.2.
# Use OpenRouter
millet run --summary-backend openrouter --summary-model anthropic/claude-sonnet-4.6
# Use any OpenAI-compatible endpoint
export MILLET_SUMMARY_BACKEND=openai
export MILLET_OPENAI_BASE_URL=http://localhost:8000/v1
export MILLET_SUMMARY_MODEL=your-model-name
# Optional: export MILLET_OPENAI_API_KEY=your-key
# Or set via environment variables
export MILLET_SUMMARY_BACKEND=openrouter
export MILLET_SUMMARY_MODEL=anthropic/claude-sonnet-4.6
Environment variables use the
MILLET_prefix. The olderMEETSCRIBE_(andMEET_) spellings still work for one more release but emit aDeprecationWarning.
If the configured backend is unavailable, millet automatically tries the
next one in the fallback chain: claudemax → tinfoil → openrouter → ollama.
The openai backend is opt-in only and never participates in the fallback
chain.
When a preset is explicitly selected (see Summarization presets below),
this fallback is disabled for that run — the chosen preset's backend
either succeeds or the whole summarization step fails loudly with a
non-zero exit. This protects the privacy/quality promise of the
confidential preset (a silent tinfoil → claudemax fallback would defeat
the entire point of choosing TEE-attested inference).
A preset is a friendly name that resolves to a concrete (backend, model)
pair. Set it via --summary-preset on transcribe, run, label,
gui, or ingest, or via the MILLET_SUMMARY_PRESET env var.
| Preset | Backend | Model | Use case |
|---|---|---|---|
high-quality | claudemax | claude-sonnet-4-6 | Default for users with a Claude Max subscription; highest summary quality |
confidential | tinfoil | glm-5-2 | Meetings where prompts must not be retained or trained on; hardware-attested TEE |
alternative | openrouter | moonshotai/kimi-k2.6 | Cheapest cloud option (~$0.017/meeting); useful when claudemax credentials are unavailable |
# Quick check of which preset is in effect
millet transcribe ~/meet-recordings/today/today.wav --summary-preset confidential
# Or set per-session via env
export MILLET_SUMMARY_PRESET=high-quality
millet run
When a preset is set, --summary-backend and --summary-model overrides
are honored within that preset (e.g. --summary-preset confidential --summary-model gpt-oss-120b swaps the model but keeps the TEE backend).
When the ollama backend is selected (the default), millet runs two LLM calls instead of one:
This dramatically improves format compliance and reduces hallucinations
on 20B-class local models (gpt-oss:20b, qwen3.6:27b) compared to a
single-pass call, at the cost of one additional LLM call (~30–90s extra).
Cloud backends (claudemax, openrouter, openai) remain single-pass — they
already produce well-structured output in one shot.
To opt out and use the previous single-pass behavior:
millet run --ollama-singlepass
# Or via environment:
export MILLET_OLLAMA_SINGLEPASS=1
The .summary.meta.json sidecar records per-pass timings
(pass1_seconds, pass2_seconds, pass1_chars) when two-pass was used.
See docs/local-model-evaluation.md for the full evaluation that motivated this design, including known failure modes of local 20B-class models.
The summarization prompts live in millet/prompts/ (e.g.
summarize_system.md, plus the summarize_extract_* / summarize_format_*
pairs used by the two-pass Ollama flow). Edit them to change the summary
format, add domain-specific instructions, or tune for your preferred model.
No Python changes needed.
millet can automatically identify speakers across meetings using voice embeddings. After you label speakers in one meeting, their voice profiles are stored and matched against future recordings.
# Build profiles from already-labeled sessions
millet enroll ~/meet-recordings/meeting-20260330-*
# Auto-label speakers in future meetings using voice profiles
millet label --auto ~/meet-recordings/meeting-20260401-093000
Profiles are stored in ~/.config/meet/speaker_profiles.json and improve
with each labeled session (running average of embeddings).
Use millet enroll --list to list enrolled speakers. Team-scoped profiles
are supported via --team <name> on enroll and label, stored under
~/.config/meet/<team>/speaker_profiles.json, so each team keeps its own
voiceprint database.
Push meeting artifacts to a Git repository on a configurable schedule.
# Create an example config
millet sync --init-config
# Edit ~/.config/meet/sync_config.json with your repo URL and schedule
# Push a session manually
millet sync ~/meet-recordings/meeting-20260331-110038_STANDUP
# View configured schedule
millet sync --list-schedule
When the GUI detects a matching scheduled meeting, it prompts for confirmation
before syncing. Sessions that don't match the schedule are skipped. The CLI
uses --force to sync unmatched sessions.
You can also configure a team_members list and min_team_members threshold
in sync_config.json to require that a minimum number of recognized speakers
are present before offering to sync.
millet sync supports --team <name> (per-team config + clone) and
--meeting-type <slug> for routing. Push failures exit non-zero so scheduled
sync jobs can detect and retry them.
millet auto-detects the spoken language by default (Whisper large-v3-turbo supports 99 languages). You can also set it explicitly:
millet run --language de # German
millet run --language tr # Turkish
millet run --language fr # French
millet run --language es # Spanish
millet run --language fa # Farsi (Persian)
millet run --language auto # Auto-detect (default)
auto, the detected language is used for
alignment and all downstream steps.| Language | Code | Alignment model | PDF font | Notes |
|---|---|---|---|---|
| English | en | wav2vec2 (torchaudio) | DejaVu Sans | |
| German | de | VoxPopuli (torchaudio) | DejaVu Sans | |
| French | fr | VoxPopuli (torchaudio) | DejaVu Sans | |
| Spanish | es | VoxPopuli (torchaudio) | DejaVu Sans | |
| Turkish | tr | wav2vec2 (HuggingFace) | DejaVu Sans | ~1.2 GB alignment model download |
| Farsi | fa | wav2vec2 (HuggingFace) | Noto Naskh Arabic | ~1.2 GB alignment model download, RTL |
Alignment models download automatically on first use, but you can pre-fetch
them (e.g. before an offline session) with millet download:
millet download # show cache status for all supported languages
millet download de tr fa # download German, Turkish, Farsi alignment models
millet download --all # download every supported alignment model
millet download parakeet # download the Parakeet ASR model (English)
It exits non-zero if any requested download fails, so it's safe to script.
millet translate renders an existing session transcript into another
language via Ollama, preserving timestamps and speaker labels. The result is
saved as <basename>.translation.<lang>.txt in the session directory.
millet translate ~/meet-recordings/meeting-20260313-231509 # to English
millet translate ~/meet-recordings/meeting-20260313-231509 --to de # to German
Farsi uses right-to-left text. For proper PDF rendering, install the optional RTL dependencies:
pip install arabic-reshaper python-bidi
# Or with the optional extra:
pip install "millet-pipeline[rtl]"
Without these libraries, Farsi text will appear in the PDF but glyphs may not be joined correctly and reading order may be wrong.
By default, millet record captures all system audio (including notification
sounds, music, etc.). For cleaner recordings, use --virtual-sink:
millet record --virtual-sink
This creates an isolated audio sink. Route your meeting app's audio to it:
pavucontrol (PulseAudio Volume Control)You'll still hear the meeting through your normal speakers via automatic loopback.
Environment variables use the MILLET_ prefix (legacy MEETSCRIBE_ / MEET_
spellings are honored for one more release with a DeprecationWarning):
| Variable | Purpose |
|---|---|
MILLET_SUMMARY_BACKEND | Default summary backend (ollama, openrouter, claudemax, openai, tinfoil) |
MILLET_SUMMARY_MODEL | Default summary model for the chosen backend |
MILLET_SUMMARY_PRESET | Default preset (high-quality, confidential, alternative) |
MILLET_OLLAMA_SINGLEPASS | Set to 1 to disable two-pass Ollama summarization |
MILLET_OPENAI_BASE_URL / MILLET_OPENAI_API_KEY | Endpoint + key for the openai-compatible backend |
OPENROUTER_API_KEY | Required for the openrouter backend |
TINFOIL_API_KEY | Required for the tinfoil backend (or a key file at ~/models/tinfoil/tinfoil.txt) |
HF_TOKEN | HuggingFace token for pyannote diarization |
MILLET_CONFIG_DIR | Override the config dir (default ~/.config/meet) |
MILLET_PROFILES_PATH | Override the voiceprint profile DB path |
MILLET_RECORDINGS_DIR | Override the recordings directory (default ~/meet-recordings) |
Set HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 to run without any network
access. millet honors these and, on a cache miss, prints actionable guidance
naming the exact model to pre-fetch (via millet download) rather than
failing with an opaque network error.
With an NVIDIA GPU (12 GB VRAM):
| Model | Transcription | + Diarization | Recommended batch_size |
|---|---|---|---|
| large-v3-turbo | ~4 GB | ~7 GB total | 16 |
| medium | ~3 GB | ~6 GB total | 16 |
| base | ~1 GB | ~4 GB total | 16 |
If you hit OOM errors:
--batch-size to 4 or 8--compute-type int8--model medium or --model base)--device cpu as a last resort[Meeting App] --> [PipeWire/PulseAudio] --> [ffmpeg dual-channel capture] --> meeting.wav
|
[WhisperX: faster-whisper + wav2vec2 alignment + pyannote diarization]
|
[Ollama LLM summary] [Diarized transcript]
| |
.summary.md .txt / .srt / .json
| |
+--------> .pdf <---------+
Capture: Records your mic (left channel) and system audio (right channel) simultaneously into a single stereo WAV file at 16 kHz.
Transcribe: Runs the WhisperX pipeline -- batched Whisper transcription, wav2vec2 forced alignment for word-level timestamps, and pyannote speaker diarization. Dual-channel energy analysis maps speakers to YOU or REMOTE.
Summarize: Sends the transcript to a local Ollama model that extracts a structured summary.
PDF: Combines the summary and full transcript into a professional page-numbered PDF document.
The pyannote diarization model requires CUDA NVRTC for JIT compilation. If your CUDA driver version doesn't match the installed libnvrtc-builtins version, millet automatically creates a compatibility symlink. This happens transparently on first use.
If you still see NVRTC errors:
export LD_LIBRARY_PATH=$HOME/.local/lib/cuda:$LD_LIBRARY_PATH
millet label or the GUI dialog to assign real namesmillet record, millet run) requires Linux with PulseAudio
or PipeWire. Transcription, labeling, summarization, and sync work on both
Linux (CUDA) and macOS Apple Silicon (MLX Whisper + MPS) as of v0.6.0.gpt-oss:20b) can hallucinate on
transcripts dominated by very short low-information utterances ("yes",
"okay") and may exceed the default 600s timeout on very large
(>100 KB) non-English transcripts. For these cases configure a cloud
backend (claudemax / openrouter) — the fallback chain takes over
automatically. See docs/local-model-evaluation.md.Is there a GUI? Yes — run millet gui for a small always-on-top GTK3
widget with Record/Stop, live timer, status indicator, and one-click
access to the resulting PDF and session folder. See
Launch the GUI widget for details.
Does it work on Windows / macOS? System-audio recording requires Linux
(PulseAudio / PipeWire). The post-capture pipeline (millet transcribe,
millet label, millet sync, etc.) works on macOS Apple Silicon as of v0.6.0
— install with pip install 'millet-pipeline[mlx]'. Windows is not
supported.
Can I run a Mac as a transcription server? Yes — see vezir, the team-scale wrapper around millet. A Mac can act as the GPU server with Linux laptops or the Android client providing the audio.
Can I use it without a GPU? Yes, with --device cpu, but
transcription will be 5–20× slower depending on the Whisper model.
See VRAM usage.
git clone https://github.com/pretyflaco/millet
cd millet
pip install -e .[dev]
/usr/bin/python3 -m pytest tests/
Pull requests welcome. Please run the test suite before submitting.
See CHANGELOG.md for release history.
Python
100.0%
Millet is a meeting transcription, summarization, and PDF output tool. It's named after the Ottoman millet system — the legal framework of communal autonomy that, in 1493, made it possible for two Sephardic Jewish brothers to establish Istanbul's first printing press, just one year after their expulsion from Spain. Like the millets it's named after, this tool operates under its own rules, on your own machine, within the broader vezir ecosystem.
Formerly known as meetscribe. PyPI distribution name:
millet-pipeline (the bare millet slot on PyPI is held by an
unrelated dormant 2021 package). See
CHANGELOG.md for the rename details and full release
history.
Meeting transcription with speaker diarization, AI-generated summaries, and professional PDF output.
Records dual-channel audio (your mic + system audio) from any meeting
app and produces diarized transcripts using WhisperX + pyannote-audio.
Works fully offline with local models, or optionally use cloud APIs
(OpenRouter, Claude Max) for higher-quality summaries. A
summarization preset selector picks one of three backends per run:
high-quality (Sonnet 4.6), confidential (GLM-5.2 inside a
hardware-attested Tinfoil TEE — the prompts never leave the secure
enclave, and the resulting PDF carries a red CONFIDENTIAL watermark on
every page), or alternative (Kimi K2.6 via OpenRouter).
Because millet captures system audio at the OS level, it works with every voice/video call application:
Any app that plays audio through your system speakers will work -- including browser-based meetings and standalone desktop clients.
openai/whisper-large-v3-turbo, word-level timestamps via wav2vec2 alignment--summary-preset high-quality | confidential | alternative resolves to a (backend, model) pair;
the confidential preset routes to a Tinfoil TEE-attested GLM-5.2
so prompts cannot be seen by the model provider or cloud
operatortinfoil backend get a red CONFIDENTIAL header + footer on every
page (auto-detected from summary.backend, survives relabeling).txt, .srt, .json, .summary.md, .pdf.summary.md carries a typed
schema (action items, decisions, participants, topics, language,
duration) plus a matching .frontmatter.json sidecar, ready for
indexers and downstream tooling like
vezirmillet record, millet transcribe, millet run, millet gui,
millet label, millet enroll, millet sync, millet ingest,
millet download, millet translate, millet devices, millet check# Install from PyPI
pip install millet-pipeline
# Set your HuggingFace token (required for speaker diarization)
export HF_TOKEN=hf_your_token_here
# Record a meeting, then auto-transcribe + summarize when you stop
millet run
# Press Ctrl+C when the meeting ends
millet runs in two configurations:
Linux desktop (full pipeline: record + transcribe + label + sync)
macOS Apple Silicon (post-capture pipeline: transcribe + label + sync)
pip install 'millet-pipeline[mlx]' to auto-select MLX Whisper for ASRmillet record / millet run (audio capture) require Linux. On a Mac,
feed in audio captured elsewhere via millet transcribe <file.wav>, or use
vezir to run a Mac as a server with
Linux/Android thin clients providing the recordings.See REQUIREMENTS.md for full hardware/software details.
# Ubuntu / Pop!_OS / Debian
sudo apt install ffmpeg pulseaudio-utils
# Fedora
sudo dnf install ffmpeg pulseaudio-utils
# From PyPI (recommended)
pip install millet-pipeline
# Optional: pull the Tinfoil TEE SDK to enable the Confidential preset
pip install 'millet-pipeline[tee]'
# From source
git clone https://github.com/pretyflaco/millet
cd millet
pip install -e .
This creates the millet command in your PATH (the older meet command is
kept as a deprecated alias). The [tee] extra adds the tinfoil Python SDK
(≈ 2 MB). Set TINFOIL_API_KEY to use the --summary-preset confidential
route; see Summarization presets below.
export HF_TOKEN=hf_your_token_here
# Add to ~/.bashrc for persistence:
echo 'export HF_TOKEN=hf_your_token_here' >> ~/.bashrc
Install from https://ollama.com, then pull the default summary model:
ollama pull qwen3.5:9b
millet check
millet devices
Start recording before or during your meeting:
millet record
Press Ctrl+C when the meeting ends. A 10-second drain buffer ensures all audio
is captured. Recordings are saved to ~/meet-recordings/.
Options:
-o /path -- save recordings elsewhere--virtual-sink -- create isolated virtual sink (avoids capturing notification sounds)--mic <source> -- specify mic source (use millet devices to find names)--monitor <source> -- specify monitor sourcemillet transcribe ~/meet-recordings/meeting-20260312-140000/meeting-20260312-140000.wav
Options:
-m large-v3-turbo -- Whisper model (default: large-v3-turbo; also: base, medium, large-v2)-l auto -- language code or auto to auto-detect (default: auto; e.g. en, de, tr, fa)--asr-backend auto -- ASR backend: auto, whisperx, mlx, or
parakeet. On Apple Silicon with mlx-whisper installed, auto uses MLX
Whisper for ASR. MLX only replaces the transcription step; millet still
requires WhisperX for audio loading, alignment, and diarization. parakeet
uses NVIDIA Parakeet ONNX (English; install with pip install 'millet-pipeline[parakeet]'); see --parakeet-model /
--parakeet-keep-alignment.--mlx-model <repo-or-path> -- MLX Whisper model path/repo (default: maps
large-v3-turbo to mlx-community/whisper-large-v3-turbo)--device cuda -- cuda or cpu. Default: auto-detected — cpu on
Apple Silicon (since macOS has no CUDA), cuda elsewhere.--torch-device mps -- optional PyTorch device for alignment/diarization;
useful with MLX ASR or CPU ASR on Apple Silicon.--compute-type float16 -- float16 or int8 for lower VRAM (default: float16)-b 16 -- batch size, reduce if running low on VRAM (default: 16)--min-speakers 2 / --max-speakers 6 -- hint for number of speakers--no-diarize -- skip speaker diarization--no-summarize -- skip AI summary generation--summary-backend openrouter -- summary backend (ollama, openrouter, claudemax, openai, tinfoil)--summary-model <model> -- model for summary (default: per-backend)--skip-alignment -- skip word-level alignment (useful if alignment model is unavailable)--mixdown mono|dual|dual-diarize -- stereo mixdown mode (default:
dual-diarize). See Dual-channel modes below.--default-language <code> -- team/operator default language that biases
low-confidence auto-detection (e.g. en); pairs with
--language-detection-segments N (default 6).--channel-correct/--no-channel-correct (+
--channel-correct-margin, default 0.30) for mic-bleed/echo correction,
--consolidate-remote-clusters/--no-... to merge phantom remote speakers,
and --single-source-fallback/--no-... for in-room (non-headphone)
recordings detected as single-source.Stereo recordings carry your mic on the left channel and system audio on the
right. transcribe defaults to --mixdown dual-diarize, which
transcribes each channel independently for best accuracy and then diarizes
the remote (system) channel to separate multiple remote participants. It
labels the local speaker as YOU (mic) and diarized remotes as REMOTE_1,
REMOTE_2, … The three modes:
dual-diarize (default) -- per-channel transcription + diarization of
remotes. Best accuracy for headphone setups where mic and system audio
don't bleed into each other.dual -- per-channel transcription with no remote diarization (channel
identity = speaker identity: YOU vs. REMOTE).mono (legacy) -- mix both channels to mono, then diarize. Use it when
your speakers play into the room and both voices appear on both channels.millet transcribe --mixdown dual-diarize ~/meet-recordings/meeting-20260312-140000/
Note: millet run still defaults to --mixdown mono and accepts mono or
dual; pass --mixdown dual there for headphone recordings.
millet run
Records until Ctrl+C, then automatically transcribes, generates a summary,
and produces a PDF. Takes options from both record and transcribe. Note
run's --mixdown defaults to mono and accepts only mono or dual
(unlike transcribe, which defaults to dual-diarize); use millet transcribe on the saved recording if you need dual-diarize.
millet gui
A small always-on-top window with:
When 2 or more speakers are detected, a speaker labeling dialog appears before the results are saved. Each speaker is shown with their channel and a sample line of text. If voice profiles exist, confident matches are shown automatically. Enter a real name or leave blank to keep the auto-assigned label (YOU, REMOTE_1, etc.).
If meeting sync is configured and the recording matches a scheduled meeting, a sync confirmation prompt appears with Push / Skip buttons.

millet label ~/meet-recordings/meeting-20260313-214133
For each speaker in the recording, millet label:
ffplay).txt, .srt, .json, .summary.md, .pdf) with the new namesWith --auto, voice profiles are used to automatically identify known speakers.
Confident matches are applied without prompting; only unrecognized speakers get
the interactive prompt:
millet label --auto ~/meet-recordings/meeting-20260313-214133
Options:
--auto -- auto-label using voice profiles (see Voiceprint speaker recognition)--no-audio -- skip audio playback, just show text samples--no-summary -- use find-and-replace instead of re-running Ollama--summary-backend / --summary-model -- override summary backend and model for regeneration--apply-json FILE (or - for stdin) -- non-interactive labeling: apply a
{"OLD_ID": "Name", ...} map (or {"labels": {...}} envelope) and exit--update-profiles -- with --apply-json, update voiceprint profiles from
the confirmed labels--summary-language <code> -- also emit a translated .summary.<lang>.md--team <name> -- use a team-scoped profile database (see below)When run without a TTY (e.g. from a worker), --auto applies confident
matches and skips the interactive prompt automatically, writing an
.autoid.json sidecar with per-speaker match confidence.
Each recording gets its own session directory:
~/meet-recordings/meeting-20260312-140000/
meeting-20260312-140000.wav # Stereo audio (16kHz)
meeting-20260312-140000.session.json # Recording metadata
meeting-20260312-140000.ffmpeg.log # ffmpeg capture log
meeting-20260312-140000.txt # Plain text transcript
meeting-20260312-140000.srt # Subtitle format
meeting-20260312-140000.json # Full detail (word-level timestamps)
meeting-20260312-140000.summary.md # AI meeting summary with YAML frontmatter
meeting-20260312-140000.summary.meta.json # Summary backend/model + timing metadata
meeting-20260312-140000.frontmatter.json # Structured frontmatter (schema_version 1)
meeting-20260312-140000.pdf # Professional PDF (summary + transcript)
Example .txt output:
[00:00:12 --> 00:00:18] YOU: So the main issue we're seeing is with the API rate limiting.
[00:00:19 --> 00:00:25] REMOTE_1: Right, I think we should implement exponential backoff.
[00:00:26 --> 00:00:31] YOU: Agreed. Can you also look at caching the responses?
Every .summary.md ships with a typed YAML frontmatter block plus a
matching .frontmatter.json sidecar. The schema is intentionally
small in v1 so downstream consumers can rely on it:
---
schema_version: 1
type: meeting
title: Q2 Pricing Discussion
date: "2026-03-17T14:00:00+00:00"
duration: PT42M17S
language: en
participants:
- name: YOU
role: null
channel: mic
- name: Alice
role: null
channel: system
topics:
- pricing
- onboarding
action_items:
- assignee: Alice
task: Send pricing doc
due: Friday
status: open
decisions:
- text: Run pricing experiment at $99/mo
topic: pricing
source:
session_id: meeting-20260312-140000
audio_sha256: null
---
## Meeting Overview
...
## Key Topics Discussed
...
schema_version: 1 is what every consumer should pin against.
The JSON sidecar contains the exact same dict for tools that don't
want to parse YAML. [vezir](https://github.com/pretyflaco/vezir) 0.2.0+ reads these files directly to build a queryable index over
your meetings.
Sessions recorded before meetscribe 0.7.0 / millet-pipeline 0.9.0 don't carry frontmatter. Re-extract it for one or more sessions with:
# Re-run the LLM to produce frontmatter; idempotent (skips sessions
# whose .summary.meta.json already records data_extracted=true).
millet ingest ~/meet-recordings/meeting-2026*
# Force re-extraction even when frontmatter is already present:
millet ingest --force ~/meet-recordings/meeting-20260312-140000
# Preview without invoking the LLM:
millet ingest --dry-run ~/meet-recordings/meeting-2026*
millet ingest accepts the same --summary-backend /
--summary-model / --ollama-singlepass flags as
millet transcribe and regenerates the PDF by default
(--no-pdf to skip).
millet generates a structured meeting summary with:
| Model | Size | Speed | Notes |
|---|---|---|---|
qwen3.5:9b | 6.6 GB | ~18-35s | Default -- best balance of quality and speed |
gemma3:12b | 8.1 GB | ~15s | Fastest |
qwen3:14b | 9.3 GB | ~39s | Good quality |
glm-4.7-flash | 19 GB | ~37s | Must use thinking-off mode (handled automatically) |
Change the model:
millet run --summary-model gemma3:12b
Disable summaries:
millet run --no-summarize
millet supports five backends with automatic fallback:
| Backend | Setup | Cost | Quality | Privacy |
|---|---|---|---|---|
ollama (default) | ollama serve + ollama pull qwen3.5:9b | Free | Good | Fully local |
openrouter | Set OPENROUTER_API_KEY | Pay-per-use | Excellent | Cloud (model-provider-visible) |
claudemax | Run claude-max-api-proxy on localhost:3457 | Claude Max subscription | Excellent | Cloud (Anthropic-visible) |
tinfoil | pip install 'millet-pipeline[tee]', set TINFOIL_API_KEY (or drop a key file at ~/models/tinfoil/tinfoil.txt) | ~$0.009/meeting | Excellent (GLM-5.2) | Hardware-attested TEE — prompts not visible to provider/operator |
openai | Set MILLET_OPENAI_BASE_URL | Varies | Varies | Depends on endpoint |
The openai backend works with any OpenAI-compatible API — Lemonade, LiteLLM,
vLLM, text-generation-webui, LocalAI, or any self-hosted endpoint.
The tinfoil backend runs inference inside a hardware-attested TEE (AMD
SEV-SNP or Intel TDX, depending on the model). The model provider can't
see the prompts, the cloud operator can't see the prompts, and the
integrity is checked against an attestation report on every request.
~$0.009 per meeting; latency ~66 s for a 30-min recording on GLM-5.2.
# Use OpenRouter
millet run --summary-backend openrouter --summary-model anthropic/claude-sonnet-4.6
# Use any OpenAI-compatible endpoint
export MILLET_SUMMARY_BACKEND=openai
export MILLET_OPENAI_BASE_URL=http://localhost:8000/v1
export MILLET_SUMMARY_MODEL=your-model-name
# Optional: export MILLET_OPENAI_API_KEY=your-key
# Or set via environment variables
export MILLET_SUMMARY_BACKEND=openrouter
export MILLET_SUMMARY_MODEL=anthropic/claude-sonnet-4.6
Environment variables use the
MILLET_prefix. The olderMEETSCRIBE_(andMEET_) spellings still work for one more release but emit aDeprecationWarning.
If the configured backend is unavailable, millet automatically tries the
next one in the fallback chain: claudemax → tinfoil → openrouter → ollama.
The openai backend is opt-in only and never participates in the fallback
chain.
When a preset is explicitly selected (see Summarization presets below),
this fallback is disabled for that run — the chosen preset's backend
either succeeds or the whole summarization step fails loudly with a
non-zero exit. This protects the privacy/quality promise of the
confidential preset (a silent tinfoil → claudemax fallback would defeat
the entire point of choosing TEE-attested inference).
A preset is a friendly name that resolves to a concrete (backend, model)
pair. Set it via --summary-preset on transcribe, run, label,
gui, or ingest, or via the MILLET_SUMMARY_PRESET env var.
| Preset | Backend | Model | Use case |
|---|---|---|---|
high-quality | claudemax | claude-sonnet-4-6 | Default for users with a Claude Max subscription; highest summary quality |
confidential | tinfoil | glm-5-2 | Meetings where prompts must not be retained or trained on; hardware-attested TEE |
alternative | openrouter | moonshotai/kimi-k2.6 | Cheapest cloud option (~$0.017/meeting); useful when claudemax credentials are unavailable |
# Quick check of which preset is in effect
millet transcribe ~/meet-recordings/today/today.wav --summary-preset confidential
# Or set per-session via env
export MILLET_SUMMARY_PRESET=high-quality
millet run
When a preset is set, --summary-backend and --summary-model overrides
are honored within that preset (e.g. --summary-preset confidential --summary-model gpt-oss-120b swaps the model but keeps the TEE backend).
When the ollama backend is selected (the default), millet runs two LLM calls instead of one:
This dramatically improves format compliance and reduces hallucinations
on 20B-class local models (gpt-oss:20b, qwen3.6:27b) compared to a
single-pass call, at the cost of one additional LLM call (~30–90s extra).
Cloud backends (claudemax, openrouter, openai) remain single-pass — they
already produce well-structured output in one shot.
To opt out and use the previous single-pass behavior:
millet run --ollama-singlepass
# Or via environment:
export MILLET_OLLAMA_SINGLEPASS=1
The .summary.meta.json sidecar records per-pass timings
(pass1_seconds, pass2_seconds, pass1_chars) when two-pass was used.
See docs/local-model-evaluation.md for the full evaluation that motivated this design, including known failure modes of local 20B-class models.
The summarization prompts live in millet/prompts/ (e.g.
summarize_system.md, plus the summarize_extract_* / summarize_format_*
pairs used by the two-pass Ollama flow). Edit them to change the summary
format, add domain-specific instructions, or tune for your preferred model.
No Python changes needed.
millet can automatically identify speakers across meetings using voice embeddings. After you label speakers in one meeting, their voice profiles are stored and matched against future recordings.
# Build profiles from already-labeled sessions
millet enroll ~/meet-recordings/meeting-20260330-*
# Auto-label speakers in future meetings using voice profiles
millet label --auto ~/meet-recordings/meeting-20260401-093000
Profiles are stored in ~/.config/meet/speaker_profiles.json and improve
with each labeled session (running average of embeddings).
Use millet enroll --list to list enrolled speakers. Team-scoped profiles
are supported via --team <name> on enroll and label, stored under
~/.config/meet/<team>/speaker_profiles.json, so each team keeps its own
voiceprint database.
Push meeting artifacts to a Git repository on a configurable schedule.
# Create an example config
millet sync --init-config
# Edit ~/.config/meet/sync_config.json with your repo URL and schedule
# Push a session manually
millet sync ~/meet-recordings/meeting-20260331-110038_STANDUP
# View configured schedule
millet sync --list-schedule
When the GUI detects a matching scheduled meeting, it prompts for confirmation
before syncing. Sessions that don't match the schedule are skipped. The CLI
uses --force to sync unmatched sessions.
You can also configure a team_members list and min_team_members threshold
in sync_config.json to require that a minimum number of recognized speakers
are present before offering to sync.
millet sync supports --team <name> (per-team config + clone) and
--meeting-type <slug> for routing. Push failures exit non-zero so scheduled
sync jobs can detect and retry them.
millet auto-detects the spoken language by default (Whisper large-v3-turbo supports 99 languages). You can also set it explicitly:
millet run --language de # German
millet run --language tr # Turkish
millet run --language fr # French
millet run --language es # Spanish
millet run --language fa # Farsi (Persian)
millet run --language auto # Auto-detect (default)
auto, the detected language is used for
alignment and all downstream steps.| Language | Code | Alignment model | PDF font | Notes |
|---|---|---|---|---|
| English | en | wav2vec2 (torchaudio) | DejaVu Sans | |
| German | de | VoxPopuli (torchaudio) | DejaVu Sans | |
| French | fr | VoxPopuli (torchaudio) | DejaVu Sans | |
| Spanish | es | VoxPopuli (torchaudio) | DejaVu Sans | |
| Turkish | tr | wav2vec2 (HuggingFace) | DejaVu Sans | ~1.2 GB alignment model download |
| Farsi | fa | wav2vec2 (HuggingFace) | Noto Naskh Arabic | ~1.2 GB alignment model download, RTL |
Alignment models download automatically on first use, but you can pre-fetch
them (e.g. before an offline session) with millet download:
millet download # show cache status for all supported languages
millet download de tr fa # download German, Turkish, Farsi alignment models
millet download --all # download every supported alignment model
millet download parakeet # download the Parakeet ASR model (English)
It exits non-zero if any requested download fails, so it's safe to script.
millet translate renders an existing session transcript into another
language via Ollama, preserving timestamps and speaker labels. The result is
saved as <basename>.translation.<lang>.txt in the session directory.
millet translate ~/meet-recordings/meeting-20260313-231509 # to English
millet translate ~/meet-recordings/meeting-20260313-231509 --to de # to German
Farsi uses right-to-left text. For proper PDF rendering, install the optional RTL dependencies:
pip install arabic-reshaper python-bidi
# Or with the optional extra:
pip install "millet-pipeline[rtl]"
Without these libraries, Farsi text will appear in the PDF but glyphs may not be joined correctly and reading order may be wrong.
By default, millet record captures all system audio (including notification
sounds, music, etc.). For cleaner recordings, use --virtual-sink:
millet record --virtual-sink
This creates an isolated audio sink. Route your meeting app's audio to it:
pavucontrol (PulseAudio Volume Control)You'll still hear the meeting through your normal speakers via automatic loopback.
Environment variables use the MILLET_ prefix (legacy MEETSCRIBE_ / MEET_
spellings are honored for one more release with a DeprecationWarning):
| Variable | Purpose |
|---|---|
MILLET_SUMMARY_BACKEND | Default summary backend (ollama, openrouter, claudemax, openai, tinfoil) |
MILLET_SUMMARY_MODEL | Default summary model for the chosen backend |
MILLET_SUMMARY_PRESET | Default preset (high-quality, confidential, alternative) |
MILLET_OLLAMA_SINGLEPASS | Set to 1 to disable two-pass Ollama summarization |
MILLET_OPENAI_BASE_URL / MILLET_OPENAI_API_KEY | Endpoint + key for the openai-compatible backend |
OPENROUTER_API_KEY | Required for the openrouter backend |
TINFOIL_API_KEY | Required for the tinfoil backend (or a key file at ~/models/tinfoil/tinfoil.txt) |
HF_TOKEN | HuggingFace token for pyannote diarization |
MILLET_CONFIG_DIR | Override the config dir (default ~/.config/meet) |
MILLET_PROFILES_PATH | Override the voiceprint profile DB path |
MILLET_RECORDINGS_DIR | Override the recordings directory (default ~/meet-recordings) |
Set HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 to run without any network
access. millet honors these and, on a cache miss, prints actionable guidance
naming the exact model to pre-fetch (via millet download) rather than
failing with an opaque network error.
With an NVIDIA GPU (12 GB VRAM):
| Model | Transcription | + Diarization | Recommended batch_size |
|---|---|---|---|
| large-v3-turbo | ~4 GB | ~7 GB total | 16 |
| medium | ~3 GB | ~6 GB total | 16 |
| base | ~1 GB | ~4 GB total | 16 |
If you hit OOM errors:
--batch-size to 4 or 8--compute-type int8--model medium or --model base)--device cpu as a last resort[Meeting App] --> [PipeWire/PulseAudio] --> [ffmpeg dual-channel capture] --> meeting.wav
|
[WhisperX: faster-whisper + wav2vec2 alignment + pyannote diarization]
|
[Ollama LLM summary] [Diarized transcript]
| |
.summary.md .txt / .srt / .json
| |
+--------> .pdf <---------+
Capture: Records your mic (left channel) and system audio (right channel) simultaneously into a single stereo WAV file at 16 kHz.
Transcribe: Runs the WhisperX pipeline -- batched Whisper transcription, wav2vec2 forced alignment for word-level timestamps, and pyannote speaker diarization. Dual-channel energy analysis maps speakers to YOU or REMOTE.
Summarize: Sends the transcript to a local Ollama model that extracts a structured summary.
PDF: Combines the summary and full transcript into a professional page-numbered PDF document.
The pyannote diarization model requires CUDA NVRTC for JIT compilation. If your CUDA driver version doesn't match the installed libnvrtc-builtins version, millet automatically creates a compatibility symlink. This happens transparently on first use.
If you still see NVRTC errors:
export LD_LIBRARY_PATH=$HOME/.local/lib/cuda:$LD_LIBRARY_PATH
millet label or the GUI dialog to assign real namesmillet record, millet run) requires Linux with PulseAudio
or PipeWire. Transcription, labeling, summarization, and sync work on both
Linux (CUDA) and macOS Apple Silicon (MLX Whisper + MPS) as of v0.6.0.gpt-oss:20b) can hallucinate on
transcripts dominated by very short low-information utterances ("yes",
"okay") and may exceed the default 600s timeout on very large
(>100 KB) non-English transcripts. For these cases configure a cloud
backend (claudemax / openrouter) — the fallback chain takes over
automatically. See docs/local-model-evaluation.md.Is there a GUI? Yes — run millet gui for a small always-on-top GTK3
widget with Record/Stop, live timer, status indicator, and one-click
access to the resulting PDF and session folder. See
Launch the GUI widget for details.
Does it work on Windows / macOS? System-audio recording requires Linux
(PulseAudio / PipeWire). The post-capture pipeline (millet transcribe,
millet label, millet sync, etc.) works on macOS Apple Silicon as of v0.6.0
— install with pip install 'millet-pipeline[mlx]'. Windows is not
supported.
Can I run a Mac as a transcription server? Yes — see vezir, the team-scale wrapper around millet. A Mac can act as the GPU server with Linux laptops or the Android client providing the audio.
Can I use it without a GPU? Yes, with --device cpu, but
transcription will be 5–20× slower depending on the Whisper model.
See VRAM usage.
git clone https://github.com/pretyflaco/millet
cd millet
pip install -e .[dev]
/usr/bin/python3 -m pytest tests/
Pull requests welcome. Please run the test suite before submitting.
See CHANGELOG.md for release history.
Python
100.0%