MeetScribe
Internal · Open source soon100% local meeting transcription and summarisation tool — a French-capable, self-hosted alternative to Otter/Fireflies/Plaud. Pipeline: ffmpeg → WhisperX large-v3 (transcription + alignment) → pyannote (speaker diarization) → summaries via Claude Code (`claude -p`). FastAPI + HTMX web UI drivable from a phone over Tailscale, background worker with a SQLite queue and FTS5 search. Runs on a local GPU (RTX 4070). Zero recurring cost, data never leaves the machine.
Why I built it
Every important meeting ended up as a recording I never had time to replay. The market tools — Otter, Fireflies, Plaud — carry a monthly subscription cost and send audio to their servers. I wanted to keep my meetings on my own machine, so I built MeetScribe to have exactly what I wanted: high-quality French transcription, reliable speaker diarization (who said what), automatic summaries, all 100% local on my own GPU. Zero additional subscription (I reuse my Claude Code subscription via `claude -p`), zero data sent outside my machine. The web interface, drivable from a phone over Tailscale, lets me upload a recording in the evening and find the meeting notes ready the next morning. The tool will be open source on release — the need for local, private transcription extends well beyond my own use.
Structural technical decisions
The worker runs ffmpeg → WhisperX → pyannote → render → summarize in sequence, but post-processing steps (chapters, actions, conversation stats) all live inside a try/except finalisation block: a failure is logged and the job still completes as `status=done`. This design guarantees that a meeting stays accessible even if an optional step crashes — the user is never blocked by a secondary post-processing failure.
`meta.py` is the only writer of `meta.json` (atomic write, merge, forced `edited_at` timestamp). The raw transcript JSON is **immutable**: segment edits are stored as index-keyed overrides inside `meta.json` and applied on-the-fly at render time. This separation guarantees that the raw WhisperX transcription is always recoverable, and that multiple features can write their data without overwriting each other.
Every function that calls Claude takes a `runner=None` parameter. In production, the default runner invokes `claude -p` against the existing subscription (via stdin). In tests, each call injects a lambda that captures the prompt without spawning a subprocess — making the full suite runnable without a GPU or subscription. The rule `ANTHROPIC_API_KEY` is **forbidden** in the environment: the presence of that variable would silently force billed API usage instead of the subscription.
A guard test (`test_css_guard.py`) verifies after every feature that no template or static file contains an external reference (`http://`, `https://`, `@import`). HTMX is vendored. This constraint is non-negotiable: the tool must work offline on a Tailscale network without Internet egress, and no data leakage via a CDN is acceptable.
Meeting corpus search uses SQLite's native FTS5 index: no ElasticSearch server, no external library. NFD normalisation (stripping combining marks) is applied before indexing and querying, making search accent-insensitive. The same principle is applied to summary section matching (action extraction, chapters): `Actions a faire` and `Actions à faire` are treated as equivalent.
The non-trivial challenge
MeetScribe's GPU stack is particularly fragile: WhisperX 3.8.6 pulls PyTorch in its CPU build; it must be overwritten with the CUDA 12.8 (`cu128`) build afterwards, in the right order. Driver 591.86 supports CUDA 12.8 but not 12.4 — the usual `cu124` wheels don't work. PyAnnote's v4.0 uses `pyannote/speaker-diarization-community-1` (not 3.1), a model gated behind HuggingFace terms — manual acceptance is required and blocking. Two subtle bugs nearly slipped through: (1) `DiarizationPipeline` takes `token=` not `use_auth_token=`; (2) `sentence-transformers` can re-resolve torch and overwrite the GPU build. The fix was a step-by-step documented setup with a constrained installation order and a GPU-validation checklist run on the real hardware before any commit.
Lesson learned
The value of a local transcription tool rests entirely on its everyday reliability — not on the sophistication of its features. Every architectural decision converges toward that principle: the worker can never block on a secondary post-processing step, the raw transcript is immutable so it stays recoverable, the AI runner is injectable so the test suite runs without a GPU or network. And the most important rule: never allow `ANTHROPIC_API_KEY` in the environment — a 'zero-cost' tool that can silently fall back to the billed API is not a zero-cost tool. The constraint is tested, not just documented.
Features
WhisperX large-v3 with word-level temporal alignment: high-accuracy French transcription including slang and domain jargon. Configurable batch size (VOICE_BATCH_SIZE) for available VRAM. Smoke-tested on 95 min of real speech, producing 733 segments.
pyannote/speaker-diarization-community-1 pipeline: attributes each segment to a speaker (SPEAKER_00, SPEAKER_01…). Participant count is configurable at upload to improve diarization accuracy; if omitted, auto-detection between 1 and 10 speakers.
Summaries are generated via `claude -p` (Claude Code subscription — never the billed API). Four formats available: full meeting minutes, detailed minutes, action extraction, and synthesis email. In manual mode, the prompt is always produced for pasting directly into the Claude interface.
Lightweight web app built on FastAPI + Jinja2 + HTMX (vendored, no JS framework). Accessible from a phone over Tailscale: audio upload, progress tracking, speaker-by-speaker transcript view, inline segment editing, and clickable chapter navigation.
Separate process polling the SQLite `jobs` table: sequential GPU processing without blocking the UI. Best-effort finalisation — a post-processing failure (chapters, actions, stats) is logged and the job still completes as `status=done`. Legacy meetings with no meta.json are handled without crashing.
SQLite FTS5 index over the entire transcript corpus: instant search across all meetings. Automatic re-indexing after a segment edit. Accent-insensitive matching (NFD normalisation before comparison).
Deterministic action extraction (accent-insensitive regex on the "Actions à faire" section) with owner detection from bold prefixes. `action_items` SQLite table with open/done status toggleable from a dedicated dashboard. Idempotent merge: checked statuses survive a re-sync.
Natural-language questions grounded in transcripts via FTS5 + `claude -p` (French instruction: "n'invente rien" — never make things up). Single-meeting and full-corpus modes. Budgeted context (VOICE_RAG_CONTEXT_CHARS); a claude failure never triggers a 500 — French fallback fragment returned instead.
SRT and VTT subtitle export (HH:MM:SS timestamp prefixed by speaker), PDF meeting-notes export (fpdf2), Obsidian Markdown export (frontmatter + summary + optional transcript, atomic write, path-traversal guard). Dated filename via `meta.export_basename`.
Tech Stack
⊙ Open source on release