← Back to projects

MeetScribe

Internal · Open source soon

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.

FR EN
~15.4K Python lines
1,020 pytest tests
189 Commits
large-v3 WhisperX model
95 min Pilot meeting (733 segments)

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

01
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.

02
`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.

03
`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.

04
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.

05
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.

Features

🎙️
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 `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.

🔍
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_items` SQLite 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`.

Tech Stack

Runtime
Python 3.12Local GPU RTX 4070
Transcription
WhisperX 3.8.6 (large-v3)pyannote/speaker-diarization-community-1ffmpeg
Web
FastAPI 0.137HTMX (vendored)Jinja2Tailscale
Data
SQLite / FTS5jobs queue + action_items
Summaries
Claude Code (claude -p)subscription — zero API
Tests
pytest 9 (1,020 tests)GPU mocked — suite runs without GPU

⊙ Open source on release