expria-backend/test-gemini-live.js
Hermann_Kitio 0662e766d4 Sprint 6d — Migrate Gemini Live to @google/genai SDK
feat(geminiLive): rewrite with GoogleGenAI SDK (vertexai: true, apiKey)
  replaces raw WebSocket to generativelanguage.googleapis.com
feat(geminiLive): restore full setup config (systemInstruction,
  inputAudioTranscription, outputAudioTranscription, VAD)
fix(geminiLive): buildSetupFrame → SDK config object (no manual JSON)
fix(useT2LiveSession): cancelTokenRef for idempotent startDialogue,
  closeAllRef for stable unmount cleanup
chore: add @google/genai@^1.50.1 dependency
test: 11 geminiLive tests rewritten with SDK mock
  292/292 backend tests green
2026-04-27 02:25:58 +03:00

150 lines
4.4 KiB
JavaScript

// 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);
}