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
150 lines
4.4 KiB
JavaScript
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);
|
|
}
|