Second Brain documentation

ADR 0094 – Service de transcription vocale (dictée souveraine)

Accepté (2026-10-17, dans le cadre de l'ADR 0086 §3 validé par Aurélien)

← Toutes les décisions

ADR 0094 – Service de transcription vocale (dictée souveraine)

Statut : Accepté (2026-10-17, dans le cadre de l’ADR 0086 §3 validé par Aurélien)

Ligne C1 du fil C du backlog du jalon 4. Le bouton « Dicter » (C2) et son intégration dans la capture (C4) viennent après ; cet ADR ne fixe que le service et l’API.

Décision

  • Service compose transcription : image fedirz/faster-whisper-server (variante 0.5.0-cpu, surchargeable par SB_TRANSCRIPTION_IMAGE), modèle Systran/faster-whisper-small (SB_TRANSCRIPTION_MODEL), int8 sur CPU. Profil compose voice : absent des lancements habituels, activé par COMPOSE_PROFILES=voice (ou --profile voice). Aucun port publié (expose: 8000) : seul le backend l’appelle, sur le réseau du compose. Le modèle (environ 500 Mo) se télécharge à la première dictée et reste dans le volume transcription-cache. Présent dans docker-compose.yml, docker-compose.prod.yml et deploy/dokploy/docker-compose.yml.
  • Réglages : app.transcription.url ← SB_TRANSCRIPTION_URL (vide ou absent = fonction inactive), timeout (60 s, SB_TRANSCRIPTION_TIMEOUT), max-size (10 Mo), model, language (fr). Le backend dit où trouver le service ; rien d’autre n’est requis.
  • POST /api/capture/transcribe : multipart, partie file, formats webm, ogg, mp4 (type annoncé, paramètres ;codecs= admis, puis contrôle de la signature du contenu : EBML, OggS, boîte ftyp). Réponse { text }. Le serveur relaie à {url}/v1/audio/transcriptions (API OpenAI, response_format=json, language=fr) ; l’audio n’est ni stocké ni journalisé.
    • 503 si inactif ; 400 fichier absent ou vide ; 413 au-delà de 10 Mo ; 415 format ou signature refusés ; 502 service en échec ou réponse inattendue ; 504 délai dépassé. Erreurs au format ProblemDetail, comme le reste de l’API.
  • GET /api/capture/capabilities → { transcription: boolean }, extensible : le front y lit s’il affiche le micro.
  • Durée : les deux minutes ne sont pas mesurées côté serveur (décoder l’audio demanderait une dépendance de plus). Elles sont bornées par la taille (10 Mo couvrent largement deux minutes d’opus à débit de voix), par le délai du relais, et par l’arrêt automatique de l’enregistreur côté navigateur (C2).
  • Code dans un paquet à part, capture/transcription/, sans toucher CaptureService, CaptureParser ni CaptureDtos (fil B) ; CaptureController est laissé intact, les deux contrôleurs partagent le préfixe /api/capture.

Conséquences

  • Sans service ni variable, l’application se comporte comme avant ; capabilities.transcription vaut false.
  • Tests : unitaires (formats, relais, erreurs) et d’intégration contre un faux service HTTP du JDK (com.sun.net.httpserver), sans nouvelle dépendance de test.
  • Coût d’exploitation : environ 1 Go de mémoire et un cœur CPU pendant une dictée ; le service décharge le modèle après inactivité.
  • À vérifier au premier déploiement : le nom de l’image et le tag (le projet amont a été renommé speaches), et les variables WHISPER__*. Le contrat reste l’API OpenAI : changer d’image (speaches, whisper.cpp serveur) ne touche pas le backend.

Alternatives écartées

  • API de transcription en ligne (OpenAI, Google) : l’audio quitterait le serveur (ADR 0086).
  • Transcription dans la JVM (whisper.cpp via JNI) : dépendance native lourde et fragile ; un service séparé est remplaçable.
  • Mesurer la durée côté serveur : voir plus haut.