expria-backend/docs/Prompt_t1live.md
Hermann_Kitio 74770b6402
Some checks are pending
CI / quality (push) Waiting to run
fix(t1-live): remove questionnaire dependency from T1 Live session
- buildT1SystemPrompt() now static (no reponses param); examiner
  formulates questions from what it hears in real-time audio stream
- Remove context guard + close 4004 CONTEXT_MISSING; Gemini session
  opens immediately after auth (aligns with T2 flow)
- Remove parseT1Context, validateReponses import from route
- Unknown WS message types silently ignored (debug log + return)
- Update Prompt_t1live.md and CHANGELOG-backend
- Tests: 309/309 green
2026-06-30 02:57:17 +03:00

198 lines
11 KiB
Markdown

# 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** (écoute en temps réel) | 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. 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`)
Le prompt est **statique** : aucune variable substituée. L'examinateur formule
ses relances à partir de ce qu'il **entend en temps réel** (son contexte audio
interne) — il n'existe ni sujet T1 en base, ni questionnaire pré-rempli (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.
Écoute attentivement ce que le candidat dit. Quand on te le signale, formule UNE question de relance courte (10-20 mots) liée à ce que le candidat vient de dire.
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.
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).
---
## 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).
La session Gemini s'ouvre **immédiatement après l'auth** (pas de message de
contexte ni de questionnaire). Le client envoie directement son audio. Tout
message non reconnu (ni `audio` ni `end`) est **ignoré silencieusement** (log
debug + return) — jamais de close.
### 4.1 Client → Backend
| Message | Forme | Effet |
| -------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
| 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` |
| 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`. |