diff --git a/docs/Prompt_t1live.md b/docs/Prompt_t1live.md new file mode 100644 index 0000000..60c4304 --- /dev/null +++ b/docs/Prompt_t1live.md @@ -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=`** +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`. | diff --git a/docs/TECH_DEBT-backend.md b/docs/TECH_DEBT-backend.md index 28af635..29746fb 100644 --- a/docs/TECH_DEBT-backend.md +++ b/docs/TECH_DEBT-backend.md @@ -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 |