Architecture
Une API hexagonale en Python, deux clients, et un worker qui partage le même code métier.
Vue d'ensemble#
Tout tient dans un docker compose. Les requêtes HTTP restent
courtes ; le travail lourd part dans une file RabbitMQ et un worker dédié s'en
charge, sans bloquer l'interface.
L'API interroge aussi Qdrant et le modèle de langage pour répondre aux questions de l'assistant ; le worker, lui, écrit transcriptions et résumés dans le dossier médias et les statuts en base.
L'API : ports et adaptateurs#
Le paquet e_learning est découpé en quatre couches. Les
dépendances vont uniquement vers l'intérieur, et c'est vérifié :
import-linter échoue si une couche importe une couche plus externe,
ou si le domaine importe FastAPI, SQLAlchemy ou Pydantic.
L'API, la CLI et le worker sont trois adaptateurs primaires sur les mêmes cas d'usage : lancer une transcription depuis le navigateur ou depuis la ligne de commande exécute le même code.
Bounded contexts#
| Contexte | Contenu |
|---|---|
user | Compte : email normalisé, empreinte argon2 du mot de passe, is_admin, is_active ; UserId UUIDv7 |
catalog | Formation, Chapter, Video, Document, Job ; position en base, slugs disque stables |
learning | Note et Progress, rattachées à un utilisateur et à une vidéo |
content | Transcription, résumé, conversion, indexation et questions RAG |
usage | Journal des tokens consommés par les appels au modèle de langage, par utilisateur |
Authentification#
- Ports applicatifs (
application/user/ports.py) :PasswordHasher,TokenService,LoginThrottle. Le domaine ne voit que l'empreinte du mot de passe, jamais le mot de passe ni l'algorithme. - Adaptateurs (
infrastructure/security/) : argon2 viapwdlib(calcul dans un thread, hors boucle d'événements), JWT HS256 viapyjwt(sub,iat,exp), compteur d'échecs en mémoire du processus. - Garde par routeur : chaque routeur FastAPI déclare
dependencies=[Depends(get_current_user)]ou[Depends(require_admin)]. Toutes les écritures du catalogue sont regroupées dansstudio_router. Une nouvelle route est donc protégée par défaut. - Relecture en base :
get_current_userdécode le jeton puis relit le compte, une requête SQL par appel. Désactivation et retrait du rôle s'appliquent sans attendre l'expiration du jeton. - Garde-fou :
tests/integration/api/test_access_matrix.pyliste chaque route avec son rôle et échoue si une route n'est pas classée ou pas gardée.
Cycle de vie d'un job#
- La requête HTTP crée un
Joben base (queued) et passe le statut métier de la vidéo àprocessing. - Après le commit, le message est publié sur l'exchange
elearning_jobs(type direct, durable). Publier avant le commit exposerait le worker à un job qui n'existe pas encore. - L'API répond
202avec la vidéo et sesactive_jobs. - Le worker consomme (
APP_WORKER_PREFETCHen parallèle), passe le job enrunninget met à jourprogressetmessage. - À la fin :
succeededoufailed, et le statut métier (transcription_status,summary_status,processing_status) passe àreadyoufailed. - Transcription et résumé réussis enchaînent un job
rag_index_video.
Les clients n'ont pas de canal temps réel : ils interrogent la formation
toutes les 3 secondes tant qu'un statut vaut processing. Les jobs
terminés disparaissent de active_jobs ; les colonnes de statut
restent la vérité métier.
| Type de job | Déclenché par |
|---|---|
media_conversion | Téléversement d'un fichier autre que MP4 / MP3, ou POST /videos/{id}/conversion |
transcription | POST /videos/{id}/transcription |
summary | POST /videos/{id}/summary/generate |
rag_index_video | Fin d'une transcription ou d'un résumé |
rag_index_formation | POST /formations/{id}/index (bouton Réindexer) |
Disque et base#
- Chaque média et document a un
relative_pathunique : c'est la clé de réconciliation entre le dossier et PostgreSQL. - Les identifiants sont des UUIDv7 (module
uuidde Python 3.14), ordonnés dans le temps, indépendants du chemin. - Renommer une formation ou un chapitre réécrit les chemins relatifs de son
contenu ; réordonner ne touche que
position. - Transcription et résumé sont des fichiers voisins du média
(
.txt,.md), pas des colonnes. - Le stream (
GET /videos/{id}/stream) gère les requêtesRange, nécessaires au déplacement dans la vidéo.
Le front#
| Route | Page |
|---|---|
/auth | Connexion ; seule page publique |
/ | Catalogue |
/formation/[formationId] | Détail, assistant, chapitres |
/player/[videoId] | Lecteur, résumé, notes, documents |
/studio | Liste des formations à éditer |
/studio/formation/new | Création |
/studio/formation/[id] | Éditeur de formation |
/studio/users | Gestion des comptes |
/usage | Consommation IA du compte |
Next.js 16 en App Router, mais toutes les pages sont des composants client :
pas de rendu serveur en pratique. L'état vit dans des stores Zustand
(auth, catalog, studio, player,
theme) ; les appels passent par un client Axios unique
(src/services/api.ts) qui envoie le cookie de session
(withCredentials) et l'en-tête anti-CSRF X-Requested-With.
SessionGate, dans le layout racine, redirige vers /auth sans
session ; app/studio/layout.tsx renvoie un apprenant au catalogue.
Un 401 efface la session, un 403 affiche « accès refusé » sans
déconnecter. L'interface est entièrement en MUI 9 ; le lecteur vidéo est video.js et
l'éditeur Markdown @uiw/react-md-editor. Le build Docker produit
une sortie standalone, exécutée par un utilisateur non root.
Cladèse