fix(t1-live): remove questionnaire dependency from T1 Live session
Some checks are pending
CI / quality (push) Waiting to run
Some checks are pending
CI / quality (push) Waiting to run
- 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
This commit is contained in:
parent
01707c0b74
commit
74770b6402
6 changed files with 105 additions and 209 deletions
|
|
@ -24,7 +24,6 @@
|
|||
*/
|
||||
|
||||
import { WebSocket as NodeWebSocket } from "ws";
|
||||
import type { PresentationReponses } from "../controllers/presentationController.js";
|
||||
import {
|
||||
GEMINI_LIVE_URL,
|
||||
buildSetupFrame,
|
||||
|
|
@ -37,33 +36,28 @@ import {
|
|||
} from "./geminiLive.js";
|
||||
|
||||
/**
|
||||
* Construit le prompt système T1 Live à partir des réponses du questionnaire
|
||||
* candidat (transmises dynamiquement — il n'existe pas de sujet T1 en base).
|
||||
* Construit le prompt système T1 Live.
|
||||
*
|
||||
* L'examinateur formule ses relances à partir de ce qu'il ENTEND en temps réel
|
||||
* (son contexte audio interne) — il n'existe pas de sujet T1 en base et le flux
|
||||
* ne dépend plus d'un questionnaire pré-rempli.
|
||||
*
|
||||
* Le prompt définit le RÔLE de l'examinateur : il reste silencieux par défaut
|
||||
* et ne prend la parole QUE lorsque le backend le lui signale (injection
|
||||
* `clientContent` au moment choisi par l'horloge probabiliste). C'est le
|
||||
* BACKEND qui décide du TIMING ; l'examinateur, lui, formule librement une
|
||||
* relance courte à partir de son contexte audio interne.
|
||||
* relance courte à partir de ce que le candidat vient de dire.
|
||||
*/
|
||||
export function buildT1SystemPrompt(input: {
|
||||
reponses: PresentationReponses;
|
||||
}): string {
|
||||
const { reponses } = input;
|
||||
export function buildT1SystemPrompt(): string {
|
||||
return `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}
|
||||
É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 ou à son contexte ci-dessus.
|
||||
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.
|
||||
|
|
@ -160,8 +154,6 @@ export function planT1InterruptionInstants(
|
|||
// ── Options de session ───────────────────────────────────────────────────────
|
||||
|
||||
export interface OpenGeminiLiveT1SessionOptions {
|
||||
/** Réponses du questionnaire candidat (contexte du prompt T1). */
|
||||
reponses: PresentationReponses;
|
||||
/** Callback de fin de session avec le transcript reconstruit. */
|
||||
onSessionEnd?: (transcript: string) => void | Promise<void>;
|
||||
/** Override timeout (défaut T1_SESSION_TIMEOUT_MS). */
|
||||
|
|
@ -208,7 +200,7 @@ export function openGeminiLiveT1Session(
|
|||
const timeoutMs = opts.timeoutMs ?? T1_SESSION_TIMEOUT_MS;
|
||||
const warningMs = opts.warningMs ?? T1_SESSION_WARNING_MS;
|
||||
const random = opts.random ?? Math.random;
|
||||
const systemPrompt = buildT1SystemPrompt({ reponses: opts.reponses });
|
||||
const systemPrompt = buildT1SystemPrompt();
|
||||
|
||||
const url = `${GEMINI_LIVE_URL}?key=${apiKey}`;
|
||||
const factory =
|
||||
|
|
@ -440,12 +432,14 @@ export function openGeminiLiveT1Session(
|
|||
return;
|
||||
}
|
||||
const audioBase64 = parseAudioChunk(data);
|
||||
if (
|
||||
audioBase64 !== null &&
|
||||
!sessionEnded &&
|
||||
candidateTurnOpen &&
|
||||
!injecting
|
||||
) {
|
||||
if (audioBase64 === null) {
|
||||
// Message non reconnu (ni audio ni end). Notamment un éventuel
|
||||
// {type:'context'} envoyé par un ancien front : ignoré silencieusement —
|
||||
// jamais de crash ni de close. Cf. point de vigilance Patch 7a.
|
||||
console.debug("[T1] ignored non-audio client message");
|
||||
return;
|
||||
}
|
||||
if (!sessionEnded && candidateTurnOpen && !injecting) {
|
||||
geminiSend(
|
||||
JSON.stringify({
|
||||
realtimeInput: {
|
||||
|
|
@ -454,7 +448,6 @@ export function openGeminiLiveT1Session(
|
|||
}),
|
||||
);
|
||||
}
|
||||
// Tout autre message client est ignoré.
|
||||
});
|
||||
|
||||
clientWs.on("close", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue