# 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 ```javascript // 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 ```javascript // 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 ```javascript 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 |