← Retour aux projets

MeetScribe

Interne · Open source bientôt

Outil de transcription et résumé de réunions 100 % local — alternative française et auto-hébergée à Otter/Fireflies/Plaud. Pipeline : ffmpeg → WhisperX large-v3 (transcription + alignement) → pyannote (diarisation locuteurs) → résumés via Claude Code (`claude -p`). Interface web FastAPI + HTMX pilotable depuis un téléphone via Tailscale, worker de fond avec file SQLite et recherche FTS5. Tourne sur GPU local (RTX 4070). Zéro coût récurrent, données jamais envoyées hors de la machine.

FR EN
~15,4K Lignes Python
1 020 Tests pytest
189 Commits
large-v3 Modèle WhisperX
95 min Réunion pilote (733 segments)

Pourquoi j'ai construit ça

Chaque réunion importante finissait par un enregistrement que je ne réécoutais jamais faute de temps. Les outils du marché — Otter, Fireflies, Plaud — coûtent un abonnement mensuel et envoient l'audio sur leurs serveurs. Je voulais garder mes réunions chez moi : j'ai construit MeetScribe pour avoir exactement ce qu'il me fallait : transcription française haute qualité, diarisation fiable (qui a dit quoi), résumés automatiques, le tout en 100 % local sur mon propre GPU. Zéro abonnement additionnel (j'utilise mon abonnement Claude Code via `claude -p`), zéro donnée envoyée hors de ma machine. L'interface web, pilotable depuis le téléphone via Tailscale, me permet d'uploader un enregistrement le soir et de trouver le compte-rendu prêt le lendemain matin. L'outil sera open source à sa sortie — le besoin de transcription locale et privée existe bien au-delà de mon propre usage.

Décisions techniques structurantes

01
Pipeline séquentiel best-effort : chaque étape peut échouer sans bloquer le job

Le worker exécute ffmpeg → WhisperX → pyannote → render → summarize en séquence, mais les étapes de post-traitement (chapitres, actions, statistiques de conversation) sont toutes dans un bloc try/except de finalisation : un échec est journalisé et le job passe quand même en `status=done`. Cette décision garantit qu'une réunion reste accessible même si une étape optionnelle plante — l'utilisateur n'est jamais bloqué par un post-traitement secondaire.

02
`meta.json` comme seul point de vérité pour les métadonnées éditables

`meta.py` est le seul writer de `meta.json` (écriture atomique, merge, horodatage `edited_at` forcé). Le transcript JSON brut est **immuable** : les éditions de segments sont stockées comme overrides indexés dans `meta.json` et appliquées à la volée au rendu. Cette séparation garantit que la transcription brute WhisperX reste toujours récupérable, et que plusieurs features peuvent écrire leurs données sans s'écraser mutuellement.

03
`claude -p` derrière une seam injectable — jamais l'API

Toute fonction qui appelle Claude prend un paramètre `runner=None`. En production, le runner par défaut invoque `claude -p` sur l'abonnement existant (via stdin). En test, chaque appel injecte une lambda qui capture le prompt sans spawner de subprocess — ce qui rend la suite entièrement testable sans GPU ni abonnement. La règle `ANTHROPIC_API_KEY` est **interdite** dans l'env : la présence de cette variable forcerait la facturation API au lieu de l'abonnement.

04
Zéro dépendance réseau dans les templates et assets statiques

Un test de garde (`test_css_guard.py`) vérifie après chaque feature qu'aucun template ni aucun fichier statique ne contient de référence externe (`http://`, `https://`, `@import`). HTMX est vendorisé. Cette contrainte est non-négociable : l'outil doit fonctionner hors ligne sur un réseau Tailscale sans sortie Internet, et aucune fuite de données via un CDN n'est acceptable.

05
FTS5 comme moteur de recherche — insensible aux accents, zéro dépendance

La recherche sur le corpus de réunions utilise l'index FTS5 natif de SQLite : pas de serveur ElasticSearch, pas de bibliothèque externe. La normalisation NFD (suppression des marques combinantes) est appliquée avant l'indexation et la requête, ce qui rend la recherche insensible aux accents. Le même principe est appliqué au matching des sections de résumé (extraction des actions, chapitres) : `Actions a faire` et `Actions à faire` sont équivalents.

Le challenge non-trivial

Faire tourner WhisperX + pyannote sur Windows avec le bon stack CUDA sans casser le build

Le stack GPU de MeetScribe est particulièrement fragile : WhisperX 3.8.6 tire PyTorch en version CPU ; il faut l'écraser avec la build CUDA 12.8 (`cu128`) après installation, dans le bon ordre. Le driver 591.86 supporte CUDA 12.8 mais pas 12.4 — les wheels `cu124` usuelles ne fonctionnent pas. PyAnnote utilise depuis sa v4.0 `pyannote/speaker-diarization-community-1` (et non plus 3.1), un modèle soumis aux CGU HuggingFace — l'acceptation est manuelle et bloquante. Deux bugs discrets ont failli passer inaperçus : (1) `DiarizationPipeline` prend `token=` et non `use_auth_token=` ; (2) `sentence-transformers` peut re-résoudre torch et écraser la build GPU. La solution a été un setup documenté étape par étape avec ordre d'installation contraint et une checklist de validation sur le vrai GPU avant tout commit.

Leçon retenue

La valeur d'un outil local de transcription repose entièrement sur sa fiabilité quotidienne — pas sur la sophistication des features. Chaque décision architecturale converge vers ce principe : le worker ne peut jamais bloquer en raison d'un post-traitement secondaire, le transcript brut est immuable pour rester récupérable, le runner IA est injectable pour que la suite tourne sans GPU ni réseau. Et la règle la plus importante : ne jamais autoriser `ANTHROPIC_API_KEY` dans l'environnement — un outil 'zéro coût' qui peut silencieusement basculer sur l'API facturée n'est pas un outil zéro coût. La contrainte est testée, pas juste documentée.

Fonctionnalités

🎙️
Transcription FR — WhisperX large-v3

WhisperX large-v3 avec alignement temporel mot à mot : transcription haute précision en français, y compris argot et jargon. Batch configurable (VOICE_BATCH_SIZE) pour s'adapter à la VRAM disponible. Smoke-testé sur 95 min de parole réelle, 733 segments produits.

🗣️
Diarisation locuteurs — pyannote

Pipeline pyannote/speaker-diarization-community-1 : attribue chaque segment à un locuteur (SPEAKER_00, SPEAKER_01…). Le nombre de participants est configurable à l'upload pour affiner la diarisation ; en cas d'omission, détection automatique de 1 à 10 locuteurs.

🤖
Résumés via Claude Code (`claude -p`)

Les résumés sont générés via `claude -p` (abonnement Claude Code — jamais l'API facturée). Quatre formats au choix : compte-rendu complet, compte-rendu détaillé, extraction des actions, email de synthèse. En mode manuel, le prompt est toujours produit pour un collage dans l'interface Claude.

🌐
Interface web FastAPI + HTMX

Mini application web FastAPI + Jinja2 + HTMX (vendorisé, zéro JS framework). Accessible depuis le téléphone via Tailscale : upload d'audio, suivi de la progression, visualisation du transcript locuteur par locuteur, édition inline des segments, navigation par chapitres cliquables.

⚙️
Worker de fond + file SQLite

Process séparé qui poll la table `jobs` SQLite : traitement séquentiel GPU sans bloquer l'UI. Finalisation best-effort — un échec de post-traitement (chapitres, actions, stats) est journalisé et le job passe quand même en `status=done`. Legacy meetings sans meta.json gérées sans crash.

🔍
Recherche plein texte FTS5

Index SQLite FTS5 sur l'ensemble des transcripts : recherche instantanée sur tout le corpus de réunions. Ré-indexation automatique après une édition de segment. Support de la recherche insensible aux accents (normalisation NFD avant comparaison).

📋
Dashboard des actions

Extraction déterministe des actions (regex accent-insensible sur la section « Actions à faire »), avec détection de l'owner en préfixe bold. Table `action_items` SQLite avec statut ouvert/fait commutable depuis un dashboard dédié. Merge idempotent : les statuts cochés survivent à une re-synchronisation.

💬
Chat RAG sur les réunions

Questions en langage naturel ancrées sur les transcripts via FTS5 + `claude -p` (instruction FR « n'invente rien »). Mode réunion unique et mode corpus (toutes les réunions). Contexte budgété (VOICE_RAG_CONTEXT_CHARS) ; une erreur claude n'envoie jamais un 500 — réponse de repli en français.

📤
Exports — SRT/VTT, PDF, Obsidian

Export des sous-titres SRT et VTT (horodatage HH:MM:SS préfixé par locuteur), export PDF du compte-rendu (fpdf2), export Obsidian Markdown (frontmatter + résumé + transcript optionnel, écriture atomique, anti-traversal). Nom de fichier daté via `meta.export_basename`.

Stack technique

Runtime
Python 3.12GPU local RTX 4070
Transcription
WhisperX 3.8.6 (large-v3)pyannote/speaker-diarization-community-1ffmpeg
Web
FastAPI 0.137HTMX (vendorisé)Jinja2Tailscale
Données
SQLite / FTS5file jobs + action_items
Résumés
Claude Code (claude -p)abonnement — zéro API
Tests
pytest 9 (1 020 tests)GPU mocké — suite sans GPU

⊙ Open source à la sortie