// test-gemini-live.js — Sprint 6d : debug du setup frame Gemini Live via SDK. // // Usage : // node --env-file=.env test-gemini-live.js minimal // node --env-file=.env test-gemini-live.js +system // node --env-file=.env test-gemini-live.js +transcription // node --env-file=.env test-gemini-live.js +vad // // Chaque mode part du `minimal` qui doit fonctionner avec une clé Express // Mode et ajoute UN champ. Si le mode reçoit `setupComplete` → le champ est // accepté. Si l'ouverture échoue → c'est ce champ qui pose problème. // // Migration Sprint 6d : passage du WebSocket brut au SDK officiel // `@google/genai` qui gère l'auth Express Mode automatiquement. import { GoogleGenAI, Modality, StartSensitivity, EndSensitivity, } from "@google/genai"; const MODES = ["minimal", "+system", "+transcription", "+vad"]; const mode = process.argv[2] ?? "minimal"; if (!MODES.includes(mode)) { console.error( `❌ Mode inconnu : "${mode}". Modes valides : ${MODES.join(", ")}`, ); process.exit(1); } const KEY = process.env.GEMINI_API_KEY; if (!KEY) { console.error("❌ GEMINI_API_KEY manquante dans l'env"); process.exit(1); } // Modèle par défaut Sprint 6d. Fallback documenté : `gemini-2.0-flash-live-001`. const MODEL = "gemini-3.1-flash-live-preview"; const SAMPLE_PROMPT = "Tu joues le rôle d'un bailleur. Tu réponds uniquement en français. " + "Tu attends que ton interlocuteur s'adresse à toi avant de parler."; function buildConfig(mode) { // Base minimal — équivalent au mode `minimal` qui doit fonctionner. const config = { responseModalities: [Modality.AUDIO], }; if (mode === "+system") { config.systemInstruction = SAMPLE_PROMPT; } if (mode === "+transcription") { config.inputAudioTranscription = {}; config.outputAudioTranscription = {}; } if (mode === "+vad") { config.realtimeInputConfig = { automaticActivityDetection: { disabled: false, startOfSpeechSensitivity: StartSensitivity.START_SENSITIVITY_LOW, endOfSpeechSensitivity: EndSensitivity.END_SENSITIVITY_LOW, silenceDurationMs: 2000, }, }; } return config; } const ai = new GoogleGenAI({ vertexai: true, apiKey: KEY }); console.log(`→ Mode : ${mode}`); console.log(`→ Modèle : ${MODEL}`); console.log("→ Connexion à Gemini Live (via SDK)…"); let setupCompleteReceived = false; let resolved = false; const config = buildConfig(mode); console.log("→ Config envoyée :"); console.log(JSON.stringify(config, null, 2)); const timeoutId = setTimeout(() => { if (!resolved) { console.log("⏱ Timeout 15 s — pas de setupComplete reçu."); process.exit(setupCompleteReceived ? 0 : 1); } }, 15000); try { const session = await ai.live.connect({ model: MODEL, config, callbacks: { onopen: () => { console.log("✅ Connexion ouverte"); }, onmessage: (msg) => { // Compat : selon la version du SDK, setupComplete arrive soit comme // propriété directe, soit dans serverContent. On loggue tout. console.log("📨 Message reçu :", JSON.stringify(msg).slice(0, 600)); if (msg.setupComplete || msg?.serverContent?.setupComplete) { setupCompleteReceived = true; resolved = true; console.log( `\n🎉 [${mode}] ACCEPTÉ — setupComplete reçu (modèle ${MODEL}).`, ); clearTimeout(timeoutId); try { session.close(); } catch { /* ignore */ } process.exit(0); } }, onerror: (err) => { console.log("❌ Erreur :", err?.message ?? err); }, onclose: (evt) => { console.log( `🔒 Fermeture${evt?.code ? ` — code ${evt.code}` : ""}${evt?.reason ? ` reason: ${evt.reason}` : ""}`, ); if (!setupCompleteReceived) { console.log(`\n⚠ [${mode}] REJETÉ — fermeture avant setupComplete.`); console.log( "→ Le ou les champs ajoutés par ce mode ne sont pas acceptés.", ); } resolved = true; clearTimeout(timeoutId); process.exit(setupCompleteReceived ? 0 : 1); }, }, }); // Conserver la session vivante jusqu'au timeout/setupComplete. void session; } catch (err) { resolved = true; clearTimeout(timeoutId); console.log( "❌ live.connect a échoué :", err instanceof Error ? err.message : String(err), ); process.exit(1); }