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

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

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

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

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

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

  6. 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).

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

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

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

MeetScribe technologies, by area
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