realtime-voice-agents 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/assets/elevator-jazz.ulaw +1 -0
- package/assets/keyboard-typing.ulaw +1 -0
- package/assets/lofi.ulaw +1 -0
- package/assets/ringing.ulaw +1 -0
- package/assets/thinking-hum.ulaw +1 -0
- package/dist/BackgroundAudioPlayer-iMcivjis.mjs +282 -0
- package/dist/BackgroundAudioPlayer-jfRULWKC.cjs +313 -0
- package/dist/BaseRealtimeProvider-BQigr5mB.mjs +62 -0
- package/dist/BaseRealtimeProvider-BehPNT1r.d.cts +239 -0
- package/dist/BaseRealtimeProvider-ClP8Wx1X.d.mts +239 -0
- package/dist/BaseRealtimeProvider-DI4pKtOb.cjs +91 -0
- package/dist/GeminiLiveProvider-DvkTgzjG.d.cts +101 -0
- package/dist/GeminiLiveProvider-x2nyx5aO.d.mts +101 -0
- package/dist/InMemorySessionStore-B0_i-DOU.cjs +34 -0
- package/dist/InMemorySessionStore-B5_rq61L.d.cts +83 -0
- package/dist/InMemorySessionStore-B5_rq61L.d.mts +83 -0
- package/dist/InMemorySessionStore-DYParOJO.mjs +29 -0
- package/dist/OpenAICompatibleProvider-Bdtl-UXH.mjs +402 -0
- package/dist/OpenAICompatibleProvider-NS4cKVQj.cjs +409 -0
- package/dist/audio.cjs +21 -0
- package/dist/audio.d.cts +165 -0
- package/dist/audio.d.mts +165 -0
- package/dist/audio.mjs +4 -0
- package/dist/env-DSnGaERV.cjs +19 -0
- package/dist/env-DUwUWTsg.mjs +14 -0
- package/dist/events-BUMYdETO.d.cts +24 -0
- package/dist/events-BUMYdETO.d.mts +24 -0
- package/dist/events-BxDTIKKq.cjs +48 -0
- package/dist/events-BylBSBW-.mjs +43 -0
- package/dist/gemini.cjs +401 -0
- package/dist/gemini.d.cts +41 -0
- package/dist/gemini.d.mts +41 -0
- package/dist/gemini.mjs +395 -0
- package/dist/index.cjs +2142 -0
- package/dist/index.d.cts +920 -0
- package/dist/index.d.mts +920 -0
- package/dist/index.mjs +2113 -0
- package/dist/mulaw--cwU2c9L.mjs +64 -0
- package/dist/mulaw-DLUObjdP.cjs +117 -0
- package/dist/openai.cjs +61 -0
- package/dist/openai.d.cts +121 -0
- package/dist/openai.d.mts +121 -0
- package/dist/openai.mjs +53 -0
- package/dist/presets-Bf75YXs5.d.cts +24 -0
- package/dist/presets-Bf75YXs5.d.mts +24 -0
- package/dist/rest-BYqiVOhe.mjs +265 -0
- package/dist/rest-BvUKut_k.cjs +300 -0
- package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
- package/dist/session-config-BVLl7-ha.mjs +74 -0
- package/dist/session-config-c8sOw1XL.cjs +85 -0
- package/dist/store.cjs +3 -0
- package/dist/store.d.cts +2 -0
- package/dist/store.d.mts +2 -0
- package/dist/store.mjs +2 -0
- package/dist/testing.cjs +544 -0
- package/dist/testing.d.cts +191 -0
- package/dist/testing.d.mts +191 -0
- package/dist/testing.mjs +538 -0
- package/dist/transcode-C9aJG7_W.cjs +215 -0
- package/dist/transcode-CsAp97G9.mjs +198 -0
- package/dist/transport-B_PJFIVd.d.cts +155 -0
- package/dist/transport-CEaLFV4E.d.mts +155 -0
- package/dist/twilio.cjs +8 -0
- package/dist/twilio.d.cts +3 -0
- package/dist/twilio.d.mts +3 -0
- package/dist/twilio.mjs +2 -0
- package/dist/twiml-z9LjoF4_.d.cts +67 -0
- package/dist/twiml-z9LjoF4_.d.mts +67 -0
- package/dist/xai.cjs +92 -0
- package/dist/xai.d.cts +35 -0
- package/dist/xai.d.mts +35 -0
- package/dist/xai.mjs +86 -0
- package/package.json +183 -0
package/dist/gemini.mjs
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { a as noopLogger, n as deepMerge, t as BaseRealtimeProvider } from "./BaseRealtimeProvider-BQigr5mB.mjs";
|
|
2
|
+
import { t as resolveApiKey } from "./env-DUwUWTsg.mjs";
|
|
3
|
+
import { n as OutboundTranscoder, t as InboundTranscoder } from "./transcode-CsAp97G9.mjs";
|
|
4
|
+
//#region src/providers/gemini/GeminiLiveProvider.ts
|
|
5
|
+
/**
|
|
6
|
+
* Gemini Live provider via the official `@google/genai` SDK (optional peer).
|
|
7
|
+
*
|
|
8
|
+
* Unlike the OpenAI-compatible path, Gemini speaks PCM (16 kHz in, 24 kHz
|
|
9
|
+
* out), so this provider owns a stateful transcoding pair: μ-law↔PCM with
|
|
10
|
+
* inter-chunk resampler state — the engine stays μ-law-only.
|
|
11
|
+
*
|
|
12
|
+
* Gemini specifics handled here:
|
|
13
|
+
* - turn ids are synthesized (`gturn_N`) — the wire has no response ids
|
|
14
|
+
* - `serverContent.interrupted` is the barge-in signal (maps to
|
|
15
|
+
* userSpeechStarted + responseDone; the server already cancelled itself)
|
|
16
|
+
* - session resumption handles are captured continuously and replayed on the
|
|
17
|
+
* next connect, so reconnects restore context server-side
|
|
18
|
+
* - `goAway` (imminent disconnect, e.g. the session time limit) is surfaced
|
|
19
|
+
* so the session can reconnect proactively
|
|
20
|
+
* - tool results go through the proper `toolResponse` message
|
|
21
|
+
*/
|
|
22
|
+
const GEMINI_INPUT_MIME = "audio/pcm;rate=16000";
|
|
23
|
+
var GeminiLiveProvider = class extends BaseRealtimeProvider {
|
|
24
|
+
name = "gemini";
|
|
25
|
+
capabilities = {
|
|
26
|
+
truncate: false,
|
|
27
|
+
sessionUpdate: false,
|
|
28
|
+
voiceChangeMidSession: false,
|
|
29
|
+
transcodeRequired: true,
|
|
30
|
+
resumption: true,
|
|
31
|
+
agentTranscriptDeltas: true,
|
|
32
|
+
vadInterruptControl: false
|
|
33
|
+
};
|
|
34
|
+
config;
|
|
35
|
+
logger;
|
|
36
|
+
session = null;
|
|
37
|
+
sessionInit = null;
|
|
38
|
+
ready = false;
|
|
39
|
+
intentionalClose = false;
|
|
40
|
+
inbound = new InboundTranscoder(16e3);
|
|
41
|
+
outbound = new OutboundTranscoder(24e3);
|
|
42
|
+
turnCounter = 0;
|
|
43
|
+
currentTurnId = null;
|
|
44
|
+
userTranscriptBuffer = "";
|
|
45
|
+
agentTranscriptBuffer = "";
|
|
46
|
+
resumptionHandle = null;
|
|
47
|
+
resumed = false;
|
|
48
|
+
constructor(config, logger = noopLogger) {
|
|
49
|
+
super();
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.logger = logger;
|
|
52
|
+
}
|
|
53
|
+
get isConnected() {
|
|
54
|
+
return this.ready && this.session !== null;
|
|
55
|
+
}
|
|
56
|
+
/** True when the last connect restored server-side context via a handle. */
|
|
57
|
+
get didResume() {
|
|
58
|
+
return this.resumed;
|
|
59
|
+
}
|
|
60
|
+
async connect(init) {
|
|
61
|
+
if (this.session) await this.close();
|
|
62
|
+
this.sessionInit = init;
|
|
63
|
+
this.intentionalClose = false;
|
|
64
|
+
this.ready = false;
|
|
65
|
+
this.resumed = false;
|
|
66
|
+
this.inbound.reset();
|
|
67
|
+
this.outbound.reset();
|
|
68
|
+
const connector = this.config.connector ?? await this.defaultConnector();
|
|
69
|
+
const handle = init.freshSession ? void 0 : init.resumptionHandle ?? this.resumptionHandle ?? void 0;
|
|
70
|
+
if (init.freshSession) this.resumptionHandle = null;
|
|
71
|
+
let setupResolve;
|
|
72
|
+
let rejectSetup;
|
|
73
|
+
const setupDone = new Promise((resolve, reject) => {
|
|
74
|
+
setupResolve = resolve;
|
|
75
|
+
rejectSetup = reject;
|
|
76
|
+
});
|
|
77
|
+
let setupError = null;
|
|
78
|
+
setupDone.catch(() => {});
|
|
79
|
+
const setupReject = (error) => {
|
|
80
|
+
setupError ??= error;
|
|
81
|
+
rejectSetup(error);
|
|
82
|
+
};
|
|
83
|
+
const timeoutMs = this.config.connectTimeoutMs ?? 1e4;
|
|
84
|
+
const timer = setTimeout(() => setupReject(/* @__PURE__ */ new Error(`gemini setup not complete within ${timeoutMs}ms`)), timeoutMs);
|
|
85
|
+
timer.unref?.();
|
|
86
|
+
try {
|
|
87
|
+
try {
|
|
88
|
+
this.session = await connector({
|
|
89
|
+
model: this.config.model,
|
|
90
|
+
config: this.buildConfig(init, handle),
|
|
91
|
+
callbacks: {
|
|
92
|
+
onopen: () => this.emit("open"),
|
|
93
|
+
onmessage: (message) => {
|
|
94
|
+
if (message?.setupComplete !== void 0) {
|
|
95
|
+
this.ready = true;
|
|
96
|
+
if (handle) this.resumed = true;
|
|
97
|
+
setupResolve();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.handleMessage(message);
|
|
101
|
+
},
|
|
102
|
+
onerror: (error) => {
|
|
103
|
+
const err = error instanceof Error ? error : new Error(String(error?.message ?? error));
|
|
104
|
+
if (!this.ready) setupReject(err);
|
|
105
|
+
else this.emit("error", err);
|
|
106
|
+
},
|
|
107
|
+
onclose: (event) => {
|
|
108
|
+
const wasReady = this.ready;
|
|
109
|
+
this.ready = false;
|
|
110
|
+
this.session = null;
|
|
111
|
+
if (!wasReady) {
|
|
112
|
+
setupReject(/* @__PURE__ */ new Error(`gemini closed during setup (${event?.code} ${event?.reason ?? ""})`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.emit("close", {
|
|
116
|
+
code: event?.code,
|
|
117
|
+
reason: event?.reason,
|
|
118
|
+
retriable: !this.intentionalClose && !isNonRetriableClose(event?.code)
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw setupError ?? (error instanceof Error ? error : new Error(String(error)));
|
|
125
|
+
}
|
|
126
|
+
await setupDone;
|
|
127
|
+
} finally {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async defaultConnector() {
|
|
132
|
+
let genai;
|
|
133
|
+
try {
|
|
134
|
+
genai = await import("@google/genai");
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw new Error("the '@google/genai' package is required for the Gemini provider — npm install @google/genai", { cause: error });
|
|
137
|
+
}
|
|
138
|
+
const { GoogleGenAI } = genai;
|
|
139
|
+
const ai = this.config.vertex ? new GoogleGenAI({
|
|
140
|
+
vertexai: true,
|
|
141
|
+
project: this.config.vertex.project,
|
|
142
|
+
location: this.config.vertex.location
|
|
143
|
+
}) : new GoogleGenAI({ apiKey: this.config.apiKey });
|
|
144
|
+
return (params) => ai.live.connect({
|
|
145
|
+
model: params.model,
|
|
146
|
+
config: params.config,
|
|
147
|
+
callbacks: params.callbacks
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
buildConfig(init, resumptionHandle) {
|
|
151
|
+
const transcription = this.config.transcription;
|
|
152
|
+
const vad = init.vad !== void 0 ? init.vad : this.config.defaultVad;
|
|
153
|
+
const config = {
|
|
154
|
+
responseModalities: ["AUDIO"],
|
|
155
|
+
systemInstruction: init.instructions,
|
|
156
|
+
speechConfig: {
|
|
157
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: init.voice ?? this.config.voice ?? "Aoede" } },
|
|
158
|
+
...this.config.languageCode ? { languageCode: this.config.languageCode } : {}
|
|
159
|
+
},
|
|
160
|
+
...init.tools?.length ? { tools: [{ functionDeclarations: init.tools.map((tool) => ({
|
|
161
|
+
name: tool.name,
|
|
162
|
+
description: tool.description ?? "",
|
|
163
|
+
parameters: tool.parameters
|
|
164
|
+
})) }] } : {},
|
|
165
|
+
realtimeInputConfig: { automaticActivityDetection: buildGeminiVad(vad) },
|
|
166
|
+
...transcription === false ? {} : {
|
|
167
|
+
...transcription?.input !== false ? { inputAudioTranscription: {} } : {},
|
|
168
|
+
...transcription?.output !== false ? { outputAudioTranscription: {} } : {}
|
|
169
|
+
},
|
|
170
|
+
...this.config.resumption !== false ? { sessionResumption: { ...resumptionHandle ? { handle: resumptionHandle } : {} } } : {},
|
|
171
|
+
...this.config.contextWindowCompression ? { contextWindowCompression: { slidingWindow: {} } } : {}
|
|
172
|
+
};
|
|
173
|
+
return deepMerge(deepMerge(config, init.providerOptions), this.config.extraConfig);
|
|
174
|
+
}
|
|
175
|
+
async close() {
|
|
176
|
+
this.intentionalClose = true;
|
|
177
|
+
this.ready = false;
|
|
178
|
+
const session = this.session;
|
|
179
|
+
this.session = null;
|
|
180
|
+
if (!session) return;
|
|
181
|
+
try {
|
|
182
|
+
session.close();
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
sendAudio(base64Mulaw) {
|
|
186
|
+
if (!this.isConnected || !this.session) return;
|
|
187
|
+
const pcm = this.inbound.process(base64Mulaw);
|
|
188
|
+
if (pcm.length === 0) return;
|
|
189
|
+
this.session.sendRealtimeInput({ audio: {
|
|
190
|
+
data: pcm,
|
|
191
|
+
mimeType: GEMINI_INPUT_MIME
|
|
192
|
+
} });
|
|
193
|
+
}
|
|
194
|
+
sendText(text, options = {}) {
|
|
195
|
+
if (!this.session) return;
|
|
196
|
+
const role = options.role === "assistant" ? "model" : "user";
|
|
197
|
+
const content = options.role === "system" ? `(system note) ${text}` : text;
|
|
198
|
+
this.session.sendClientContent({
|
|
199
|
+
turns: [{
|
|
200
|
+
role,
|
|
201
|
+
parts: [{ text: content }]
|
|
202
|
+
}],
|
|
203
|
+
turnComplete: options.triggerResponse !== false
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
sendToolResult(callId, output, options = {}) {
|
|
207
|
+
if (!this.session) return;
|
|
208
|
+
const response = output !== null && typeof output === "object" && !Array.isArray(output) ? output : { result: output };
|
|
209
|
+
this.session.sendToolResponse({ functionResponses: [{
|
|
210
|
+
id: callId,
|
|
211
|
+
name: this.toolNameForCall(callId),
|
|
212
|
+
response
|
|
213
|
+
}] });
|
|
214
|
+
}
|
|
215
|
+
createResponse(options = {}) {
|
|
216
|
+
if (!this.session) return;
|
|
217
|
+
const text = options.instructions ? `(instruction) ${options.instructions}` : "(instruction) Continue the conversation now — speak to the caller.";
|
|
218
|
+
this.session.sendClientContent({
|
|
219
|
+
turns: [{
|
|
220
|
+
role: "user",
|
|
221
|
+
parts: [{ text }]
|
|
222
|
+
}],
|
|
223
|
+
turnComplete: true
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async updateSession(_patch) {
|
|
227
|
+
this.logger.warn("gemini: updateSession ignored (no mid-session config on Live API)");
|
|
228
|
+
}
|
|
229
|
+
handleMessage(message) {
|
|
230
|
+
const serverContent = message?.serverContent;
|
|
231
|
+
if (message?.toolCall?.functionCalls) {
|
|
232
|
+
for (const call of message.toolCall.functionCalls) {
|
|
233
|
+
const id = call.id ?? `gcall_${++this.turnCounter}`;
|
|
234
|
+
this.callNames.set(id, call.name);
|
|
235
|
+
this.emit("toolCall", {
|
|
236
|
+
id,
|
|
237
|
+
name: call.name,
|
|
238
|
+
argumentsJson: JSON.stringify(call.args ?? {}),
|
|
239
|
+
responseId: this.currentTurnId ?? void 0
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (message?.sessionResumptionUpdate) {
|
|
245
|
+
const update = message.sessionResumptionUpdate;
|
|
246
|
+
if (update.resumable && update.newHandle) {
|
|
247
|
+
this.resumptionHandle = update.newHandle;
|
|
248
|
+
this.emit("resumptionUpdate", update.newHandle);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (message?.goAway) {
|
|
253
|
+
const timeLeftMs = parseGoAwayTime(message.goAway.timeLeft);
|
|
254
|
+
this.logger.info("gemini goAway received", { timeLeftMs });
|
|
255
|
+
this.emit("goAway", { timeLeftMs });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (message?.usageMetadata) {
|
|
259
|
+
const usage = message.usageMetadata;
|
|
260
|
+
this.emit("usage", {
|
|
261
|
+
inputTokens: usage.promptTokenCount ?? 0,
|
|
262
|
+
outputTokens: usage.responseTokenCount ?? usage.candidatesTokenCount ?? 0,
|
|
263
|
+
totalTokens: usage.totalTokenCount ?? 0,
|
|
264
|
+
raw: usage
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (!serverContent) return;
|
|
268
|
+
if (serverContent.interrupted) {
|
|
269
|
+
const turnId = this.currentTurnId;
|
|
270
|
+
this.emit("userSpeechStarted");
|
|
271
|
+
if (turnId) {
|
|
272
|
+
this.flushAgentTranscript(turnId);
|
|
273
|
+
this.currentTurnId = null;
|
|
274
|
+
this.emit("responseDone", { responseId: turnId });
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (serverContent.inputTranscription?.text) this.userTranscriptBuffer += serverContent.inputTranscription.text;
|
|
279
|
+
if (serverContent.outputTranscription?.text) {
|
|
280
|
+
const turnId = this.ensureTurn();
|
|
281
|
+
this.agentTranscriptBuffer += serverContent.outputTranscription.text;
|
|
282
|
+
this.emit("agentTranscriptDelta", {
|
|
283
|
+
responseId: turnId,
|
|
284
|
+
delta: serverContent.outputTranscription.text
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const parts = serverContent.modelTurn?.parts ?? [];
|
|
288
|
+
for (const part of parts) if (part.inlineData?.data) {
|
|
289
|
+
const turnId = this.ensureTurn();
|
|
290
|
+
const mulaw = this.outbound.process(part.inlineData.data);
|
|
291
|
+
if (mulaw.length > 0) this.emit("audio", {
|
|
292
|
+
base64Mulaw: mulaw,
|
|
293
|
+
responseId: turnId
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (serverContent.turnComplete) {
|
|
297
|
+
this.flushUserTranscript();
|
|
298
|
+
const turnId = this.currentTurnId;
|
|
299
|
+
if (turnId) {
|
|
300
|
+
this.flushAgentTranscript(turnId);
|
|
301
|
+
this.currentTurnId = null;
|
|
302
|
+
this.emit("responseDone", { responseId: turnId });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
callNames = /* @__PURE__ */ new Map();
|
|
307
|
+
toolNameForCall(callId) {
|
|
308
|
+
const name = this.callNames.get(callId) ?? callId;
|
|
309
|
+
this.callNames.delete(callId);
|
|
310
|
+
return name;
|
|
311
|
+
}
|
|
312
|
+
ensureTurn() {
|
|
313
|
+
if (!this.currentTurnId) {
|
|
314
|
+
this.currentTurnId = `gturn_${++this.turnCounter}`;
|
|
315
|
+
this.flushUserTranscript();
|
|
316
|
+
this.emit("responseStarted", { responseId: this.currentTurnId });
|
|
317
|
+
}
|
|
318
|
+
return this.currentTurnId;
|
|
319
|
+
}
|
|
320
|
+
flushUserTranscript() {
|
|
321
|
+
const text = this.userTranscriptBuffer.trim();
|
|
322
|
+
this.userTranscriptBuffer = "";
|
|
323
|
+
if (text) this.emit("userTranscript", { text });
|
|
324
|
+
}
|
|
325
|
+
flushAgentTranscript(turnId) {
|
|
326
|
+
const text = this.agentTranscriptBuffer.trim();
|
|
327
|
+
this.agentTranscriptBuffer = "";
|
|
328
|
+
if (text) this.emit("agentTranscript", {
|
|
329
|
+
responseId: turnId,
|
|
330
|
+
text
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
/** Protocol/auth/policy closes where retrying cannot help. */
|
|
335
|
+
function isNonRetriableClose(code) {
|
|
336
|
+
if (code === void 0) return false;
|
|
337
|
+
return code === 1002 || code === 1003 || code === 1007 || code === 1008 || code >= 4400 && code < 4500;
|
|
338
|
+
}
|
|
339
|
+
function buildGeminiVad(vad) {
|
|
340
|
+
if (vad === null) return { disabled: true };
|
|
341
|
+
return {
|
|
342
|
+
disabled: false,
|
|
343
|
+
...vad?.startSensitivity ? { startOfSpeechSensitivity: `START_SENSITIVITY_${vad.startSensitivity.toUpperCase()}` } : {},
|
|
344
|
+
...vad?.endSensitivity ? { endOfSpeechSensitivity: `END_SENSITIVITY_${vad.endSensitivity.toUpperCase()}` } : {},
|
|
345
|
+
...vad?.prefixPaddingMs !== void 0 ? { prefixPaddingMs: vad.prefixPaddingMs } : {},
|
|
346
|
+
...vad?.silenceDurationMs !== void 0 ? { silenceDurationMs: vad.silenceDurationMs } : {}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function parseGoAwayTime(timeLeft) {
|
|
350
|
+
if (typeof timeLeft === "number") return timeLeft;
|
|
351
|
+
if (typeof timeLeft === "string") {
|
|
352
|
+
const seconds = Number.parseFloat(timeLeft.replace(/s$/, ""));
|
|
353
|
+
if (!Number.isNaN(seconds)) return Math.round(seconds * 1e3);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/gemini.ts
|
|
358
|
+
/** Google Gemini Live provider — `realtime-voice-agents/gemini`. */
|
|
359
|
+
/** Env vars checked (in order) when no explicit apiKey is passed. */
|
|
360
|
+
const GEMINI_KEY_ENV_VARS = [
|
|
361
|
+
"GOOGLE_API_KEY",
|
|
362
|
+
"GEMINI_API_KEY",
|
|
363
|
+
"GOOGLE_GENAI_API_KEY"
|
|
364
|
+
];
|
|
365
|
+
const GEMINI_DEFAULT_MODEL = "gemini-2.5-flash-native-audio-preview-12-2025";
|
|
366
|
+
const GEMINI_DEFAULT_VOICE = "Aoede";
|
|
367
|
+
const GEMINI_VOICES = [
|
|
368
|
+
"Aoede",
|
|
369
|
+
"Charon",
|
|
370
|
+
"Fenrir",
|
|
371
|
+
"Kore",
|
|
372
|
+
"Puck"
|
|
373
|
+
];
|
|
374
|
+
/** Create a Gemini Live provider factory for the bridge. */
|
|
375
|
+
function geminiLive(options = {}) {
|
|
376
|
+
const apiKey = resolveApiKey(options.apiKey, GEMINI_KEY_ENV_VARS);
|
|
377
|
+
if (!apiKey && !options.vertex && !options.connector) throw new Error(`geminiLive: credentials missing (pass apiKey, set one of ${GEMINI_KEY_ENV_VARS.join("/")}, or configure vertex)`);
|
|
378
|
+
const config = {
|
|
379
|
+
apiKey,
|
|
380
|
+
vertex: options.vertex,
|
|
381
|
+
model: options.model ?? "gemini-2.5-flash-native-audio-preview-12-2025",
|
|
382
|
+
voice: options.voice ?? "Aoede",
|
|
383
|
+
languageCode: options.languageCode,
|
|
384
|
+
defaultVad: options.vad,
|
|
385
|
+
transcription: options.transcription,
|
|
386
|
+
resumption: options.resumption,
|
|
387
|
+
contextWindowCompression: options.contextWindowCompression,
|
|
388
|
+
extraConfig: options.config,
|
|
389
|
+
connectTimeoutMs: options.connectTimeoutMs,
|
|
390
|
+
connector: options.connector
|
|
391
|
+
};
|
|
392
|
+
return ({ logger }) => new GeminiLiveProvider(config, logger);
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
export { GEMINI_DEFAULT_MODEL, GEMINI_DEFAULT_VOICE, GEMINI_KEY_ENV_VARS, GEMINI_VOICES, GeminiLiveProvider, geminiLive };
|