MeetScribe
Interne · Open source bientôtOutil 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.
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
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.
`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.
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.
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.
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
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
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.
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.
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.
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.
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.
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).
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.
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.
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
⊙ Open source à la sortie