docs(t1-live): contrat WS T1 + dette technique (Sprint 7a)

- Prompt_t1live.md : spec prompt examinateur T1, note anti-TD-22 (regle 7 du T2 non propagee), contrat WS complet (messages + close codes) pour le frontend 7b, comportement de fin (activityEnd final + relance terminale coupee).
- TECH_DEBT-backend.md : TD-23 (non-determinisme Gemini Live T1 + decouverte flush inputTranscription a activityEnd en VAD manuel), TD-24 (dette nommage gate oral_t2_live couvrant aussi T1), TD-25 (dette DRY runT1LiveCorrection ~= runT2LiveCorrection, report conscient).
This commit is contained in:
Hermann_Kitio 2026-06-29 22:10:15 +03:00
parent 868bd09397
commit 3722e2aaf5
2 changed files with 271 additions and 0 deletions

206
docs/Prompt_t1live.md Normal file
View file

@ -0,0 +1,206 @@
# Prompt_t1live.md — Expria Backend
# Spécification du prompt système T1 EO Live + contrat WebSocket
> **Document de référence — Sprint 7a**
> À lire conjointement avec `Prompt_t2live.md` (symétrie / divergences T1↔T2) et
> `TECH_DEBT-backend.md` (TD-22, TD-23, TD-24, TD-25).
> Source de vérité du prompt : `buildT1SystemPrompt` dans `src/lib/geminiLiveT1.ts`.
---
> **⚠ NOTE LIMINAIRE — anti-TD-22 (symétrie avec `Prompt_t2live.md §3`)**
>
> La **règle 7 du T2** (« STRICTE INTERDICTION DE POSER DES QUESTIONS / ban du
> point d'interrogation ») n'est **PAS** propagée au T1. En **Tâche 1**,
> l'examinateur **DOIT relancer le candidat par des questions** : c'est le cœur
> de son rôle. Le point d'interrogation est utilisé normalement.
>
> Le prompt T1 (`buildT1SystemPrompt`) et le prompt T2 (`buildT2SystemPrompt`)
> vivent dans des fonctions distinctes de `geminiLive.ts` / `geminiLiveT1.ts`
> précisément pour éviter toute contamination de règle.
---
## 1. Contexte pédagogique
La **Tâche 1** de l'Expression Orale TCF Canada est un **entretien dirigé** : le
candidat se présente (identité, parcours, situation familiale, loisirs, projet
d'immigration au Canada) sous forme de **monologue**, et l'examinateur le
**relance** ponctuellement par des questions courtes pour approfondir.
**Différence structurelle avec le T2 :**
| Axe | T1 (entretien dirigé) | T2 (interaction de service) |
| ----------------- | -------------------------------- | ---------------------------- |
| Qui mène | L'examinateur relance | Le candidat mène |
| Questions de l'IA | **Obligatoires** (relances) | **Interdites** (rôle passif) |
| Forme candidat | Monologue + relances | Dialogue |
| Subject-based | **Non** (questionnaire candidat) | Oui (table `sujets`) |
---
## 2. Rôle de l'IA (examinateur)
L'IA joue un **examinateur bienveillant** du TCF Canada. Son comportement :
1. **Silencieux par défaut.** Tant que le candidat parle, elle n'intervient
jamais de sa propre initiative.
2. **Relance sur signal uniquement.** Elle ne prend la parole que lorsque le
**backend** le lui signale (injection `clientContent`). C'est le backend —
via une **horloge probabiliste** — qui décide du **TIMING** ; l'examinateur,
lui, **formule librement** une relance courte à partir de son contexte audio
interne. Le backend ne lit PAS la transcription partielle pour décider
(Modèle 1 acté — cf. ROADMAP / `geminiLiveT1.ts`).
3. **Relance courte et unique.** Une seule question de 10 à 20 mots, liée à ce
que le candidat vient de dire ou à son contexte. Jamais d'enchaînement.
4. **Ton bienveillant et professionnel**, français B2-C1.
5. **N'évalue jamais** le candidat, ne corrige pas ses erreurs, ne commente pas
sa langue.
6. **Ne sort jamais du rôle**, ne mentionne jamais être une IA.
---
## 3. Prompt système (source : `buildT1SystemPrompt`)
Les variables `${...}` sont substituées dynamiquement depuis les réponses du
**questionnaire candidat** (`PresentationReponses`) — il n'existe pas de sujet T1
en base (T1 EO n'est PAS subject-based).
```
RÔLE : Tu es un examinateur bienveillant de l'épreuve d'Expression Orale du TCF Canada (Tâche 1, entretien dirigé). Le candidat se présente en monologue : identité, parcours, situation familiale, loisirs, et projet d'immigration au Canada.
CONTEXTE DU CANDIDAT (pour formuler des relances pertinentes et personnalisées) :
- Identité : ${reponses.prenom_age_ville}
- Formation / métier : ${reponses.formation_metier}
- Situation familiale : ${reponses.situation_familiale}
- Loisirs : ${reponses.loisirs}
- Projet Canada : ${reponses.motivation_canada}
RÈGLES :
1. Tu parles TOUJOURS en français naturel et courant, niveau B2-C1, sur un ton bienveillant et professionnel.
2. Tu RESTES SILENCIEUX par défaut. Tant que le candidat parle, tu n'interviens JAMAIS de ta propre initiative.
3. Tu prends la parole UNIQUEMENT lorsqu'on te le signale, et alors UNIQUEMENT pour relancer le candidat par UNE question.
4. Ta relance est COURTE : une seule question de 10 à 20 mots, liée à ce que le candidat vient de dire ou à son contexte ci-dessus.
5. Tu PEUX et tu DOIS poser des questions : c'est le cœur de ton rôle d'examinateur en Tâche 1. Utilise le point d'interrogation normalement.
6. Une seule question à la fois. Jamais de liste, jamais d'enchaînement de plusieurs questions dans la même prise de parole.
7. Tu ne corriges JAMAIS les erreurs du candidat et tu ne commentes jamais sa langue, ses erreurs ou sa performance.
8. Tu restes toujours dans ton rôle d'examinateur. Tu ne mentionnes jamais que tu es une IA ou un modèle.
```
> **⚠ Spécificité T1 — règle 5 :** elle est l'**exact inverse** de la règle 7 du
> T2. Toute fusion des deux prompts est interdite (TD-22 / TD-23).
**Variables à substituer dynamiquement** (depuis le questionnaire candidat, pas
d'un sujet en base) :
- `prenom_age_ville`, `formation_metier`, `situation_familiale`, `loisirs`,
`motivation_canada` — validés par `validateReponses`
(`presentationController.ts`).
---
## 4. Contrat WebSocket T1 (figé — la suite Sprint 7b en dépend)
Route : **`WS /t1/live?token=<jwt>`**
Auth : JWT Supabase + permission Premium `oral_t2_live` (réutilise
`authenticate` de `t2live.ts` — cf. dette de nommage TD-24).
### 4.1 Client → Backend
| Message | Forme | Effet |
| ---------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| Contexte (1er message obligatoire) | `{type:'context', reponses}` | Validé par `validateReponses` ; démarre la session Gemini. Absent/invalide → close `4004`. |
| Audio candidat | `{type:'audio', data}` (PCM 16 kHz base64) | Relayé à Gemini tant qu'un tour candidat est ouvert et qu'aucune interruption n'est en cours. |
| Fin de session | `{type:'end'}` | Déclenche `endSession()` (flush terminal + correction). |
### 4.2 Backend → Client
| Message | Forme | Sens |
| -------------------- | ---------------------------------------- | ---------------------------------------------------------------------------- |
| Audio examinateur | frames Gemini verbatim (PCM 24 kHz) | Relances audio de l'examinateur. |
| Début d'interruption | `{type:'interruption_start'}` | L'examinateur prend la parole ; le front doit suspendre la capture candidat. |
| Fin d'interruption | `{type:'interruption_end'}` | Le candidat peut reprendre. |
| Avertissement temps | `{type:'warning', message}` | 30 s avant le timeout (`T1_SESSION_WARNING_MS`). |
| Rapport final | `{type:'report', data}` + **close 1000** | Évaluation EO_T1 prête. |
| Erreur applicative | `{type:'error', code, message}` | Codes : `EMPTY_TRANSCRIPT`, `PERSISTENCE_FAILED`, `CORRECTION_FAILED`. |
### 4.3 Codes de fermeture WebSocket
| Close code | Cause | Origine |
| ---------- | -------------------------------------------------------- | ------------------------- |
| 1000 | Fin normale + rapport prêt (ou transcript vide) | `runT1LiveCorrection` |
| 1011 | `PERSISTENCE_FAILED` / `CORRECTION_FAILED` | `runT1LiveCorrection` |
| 4001 | `AUTH_REQUIRED` (JWT absent/invalide) | `authenticate` |
| 4003 | `PLAN_INSUFFICIENT` (pas Premium) | `authenticate` |
| 4004 | `CONTEXT_MISSING` (1er message contexte absent/invalide) | route `t1live.ts` |
| 4005 | `GEMINI_CONFIG` (clé API Gemini manquante côté serveur) | `openGeminiLiveT1Session` |
| 4006 | `GEMINI_DISCONNECTED` (WS Gemini fermé/erreur) | `openGeminiLiveT1Session` |
---
## 5. Comportement de fin de session (flush terminal)
**Contrainte VAD manuel (découverte spike — TD-23).** En VAD manuel
(`realtimeInputConfig.automaticActivityDetection.disabled = true`), Gemini ne
flushe `inputTranscription` (le texte candidat) **qu'à l'envoi d'un
`activityEnd`**, pas en continu. Le backend doit donc envoyer un **`activityEnd`
FINAL** aux bornes de tour pour récupérer le dernier segment candidat.
**Effet de bord et son traitement.** Cet `activityEnd` final déclenche AUSSI une
**relance examinateur « terminale »** non désirée. Elle est **coupée** :
- L'**audio** de cette relance terminale (`modelTurn … inlineData`) **n'est pas
forwardé** au client (le candidat ne l'entend jamais).
- Le **texte** de cette relance terminale (`outputTranscription`) est **jeté**
(non ajouté au transcript).
- Seul le **texte candidat final** (`inputTranscription`) est **conservé** pour
l'évaluation.
Le tri se fait **champ par champ** (pas message par message), car le segment
candidat à garder et la relance terminale à couper peuvent arriver dans le
**même** message Gemini. Implémentation : flag `terminalFlush` +
`T1_TERMINAL_FLUSH_GRACE_MS` (3 s) avant `finalize()`
(cf. `geminiLiveT1.ts`).
---
## 6. Évaluation finale (pipeline post-session)
`runT1LiveCorrection` (`src/routes/t1live.ts`) :
1. Insert `productions` : `tache='EO_T1'`, `sujet_id=null` (T1 non subject-based),
`mode='entrainement'`, `contenu=transcript`.
2. `correctEO(transcript, 'EO_T1', 9, null)` (DeepSeek — pas de consigne de
sujet en T1).
3. Phonologie = `PHONOLOGY_STUB` (TD-08 — pas d'audio brut côté backend) :
score textuel /16 + phonologie /4 = /20.
4. Update `productions` (`rapport`, `score`, `nclc`).
5. `{type:'report', data}` + close 1000.
> **Rappel TD-08 :** la phonologie live reste gelée (stub) tant qu'aucun audio
> brut n'est bufferisé côté backend.
---
## 7. Spécifications audio
| Direction | Format | Sample rate | Encoding |
| ----------------- | -------- | ----------- | ---------------------------- |
| Frontend → Gemini | PCM brut | 16 kHz | 16 bits, little-endian, mono |
| Gemini → Frontend | PCM brut | 24 kHz | 16 bits, little-endian, mono |
**MIME envoyé à Gemini :** `audio/pcm;rate=16000` (`T1_INPUT_AUDIO_MIME`).
---
## 8. Constantes de session (source : `geminiLiveT1.ts`)
| Constante | Valeur | Rôle |
| ------------------------------------- | --------------- | ------------------------------------------------- |
| `T1_SESSION_TIMEOUT_MS` | 180 000 | Filet de sécurité (fin forcée). |
| `T1_SESSION_WARNING_MS` | 150 000 | Émet `{type:'warning'}` 30 s avant timeout. |
| `T1_INTERRUPTION_P0/P1/P2` | 0.2 / 0.6 / 0.2 | Distribution du nombre de relances (0/1/2). |
| `T1_INTERRUPTION_WINDOW_START/END_MS` | 25 000 / 75 000 | Fenêtre où placer les relances. |
| `T1_INTERRUPTION_MIN_SPACING_MS` | 20 000 | Espacement minimal entre 2 relances. |
| `T1_TERMINAL_FLUSH_GRACE_MS` | 3 000 | Délai après `activityEnd` final avant `finalize`. |

View file

@ -256,6 +256,71 @@ Gate de qualité actuel : npm run test.
---
### TD-23 — Comportement Gemini Live T1 non déterministe + contrainte VAD manuel
**Priorité :** 🟡 Important
**Statut :** Ouvert — introduit au Sprint 7a
**Description :** Deux risques distincts sur le flux T1 Live (`geminiLiveT1.ts`) :
1. **Relance non garantie.** Comme pour le T2 (TD-22), le modèle Gemini Flash
Live n'offre aucune garantie déterministe : sur le signal d'injection
(`clientContent` de relance), il peut ignorer la consigne, formuler une
relance hors-sujet, enchaîner plusieurs questions, ou commenter la langue du
candidat malgré l'interdiction du prompt.
2. **Découverte spike — flush VAD manuel.** En VAD manuel
(`realtimeInputConfig.automaticActivityDetection.disabled = true`), Gemini ne
flushe `inputTranscription` (texte candidat) **qu'à l'envoi d'un
`activityEnd`**, pas en continu. Le backend doit donc envoyer `activityEnd`
aux bornes de tour pour récupérer le transcript. **Effet de bord :**
`activityEnd` déclenche AUSSI une réponse audio de l'examinateur (relance
« terminale »), qu'il faut couper en fin de session (audio non forwardé,
texte jeté — cf. `Prompt_t1live.md §5`).
**À faire :** Surveiller en tests manuels (Groupe D étendu) la pertinence des
relances et l'absence de relance terminale audible. Pistes si dérive :
renforcement du prompt, post-filtrage des sorties, ajustement du grace delay
(`T1_TERMINAL_FLUSH_GRACE_MS`).
**Session concernée :** T1 Live — Sprint 7a.
---
### TD-24 — Dette de nommage : `oral_t2_live` gate aussi le T1 Live
**Priorité :** 🟢 Mineur
**Statut :** Ouvert — introduit au Sprint 7a
**Description :** La route `WS /t1/live` réutilise `authenticate` de
`t2live.ts`, qui gate sur la permission `checkFeatureAccess(plan, 'oral_t2_live')`.
La feature `oral_t2_live` contrôle donc **aussi** l'accès au T1 Live, ce qui est
sémantiquement trompeur. Le couplage casserait si une différenciation d'accès
T1 vs T2 était souhaitée un jour (ex. T1 ouvert à Standard, T2 réservé Premium).
**À faire :** Renommer en `oral_live` (générique) ou introduire `oral_t1_live`
distinct. C'est une décision d'architecture + une migration `lib/access.ts`
(et potentiellement l'enum de features). Hors scope Sprint 7a.
**Session concernée :** T1 Live — Sprint 7a.
---
### TD-25 — Dette DRY : `runT1LiveCorrection``runT2LiveCorrection`
**Priorité :** 🟢 Mineur
**Statut :** Ouvert — report conscient (Sprint 7a)
**Description :** `runT1LiveCorrection` (`t1live.ts`) est à ~90 % identique à
`runT2LiveCorrection` (`t2live.ts`) : même pipeline (guard transcript vide →
insert `productions` → DeepSeek `correctEO` → phonologie stub → update → frame
`report` + close), mêmes codes d'erreur (`EMPTY_TRANSCRIPT`,
`PERSISTENCE_FAILED`, `CORRECTION_FAILED`) et de fermeture (1000 / 1011). Les
seules divergences : `tache` (`EO_T1` vs `EO_T2_LIVE`), `sujet_id` (`null` vs
`sujet.id`), arguments DeepSeek (`'EO_T1', 9, null` vs `'EO_T2', 9,
sujet.consigne`), signature (présence ou non de `sujet`), préfixe de log.
**À faire :** Factoriser en un helper partagé
`runEoLiveCorrection({ clientWs, profile, transcript, tache, sujetId, consigne, logTag })`.
**Report assumé :** factoriser à 2 cas seulement risque l'abstraction prématurée
et touche `t2live.ts` (stable, déjà commité). À factoriser quand un **3e cas
live** apparaîtra (ex. T3 Live).
**Session concernée :** T1 Live — Sprint 7a.
---
## 5. Historique des résolutions
| ID | Description | Résolu le | Comment |