feat(corrections/eo): évaluation phonologique Gemini — 5 critères × /4 (Sprint 4.8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
34b4bcdd82
commit
ec0598d122
15 changed files with 2086 additions and 290 deletions
|
|
@ -6,6 +6,27 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/).
|
|||
|
||||
---
|
||||
|
||||
## [Unreleased] — 2026-04-26 — Sprint 4.8 — Phonologie EO
|
||||
|
||||
### Added
|
||||
|
||||
- `src/lib/geminiPhonology.ts` — évaluation phonologique via Gemini 2.5 Flash (audio brut, JSON strict, timeout 45s + 1 retry). `PHONOLOGY_STUB` pour Mode A (transcript sans audio).
|
||||
- `src/lib/__tests__/geminiPhonology.test.ts` — 9 tests (parse, cap 0..4, retry, erreurs HTTP).
|
||||
- `src/controllers/__tests__/correctionEoPhonology.test.ts` — 4 tests (injection 5e critère, stub Mode A, fallback Gemini down, score max 20).
|
||||
|
||||
### Changed
|
||||
|
||||
- `POST /corrections/eo` — passe de 4 critères × /5 à 5 critères × /4 (score total /20 inchangé). Phonologie évaluée par Gemini en parallèle de la transcription (Mode B audio). Fallback stub si Gemini phonologie échoue.
|
||||
- `src/lib/deepseek.ts` — prompt EO : cap critère /5 → /4, libellés officiels TCF Canada, mention phonologie évaluée séparément. Cap EE inchangé /5.
|
||||
- TD-08 partiellement résolu (T1/T3). T2 Live reste à 0 (Sprint 6).
|
||||
|
||||
### Notes
|
||||
|
||||
- ⚠️ Breaking change frontend : criteres.length passe de 4 à 5, échelle /5 → /4.
|
||||
- Tests : 248 → 261 verts (+13).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased] — 2026-04-25 — Sprint 4a/4b — Backend EO
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
# Changelog — Expria Backend
|
||||
|
||||
Toutes les modifications notables du backend sont documentées dans ce fichier.
|
||||
|
||||
Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased] — 2026-04-22 — Sprint 3.6a — Qualité correction Backend
|
||||
|
||||
### Added
|
||||
- Nouveaux prompts DeepSeek spécifiés dans `docs/Prompt_maître.md` et `docs/Prompt_production_modèle.md` — builders dynamiques `buildCorrectionPrompt`, `buildModelPrompt`, `buildExercicesPrompt` dans `src/lib/deepseek.ts`.
|
||||
- `expria-frontend/docs/TAXONOMIE_ERREURS.md` — 63 codes d'erreurs TCF Canada sur 4 critères + 4 codes « autre ». Validation runtime via `src/lib/taxonomieErreurs.ts` (`isValidCode`, `isValidCritere`, `buildTaxonomyPromptSection`). Codes invalides retournés par DeepSeek sont filtrés ; le code `autre` sans description est rejeté.
|
||||
- Génération parallèle correction + modèle — option (b) : `generateProductionModele` démarre en même temps que `correctEE` avec `nclcObtenu = nclcCible - 1` comme estimation provisoire, `await` uniquement sur la correction pour répondre à la requête HTTP.
|
||||
- Exercices personnalisés fire-and-forget déclenchés après la résolution de la correction (dépendent de `rapport.erreurs_codes` et `rapport.criteres`). Format aligné sur les captures d'écran : `{difficulte, theme, diagnostic, consigne, extrait, indice, correction, explication}`.
|
||||
- Nouveaux champs dans `productions` : `revelation` (JSONB), `diagnostic` (TEXT), `conseil_nclc` (JSONB), `erreurs_codes` (JSONB), `exercices` (JSONB), `modele` (JSONB), `nclc_cible` (INTEGER), `exercices_status` / `modele_status` (TEXT, 'pending'/'ready'/'error').
|
||||
- Migration SQL `supabase/migrations/004_sprint_3_6a_qualite_correction.sql` — première migration versionnée du projet (cf. backend TD-06) ; idempotente grâce à `IF NOT EXISTS`.
|
||||
- Paramètre `nclc_cible` optionnel sur `POST /corrections/ee` (défaut 9, valeurs acceptées : 9 ou 10 ; sinon 400 VALIDATION_ERROR).
|
||||
- Index GIN sur `erreurs_codes` pour préparer l'agrégation du Sprint 3.6c (analyse patterns).
|
||||
- Nouveau fichier de tests `src/controllers/__tests__/correctionController.test.ts` — 8 tests (parallélisme option b, statuts ready/error, nclc_cible propagé, simulation introuvable, autre utilisateur).
|
||||
- 2 tests ajoutés à `simulationController.test.ts` — `getById` renvoie `nclc_cible`, `exercices`, `modele` + statuts.
|
||||
- Logs d'erreur détaillés : `callDeepSeek` classifie TIMEOUT / ABORT / JSON_PARSE / NETWORK / OTHER ; `correctionController.correctEE` logue `{simulationId, tache, nclcCible, message, stack}` avant de retourner 500.
|
||||
- FTD-23 🟡 ajoutée dans `expria-frontend/docs/TECH_DEBT.md` — `useAutosave` peut fire un PATCH `/simulations/:id/contenu` après correction, ce qui retourne 400 VALIDATION_ERROR. À corriger dans une session dédiée (préexistant au Sprint 3.6a, détecté lors des tests manuels).
|
||||
|
||||
### Changed
|
||||
- `correctEE` dans `src/lib/deepseek.ts` — nouvelle signature `correctEE(CorrectionInput)` (contenu, tache, sujet, sourceDoc1/2, nclcCible) et nouvelle forme de retour `CorrectionRapport` (revelation, diagnostic, criteres avec exemple/suggestion/astuce, conseil_nclc, erreurs_codes). `EERapport` devient alias de `CorrectionRapport`. EO inchangé.
|
||||
- `correctionController.correctEE` — charge le sujet + documents T3 depuis Supabase pour alimenter le prompt maître ; persiste les nouveaux champs (revelation, diagnostic, conseil_nclc, erreurs_codes, nclc_cible) + statuts pending initiaux ; lance `runModeleJob` en parallèle (option b) et `runExercicesJob` après correction.
|
||||
- `simulationController.getById` — retourne désormais `nclc_cible`, `exercices`, `exercices_status`, `modele`, `modele_status` en plus du `rapport` enrichi ; fallback `'pending'` si les colonnes sont absentes (compat avec productions pré-migration).
|
||||
- Timeout DeepSeek côté backend : `callDeepSeek` abort à **55 s** via `AbortSignal.timeout(55_000)` (avant : aucun timeout) ; timeout frontend corrections monte de **30 s à 60 s** — marge de 5 s entre abort backend et abort client.
|
||||
- Routes `/simulations/*` : réorganisation défensive — les `PATCH /:id/contenu` et `PATCH /:id/sujet` sont déclarées avant `GET /:id` pour éviter tout risque de masquage.
|
||||
- `deepseek.test.ts` réécrit (25 tests) — couvre correctEE nouvelle signature, generateProductionModele, generateExercices, helpers post-traitement, EO inchangé.
|
||||
|
||||
### Notes
|
||||
- **Option A retenue** pour la compatibilité frontend : backend renvoie uniquement la nouvelle forme. Le Sprint 3.6b (frontend) est immédiatement suivant et corrige l'écran blanc sur `RapportPage`.
|
||||
- **Option (b) retenue** pour le parallélisme : modèle en parallèle avec correction (nclcObtenu estimé), exercices strictement après correction.
|
||||
- Migration SQL à exécuter manuellement via `supabase db push` ou SQL Editor du dashboard (cf. Règle F) — aucune exécution automatique.
|
||||
- Tests : **174 tests verts** (+19 vs baseline 155), 18 fichiers de tests.
|
||||
- TD-15 🟡 ouvert : si le process redémarre pendant un job fire-and-forget (modèle/exercices), le statut reste `pending` indéfiniment. À traiter après observation en production.
|
||||
697
docs/IMPLEMENTATION_T2_LIVE.md
Normal file
697
docs/IMPLEMENTATION_T2_LIVE.md
Normal file
|
|
@ -0,0 +1,697 @@
|
|||
# IMPLEMENTATION_T2_LIVE.md — Algorithme d'implémentation T2 EO Live
|
||||
|
||||
> **Document de référence technique — Sprint 6**
|
||||
> Basé exclusivement sur la documentation officielle Google Gemini Live API.
|
||||
> Sources : ai.google.dev/gemini-api/docs/live-api (get-started-websocket, capabilities, session-management, ephemeral-tokens, best-practices, rate-limits, pricing)
|
||||
> Date de vérification : 2026-04-26
|
||||
|
||||
---
|
||||
|
||||
## 1. Spécifications officielles vérifiées
|
||||
|
||||
### 1.1 Modèle
|
||||
|
||||
| Paramètre | Valeur | Source |
|
||||
|---|---|---|
|
||||
| Modèle cible | `gemini-2.5-flash-native-audio` ou `gemini-2.5-flash-native-audio-preview-12-2025` | ai.google.dev/gemini-api/docs/models |
|
||||
| Accès Hermann | Confirmé | Session 2026-04-26 |
|
||||
|
||||
### 1.2 Audio — formats officiels
|
||||
|
||||
| Direction | Format | Sample rate | Encoding | MIME type |
|
||||
|---|---|---|---|---|
|
||||
| Client → Gemini | PCM brut | 16 kHz | 16 bits, little-endian, mono | `audio/pcm;rate=16000` |
|
||||
| Gemini → Client | PCM brut | 24 kHz | 16 bits, little-endian, mono | `audio/pcm;rate=24000` |
|
||||
|
||||
Source : « Audio data in the Live API is always raw, little-endian, 16-bit PCM. Audio output always uses a sample rate of 24kHz. Input audio is natively 16kHz, but the Live API will resample if needed so any sample rate can be sent. » — ai.google.dev/gemini-api/docs/live-guide
|
||||
|
||||
### 1.3 Endpoint WebSocket
|
||||
|
||||
```
|
||||
wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key={API_KEY}
|
||||
```
|
||||
|
||||
Source : ai.google.dev/gemini-api/docs/live-api/get-started-websocket
|
||||
|
||||
### 1.4 Limites de session
|
||||
|
||||
| Paramètre | Valeur | Source |
|
||||
|---|---|---|
|
||||
| Durée max session audio-only | **15 minutes** | ai.google.dev/gemini-api/docs/live-guide §Limitations |
|
||||
| Context window | 128k tokens (native audio) | idem |
|
||||
| Session state | Stateful dans une session, pas de mémoire inter-session | idem |
|
||||
|
||||
### 1.5 Fonctionnalités natives utilisées
|
||||
|
||||
| Fonctionnalité | Activation | Utilité Expria |
|
||||
|---|---|---|
|
||||
| Voice Activity Detection (VAD) | Automatique, configuré : `endOfSpeechSensitivity: LOW`, `silenceDurationMs: 2000` | Détecte quand le candidat parle/s'arrête. 2s de silence avant que l'IA réponde — laisse le temps de réfléchir |
|
||||
| Barge-in (interruption) | Natif, non configurable | L'utilisateur peut interrompre l'IA naturellement |
|
||||
| Input transcription | `inputAudioTranscription: {}` dans config | Transcript de ce que dit le candidat |
|
||||
| Output transcription | `outputAudioTranscription: {}` dans config | Transcript de ce que dit l'IA |
|
||||
| Affective dialog | `enableAffectiveDialog: true` (v1alpha) | Optionnel — ton naturel |
|
||||
|
||||
### 1.6 Configuration VAD — justification
|
||||
|
||||
| Paramètre | Valeur retenue | Justification |
|
||||
|---|---|---|
|
||||
| `disabled` | `false` | VAD automatique côté Gemini — le frontend n'a pas à gérer la détection de parole |
|
||||
| `startOfSpeechSensitivity` | `START_SENSITIVITY_LOW` | Évite les faux positifs (bruits ambiants, respiration) |
|
||||
| `endOfSpeechSensitivity` | `END_SENSITIVITY_LOW` | Tolère les pauses de réflexion du candidat sans couper la parole |
|
||||
| `silenceDurationMs` | `2000` | 2 secondes de silence avant que l'IA considère que le candidat a fini. À ajuster entre 1500-3000ms après tests manuels |
|
||||
|
||||
**Fallback si le VAD automatique ne convient pas :**
|
||||
Désactiver le VAD (`disabled: true`) et basculer sur un mode "talkie-walkie" :
|
||||
le frontend envoie `activityStart` quand le candidat appuie sur un bouton "Parler",
|
||||
et `activityEnd` quand il relâche. Moins naturel mais fiable à 100%.
|
||||
|
||||
### 1.6 Tarification
|
||||
|
||||
| Tier | Audio input | Audio output | Source |
|
||||
|---|---|---|---|
|
||||
| Free | Disponible avec rate limits | idem | ai.google.dev/gemini-api/docs/pricing |
|
||||
| Paid (Tier 1+) | Inclus dans le token count | idem | idem |
|
||||
|
||||
Note : le pricing Live API est basé sur le token count, pas sur la durée. Les tokens audio sont comptés différemment des tokens texte. Vérifier les rate limits réels dans AI Studio pour le projet Expria.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture — vue d'ensemble
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ Navigateur candidat │
|
||||
│ (React + AudioWorklet) │
|
||||
│ PCM 16kHz → base64 │
|
||||
│ base64 → PCM 24kHz │
|
||||
└──────────┬──────────────┘
|
||||
│ WebSocket (wss://api.expria.app/t2/live?token=jwt&sujet=uuid)
|
||||
│
|
||||
┌──────────▼──────────────┐
|
||||
│ Backend Expria │
|
||||
│ (Hono / Node.js) │
|
||||
│ Render Frankfurt │
|
||||
│ │
|
||||
│ 1. Auth JWT + plan │
|
||||
│ 2. Fetch sujet │
|
||||
│ 3. Build prompt │
|
||||
│ 4. Proxy bidirectionnel│
|
||||
│ 5. Accumule transcript │
|
||||
│ 6. Évaluation finale │
|
||||
│ 7. Sauvegarde BDD │
|
||||
└──────────┬──────────────┘
|
||||
│ WebSocket (wss://generativelanguage.googleapis.com/ws/...)
|
||||
│
|
||||
┌──────────▼──────────────┐
|
||||
│ Gemini Live API │
|
||||
│ gemini-2.5-flash- │
|
||||
│ native-audio │
|
||||
│ Google Cloud │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
### Pourquoi un proxy backend (et pas client-to-server direct)
|
||||
|
||||
Google recommande officiellement les tokens éphémères pour les apps client-to-server. Cependant, pour Expria :
|
||||
|
||||
1. La clé API (`GEMINI_API_KEY`) ne doit jamais être exposée côté frontend (règle absolue SECURITY.md)
|
||||
2. Le backend doit accumuler le transcript pour l'évaluation finale
|
||||
3. Le backend doit sauvegarder la production en base après la session
|
||||
4. Le gating plan Premium doit être vérifié côté serveur
|
||||
|
||||
Le proxy backend est la seule architecture viable.
|
||||
|
||||
---
|
||||
|
||||
## 3. Algorithme d'exécution — Backend
|
||||
|
||||
### Phase 1 : Connexion (< 2 secondes)
|
||||
|
||||
```
|
||||
ENTRÉE : WebSocket client avec ?token=jwt&sujet=uuid
|
||||
|
||||
1. EXTRAIRE jwt et sujet_id des query params
|
||||
2. VÉRIFIER jwt via Supabase → obtenir profile
|
||||
├─ INVALIDE → close(4001, "AUTH_REQUIRED")
|
||||
└─ VALIDE → continuer
|
||||
3. VÉRIFIER hasAccess(profile.plan, 'oral_t2_live')
|
||||
├─ INSUFFISANT → close(4003, "PLAN_INSUFFICIENT")
|
||||
└─ OK → continuer
|
||||
4. FETCH sujet FROM sujets WHERE id = sujet_id AND mode = 'EO' AND tache = 2
|
||||
├─ NOT FOUND → close(4004, "SUJET_NOT_FOUND")
|
||||
└─ TROUVÉ → extraire consigne, contexte, role
|
||||
5. CONSTRUIRE systemPrompt à partir du template Prompt_t2live.md §3
|
||||
avec substitution {role} et {contexte}
|
||||
6. OUVRIR WebSocket vers Gemini Live API :
|
||||
URL = wss://generativelanguage.googleapis.com/ws/
|
||||
google.ai.generativelanguage.v1beta.
|
||||
GenerativeService.BidiGenerateContent?key={GEMINI_API_KEY}
|
||||
7. ENVOYER setup frame :
|
||||
{
|
||||
"config": {
|
||||
"model": "models/gemini-2.5-flash-native-audio",
|
||||
"responseModalities": ["AUDIO"],
|
||||
"systemInstruction": {
|
||||
"parts": [{ "text": systemPrompt }]
|
||||
},
|
||||
"inputAudioTranscription": {},
|
||||
"outputAudioTranscription": {},
|
||||
"speechConfig": {
|
||||
"voiceConfig": {
|
||||
"prebuiltVoiceConfig": { "voiceName": "Kore" }
|
||||
}
|
||||
},
|
||||
"realtimeInputConfig": {
|
||||
"automaticActivityDetection": {
|
||||
"disabled": false,
|
||||
"startOfSpeechSensitivity": "START_SENSITIVITY_LOW",
|
||||
"endOfSpeechSensitivity": "END_SENSITIVITY_LOW",
|
||||
"silenceDurationMs": 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// VAD : END_SENSITIVITY_LOW + 2s de silence avant que l'IA réponde
|
||||
// → le candidat peut réfléchir entre ses phrases sans être interrompu
|
||||
// À ajuster entre 1500-3000ms après tests manuels
|
||||
// Si le VAD automatique ne convient pas : option fallback VAD manuel
|
||||
// disabled: true + activityStart/activityEnd côté client (mode talkie-walkie)
|
||||
8. INITIALISER accumulateurs :
|
||||
- inputTranscript = [] // ce que dit le candidat
|
||||
- outputTranscript = [] // ce que dit l'IA
|
||||
- sessionStartTime = Date.now()
|
||||
```
|
||||
|
||||
### Phase 2 : Proxy bidirectionnel (durée libre, max 15 min)
|
||||
|
||||
```
|
||||
BOUCLE PARALLÈLE :
|
||||
|
||||
THREAD A — Client → Gemini :
|
||||
POUR CHAQUE message reçu du client :
|
||||
SI message.type === 'audio' :
|
||||
TRANSMETTRE à Gemini : {
|
||||
"realtimeInput": {
|
||||
"audio": {
|
||||
"data": message.data, // base64 PCM 16kHz
|
||||
"mimeType": "audio/pcm;rate=16000"
|
||||
}
|
||||
}
|
||||
}
|
||||
SI message.type === 'end' :
|
||||
DÉCLENCHER Phase 3
|
||||
|
||||
THREAD B — Gemini → Client :
|
||||
POUR CHAQUE message reçu de Gemini :
|
||||
SI message.serverContent.modelTurn?.parts :
|
||||
POUR CHAQUE part :
|
||||
SI part.inlineData :
|
||||
TRANSMETTRE au client : {
|
||||
type: 'audio',
|
||||
data: part.inlineData.data, // base64 PCM 24kHz
|
||||
mimeType: part.inlineData.mimeType
|
||||
}
|
||||
|
||||
SI message.serverContent.inputTranscription :
|
||||
ACCUMULER dans inputTranscript[]
|
||||
|
||||
SI message.serverContent.outputTranscription :
|
||||
ACCUMULER dans outputTranscript[]
|
||||
|
||||
SI message.serverContent.interrupted :
|
||||
TRANSMETTRE au client : { type: 'interrupted' }
|
||||
|
||||
SI message.serverContent.turnComplete :
|
||||
TRANSMETTRE au client : { type: 'turnComplete' }
|
||||
|
||||
GUARD — Timeout 15 min :
|
||||
SI Date.now() - sessionStartTime > 14 * 60 * 1000 :
|
||||
TRANSMETTRE au client : { type: 'warning', message: '1 minute restante' }
|
||||
SI Date.now() - sessionStartTime > 15 * 60 * 1000 :
|
||||
DÉCLENCHER Phase 3
|
||||
```
|
||||
|
||||
### Phase 3 : Fin de session + évaluation (< 30 secondes)
|
||||
|
||||
```
|
||||
1. FERMER WebSocket Gemini (close 1000)
|
||||
2. RECONSTRUIRE le transcript complet :
|
||||
fullTranscript = inputTranscript.map(t => "Candidat : " + t.text)
|
||||
.interleave(outputTranscript.map(t => "Examinateur : " + t.text))
|
||||
3. CRÉER production en base :
|
||||
INSERT INTO productions (
|
||||
user_id, tache, mode, contenu, created_at
|
||||
) VALUES (
|
||||
profile.id, 'EO_T2_LIVE', 'entrainement', fullTranscript, NOW()
|
||||
)
|
||||
→ obtenir production.id
|
||||
4. ENVOYER le transcript au pipeline de correction EO existant :
|
||||
correctionResult = await correctEO({
|
||||
transcript: fullTranscript,
|
||||
tache: 'EO_T2',
|
||||
nclcCible: profile.nclc_cible || 9,
|
||||
productionId: production.id
|
||||
})
|
||||
// Réutilise le prompt de correction EO + DeepSeek
|
||||
// Note : phonologie = 0 (TD-08, pas d'audio brut disponible)
|
||||
5. METTRE À JOUR la production :
|
||||
UPDATE productions SET rapport = correctionResult.rapport,
|
||||
score = correctionResult.score,
|
||||
nclc = correctionResult.nclc
|
||||
WHERE id = production.id
|
||||
6. TRANSMETTRE au client :
|
||||
{ type: 'report', data: correctionResult }
|
||||
7. FERMER WebSocket client (close 1000)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Algorithme d'exécution — Frontend
|
||||
|
||||
### Phase 1 : Initialisation
|
||||
|
||||
```
|
||||
1. PAGE DE SÉLECTION SUJET :
|
||||
- Fetch GET /sujets?mode=EO&tache=2 → liste sujets
|
||||
- Afficher grille de sujets (réutiliser SujetsEOPage)
|
||||
- Clic sujet → stocker sujet.id + sujet.consigne
|
||||
|
||||
2. PAGE DE PRÉPARATION :
|
||||
- Afficher consigne + contexte du sujet
|
||||
- Explication : "Vous êtes le candidat. C'est à vous de prendre la parole
|
||||
en premier pour initier la conversation, comme à l'examen réel."
|
||||
- Bouton "Démarrer le dialogue"
|
||||
- Demander permission micro (navigator.mediaDevices.getUserMedia)
|
||||
```
|
||||
|
||||
### Phase 2 : Connexion audio + WebSocket
|
||||
|
||||
```
|
||||
AU CLIC "Démarrer" :
|
||||
|
||||
1. OUVRIR WebSocket :
|
||||
ws = new WebSocket(`wss://api.expria.app/t2/live?token=${jwt}&sujet=${sujetId}`)
|
||||
|
||||
2. STATE MACHINE → 'connecting'
|
||||
|
||||
3. INITIALISER AudioContext capture (16kHz) :
|
||||
captureCtx = new AudioContext({ sampleRate: 16000 })
|
||||
// Si le navigateur ne supporte pas 16kHz nativement,
|
||||
// créer à sampleRate par défaut et rééchantillonner dans le worklet
|
||||
|
||||
4. CHARGER AudioWorklet :
|
||||
await captureCtx.audioWorklet.addModule('pcm-capture-processor.js')
|
||||
// Le worklet :
|
||||
// - Reçoit des Float32 du micro
|
||||
// - Rééchantillonne à 16kHz si nécessaire
|
||||
// - Convertit Float32 → Int16 PCM little-endian
|
||||
// - Envoie les chunks via port.postMessage
|
||||
|
||||
5. CONNECTER le micro :
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true }
|
||||
})
|
||||
source = captureCtx.createMediaStreamSource(stream)
|
||||
workletNode = new AudioWorkletNode(captureCtx, 'pcm-capture-processor')
|
||||
source.connect(workletNode)
|
||||
|
||||
6. INITIALISER AudioContext playback (24kHz) :
|
||||
playbackCtx = new AudioContext({ sampleRate: 24000 })
|
||||
// File d'attente de buffers audio pour lecture séquentielle
|
||||
|
||||
7. ÉCOUTER les chunks du worklet :
|
||||
workletNode.port.onmessage = (e) => {
|
||||
const pcmBase64 = arrayBufferToBase64(e.data)
|
||||
ws.send(JSON.stringify({ type: 'audio', data: pcmBase64 }))
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3 : Dialogue en temps réel
|
||||
|
||||
```
|
||||
STATE MACHINE :
|
||||
'connecting' → ws.onopen → 'ready' (candidat peut parler)
|
||||
'ready' → audio candidat détecté → 'speaking'
|
||||
'speaking' → silence détecté (VAD) → 'listening' (IA répond)
|
||||
'listening' → audio candidat détecté → 'speaking'
|
||||
'speaking' ↔ 'listening' (boucle dialogue)
|
||||
'*' → bouton "Terminer" → 'processing'
|
||||
'processing' → rapport reçu → 'ended'
|
||||
'*' → erreur WS → 'error'
|
||||
|
||||
// Le candidat initie la conversation (Option A — conforme à l'examen réel).
|
||||
// L'IA attend en silence que le candidat prenne la parole.
|
||||
// Gemini VAD (silenceDurationMs: 2000) gère la détection automatiquement.
|
||||
|
||||
RÉCEPTION messages WebSocket :
|
||||
POUR CHAQUE message reçu :
|
||||
SI message.type === 'audio' :
|
||||
DÉCODER base64 → Int16 PCM
|
||||
CRÉER AudioBuffer (24kHz, mono)
|
||||
AJOUTER à la file de lecture
|
||||
SI pas déjà en lecture → DÉMARRER lecture
|
||||
|
||||
SI message.type === 'turnComplete' :
|
||||
STATE → 'ready' (le candidat peut reprendre la parole)
|
||||
|
||||
SI message.type === 'interrupted' :
|
||||
ARRÊTER lecture audio en cours
|
||||
VIDER file de lecture
|
||||
|
||||
SI message.type === 'report' :
|
||||
STATE → 'ended'
|
||||
NAVIGUER vers /rapport/:productionId
|
||||
|
||||
SI message.type === 'warning' :
|
||||
AFFICHER notification "1 minute restante"
|
||||
|
||||
SI message.type === 'error' :
|
||||
STATE → 'error'
|
||||
AFFICHER message + bouton "Réessayer"
|
||||
|
||||
ENVOI fin de dialogue :
|
||||
AU CLIC "Terminer" :
|
||||
ws.send(JSON.stringify({ type: 'end' }))
|
||||
STATE → 'processing'
|
||||
ARRÊTER capture micro
|
||||
AFFICHER spinner "Évaluation en cours..."
|
||||
```
|
||||
|
||||
### Phase 4 : Cleanup
|
||||
|
||||
```
|
||||
À LA FERMETURE (fin normale, erreur, ou navigation) :
|
||||
1. FERMER WebSocket si ouvert
|
||||
2. ARRÊTER MediaStream (stream.getTracks().forEach(t => t.stop()))
|
||||
3. FERMER captureCtx (captureCtx.close())
|
||||
4. FERMER playbackCtx (playbackCtx.close())
|
||||
5. ANNULER tout rAF ou timer en cours
|
||||
6. SI fin normale (state === 'ended') :
|
||||
- Conserver recordingChunks pour le bouton "Télécharger"
|
||||
SINON :
|
||||
- Libérer recordingChunks (= [])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. AudioWorklet — pcm-capture-processor.js
|
||||
|
||||
```javascript
|
||||
// Exécuté dans un thread séparé (Audio Worklet Thread)
|
||||
class PcmCaptureProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super()
|
||||
this.buffer = new Float32Array(0)
|
||||
// Chunk size : 4096 samples à 16kHz = 256ms de latence
|
||||
// Compromis entre latence et overhead réseau
|
||||
this.chunkSize = 4096
|
||||
}
|
||||
|
||||
process(inputs) {
|
||||
const input = inputs[0]
|
||||
if (!input || !input[0]) return true
|
||||
|
||||
const channelData = input[0] // mono
|
||||
|
||||
// Accumuler
|
||||
const newBuffer = new Float32Array(this.buffer.length + channelData.length)
|
||||
newBuffer.set(this.buffer)
|
||||
newBuffer.set(channelData, this.buffer.length)
|
||||
this.buffer = newBuffer
|
||||
|
||||
// Envoyer quand on a assez de samples
|
||||
while (this.buffer.length >= this.chunkSize) {
|
||||
const chunk = this.buffer.slice(0, this.chunkSize)
|
||||
this.buffer = this.buffer.slice(this.chunkSize)
|
||||
|
||||
// Float32 → Int16 PCM little-endian
|
||||
const pcm = new ArrayBuffer(chunk.length * 2)
|
||||
const view = new DataView(pcm)
|
||||
for (let i = 0; i < chunk.length; i++) {
|
||||
const s = Math.max(-1, Math.min(1, chunk[i]))
|
||||
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7FFF, true) // true = little-endian
|
||||
}
|
||||
|
||||
this.port.postMessage(pcm, [pcm]) // Transferable
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('pcm-capture-processor', PcmCaptureProcessor)
|
||||
```
|
||||
|
||||
Note : si le navigateur crée l'AudioContext à 44.1kHz ou 48kHz au lieu de 16kHz, un rééchantillonnage est nécessaire dans le processor. Gemini accepte tout sample rate (il rééchantillonne côté serveur), mais envoyer du 48kHz triple la bande passante inutilement. Privilégier `new AudioContext({ sampleRate: 16000 })` — supporté sur Chrome, Firefox, Edge modernes.
|
||||
|
||||
---
|
||||
|
||||
## 5bis. Enregistrement audio téléchargeable
|
||||
|
||||
La conversation complète (candidat + IA) est enregistrée côté frontend pour
|
||||
permettre le téléchargement en fin de session — comme pour EO T1/T3.
|
||||
|
||||
### Principe : buffer chronologique unique
|
||||
|
||||
Les chunks audio du candidat (16kHz) et de l'IA (24kHz) arrivent en temps réel.
|
||||
Ils sont **horodatés et accumulés dans un buffer unique** dans l'ordre
|
||||
chronologique réel, puis assemblés en un fichier WAV mono 24kHz en fin de session.
|
||||
|
||||
```
|
||||
AU DÉMARRAGE de la session :
|
||||
recordingChunks = [] // { data: Int16Array, source: 'candidate'|'ai', time: number }
|
||||
sessionStartTime = Date.now()
|
||||
|
||||
À CHAQUE chunk envoyé par le candidat (PCM 16kHz) :
|
||||
1. RÉÉCHANTILLONNER 16kHz → 24kHz (interpolation linéaire : chaque sample
|
||||
est dupliqué × 1.5 — ou utiliser OfflineAudioContext pour un résultat propre)
|
||||
2. recordingChunks.push({
|
||||
data: resampled24k, // Int16Array à 24kHz
|
||||
source: 'candidate',
|
||||
time: Date.now()
|
||||
})
|
||||
|
||||
À CHAQUE chunk reçu de l'IA (PCM 24kHz) :
|
||||
recordingChunks.push({
|
||||
data: chunk, // Int16Array déjà à 24kHz
|
||||
source: 'ai',
|
||||
time: Date.now()
|
||||
})
|
||||
|
||||
EN FIN DE SESSION (après réception du rapport) :
|
||||
1. TRIER recordingChunks par time (normalement déjà ordonné)
|
||||
2. CONCATÉNER tous les .data en un seul Int16Array
|
||||
3. ENCODER en WAV :
|
||||
- Header WAV : 44 octets (PCM, mono, 24kHz, 16 bits)
|
||||
- Data : le buffer concaténé
|
||||
4. CRÉER Blob + URL.createObjectURL
|
||||
5. PROPOSER bouton "Télécharger l'audio" (download filename :
|
||||
expria-t2-{date}.wav)
|
||||
```
|
||||
|
||||
### Détail du rééchantillonnage candidat 16kHz → 24kHz
|
||||
|
||||
```javascript
|
||||
// Méthode simple : interpolation linéaire
|
||||
// Ratio : 24000 / 16000 = 1.5 → pour 2 samples en entrée, 3 en sortie
|
||||
function resample16to24(input16k) {
|
||||
const ratio = 24000 / 16000 // 1.5
|
||||
const outputLength = Math.ceil(input16k.length * ratio)
|
||||
const output = new Int16Array(outputLength)
|
||||
|
||||
for (let i = 0; i < outputLength; i++) {
|
||||
const srcIndex = i / ratio
|
||||
const srcFloor = Math.floor(srcIndex)
|
||||
const srcCeil = Math.min(srcFloor + 1, input16k.length - 1)
|
||||
const frac = srcIndex - srcFloor
|
||||
|
||||
output[i] = Math.round(
|
||||
input16k[srcFloor] * (1 - frac) + input16k[srcCeil] * frac
|
||||
)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
```
|
||||
|
||||
### Estimation mémoire
|
||||
|
||||
- Dialogue de 10 minutes = 600 secondes
|
||||
- PCM 24kHz mono 16 bits = 48 000 octets/seconde
|
||||
- Total : 600 × 48 000 = **~28 Mo en mémoire**
|
||||
- Acceptable pour un navigateur moderne (RAM > 1 Go)
|
||||
|
||||
### Fichier WAV header
|
||||
|
||||
```javascript
|
||||
function createWavFile(pcmData, sampleRate = 24000) {
|
||||
const numChannels = 1
|
||||
const bitsPerSample = 16
|
||||
const byteRate = sampleRate * numChannels * (bitsPerSample / 8)
|
||||
const blockAlign = numChannels * (bitsPerSample / 8)
|
||||
const dataSize = pcmData.byteLength
|
||||
const buffer = new ArrayBuffer(44 + dataSize)
|
||||
const view = new DataView(buffer)
|
||||
|
||||
// RIFF header
|
||||
writeString(view, 0, 'RIFF')
|
||||
view.setUint32(4, 36 + dataSize, true)
|
||||
writeString(view, 8, 'WAVE')
|
||||
// fmt chunk
|
||||
writeString(view, 12, 'fmt ')
|
||||
view.setUint32(16, 16, true) // chunk size
|
||||
view.setUint16(20, 1, true) // PCM format
|
||||
view.setUint16(22, numChannels, true)
|
||||
view.setUint32(24, sampleRate, true)
|
||||
view.setUint32(28, byteRate, true)
|
||||
view.setUint16(32, blockAlign, true)
|
||||
view.setUint16(34, bitsPerSample, true)
|
||||
// data chunk
|
||||
writeString(view, 36, 'data')
|
||||
view.setUint32(40, dataSize, true)
|
||||
// PCM data
|
||||
new Uint8Array(buffer, 44).set(new Uint8Array(pcmData.buffer))
|
||||
|
||||
return new Blob([buffer], { type: 'audio/wav' })
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Scalabilité et limites
|
||||
|
||||
### 6.1 Render (plan Starter)
|
||||
|
||||
| Contrainte | Valeur | Impact Expria |
|
||||
|---|---|---|
|
||||
| Connexions WS simultanées | Pas de limite documentée (Starter) | OK pour MVP |
|
||||
| Timeout connexion | Pas de hard limit WS | OK — Gemini a son propre cap de 15 min |
|
||||
| Mémoire | 512 Mo (Starter) | Chaque session T2 = 2 WS + buffers audio ≈ 5-10 Mo. ~50 sessions simultanées max théorique |
|
||||
| CPU | Partagé (Starter) | Le backend est un proxy passif (pas de traitement audio) — charge CPU minimale |
|
||||
|
||||
**Scalabilité :** le goulot d'étranglement n'est pas Render mais le rate limit Gemini. Avec le plan Paid Tier 1 Google, le nombre de sessions simultanées est limité par les RPM/TPM du projet Google AI.
|
||||
|
||||
### 6.2 Gemini Live API
|
||||
|
||||
| Contrainte | Valeur | Impact |
|
||||
|---|---|---|
|
||||
| Session max | 15 min (audio-only) | Suffisant pour T2 EO — dialogue libre en entraînement |
|
||||
| Context window | 128k tokens | Largement suffisant pour un dialogue oral de 15 min |
|
||||
| Rate limits | Variables par tier — vérifier dans AI Studio | À monitorer en production |
|
||||
| Sessions simultanées | Non documenté précisément — dépend du tier | Commencer avec 1-3 simultanées, scaler au besoin |
|
||||
|
||||
### 6.3 Stratégie de scalabilité progressive
|
||||
|
||||
```
|
||||
Phase 1 — MVP (Sprint 6) :
|
||||
- 1 seul projet Google AI
|
||||
- Plan Free ou Paid Tier 1
|
||||
- Objectif : < 5 sessions T2 simultanées
|
||||
- Monitoring : log chaque session (durée, tokens, erreurs)
|
||||
|
||||
Phase 2 — Production (post-launch) :
|
||||
- Passer en Paid Tier 2 si nécessaire
|
||||
- Ajouter un rate limiter côté backend (max 1 session T2 par utilisateur)
|
||||
- Queue de sessions si le rate limit Gemini est atteint
|
||||
- Monitoring : alertes sur le coût token mensuel
|
||||
|
||||
Phase 3 — Scale (si croissance) :
|
||||
- Considérer Vertex AI pour SLA et rate limits supérieurs
|
||||
- Load balancing multi-instance Render
|
||||
- Session affinity (sticky sessions pour les WS)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Gestion des erreurs
|
||||
|
||||
### 7.1 Erreurs de connexion Gemini
|
||||
|
||||
| Erreur | Cause probable | Action backend |
|
||||
|---|---|---|
|
||||
| Gemini WS refuse connexion | Rate limit atteint ou clé invalide | close(4005, "GEMINI_UNAVAILABLE") → client affiche "Service temporairement indisponible" |
|
||||
| Gemini WS drop en cours | Instabilité réseau | Tenter 1 reconnexion automatique. Si échec → close(4006, "GEMINI_DISCONNECTED") |
|
||||
| Gemini setup frame rejeté | Modèle invalide ou config incorrecte | Log erreur + close(4005) |
|
||||
|
||||
### 7.2 Erreurs côté client
|
||||
|
||||
| Erreur | Cause | Action frontend |
|
||||
|---|---|---|
|
||||
| `getUserMedia` refusé | Permission micro refusée | Afficher message explicite + lien vers paramètres navigateur |
|
||||
| AudioContext non supporté | Navigateur ancien | Afficher "Navigateur non supporté" (Firefox < 76, Safari < 14.1) |
|
||||
| WebSocket drop | Réseau instable | State → 'error' + bouton "Réessayer" |
|
||||
|
||||
---
|
||||
|
||||
## 8. Fichiers à créer / modifier — inventaire Sprint 6
|
||||
|
||||
### Backend (expria-backend)
|
||||
|
||||
| Fichier | Action | Description |
|
||||
|---|---|---|
|
||||
| `src/lib/geminiLive.ts` | **Modifier** | Remplacer prompt agent immobilier par prompt dynamique, ajouter inputAudioTranscription + outputAudioTranscription dans config, accumuler transcript |
|
||||
| `src/routes/t2live.ts` | **Modifier** | Ajouter fetch sujet, passer consigne/contexte/role à openGeminiLiveSession, déclencher évaluation finale + sauvegarde BDD après fin de session |
|
||||
| `docs/Prompt_t2live.md` | **Créer** | Déjà rédigé — à committer |
|
||||
|
||||
### Frontend (expria-frontend)
|
||||
|
||||
| Fichier | Action | Description |
|
||||
|---|---|---|
|
||||
| `public/pcm-capture-processor.js` | **Créer** | AudioWorklet pour capture PCM 16kHz |
|
||||
| `src/features/t2-live/pages/T2LivePage.tsx` | **Créer** | Page de sélection sujet T2 |
|
||||
| `src/features/t2-live/pages/T2PreparationPage.tsx` | **Créer** | Page de préparation (consigne + bouton démarrer) |
|
||||
| `src/features/t2-live/pages/T2DialoguePage.tsx` | **Créer** | Page de dialogue live (waveform, état IA, bouton terminer) |
|
||||
| `src/features/t2-live/hooks/useT2LiveSession.ts` | **Créer** | Hook WebSocket + state machine |
|
||||
| `src/features/t2-live/hooks/useAudioCapture.ts` | **Créer** | AudioContext + AudioWorklet + envoi PCM |
|
||||
| `src/features/t2-live/hooks/useAudioPlayback.ts` | **Créer** | Réception PCM 24kHz + file de lecture |
|
||||
| `src/features/t2-live/hooks/useAudioRecording.ts` | **Créer** | Buffer chronologique candidat+IA, rééchantillonnage 16→24kHz, export WAV, bouton télécharger |
|
||||
| `src/features/t2-live/state/t2-machine.ts` | **Créer** | State machine pure (testable — FTD-09) |
|
||||
| `src/features/t2-live/state/__tests__/t2-machine.test.ts` | **Créer** | 7+ tests state machine |
|
||||
| `src/app/router.tsx` | **Modifier** | Ajouter routes /simulation/eo/t2/* |
|
||||
|
||||
---
|
||||
|
||||
## 9. Découpage en sous-sprints recommandé
|
||||
|
||||
Le Sprint 6 est trop large pour une seule session. Découpage proposé :
|
||||
|
||||
```
|
||||
Sprint 6a — Backend T2 Live (1 session)
|
||||
- Modifier geminiLive.ts (prompt dynamique, transcription, accumulation)
|
||||
- Modifier t2live.ts (fetch sujet, évaluation finale, sauvegarde)
|
||||
- Tests backend
|
||||
- Test manuel : connexion WS via wscat ou script Node
|
||||
|
||||
Sprint 6b — Frontend capture + playback audio (1 session)
|
||||
- pcm-capture-processor.js (AudioWorklet)
|
||||
- useAudioCapture.ts
|
||||
- useAudioPlayback.ts
|
||||
- Test manuel : enregistrer + lire du PCM dans le navigateur
|
||||
|
||||
Sprint 6c — Frontend state machine + UI (1 session)
|
||||
- t2-machine.ts + tests
|
||||
- useT2LiveSession.ts
|
||||
- Pages T2 (sélection, préparation, dialogue)
|
||||
- Intégration complète
|
||||
|
||||
Sprint 6d — Clean + Golden Dataset (1 session)
|
||||
- Tests Groupe D (D2-D6)
|
||||
- Factorisation
|
||||
- CHANGELOG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Références officielles
|
||||
|
||||
| Document | URL |
|
||||
|---|---|
|
||||
| Get started WebSockets | https://ai.google.dev/gemini-api/docs/live-api/get-started-websocket |
|
||||
| Capabilities guide | https://ai.google.dev/gemini-api/docs/live-api/capabilities |
|
||||
| Session management | https://ai.google.dev/gemini-api/docs/live-api/session-management |
|
||||
| Ephemeral tokens | https://ai.google.dev/gemini-api/docs/live-api/ephemeral-tokens |
|
||||
| Best practices | https://ai.google.dev/gemini-api/docs/live-api/best-practices |
|
||||
| WebSocket API reference | https://ai.google.dev/api/live |
|
||||
| Rate limits | https://ai.google.dev/gemini-api/docs/rate-limits |
|
||||
| Pricing | https://ai.google.dev/gemini-api/docs/pricing |
|
||||
| Example app (JS + proxy) | https://github.com/google-gemini/gemini-live-api-examples |
|
||||
176
docs/Prompt_exercices_long_terme.md
Normal file
176
docs/Prompt_exercices_long_terme.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# Prompt Exercices Long Terme — Analyse patterns TCF Canada
|
||||
|
||||
> **Source :** `src/lib/deepseek.ts` → fonction `generatePatternExercices(patterns)`
|
||||
> **Modèle :** DeepSeek Chat (`deepseek-chat`) · `temperature: 0.4` · `response_format: json_object` · `AbortSignal.timeout(20_000)`
|
||||
> **Introduit :** Sprint 3.6c (2026-04-22) — commit `c48ae8d`
|
||||
|
||||
---
|
||||
|
||||
## Contexte & Variables dynamiques
|
||||
|
||||
Ce prompt est déclenché par `GET /users/patterns` (Premium uniquement) quand l'analyse des 5 dernières productions révèle un ou plusieurs **patterns confirmés** (même code d'erreur présent dans ≥ 3 productions sur 5).
|
||||
|
||||
Les exercices produits sont **distincts** des exercices individuels générés par correction (prompt dans `Prompt_maître.md`) : ils ciblent des faiblesses *structurelles récurrentes* plutôt qu'une production spécifique.
|
||||
|
||||
| Variable | Description | Exemple |
|
||||
|---|---|---|
|
||||
| `patterns` | Liste des patterns confirmés issus de `aggregatePatterns` | Voir structure ci-dessous |
|
||||
|
||||
### Structure d'un pattern en entrée
|
||||
|
||||
```typescript
|
||||
interface PatternInput {
|
||||
code: string // Code taxonomie (cf. TAXONOMIE_ERREURS.md)
|
||||
critere: Critere // 'adequation_tache' | 'coherence_cohesion' |
|
||||
// 'competence_lexicale' | 'competence_grammaticale'
|
||||
frequency: number // 3, 4 ou 5 (seuil d'agrégation)
|
||||
description: string | null // non-null uniquement pour code === 'autre'
|
||||
}
|
||||
```
|
||||
|
||||
Exemple d'entrée (3 patterns) :
|
||||
```
|
||||
- accord_sujet_verbe (competence_grammaticale) — apparu 4/5 fois
|
||||
- connecteurs_repetes (coherence_cohesion) — apparu 3/5 fois
|
||||
- repetition_lexicale (competence_lexicale) — apparu 3/5 fois
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt système envoyé au modèle
|
||||
|
||||
```
|
||||
Tu es un coach spécialisé dans la préparation au TCF Canada (Test de connaissance du français).
|
||||
|
||||
Un candidat commet systématiquement les mêmes erreurs sur ses 5 dernières productions écrites. Tu dois produire UN exercice ciblé par pattern d'erreur récurrent identifié.
|
||||
|
||||
CONTEXTE :
|
||||
- Ces exercices sont des exercices LONG TERME destinés à corriger des faiblesses structurelles récurrentes.
|
||||
- Ils sont DISTINCTS des exercices individuels générés après chaque correction (qui ciblent une production spécifique).
|
||||
- Tu n'as PAS accès au texte du candidat. Tes exemples doivent être génériques et représentatifs de l'erreur.
|
||||
|
||||
RÈGLES :
|
||||
1. Un exercice par pattern en entrée, dans le même ordre.
|
||||
2. Le diagnostic explique en 1-2 phrases POURQUOI cette erreur est problématique pour le TCF Canada.
|
||||
3. La consigne demande au candidat de corriger ou reformuler une phrase.
|
||||
4. L'exemple est une phrase incorrecte illustrant le pattern (inventée, pas tirée du candidat).
|
||||
5. La correction est la version correcte de l'exemple.
|
||||
6. L'astuce est un procédé mnémotechnique, une règle pratique ou un réflexe de relecture que le candidat doit appliquer APRÈS avoir rédigé son texte pour détecter et corriger cette erreur lui-même. Formulée comme un conseil direct et actionnable.
|
||||
Exemples d'astuces :
|
||||
- Subjonctif : "Après 'bien que', 'pourvu que', 'avant que' → le verbe qui suit est TOUJOURS au subjonctif. Relisez votre texte en cherchant ces expressions."
|
||||
- Accords : "Relisez chaque phrase en pointant du doigt le sujet et son verbe. S'ils sont éloignés, vérifiez l'accord."
|
||||
- Connecteurs : "Après rédaction, surlignez tous vos connecteurs. Si le même revient plus de 2 fois, remplacez-en un."
|
||||
7. Niveau de langue : NCLC 7-9 (ni trop simple, ni trop littéraire).
|
||||
8. Les exemples doivent être en contexte TCF Canada : courriels, lettres formelles, essais argumentatifs, situations professionnelles canadiennes.
|
||||
|
||||
FORMAT DE SORTIE — JSON strict, aucun texte avant ni après :
|
||||
{
|
||||
"exercises": [
|
||||
{
|
||||
"code": "<code_taxonomie>",
|
||||
"critere": "<critere>",
|
||||
"diagnostic": "<1-2 phrases>",
|
||||
"exercice": {
|
||||
"consigne": "<instruction au candidat>",
|
||||
"exemple": "<phrase incorrecte>",
|
||||
"correction": "<phrase corrigée>",
|
||||
"astuce": "<procédé mnémotechnique ou réflexe de relecture>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt utilisateur — template dynamique
|
||||
|
||||
Construit par `buildPatternExercicesUserPrompt(patterns)` dans `src/lib/deepseek.ts` :
|
||||
|
||||
```
|
||||
Voici les patterns d'erreurs récurrents détectés sur les 5 dernières productions du candidat :
|
||||
|
||||
- <code> (<critere>) — apparu <frequency>/5 fois[ — « <description> »]
|
||||
- ...
|
||||
|
||||
Produis un exercice ciblé par pattern. JSON strict uniquement.
|
||||
```
|
||||
|
||||
Le fragment `— « <description> »` n'apparaît que lorsque le code est `autre` (description textuelle obligatoire selon la taxonomie — cf. `TAXONOMIE_ERREURS.md` §Règles d'utilisation).
|
||||
|
||||
---
|
||||
|
||||
## Structure de la réponse JSON attendue
|
||||
|
||||
```json
|
||||
{
|
||||
"exercises": [
|
||||
{
|
||||
"code": "accord_sujet_verbe",
|
||||
"critere": "competence_grammaticale",
|
||||
"diagnostic": "<1-2 phrases expliquant pourquoi cette erreur coûte des points au TCF>",
|
||||
"exercice": {
|
||||
"consigne": "<instruction claire au candidat>",
|
||||
"exemple": "<phrase incorrecte générique, contexte TCF Canada>",
|
||||
"correction": "<version correcte>",
|
||||
"astuce": "<procédé mnémotechnique ou réflexe de relecture actionnable>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Champs expliqués
|
||||
|
||||
| Champ | Rôle |
|
||||
|---|---|
|
||||
| `code` | Code taxonomie propagé depuis l'entrée — permet au frontend de rattacher l'exercice à son pattern |
|
||||
| `critere` | Critère TCF parmi les 4 officiels — validé en runtime via `isValidCritere` |
|
||||
| `diagnostic` | Explication pédagogique courte : pourquoi cette erreur pénalise au TCF |
|
||||
| `exercice.consigne` | Instruction explicite au candidat (« Corrigez », « Reformulez », « Complétez ») |
|
||||
| `exercice.exemple` | Phrase **incorrecte** illustrant l'erreur — inventée par le modèle, en contexte TCF Canada (courriel formel, lettre, essai, cadre professionnel) |
|
||||
| `exercice.correction` | Version correcte de l'exemple |
|
||||
| `exercice.astuce` | **Conseil de relecture actionnable** — procédé mnémotechnique ou règle pratique que le candidat applique lors de la phase de relecture pour détecter ses propres erreurs |
|
||||
|
||||
---
|
||||
|
||||
## Post-traitement côté serveur
|
||||
|
||||
Après réception de la réponse DeepSeek, `generatePatternExercices` valide chaque item :
|
||||
|
||||
1. **Présence stricte** des champs `code`, `critere`, `diagnostic`, `exercice.consigne`, `exercice.exemple`, `exercice.correction`, `exercice.astuce` — tous doivent être des chaînes non vides.
|
||||
2. **Validation du critère** via `isValidCritere` (cf. `src/lib/taxonomieErreurs.ts`) — tout critère hors taxonomie est **filtré**.
|
||||
3. **Les items invalides sont silencieusement ignorés** — pas de throw. La liste retournée peut donc être plus courte que la liste de patterns en entrée.
|
||||
|
||||
La réponse persistée dans `pattern_analyses.exercises` (JSONB) est la liste filtrée.
|
||||
|
||||
---
|
||||
|
||||
## Dégradation gracieuse
|
||||
|
||||
Si l'appel DeepSeek échoue (timeout, API error, JSON invalide) :
|
||||
- `patternsController.list` capture l'erreur, logue `[patternsController.list] generatePatternExercices failed`.
|
||||
- L'analyse est persistée avec `exercises: []` — patterns et indice de préparation restent disponibles.
|
||||
- Le frontend affiche la liste des patterns sans section exercices (cf. `ProgressionPremium`).
|
||||
- Un refetch ultérieur (après `staleTime` ou nouvelle correction) retentera la génération.
|
||||
|
||||
---
|
||||
|
||||
## Contraintes opérationnelles
|
||||
|
||||
| Paramètre | Valeur | Justification |
|
||||
|---|---|---|
|
||||
| Modèle | `deepseek-chat` | Cohérent avec les autres prompts (correction, modèle, idées) |
|
||||
| Température | `0.4` | Légèrement plus élevée que la correction (`0.2`) pour favoriser la variété des exemples |
|
||||
| Format | `json_object` | Strict, pas de markdown parasite |
|
||||
| Timeout | 20 000 ms | Plus long que `generateIdees` (15 s) car la réponse peut contenir jusqu'à ~10 exercices (4 critères × patterns) |
|
||||
|
||||
---
|
||||
|
||||
## Historique de ce document
|
||||
|
||||
| Version | Date | Changements |
|
||||
|---|---|---|
|
||||
| 1.0 | 2026-04-23 | Création — documentation du prompt validé par Hermann lors du Sprint 3.6c (2026-04-22) |
|
||||
215
docs/Prompt_t2live.md
Normal file
215
docs/Prompt_t2live.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Prompt_t2live.md — Expria Backend
|
||||
# Spécification du prompt système T2 EO Live
|
||||
|
||||
> **Document de référence — Sprint 6**
|
||||
> À lire conjointement avec PARCOURS_UTILISATEURS.md et PLANS_TARIFAIRES.md.
|
||||
> Ce document doit être commité dans `expria-backend/docs/` avant le démarrage du Sprint 6.
|
||||
|
||||
---
|
||||
|
||||
## 1. Contexte pédagogique
|
||||
|
||||
La Tâche 2 de l'Expression Orale TCF Canada est une **interaction de service** :
|
||||
le candidat joue le rôle d'une personne dans une situation concrète du quotidien
|
||||
qui a besoin d'informations pour prendre une décision. Il pose des questions à
|
||||
un interlocuteur (joué par l'IA) qui détient ces informations.
|
||||
|
||||
**Ce que cette tâche évalue :**
|
||||
- La capacité à initier et maintenir une conversation en français
|
||||
- La formulation de questions claires et adaptées au registre
|
||||
- Le lexique lié à la vie quotidienne
|
||||
- La morphosyntaxe en situation d'interaction orale
|
||||
- La phonologie (évaluée sur l'audio)
|
||||
|
||||
**Ce que cette tâche n'est pas :**
|
||||
- Un débat d'opinions
|
||||
- Un exposé monologique
|
||||
- Un jeu de questions-réponses guidé par l'examinateur
|
||||
|
||||
---
|
||||
|
||||
## 2. Rôle de l'IA
|
||||
|
||||
L'IA joue le rôle de l'interlocuteur de la situation décrite dans le sujet
|
||||
(ex : un bailleur, un employeur, un vendeur, un agent de voyage, etc.).
|
||||
|
||||
**Règles absolues du comportement de l'IA :**
|
||||
|
||||
1. **Répondre uniquement en français** — quelle que soit la langue utilisée
|
||||
par le candidat.
|
||||
2. **Ne pas faciliter la tâche** — ne pas reformuler les questions du candidat,
|
||||
ne pas anticiper ce qu'il veut savoir, ne pas lui souffler les mots.
|
||||
3. **Répondre aux questions posées** — réponses naturelles, réalistes,
|
||||
ni trop courtes (monosyllabiques) ni trop longues (monologues).
|
||||
4. **Ne pas relancer au-delà de** : *"Avez-vous d'autres questions ?"*
|
||||
si le candidat marque une pause prolongée ou semble avoir terminé.
|
||||
5. **Ne pas évaluer le candidat** pendant la conversation — aucun commentaire
|
||||
sur sa langue, ses erreurs, ou sa performance.
|
||||
6. **Ne pas sortir du rôle** — même si le candidat pose des questions hors sujet
|
||||
ou tente de changer de registre.
|
||||
7. **Attendre que le candidat prenne la parole** — c'est le candidat qui initie
|
||||
la conversation, comme à l'examen réel. L'IA ne parle pas en premier.
|
||||
Elle attend en silence et répond dès que le candidat s'adresse à elle.
|
||||
|
||||
---
|
||||
|
||||
## 3. Prompt système (à injecter dans `geminiLive.ts`)
|
||||
|
||||
```
|
||||
Tu joues le rôle de {role} dans la situation suivante : {contexte}
|
||||
|
||||
Règles à respecter impérativement :
|
||||
- Tu réponds uniquement en français, quelle que soit la langue de ton interlocuteur.
|
||||
- Tu joues ton rôle de façon naturelle et réaliste. Tu n'es pas un examinateur —
|
||||
tu es {role}.
|
||||
- Tu réponds aux questions qu'on te pose de façon honnête et naturelle,
|
||||
comme le ferait une vraie personne dans cette situation.
|
||||
- Tu ne facilites pas la tâche : tu ne reformules pas les questions,
|
||||
tu n'anticipes pas ce que l'interlocuteur veut savoir,
|
||||
tu ne lui suggères pas quoi demander.
|
||||
- Si ton interlocuteur marque une longue pause ou semble avoir terminé,
|
||||
tu peux dire : "Avez-vous d'autres questions ?" — c'est la seule relance autorisée.
|
||||
- Tu ne fais aucun commentaire sur la langue, les erreurs ou le niveau de français
|
||||
de ton interlocuteur.
|
||||
- Tu ne sors jamais de ton rôle.
|
||||
- Tu ne prends PAS la parole en premier. Tu attends que ton interlocuteur
|
||||
s'adresse à toi, puis tu réponds naturellement dans ton rôle.
|
||||
- Tes réponses sont concises et naturelles : ni monosyllabiques, ni des monologues.
|
||||
```
|
||||
|
||||
**Variables à substituer dynamiquement depuis le sujet :**
|
||||
- `{role}` — ex : "un bailleur qui loue un appartement"
|
||||
- `{contexte}` — la consigne + contexte du sujet issu de la table `sujets`
|
||||
|
||||
---
|
||||
|
||||
## 4. Format du sujet T2 en base
|
||||
|
||||
Les sujets T2 sont stockés dans la table `sujets` avec les champs :
|
||||
- `consigne` — la situation décrite au candidat (ce qu'il doit faire)
|
||||
- `contexte` — les informations de cadrage (lieu, situation, interlocuteur)
|
||||
- `tache` — valeur `'EO_T2'`
|
||||
- `mode` — valeur `'entrainement'`
|
||||
|
||||
**Exemple de sujet :**
|
||||
```
|
||||
consigne : "Vous avez vu une annonce pour un appartement à louer.
|
||||
Appelez le bailleur pour obtenir les informations
|
||||
nécessaires avant de prendre votre décision."
|
||||
contexte : "Vous cherchez un appartement de 2 pièces dans le
|
||||
centre-ville, votre budget est limité et vous souhaitez
|
||||
emménager le mois prochain."
|
||||
role : "un bailleur qui propose un appartement à louer"
|
||||
```
|
||||
|
||||
> **Note :** Le champ `role` existe dans la table `sujets` et a été
|
||||
> alimenté pour les 9 sujets EO T2 (session 2026-04-26, script SQL
|
||||
> `update_sujets_t2.sql`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Structure du rapport T2
|
||||
|
||||
Le rapport T2 suit **exactement la même structure** que les rapports EO T1 et T3 :
|
||||
4 critères officiels TCF Canada :
|
||||
|
||||
| Critère | Pondération |
|
||||
|---|---|
|
||||
| Cohérence et cohésion | 25 % |
|
||||
| Étendue et maîtrise du lexique | 25 % |
|
||||
| Maîtrise morphosyntaxique | 25 % |
|
||||
| Phonologie | 25 % |
|
||||
|
||||
**Conséquence :** l'évaluation finale peut réutiliser le prompt de correction EO
|
||||
existant (`POST /corrections/eo`) en passant le transcript de la session comme
|
||||
entrée, avec `tache: 'EO_T2'`.
|
||||
|
||||
> **Rappel TD-08 (backend) :** la phonologie est temporairement fixée à 0
|
||||
> pour les tâches EO live car l'évaluation nécessite l'audio brut.
|
||||
> Applicable à T2 également — à résoudre post-MVP.
|
||||
|
||||
---
|
||||
|
||||
## 6. Flux technique complet Sprint 6
|
||||
|
||||
```
|
||||
1. Candidat choisit un sujet T2 dans la liste → clic → page préparation
|
||||
2. Page préparation : consigne affichée + bouton "Démarrer le dialogue"
|
||||
3. Frontend ouvre WS : wss://api.expria.app/t2/live?token=<jwt>&sujet=<uuid>
|
||||
4. Backend vérifie JWT + plan oral_t2_live (Premium)
|
||||
5. Backend lit le sujet depuis Supabase (id sujet passé en query param)
|
||||
6. Backend ouvre WS vers Gemini Live API avec prompt système construit
|
||||
dynamiquement depuis {role} + {contexte} du sujet
|
||||
7. Backend → Gemini : setup frame (modèle + prompt + responseModalities: AUDIO
|
||||
+ VAD : endOfSpeechSensitivity LOW, silenceDurationMs 2000)
|
||||
8. L'IA attend en silence — c'est le CANDIDAT qui prend la parole en premier
|
||||
(conforme à l'examen réel TCF Canada)
|
||||
9. Frontend → Backend → Gemini : audio candidat (PCM 16kHz base64) en continu
|
||||
+ accumulation dans le buffer d'enregistrement chronologique (rééchantillonné 24kHz)
|
||||
10. Gemini → Backend → Frontend : réponses audio (PCM 24kHz base64) en continu
|
||||
+ accumulation dans le buffer d'enregistrement
|
||||
11. Candidat clique "Terminer" → Frontend envoie signal de fin
|
||||
12. Backend ferme WS Gemini, récupère transcript complet (inputTranscription
|
||||
+ outputTranscription accumulés pendant la session)
|
||||
13. Backend POST /corrections/eo avec transcript + tache='EO_T2'
|
||||
→ rapport généré (même pipeline que T1/T3)
|
||||
14. Backend sauvegarde production en base (tache='EO_T2_LIVE')
|
||||
15. Backend envoie rapport au Frontend via WS (close code 1000 + payload rapport)
|
||||
16. Frontend → state machine 'ended' → affichage rapport
|
||||
17. Frontend propose bouton "Télécharger l'audio" (WAV mono 24kHz assemblé
|
||||
depuis le buffer chronologique)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Spécifications audio
|
||||
|
||||
| Direction | Format | Sample rate | Encoding |
|
||||
|---|---|---|---|
|
||||
| Frontend → Gemini | PCM brut | 16kHz | 16 bits, little-endian, mono |
|
||||
| Gemini → Frontend | PCM brut | 24kHz | 16 bits, little-endian, mono |
|
||||
|
||||
**MIME type à envoyer à Gemini :** `audio/pcm;rate=16000`
|
||||
|
||||
**Côté frontend :**
|
||||
- Capture via `AudioContext` + `AudioWorklet` (ou `ScriptProcessorNode` provisoire)
|
||||
- Rééchantillonnage obligatoire : le navigateur capture à 44.1kHz ou 48kHz → downsampler à 16kHz
|
||||
- Conversion Float32 → Int16 PCM avant envoi
|
||||
- Lecture de l'audio reçu : `AudioContext` à 24kHz + `AudioBufferSourceNode` par chunk
|
||||
|
||||
---
|
||||
|
||||
## 8. Gestion des erreurs WebSocket
|
||||
|
||||
| Close code | Cause | Action frontend |
|
||||
|---|---|---|
|
||||
| 1000 | Fin normale + rapport prêt | State → 'ended', afficher rapport |
|
||||
| 4001 | AUTH_REQUIRED | State → 'error', redirect /login |
|
||||
| 4003 | PLAN_INSUFFICIENT | State → 'error', PaywallModal Premium |
|
||||
| 4004 | SUJET_NOT_FOUND | State → 'error', retour liste sujets |
|
||||
| Autre | Erreur réseau / Gemini | State → 'error', bouton "Réessayer" |
|
||||
|
||||
---
|
||||
|
||||
## 9. Questions ouvertes à trancher au Sprint 6
|
||||
|
||||
| # | Question | Impact |
|
||||
|---|---|---|
|
||||
| Q1 | Le champ `role` existe-t-il dans la table `sujets` ou faut-il le dériver du `contexte` ? | Migration SQL ou prompt engineering |
|
||||
| Q2 | L'id du sujet est-il passé en query param WS (`?token=jwt&sujet=uuid`) ou via le premier message WS ? | Protocole de connexion |
|
||||
| Q3 | Le transcript est-il accumulé côté backend pendant la session ou demandé à Gemini en fin de session ? | Architecture geminiLive.ts |
|
||||
|
||||
---
|
||||
|
||||
## 10. Ce qui existe déjà (à ne pas recoder)
|
||||
|
||||
- `src/routes/t2live.ts` — 101 lignes, route WS + auth + gating ✅
|
||||
- `src/lib/geminiLive.ts` — 154 lignes, proxy bidirectionnel + setup frame ✅
|
||||
- Pipeline correction EO (`POST /corrections/eo`) — réutilisable pour évaluation finale ✅
|
||||
- Modèle `gemini-live-2.5-flash-native-audio` — accès confirmé ✅
|
||||
|
||||
**À modifier :**
|
||||
- `src/lib/geminiLive.ts` — remplacer le prompt agent immobilier par le prompt
|
||||
dynamique §3, brancher la récupération du sujet depuis Supabase,
|
||||
accumuler le transcript, déclencher l'évaluation finale.
|
||||
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
## 1. Stubs temporaires — à compléter
|
||||
|
||||
### TD-01 — src/lib/supabase.ts (backend)
|
||||
|
||||
**Priorité :** 🔴 Critique
|
||||
**Statut :** Ouvert
|
||||
**Description :** Client Supabase créé comme stub. Fonctionne en développement avec les variables d'environnement mais n'a pas de gestion d'erreur robuste si `SUPABASE_URL` ou `SUPABASE_SERVICE_ROLE_KEY` sont absentes.
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
---
|
||||
|
||||
### TD-02 — src/lib/planController.ts (backend)
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Résolu — session Stripe
|
||||
**Description :** Stub créé pour permettre les tests de `updateUserPlan`. La vraie implémentation (mise à jour Supabase + gestion Stripe) n'est pas encore codée.
|
||||
|
|
@ -31,6 +33,7 @@
|
|||
---
|
||||
|
||||
### TD-03 — src/lib/stripe.ts (backend)
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Résolu — session Stripe
|
||||
**Description :** Stub créé pour permettre les tests de `verifyStripeWebhook` et `calculateProrata`. La vraie implémentation Stripe n'est pas encore codée.
|
||||
|
|
@ -42,6 +45,7 @@
|
|||
## 2. Décisions pragmatiques — à revisiter
|
||||
|
||||
### TD-04 — Déploiement manuel (frontend + backend)
|
||||
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert — accepté jusqu'aux premiers revenus
|
||||
**Description :** Cloudflare Pages et Render ne supportent pas l'auto-deploy depuis Codeberg. Le déploiement est manuel (CLI + dashboard).
|
||||
|
|
@ -51,31 +55,36 @@
|
|||
---
|
||||
|
||||
### TD-05 — Comptes de test avec emails @gmail.com
|
||||
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert
|
||||
**Description :** Les comptes de test utilisent `@gmail.com` au lieu de `@expria.local` prévu dans TEST_ENVIRONMENT.md. Raison : Supabase bloque la création d'utilisateurs avec des domaines non standards via l'API admin, et le dashboard est inaccessible depuis la Russie.
|
||||
**Emails actuels :**
|
||||
|
||||
- `test.free@gmail.com`
|
||||
- `test.standard@gmail.com`
|
||||
- `test.premium@gmail.com`
|
||||
- `test.quota@gmail.com`
|
||||
**À faire :** Mettre à jour TEST_ENVIRONMENT.md pour refléter les vrais emails. Vérifier que la validation `@expria.local` dans le middleware n'est pas implémentée (elle ne l'est pas).
|
||||
**À faire :** Mettre à jour TEST_ENVIRONMENT.md pour refléter les vrais emails. Vérifier que la validation `@expria.local` dans le middleware n'est pas implémentée (elle ne l'est pas).
|
||||
|
||||
---
|
||||
|
||||
### TD-06 — Pas de migration SQL versionnée pour les tables initiales
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** Les tables `profiles` et `productions` ont été créées directement via SQL Editor, sans fichier de migration dans `supabase/migrations/`. Viole la Règle F de DEVELOPMENT_PRINCIPLES.md.
|
||||
**À faire :** Créer les fichiers de migration correspondants :
|
||||
|
||||
- `supabase/migrations/001_create_profiles.sql`
|
||||
- `supabase/migrations/002_create_productions.sql`
|
||||
- `supabase/migrations/003_create_test_accounts.sql`
|
||||
**Impact :** Si la base doit être recréée (nouveau projet Supabase), les migrations permettent de tout reconstruire en une commande.
|
||||
**Impact :** Si la base doit être recréée (nouveau projet Supabase), les migrations permettent de tout reconstruire en une commande.
|
||||
|
||||
---
|
||||
|
||||
### TD-07 — Ancien projet Supabase partagé
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert — accepté temporairement
|
||||
**Description :** Le nouveau projet Expria V2 utilise la même base Supabase que l'ancien projet (en maintenance). Les anciennes tables ont été remplacées mais d'autres tables de l'ancien projet subsistent (`sujets`, `eo_t2_results`, `payment_transactions`, etc.).
|
||||
|
|
@ -86,33 +95,38 @@
|
|||
---
|
||||
|
||||
### TD-13 — Webhook Stripe non idempotent
|
||||
|
||||
**Priorité :** 🔴 Critique
|
||||
**Statut :** Ouvert — à faire avant mise en production
|
||||
**Description :** Stripe peut livrer un même event webhook deux fois (retries réseau, rejeu manuel depuis le dashboard). La route `POST /stripe/webhook` traite chaque réception sans dédoublonnage. En pratique, les opérations `updateUserPlan` et `updateUserStripeInfo` sont idempotentes par nature (même résultat en cas de double appel), mais si de la logique non idempotente est ajoutée plus tard (ex: compteur, envoi d'email, crédit utilisateur), un double traitement causerait un bug.
|
||||
**À faire :**
|
||||
|
||||
- Créer une table `stripe_webhook_events(id TEXT PRIMARY KEY, processed_at TIMESTAMPTZ)`
|
||||
- Avant traitement, vérifier si `event.id` est déjà en base → si oui, retourner 200 sans rien faire
|
||||
- Après traitement, insérer l'`event.id` dans la table
|
||||
**Session concernée :** Stripe (POST /stripe/webhook)
|
||||
**Condition de résolution :** Avant la mise en production publique.
|
||||
**Session concernée :** Stripe (POST /stripe/webhook)
|
||||
**Condition de résolution :** Avant la mise en production publique.
|
||||
|
||||
---
|
||||
|
||||
### TD-15 — Jobs asynchrones modèle/exercices : status peut rester "pending" indéfiniment
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert — introduit au Sprint 3.6a
|
||||
**Description :** Le flux POST /corrections/ee lance deux jobs DeepSeek en fire-and-forget (`runModeleJob`, `runExercicesJob` dans `correctionController.ts`). Si le process Node redémarre (deploy Render, crash, OOM) pendant l'exécution d'un de ces jobs, la colonne `exercices_status` ou `modele_status` reste figée à `'pending'` — l'utilisateur voit un loader infini côté frontend.
|
||||
**Impact actuel :** faible en conditions normales (DeepSeek répond en ~5-15 s, Render redémarre rarement). Perceptible uniquement si un deploy a lieu pendant une correction active.
|
||||
**À faire :**
|
||||
|
||||
- Option 1 (simple) : job de reprise au boot → scanner `productions WHERE (exercices_status='pending' OR modele_status='pending') AND created_at < NOW() - INTERVAL '2 minutes'` → relancer.
|
||||
- Option 2 (robuste) : file d'attente persistée (pg-boss, BullMQ) au lieu de fire-and-forget.
|
||||
- Option 3 (minimal) : timeout côté frontend → si `pending` depuis > 2 min, afficher "La génération a échoué, réessayer ?" + endpoint `POST /simulations/:id/retry-jobs`.
|
||||
**Session concernée :** à planifier après livraison Sprint 3.6a/3.6b en prod stable.
|
||||
**Condition de résolution :** après 7 jours d'observation en prod avec monitoring des colonnes `*_status='pending'` âgées.
|
||||
**Session concernée :** à planifier après livraison Sprint 3.6a/3.6b en prod stable.
|
||||
**Condition de résolution :** après 7 jours d'observation en prod avec monitoring des colonnes `*_status='pending'` âgées.
|
||||
|
||||
---
|
||||
|
||||
### TD-14 — Erreurs TypeScript TS2835 pré-existantes
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Résolu — session correction build TypeScript
|
||||
**Description :** Erreurs TS2835 sur plusieurs fichiers de routes.
|
||||
|
|
@ -125,16 +139,17 @@ Gate de qualité actuel : npm run test.
|
|||
## 3. Fonctionnalités reportées
|
||||
|
||||
### TD-08 — Phonologie T2 EO à 0
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** L'évaluation de la phonologie pour la T2 EO live est temporairement à 0 (non évaluée). L'évaluation se fait sur 4 critères au lieu de 5.
|
||||
**Raison :** La T2 live utilise un transcript texte — évaluer la phonologie nécessite l'audio brut, ce qui dépasse la limite de taille des requêtes.
|
||||
**À faire :** Implémenter l'évaluation phonologique via un endpoint séparé qui traite l'audio en chunks.
|
||||
**Session concernée :** T2 live (WebSocket)
|
||||
**Statut :** Partiellement résolu — Sprint 4.8
|
||||
**Description :** L'évaluation de la phonologie est désormais opérationnelle pour **EO T1 et T3** : `POST /corrections/eo` reçoit l'audio brut (Mode B), Gemini 2.5 Flash évalue la phonologie en parallèle de la transcription via `evaluatePhonology` (cf. `src/lib/geminiPhonology.ts`), et le score `/4` est injecté comme 5e critère du rapport. Le format passe officiellement à 5 critères × /4 (total /20 inchangé).
|
||||
**Reste à faire :** **EO T2 Live (Sprint 6)** continue de retourner phonologie 0/4 — pas d'audio brut côté backend dans le pipeline WebSocket actuel (`t2live.ts` proxifie l'audio entre client et Gemini Live sans le bufferiser pour évaluation différée). À résoudre lors du Sprint 6 en accumulant l'audio côté backend ou en demandant à Gemini Live de produire une note phonologique en fin de session.
|
||||
**Session concernée :** T2 Live (WebSocket) — Sprint 6.
|
||||
|
||||
---
|
||||
|
||||
### TD-09 — ScriptProcessorNode déprécié (T2 live)
|
||||
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Reporté à après le lancement
|
||||
**Description :** Le traitement audio côté client utilise `ScriptProcessorNode` qui est déprécié. Doit être remplacé par `AudioWorklet`.
|
||||
|
|
@ -144,6 +159,7 @@ Gate de qualité actuel : npm run test.
|
|||
---
|
||||
|
||||
### TD-10 — Analyse des patterns (Premium) non implémentée
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Résolu — Sprint 3.6c
|
||||
**Description :** La feature d'analyse des patterns sur les 5 dernières productions (Premium) a été livrée Sprint 3.6c (table `pattern_analyses`, `generatePatternExercices`).
|
||||
|
|
@ -151,6 +167,7 @@ Gate de qualité actuel : npm run test.
|
|||
---
|
||||
|
||||
### TD-11 — Indice de préparation non implémenté
|
||||
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Résolu — Sprint 3.6c
|
||||
**Description :** Le calcul de l'indice de préparation (0-100) a été livré Sprint 3.6c en même temps que l'analyse des patterns (colonne `preparation_index` + `preparation_message`).
|
||||
|
|
@ -160,6 +177,7 @@ Gate de qualité actuel : npm run test.
|
|||
## 4. Tests à automatiser
|
||||
|
||||
### TD-12 — Tests manuels du Golden Dataset non automatisés
|
||||
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Accepté — par conception
|
||||
**Description :** Les 41 tests du Golden Dataset sont manuels. Certains pourraient être automatisés (tests d'intégration HTTP avec Supertest).
|
||||
|
|
@ -168,6 +186,7 @@ Gate de qualité actuel : npm run test.
|
|||
---
|
||||
|
||||
### TD-16 — Bucket Supabase Storage `audio-productions` créé manuellement
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Résolu — Sprint 4b
|
||||
**Description :** Décision Hermann (2026-04-25) : abandon du stockage audio backend. La transcription live passe par Deepgram en connexion directe navigateur ↔ Deepgram via token éphémère. L'audio brut est téléchargé en local par l'utilisateur. Plus aucun bucket Storage requis côté serveur.
|
||||
|
|
@ -175,6 +194,7 @@ Gate de qualité actuel : npm run test.
|
|||
---
|
||||
|
||||
### TD-17 — Limite audioBase64 in-memory à 14 Mo (≈ 10 Mo binaire)
|
||||
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Résolu — Sprint 4b
|
||||
**Description :** Plus de payload audio reçu côté backend (POST /corrections/eo accepte uniquement `transcript`). La limite n'a plus lieu d'être.
|
||||
|
|
@ -182,6 +202,7 @@ Gate de qualité actuel : npm run test.
|
|||
---
|
||||
|
||||
### TD-18 — RLS Storage `audio-productions` non testée en intégration
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Résolu — Sprint 4b
|
||||
**Description :** Plus de bucket Storage backend à protéger. Les policies RLS de la migration 006 sont supprimées (DROP IF EXISTS) au profit d'un commentaire historique.
|
||||
|
|
@ -189,45 +210,51 @@ Gate de qualité actuel : npm run test.
|
|||
---
|
||||
|
||||
### TD-19 — Token Deepgram non rotatif côté frontend
|
||||
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert — introduit au Sprint 4b
|
||||
**Description :** `POST /transcriptions/token` retourne un token Deepgram éphémère valide 600 s (10 min). Une session EO T1 (2 min) tient largement, mais une session T3 (4:30) ou un enchaînement de 2 tâches dépasse la fenêtre si l'utilisateur prend des pauses. Si le token expire en cours de session, la connexion Deepgram drop sans renégociation automatique.
|
||||
**À faire (côté frontend Sprint 4c) :**
|
||||
|
||||
- Demander un nouveau token via `/transcriptions/token` à T-60 s avant expiration.
|
||||
- Reconnecter Deepgram en réutilisant la même session WebSocket si supporté.
|
||||
**Condition de résolution :** stratégie de rotation de token implémentée et testée côté frontend.
|
||||
**Condition de résolution :** stratégie de rotation de token implémentée et testée côté frontend.
|
||||
|
||||
---
|
||||
|
||||
### TD-20 — `transcribeAudio` (Gemini) sans consommateur
|
||||
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert — introduit au Sprint 4b
|
||||
**Description :** La fonction `transcribeAudio` dans `src/lib/gemini.ts` n'est plus appelée par le flux EO (Deepgram a remplacé Gemini batch). Conservée volontairement comme point d'extension futur pour TD-08 (évaluation phonologique séparée) ou un fallback si Deepgram est indisponible.
|
||||
**À faire :**
|
||||
|
||||
- Si TD-08 reste fermé 30 jours après la mise en prod du Sprint 4b sans plan d'usage, supprimer `transcribeAudio` et `gemini.ts` complet.
|
||||
**Condition de résolution :** décision sur TD-08 (résolution ou abandon).
|
||||
**Condition de résolution :** décision sur TD-08 (résolution ou abandon).
|
||||
|
||||
---
|
||||
|
||||
### TD-21 — Pas de rate limiting sur `/transcriptions/token`
|
||||
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert — introduit au Sprint 4b
|
||||
**Description :** Un utilisateur authentifié peut générer un nombre illimité de tokens Deepgram. Chaque token consomme un crédit côté Deepgram (selon usage de la connexion live qui suit). Un user malveillant pourrait scripter des appels en boucle pour épuiser le quota Deepgram.
|
||||
**À faire :**
|
||||
|
||||
- Ajouter un rate limit (par user, ex. 30 tokens/heure) via le middleware `rateLimit.ts` existant.
|
||||
**Condition de résolution :** middleware rate-limit branché sur la route et testé.
|
||||
**Condition de résolution :** middleware rate-limit branché sur la route et testé.
|
||||
|
||||
---
|
||||
|
||||
## 5. Historique des résolutions
|
||||
|
||||
| ID | Description | Résolu le | Comment |
|
||||
|---|---|---|---|
|
||||
| TD-02 | planController.ts complété | 2026-04-16 | Session Stripe |
|
||||
| TD-03 | stripe.ts complété | 2026-04-16 | Session Stripe |
|
||||
| TD-14 | Erreurs TS2835 + TS18046 + TS7053 corrigées | 2026-04-17 | Session build Render |
|
||||
| TD-10 | Analyse des patterns (Premium) livrée | 2026-04-25 | Sprint 3.6c |
|
||||
| TD-11 | Indice de préparation livré | 2026-04-25 | Sprint 3.6c |
|
||||
| TD-16 | Bucket Storage abandonné | 2026-04-25 | Sprint 4b — Deepgram direct |
|
||||
| TD-17 | Limite audio in-memory caduque | 2026-04-25 | Sprint 4b |
|
||||
| TD-18 | RLS Storage caduque | 2026-04-25 | Sprint 4b |
|
||||
| ID | Description | Résolu le | Comment |
|
||||
| ----- | ------------------------------------------- | ---------- | --------------------------- |
|
||||
| TD-02 | planController.ts complété | 2026-04-16 | Session Stripe |
|
||||
| TD-03 | stripe.ts complété | 2026-04-16 | Session Stripe |
|
||||
| TD-14 | Erreurs TS2835 + TS18046 + TS7053 corrigées | 2026-04-17 | Session build Render |
|
||||
| TD-10 | Analyse des patterns (Premium) livrée | 2026-04-25 | Sprint 3.6c |
|
||||
| TD-11 | Indice de préparation livré | 2026-04-25 | Sprint 3.6c |
|
||||
| TD-16 | Bucket Storage abandonné | 2026-04-25 | Sprint 4b — Deepgram direct |
|
||||
| TD-17 | Limite audio in-memory caduque | 2026-04-25 | Sprint 4b |
|
||||
| TD-18 | RLS Storage caduque | 2026-04-25 | Sprint 4b |
|
||||
|
|
|
|||
|
|
@ -1,178 +0,0 @@
|
|||
# TECH_DEBT.md — Expria / Coach TCF Canada
|
||||
|
||||
> **Document de référence — Version 1.0**
|
||||
> Ce document recense les décisions techniques prises par pragmatisme
|
||||
> qui devront être revisitées, les stubs temporaires, et les fonctionnalités
|
||||
> reportées. À mettre à jour après chaque session de développement.
|
||||
>
|
||||
> Format : chaque entrée a un identifiant (TD-XX), une priorité, et un statut.
|
||||
> Priorités : 🔴 Critique (bloque la production) / 🟡 Important / 🟢 Mineur
|
||||
|
||||
---
|
||||
|
||||
## 1. Stubs temporaires — à compléter
|
||||
|
||||
### TD-01 — src/lib/supabase.ts (backend)
|
||||
**Priorité :** 🔴 Critique
|
||||
**Statut :** Ouvert
|
||||
**Description :** Client Supabase créé comme stub. Fonctionne en développement avec les variables d'environnement mais n'a pas de gestion d'erreur robuste si `SUPABASE_URL` ou `SUPABASE_SERVICE_ROLE_KEY` sont absentes.
|
||||
**À faire :** Ajouter une validation au démarrage — si les variables manquent, le serveur refuse de démarrer avec un message clair.
|
||||
**Session concernée :** Initialisation backend
|
||||
|
||||
---
|
||||
|
||||
### TD-02 — src/lib/planController.ts (backend)
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Résolu — session Stripe
|
||||
**Description :** Stub créé pour permettre les tests de `updateUserPlan`. La vraie implémentation (mise à jour Supabase + gestion Stripe) n'est pas encore codée.
|
||||
**À faire :** Implémenter lors de la session Stripe (POST /stripe/webhook).
|
||||
**Session concernée :** Tests automatisés
|
||||
|
||||
---
|
||||
|
||||
### TD-03 — src/lib/stripe.ts (backend)
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Résolu — session Stripe
|
||||
**Description :** Stub créé pour permettre les tests de `verifyStripeWebhook` et `calculateProrata`. La vraie implémentation Stripe n'est pas encore codée.
|
||||
**À faire :** Implémenter lors de la session Stripe.
|
||||
**Session concernée :** Tests automatisés
|
||||
|
||||
---
|
||||
|
||||
## 2. Décisions pragmatiques — à revisiter
|
||||
|
||||
### TD-04 — Déploiement manuel (frontend + backend)
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert — accepté jusqu'aux premiers revenus
|
||||
**Description :** Cloudflare Pages et Render ne supportent pas l'auto-deploy depuis Codeberg. Le déploiement est manuel (CLI + dashboard).
|
||||
**À faire :** Migrer vers VPS Hetzner + Coolify pour restaurer l'auto-deploy. Voir ARCHITECTURE.md §9 Phase 2.
|
||||
**Condition de résolution :** Quand Expria génère ses premiers revenus réguliers.
|
||||
|
||||
---
|
||||
|
||||
### TD-05 — Comptes de test avec emails @gmail.com
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert
|
||||
**Description :** Les comptes de test utilisent `@gmail.com` au lieu de `@expria.local` prévu dans TEST_ENVIRONMENT.md. Raison : Supabase bloque la création d'utilisateurs avec des domaines non standards via l'API admin, et le dashboard est inaccessible depuis la Russie.
|
||||
**Emails actuels :**
|
||||
- `test.free@gmail.com`
|
||||
- `test.standard@gmail.com`
|
||||
- `test.premium@gmail.com`
|
||||
- `test.quota@gmail.com`
|
||||
**À faire :** Mettre à jour TEST_ENVIRONMENT.md pour refléter les vrais emails. Vérifier que la validation `@expria.local` dans le middleware n'est pas implémentée (elle ne l'est pas).
|
||||
|
||||
---
|
||||
|
||||
### TD-06 — Pas de migration SQL versionnée pour les tables initiales
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** Les tables `profiles` et `productions` ont été créées directement via SQL Editor, sans fichier de migration dans `supabase/migrations/`. Viole la Règle F de DEVELOPMENT_PRINCIPLES.md.
|
||||
**À faire :** Créer les fichiers de migration correspondants :
|
||||
- `supabase/migrations/001_create_profiles.sql`
|
||||
- `supabase/migrations/002_create_productions.sql`
|
||||
- `supabase/migrations/003_create_test_accounts.sql`
|
||||
**Impact :** Si la base doit être recréée (nouveau projet Supabase), les migrations permettent de tout reconstruire en une commande.
|
||||
|
||||
---
|
||||
|
||||
### TD-07 — Ancien projet Supabase partagé
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert — accepté temporairement
|
||||
**Description :** Le nouveau projet Expria V2 utilise la même base Supabase que l'ancien projet (en maintenance). Les anciennes tables ont été remplacées mais d'autres tables de l'ancien projet subsistent (`sujets`, `eo_t2_results`, `payment_transactions`, etc.).
|
||||
**À faire :** Nettoyer les tables inutilisées quand V2 est stable en production.
|
||||
**Tables à évaluer :** `anon_rate_limits`, `contact_submissions`, `eo_t2_results`, `events`, `payment_transactions`, `sujets`, `waitlist`
|
||||
**Condition de résolution :** Après 30 jours de production stable de V2.
|
||||
|
||||
---
|
||||
|
||||
### TD-13 — Webhook Stripe non idempotent
|
||||
**Priorité :** 🔴 Critique
|
||||
**Statut :** Ouvert — à faire avant mise en production
|
||||
**Description :** Stripe peut livrer un même event webhook deux fois (retries réseau, rejeu manuel depuis le dashboard). La route `POST /stripe/webhook` traite chaque réception sans dédoublonnage. En pratique, les opérations `updateUserPlan` et `updateUserStripeInfo` sont idempotentes par nature (même résultat en cas de double appel), mais si de la logique non idempotente est ajoutée plus tard (ex: compteur, envoi d'email, crédit utilisateur), un double traitement causerait un bug.
|
||||
**À faire :**
|
||||
- Créer une table `stripe_webhook_events(id TEXT PRIMARY KEY, processed_at TIMESTAMPTZ)`
|
||||
- Avant traitement, vérifier si `event.id` est déjà en base → si oui, retourner 200 sans rien faire
|
||||
- Après traitement, insérer l'`event.id` dans la table
|
||||
**Session concernée :** Stripe (POST /stripe/webhook)
|
||||
**Condition de résolution :** Avant la mise en production publique.
|
||||
|
||||
---
|
||||
|
||||
### TD-15 — Jobs asynchrones modèle/exercices : status peut rester "pending" indéfiniment
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert — introduit au Sprint 3.6a
|
||||
**Description :** Le flux POST /corrections/ee lance deux jobs DeepSeek en fire-and-forget (`runModeleJob`, `runExercicesJob` dans `correctionController.ts`). Si le process Node redémarre (deploy Render, crash, OOM) pendant l'exécution d'un de ces jobs, la colonne `exercices_status` ou `modele_status` reste figée à `'pending'` — l'utilisateur voit un loader infini côté frontend.
|
||||
**Impact actuel :** faible en conditions normales (DeepSeek répond en ~5-15 s, Render redémarre rarement). Perceptible uniquement si un deploy a lieu pendant une correction active.
|
||||
**À faire :**
|
||||
- Option 1 (simple) : job de reprise au boot → scanner `productions WHERE (exercices_status='pending' OR modele_status='pending') AND created_at < NOW() - INTERVAL '2 minutes'` → relancer.
|
||||
- Option 2 (robuste) : file d'attente persistée (pg-boss, BullMQ) au lieu de fire-and-forget.
|
||||
- Option 3 (minimal) : timeout côté frontend → si `pending` depuis > 2 min, afficher "La génération a échoué, réessayer ?" + endpoint `POST /simulations/:id/retry-jobs`.
|
||||
**Session concernée :** à planifier après livraison Sprint 3.6a/3.6b en prod stable.
|
||||
**Condition de résolution :** après 7 jours d'observation en prod avec monitoring des colonnes `*_status='pending'` âgées.
|
||||
|
||||
---
|
||||
|
||||
### TD-14 — Erreurs TypeScript TS2835 pré-existantes
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Résolu — session correction build TypeScript
|
||||
**Description :** Erreurs TS2835 sur plusieurs fichiers de routes.
|
||||
Non bloquant (tests verts) mais à corriger.
|
||||
Gate de qualité actuel : npm run test.
|
||||
**À faire :** Ajouter les extensions `.js` aux imports relatifs ou ajuster `moduleResolution` dans `tsconfig.json` pour permettre `npm run build` de passer.
|
||||
|
||||
---
|
||||
|
||||
## 3. Fonctionnalités reportées
|
||||
|
||||
### TD-08 — Phonologie T2 EO à 0
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** L'évaluation de la phonologie pour la T2 EO live est temporairement à 0 (non évaluée). L'évaluation se fait sur 4 critères au lieu de 5.
|
||||
**Raison :** La T2 live utilise un transcript texte — évaluer la phonologie nécessite l'audio brut, ce qui dépasse la limite de taille des requêtes.
|
||||
**À faire :** Implémenter l'évaluation phonologique via un endpoint séparé qui traite l'audio en chunks.
|
||||
**Session concernée :** T2 live (WebSocket)
|
||||
|
||||
---
|
||||
|
||||
### TD-09 — ScriptProcessorNode déprécié (T2 live)
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Reporté à après le lancement
|
||||
**Description :** Le traitement audio côté client utilise `ScriptProcessorNode` qui est déprécié. Doit être remplacé par `AudioWorklet`.
|
||||
**Impact :** Fonctionne mais génère des warnings dans la console. Peut poser problème sur certains navigateurs futurs.
|
||||
**À faire :** Migrer vers AudioWorklet après le lancement MVP.
|
||||
|
||||
---
|
||||
|
||||
### TD-10 — Analyse des patterns (Premium) non implémentée
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Planifié
|
||||
**Description :** La feature d'analyse des patterns sur les 5 dernières productions (Premium) n'est pas encore implémentée côté backend.
|
||||
**À faire :** Implémenter après les corrections EE/EO et Stripe.
|
||||
|
||||
---
|
||||
|
||||
### TD-11 — Indice de préparation non implémenté
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Planifié
|
||||
**Description :** Le calcul de l'indice de préparation (0-100) basé sur progression + régularité n'est pas encore implémenté.
|
||||
**À faire :** Implémenter en même temps que l'analyse des patterns (TD-10).
|
||||
|
||||
---
|
||||
|
||||
## 4. Tests à automatiser
|
||||
|
||||
### TD-12 — Tests manuels du Golden Dataset non automatisés
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Accepté — par conception
|
||||
**Description :** Les 41 tests du Golden Dataset sont manuels. Certains pourraient être automatisés (tests d'intégration HTTP avec Supertest).
|
||||
**À faire :** Ajouter des tests d'intégration pour les routes critiques après le lancement MVP.
|
||||
|
||||
---
|
||||
|
||||
## 5. Historique des résolutions
|
||||
|
||||
| ID | Description | Résolu le | Comment |
|
||||
|---|---|---|---|
|
||||
| TD-02 | planController.ts complété | 2026-04-16 | Session Stripe |
|
||||
| TD-03 | stripe.ts complété | 2026-04-16 | Session Stripe |
|
||||
| TD-14 | Erreurs TS2835 + TS18046 + TS7053 corrigées | 2026-04-17 | Session build Render |
|
||||
Loading…
Add table
Add a link
Reference in a new issue