Santé / SaaS
Un assistant de compte rendu pour les médecins
Une application qui transcrit la consultation et rédige le compte rendu, conçue autour d'une règle : le système propose, le médecin décide.
- Rôle
- Conception et développement — projet personnel
- Période
- Juillet 2025 — avril 2026
- Patients couverts en bêta
- ~1 000
- Appels LLM par consultation
- 3
- Audio conservé côté serveur
- 0
- Diagnostics posés par le système
- 0
- Java 17
- Spring Boot 3
- Spring AI
- PostgreSQL
- React 19
- TypeScript
- Deepgram
- OpenAI
- Stripe
Contexte
Après chaque consultation, le médecin rédige un compte rendu. Ce travail est indispensable — il fait le dossier médical, la coordination avec les confrères, la trace en cas de litige — et il se fait quand le patient suivant attend déjà, ou le soir, accumulé. C’est du temps de médecin passé à de la saisie.
La consultation, elle, contient déjà tout ce qu’il faut : le motif, les antécédents évoqués, l’examen, la décision. Le compte rendu est en grande partie une restitution structurée de ce qui vient d’être dit — précisément le type de tâche qu’un modèle de langage fait bien.
J’ai construit cette application de bout en bout — backend Spring Boot, frontend React, facturation Stripe — pour couvrir la chaîne complète : enregistrement, transcription, génération du compte rendu, dossier patient, abonnement. Elle a ensuite été éprouvée en bêta, et c’est cette confrontation au réel qui a réécrit une bonne partie de l’architecture.
Contrainte
Trois contraintes ont structuré le projet, et elles tirent dans des directions différentes.
La première est la nature de la donnée. Une consultation médicale est la catégorie de donnée personnelle la plus sensible qui existe. Toute donnée conservée est une donnée à protéger, à justifier, et un jour à expliquer. La bonne stratégie n’est pas de mieux chiffrer : c’est d’en garder moins.
La deuxième est la responsabilité. Un modèle de langage ne peut pas poser de diagnostic. Le médecin est responsable de ce qu’il signe, juridiquement et déontologiquement. Un système qui produirait une conclusion médicale d’apparence autoritaire serait dangereux — pas seulement parce qu’il peut se tromper, mais parce qu’il déplacerait insidieusement la décision hors du praticien.
La troisième est la plus prosaïque, et c’est celle qui a failli tuer le produit : le médecin est en consultation. Un outil qui impose une minute d’attente devant un écran figé ne sera pas utilisé, quelle que soit la qualité de ce qu’il finit par produire.
Décisions
Transcrire pendant la consultation, pas après. La première version
faisait la chose évidente : le navigateur enregistrait, envoyait le fichier
audio au backend à la fin, et la transcription démarrait là. Sur une
consultation de vingt minutes ou plus, l’attente devenait indéfendable — et
elle tombait au pire moment, quand le médecin veut clore et passer au
patient suivant. La correction n’a pas consisté à accélérer la
transcription mais à cesser de la faire à ce moment-là : le navigateur
transcrit désormais en direct, pendant la consultation, en parlant
directement au service de reconnaissance vocale. Quand la consultation
s’arrête, la transcription est déjà là. Pour que le navigateur puisse
s’adresser au service sans détenir de secret, le backend émet à la demande
un jeton temporaire à durée de vie courte : la clé maîtresse ne quitte
jamais le serveur. Cette décision prise pour la latence a réglé au passage
la première contrainte : l’audio ne transite plus par mon infrastructure et
n’y est jamais stocké — l’entité Consultation n’a pas de champ audio, et
le seul fichier que l’API accepte est une image contextuelle optionnelle.
Le compromis est réel : sans enregistrement, pas de re-transcription
ultérieure ni de vérification à la source. Je l’accepte volontiers, parce
qu’un serveur qui accumulerait des heures de consultations enregistrées
serait de loin le plus gros risque du système.
Ne pas transcrire le silence. Une consultation est pleine de moments sans parole : l’examen clinique, la prise de tension, le médecin qui écrit, le patient qui se rhabille. Diffuser ce silence à un service de transcription facturé au temps revient à payer pour du vide. Le client détecte donc l’activité vocale et suspend le flux quand personne ne parle. L’implémentation naïve — un seuil unique sur le volume — produit deux défauts classiques : elle oscille autour du seuil, et elle coupe les débuts et fins de phrase. La version retenue lisse l’énergie du signal par moyenne mobile, utilise deux seuils distincts pour ouvrir et pour fermer, conserve une demi-seconde de son avant la détection de parole, et maintient le flux une seconde après l’arrêt. Autrement dit : on ne coupe qu’à coup sûr, et jamais au milieu d’un mot. L’économie sur la facture de transcription est substantielle, et le texte n’y perd rien.
Séparer l’extraction du raisonnement, et payer le modèle cher seulement où il compte. Le pipeline fait deux appels distincts plutôt qu’un. Le premier, sur un modèle léger et à température zéro, n’extrait que les faits effectivement énoncés — motifs, interrogatoire, données biométriques — dans un schéma JSON strict. Le second, sur un modèle nettement plus fort, ne voit jamais la transcription brute : il ne reçoit que ce JSON de faits, et en tire des hypothèses, une proposition de prise en charge et un suivi. Tout faire en un appel aurait été plus simple. La séparation achète trois choses. D’abord la sûreté : un fait halluciné ne peut pas se transformer silencieusement en argument diagnostique, puisque l’étape de raisonnement ne travaille que sur des faits déjà figés et inspectables. Ensuite le coût : le gros du volume de tokens — une transcription complète — est absorbé par le modèle le moins cher, et le modèle cher ne traite qu’un JSON compact, là où son avantage se voit vraiment. Enfin la traçabilité : les deux JSON intermédiaires sont conservés, ce qui rend chaque compte rendu remontable jusqu’aux faits dont il est issu. Sans cette trace, « je ne sais pas pourquoi le système a écrit ça » serait la seule réponse possible — et sur ce domaine, ce n’est pas une réponse.
Contraindre le modèle à proposer, jamais à conclure. Les instructions imposent des qualificatifs d’incertitude — probable, possible, à discuter, diagnostic différentiel — et interdisent le diagnostic ferme tant que l’incertitude persiste. Les hypothèses sont limitées à trois, chacune assortie d’une probabilité et d’une justification adossée aux faits extraits : le médecin voit sur quoi la machine s’appuie, et peut donc la contredire. Toute proposition médicamenteuse doit porter la dénomination commune, la posologie, la durée, les adaptations en insuffisance rénale et chez le sujet âgé, et les alternatives en cas d’allergie — le tout recoupé avec les allergies et traitements en cours du dossier. Un indicateur distinct signale les signes de gravité. Les sorties sont validées contre leur schéma à la lecture, et toute dérive structurelle est journalisée plutôt qu’absorbée en silence. Le résultat n’est pas un document : c’est un brouillon que le médecin relit, corrige et s’approprie.
Rendre l’attente lisible, et la génération indépendante de la fenêtre. Même transcription réglée, la rédaction du compte rendu prend des dizaines de secondes. Plutôt que de livrer le texte d’un bloc, la dernière étape le diffuse au fil de l’eau : le médecin voit le compte rendu s’écrire, ce qui change complètement la perception du délai. Le coût d’ingénierie dépasse l’effet visuel — les fragments sont regroupés par petits lots avant émission pour ne pas saturer le navigateur, un battement régulier maintient la connexion à travers les proxies, et la persistance en base est amortie tous les cinquante lots au lieu d’écrire à chaque fragment. Surtout, la génération vit côté serveur, pas dans l’onglet : si le médecin ferme la fenêtre, elle continue, et il retrouve le compte rendu terminé en revenant. Le flux est reprenable — une connexion perdue reprend où elle en était — et le démarrage est idempotent, pour qu’une génération déjà en cours ou déjà terminée ne reparte pas et ne soit pas payée deux fois.
Résultat
L’application couvre la chaîne complète : authentification par jeton avec rafraîchissement en cookie httpOnly, cloisonnement strict des données entre médecins — un praticien n’atteint que ses propres patients, et l’isolation est appliquée côté serveur à partir du contexte de sécurité, jamais d’un identifiant fourni par le client —, dossier patient, génération de comptes rendus, et abonnement Stripe à trois formules avec webhooks idempotents et réconciliation horaire. Dix-sept migrations de schéma, environ deux cents classes et composants, sur neuf mois de développement.
Surtout, elle est passée par une bêta couvrant environ un millier de patients — et c’est de là que viennent les trois décisions les plus importantes de la liste ci-dessus. Aucune n’était dans la conception initiale : la latence de transcription, le coût du silence et l’abandon quand la fenêtre se ferme ne sont apparus qu’au contact d’usages réels.
Cette bêta a été menée sur dossiers non nominatifs : aucun élément identifiant réel n’a été saisi, et un compte rendu généré ne permet pas de remonter à un patient identifié. C’était la condition pour éprouver le pipeline en conditions réelles sans constituer, pour un test, un patrimoine de données de santé nominatives — le même raisonnement que pour l’audio, appliqué au protocole plutôt qu’au code.
Ce que le projet m’a appris tient dans sa contrainte centrale : sur un domaine où l’erreur coûte cher, la question utile n’est pas « jusqu’où le modèle peut-il aller ? » mais « qu’est-ce qu’on refuse de lui déléguer ? ». Ici, trois choses : la garde de l’audio, la conclusion diagnostique et la signature. Le reste de l’architecture découle de ces trois refus — et de mille consultations qui ont montré où la première version se trompait.