expria-backend/docs/IMPLEMENTATION_T2_LIVE.md
2026-04-26 03:09:13 +03:00

26 KiB
Raw Blame History

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

// 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

// 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

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