Internal
MeetScribe
Meeting transcription, without the audio ever leaving home.
In short
100% 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.
Highlights
-
French transcription: WhisperX large-v3
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.
-
Speaker diarization: pyannote
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 via Claude Code (
claude -p)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. -
FastAPI + HTMX web 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.
-
Background worker + SQLite queue
Separate process polling the SQLite
jobstable: sequential GPU processing without blocking the UI. Best-effort finalisation: a post-processing failure (chapters, actions, stats) is logged and the job still completes asstatus=done. Legacy meetings with no meta.json are handled without crashing. -
Full-text search: FTS5
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).
-
Action-item dashboard
Deterministic action extraction (accent-insensitive regex on the "Actions à faire" section) with owner detection from bold prefixes.
action_itemsSQLite table with open/done status toggleable from a dedicated dashboard. Idempotent merge: checked statuses survive a re-sync. -
RAG chat over meetings
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. -
Exports: SRT/VTT, PDF, Obsidian
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.
By the numbers
- ~15.4K Python lines
- 1,020 pytest tests
- large-v3 WhisperX model
- 95 min Pilot meeting (733 segments)
The story
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
Sequential best-effort pipeline: every step can fail without blocking the job
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.json as the single source of truth for editable metadata
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.
claude -p behind an injectable seam, never the API
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.
Zero external network references in templates and static assets
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.
FTS5 as the search engine: accent-insensitive, zero external dependency
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
Running WhisperX + pyannote on Windows with the right CUDA stack without breaking the build
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.
Tech specs
Where it runs Python 3.12 · Local GPU RTX 4070 · FastAPI 0.137 · SQLite / FTS5 · Tailscale
| Runtime | Python 3.12 · Local GPU RTX 4070 |
|---|---|
| Transcription | WhisperX 3.8.6 (large-v3) · pyannote/speaker-diarization-community-1 · ffmpeg |
| Web | FastAPI 0.137 · HTMX (vendored) · Jinja2 · Tailscale |
| Data | SQLite / FTS5 · jobs queue + action_items |
| Summaries | Claude Code (claude -p) · subscription: zero API |
| Tests | pytest 9 (1,020 tests) · GPU mocked: suite runs without GPU |
Meeting transcription, without the audio ever leaving home.
Open source on release