@gakr-gakr/google-meet 0.1.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.
@@ -0,0 +1,84 @@
1
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
2
+
3
+ type LegacyConfigRule = {
4
+ path: Array<string | number>;
5
+ message: string;
6
+ match: (value: unknown) => boolean;
7
+ };
8
+
9
+ function asRecord(value: unknown): Record<string, unknown> | null {
10
+ return value && typeof value === "object" && !Array.isArray(value)
11
+ ? (value as Record<string, unknown>)
12
+ : null;
13
+ }
14
+
15
+ function normalizeProviderId(value: unknown): string | undefined {
16
+ return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : undefined;
17
+ }
18
+
19
+ function hasOwn(record: Record<string, unknown>, key: string): boolean {
20
+ return Object.prototype.hasOwnProperty.call(record, key);
21
+ }
22
+
23
+ function hasLegacyGoogleRealtimeProvider(value: unknown): boolean {
24
+ const realtime = asRecord(value);
25
+ if (!realtime || normalizeProviderId(realtime.provider) !== "google") {
26
+ return false;
27
+ }
28
+ return !hasOwn(realtime, "voiceProvider") || !hasOwn(realtime, "transcriptionProvider");
29
+ }
30
+
31
+ export const legacyConfigRules: LegacyConfigRule[] = [
32
+ {
33
+ path: ["plugins", "entries", "google-meet", "config", "realtime"],
34
+ message:
35
+ 'plugins.entries.google-meet.config.realtime.provider="google" is legacy for Gemini Live bidi mode; use realtime.voiceProvider="google" and realtime.transcriptionProvider="openai". Run "autobot doctor --fix".',
36
+ match: hasLegacyGoogleRealtimeProvider,
37
+ },
38
+ ];
39
+
40
+ export function migrateGoogleMeetLegacyRealtimeProvider(config: AutoBotConfig): {
41
+ config: AutoBotConfig;
42
+ changes: string[];
43
+ } | null {
44
+ const rawEntry = asRecord(config.plugins?.entries?.["google-meet"]);
45
+ const rawPluginConfig = asRecord(rawEntry?.config);
46
+ const rawRealtime = asRecord(rawPluginConfig?.realtime);
47
+ if (!rawRealtime || !hasLegacyGoogleRealtimeProvider(rawRealtime)) {
48
+ return null;
49
+ }
50
+
51
+ const nextConfig = structuredClone(config);
52
+ const nextPlugins = asRecord(nextConfig.plugins) ?? {};
53
+ nextConfig.plugins = nextPlugins;
54
+ const nextEntries = asRecord(nextPlugins.entries) ?? {};
55
+ nextPlugins.entries = nextEntries;
56
+ const nextEntry = asRecord(nextEntries["google-meet"]) ?? {};
57
+ nextEntries["google-meet"] = nextEntry;
58
+ const nextPluginConfig = asRecord(nextEntry.config) ?? {};
59
+ nextEntry.config = nextPluginConfig;
60
+ const nextRealtime = asRecord(nextPluginConfig.realtime) ?? {};
61
+ nextPluginConfig.realtime = nextRealtime;
62
+
63
+ nextRealtime.provider = "openai";
64
+ if (!hasOwn(nextRealtime, "transcriptionProvider")) {
65
+ nextRealtime.transcriptionProvider = "openai";
66
+ }
67
+ if (!hasOwn(nextRealtime, "voiceProvider")) {
68
+ nextRealtime.voiceProvider = "google";
69
+ }
70
+
71
+ return {
72
+ config: nextConfig,
73
+ changes: [
74
+ 'Moved Google Meet legacy realtime.provider="google" intent to realtime.voiceProvider="google" and realtime.transcriptionProvider="openai".',
75
+ ],
76
+ };
77
+ }
78
+
79
+ export function normalizeCompatibilityConfig({ cfg }: { cfg: AutoBotConfig }): {
80
+ config: AutoBotConfig;
81
+ changes: string[];
82
+ } {
83
+ return migrateGoogleMeetLegacyRealtimeProvider(cfg) ?? { config: cfg, changes: [] };
84
+ }
package/src/config.ts ADDED
@@ -0,0 +1,589 @@
1
+ import {
2
+ REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME,
3
+ resolveRealtimeVoiceAgentConsultToolPolicy,
4
+ type RealtimeVoiceAgentConsultToolPolicy,
5
+ } from "autobot/plugin-sdk/realtime-voice";
6
+ import {
7
+ normalizeOptionalLowercaseString,
8
+ normalizeOptionalString,
9
+ } from "autobot/plugin-sdk/string-coerce-runtime";
10
+
11
+ export type GoogleMeetTransport = "chrome" | "chrome-node" | "twilio";
12
+ export type GoogleMeetMode = "agent" | "bidi" | "transcribe";
13
+ export type GoogleMeetModeInput = GoogleMeetMode | "realtime";
14
+ export type GoogleMeetRealtimeStrategy = "agent" | "bidi";
15
+ type GoogleMeetChromeAudioFormat = "pcm16-24khz" | "g711-ulaw-8khz";
16
+ export type GoogleMeetToolPolicy = RealtimeVoiceAgentConsultToolPolicy;
17
+
18
+ export type GoogleMeetConfig = {
19
+ enabled: boolean;
20
+ defaults: {
21
+ meeting?: string;
22
+ };
23
+ preview: {
24
+ enrollmentAcknowledged: boolean;
25
+ };
26
+ defaultTransport: GoogleMeetTransport;
27
+ defaultMode: GoogleMeetMode;
28
+ chrome: {
29
+ audioBackend: "blackhole-2ch";
30
+ audioFormat: GoogleMeetChromeAudioFormat;
31
+ audioBufferBytes: number;
32
+ launch: boolean;
33
+ browserProfile?: string;
34
+ guestName: string;
35
+ reuseExistingTab: boolean;
36
+ autoJoin: boolean;
37
+ joinTimeoutMs: number;
38
+ waitForInCallMs: number;
39
+ audioInputCommand?: string[];
40
+ audioOutputCommand?: string[];
41
+ bargeInInputCommand?: string[];
42
+ bargeInRmsThreshold: number;
43
+ bargeInPeakThreshold: number;
44
+ bargeInCooldownMs: number;
45
+ audioBridgeCommand?: string[];
46
+ audioBridgeHealthCommand?: string[];
47
+ };
48
+ chromeNode: {
49
+ node?: string;
50
+ };
51
+ twilio: {
52
+ defaultDialInNumber?: string;
53
+ defaultPin?: string;
54
+ defaultDtmfSequence?: string;
55
+ };
56
+ voiceCall: {
57
+ enabled: boolean;
58
+ gatewayUrl?: string;
59
+ token?: string;
60
+ requestTimeoutMs: number;
61
+ dtmfDelayMs: number;
62
+ postDtmfSpeechDelayMs: number;
63
+ introMessage?: string;
64
+ };
65
+ realtime: {
66
+ strategy: GoogleMeetRealtimeStrategy;
67
+ provider?: string;
68
+ transcriptionProvider?: string;
69
+ voiceProvider?: string;
70
+ model?: string;
71
+ instructions?: string;
72
+ introMessage?: string;
73
+ agentId?: string;
74
+ toolPolicy: GoogleMeetToolPolicy;
75
+ providers: Record<string, Record<string, unknown>>;
76
+ };
77
+ oauth: {
78
+ clientId?: string;
79
+ clientSecret?: string;
80
+ refreshToken?: string;
81
+ accessToken?: string;
82
+ expiresAt?: number;
83
+ };
84
+ auth: {
85
+ provider: "google-oauth";
86
+ clientId?: string;
87
+ clientSecret?: string;
88
+ tokenPath?: string;
89
+ };
90
+ };
91
+
92
+ const SOX_DEFAULT_BUFFER_BYTES = 8192;
93
+ const SOX_MIN_BUFFER_BYTES = 17;
94
+ export const DEFAULT_GOOGLE_MEET_AUDIO_BUFFER_BYTES = SOX_DEFAULT_BUFFER_BYTES / 2;
95
+
96
+ function withSoxBuffer(command: readonly string[], bufferBytes: number): string[] {
97
+ return [command[0] ?? "sox", "-q", "--buffer", String(bufferBytes), ...command.slice(2)];
98
+ }
99
+
100
+ const DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND_BASE = [
101
+ "sox",
102
+ "-q",
103
+ "-t",
104
+ "coreaudio",
105
+ "BlackHole 2ch",
106
+ "-t",
107
+ "raw",
108
+ "-r",
109
+ "24000",
110
+ "-c",
111
+ "1",
112
+ "-e",
113
+ "signed-integer",
114
+ "-b",
115
+ "16",
116
+ "-L",
117
+ "-",
118
+ ] as const;
119
+
120
+ const DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND_BASE = [
121
+ "sox",
122
+ "-q",
123
+ "-t",
124
+ "raw",
125
+ "-r",
126
+ "24000",
127
+ "-c",
128
+ "1",
129
+ "-e",
130
+ "signed-integer",
131
+ "-b",
132
+ "16",
133
+ "-L",
134
+ "-",
135
+ "-t",
136
+ "coreaudio",
137
+ "BlackHole 2ch",
138
+ ] as const;
139
+
140
+ const LEGACY_GOOGLE_MEET_AUDIO_INPUT_COMMAND_BASE = [
141
+ "rec",
142
+ "-q",
143
+ "-t",
144
+ "raw",
145
+ "-r",
146
+ "8000",
147
+ "-c",
148
+ "1",
149
+ "-e",
150
+ "mu-law",
151
+ "-b",
152
+ "8",
153
+ "-",
154
+ ] as const;
155
+
156
+ const LEGACY_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND_BASE = [
157
+ "play",
158
+ "-q",
159
+ "-t",
160
+ "raw",
161
+ "-r",
162
+ "8000",
163
+ "-c",
164
+ "1",
165
+ "-e",
166
+ "mu-law",
167
+ "-b",
168
+ "8",
169
+ "-",
170
+ ] as const;
171
+
172
+ export const DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND = withSoxBuffer(
173
+ DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND_BASE,
174
+ DEFAULT_GOOGLE_MEET_AUDIO_BUFFER_BYTES,
175
+ );
176
+
177
+ export const DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND = withSoxBuffer(
178
+ DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND_BASE,
179
+ DEFAULT_GOOGLE_MEET_AUDIO_BUFFER_BYTES,
180
+ );
181
+
182
+ const DEFAULT_GOOGLE_MEET_CHROME_AUDIO_FORMAT: GoogleMeetChromeAudioFormat = "pcm16-24khz";
183
+ const DEFAULT_GOOGLE_MEET_BARGE_IN_RMS_THRESHOLD = 650;
184
+ const DEFAULT_GOOGLE_MEET_BARGE_IN_PEAK_THRESHOLD = 2500;
185
+ const DEFAULT_GOOGLE_MEET_BARGE_IN_COOLDOWN_MS = 900;
186
+
187
+ const DEFAULT_GOOGLE_MEET_REALTIME_INSTRUCTIONS = `You are joining a private Google Meet as an AutoBot voice transport. Keep spoken replies brief and natural. In agent mode, wait for AutoBot consult results and speak them exactly. In bidi mode, answer directly and call ${REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME} for deeper reasoning, current information, or tools.`;
188
+ const DEFAULT_GOOGLE_MEET_REALTIME_INTRO_MESSAGE = "Say exactly: I'm here and listening.";
189
+
190
+ const DEFAULT_GOOGLE_MEET_CONFIG: GoogleMeetConfig = {
191
+ enabled: true,
192
+ defaults: {},
193
+ preview: {
194
+ enrollmentAcknowledged: false,
195
+ },
196
+ defaultTransport: "chrome",
197
+ defaultMode: "agent",
198
+ chrome: {
199
+ audioBackend: "blackhole-2ch",
200
+ audioFormat: DEFAULT_GOOGLE_MEET_CHROME_AUDIO_FORMAT,
201
+ audioBufferBytes: DEFAULT_GOOGLE_MEET_AUDIO_BUFFER_BYTES,
202
+ launch: true,
203
+ guestName: "AutoBot Agent",
204
+ reuseExistingTab: true,
205
+ autoJoin: true,
206
+ joinTimeoutMs: 30_000,
207
+ waitForInCallMs: 20_000,
208
+ audioInputCommand: [...DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND],
209
+ audioOutputCommand: [...DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND],
210
+ bargeInRmsThreshold: DEFAULT_GOOGLE_MEET_BARGE_IN_RMS_THRESHOLD,
211
+ bargeInPeakThreshold: DEFAULT_GOOGLE_MEET_BARGE_IN_PEAK_THRESHOLD,
212
+ bargeInCooldownMs: DEFAULT_GOOGLE_MEET_BARGE_IN_COOLDOWN_MS,
213
+ },
214
+ chromeNode: {},
215
+ twilio: {},
216
+ voiceCall: {
217
+ enabled: true,
218
+ requestTimeoutMs: 30_000,
219
+ dtmfDelayMs: 12_000,
220
+ postDtmfSpeechDelayMs: 5_000,
221
+ },
222
+ realtime: {
223
+ strategy: "agent",
224
+ provider: "openai",
225
+ transcriptionProvider: "openai",
226
+ instructions: DEFAULT_GOOGLE_MEET_REALTIME_INSTRUCTIONS,
227
+ introMessage: DEFAULT_GOOGLE_MEET_REALTIME_INTRO_MESSAGE,
228
+ toolPolicy: "safe-read-only",
229
+ providers: {},
230
+ },
231
+ oauth: {},
232
+ auth: {
233
+ provider: "google-oauth",
234
+ },
235
+ };
236
+
237
+ const GOOGLE_MEET_CLIENT_ID_KEYS = ["AUTOBOT_GOOGLE_MEET_CLIENT_ID", "GOOGLE_MEET_CLIENT_ID"];
238
+ const GOOGLE_MEET_CLIENT_SECRET_KEYS = [
239
+ "AUTOBOT_GOOGLE_MEET_CLIENT_SECRET",
240
+ "GOOGLE_MEET_CLIENT_SECRET",
241
+ ] as const;
242
+ const GOOGLE_MEET_REFRESH_TOKEN_KEYS = [
243
+ "AUTOBOT_GOOGLE_MEET_REFRESH_TOKEN",
244
+ "GOOGLE_MEET_REFRESH_TOKEN",
245
+ ] as const;
246
+ const GOOGLE_MEET_ACCESS_TOKEN_KEYS = [
247
+ "AUTOBOT_GOOGLE_MEET_ACCESS_TOKEN",
248
+ "GOOGLE_MEET_ACCESS_TOKEN",
249
+ ] as const;
250
+ const GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT_KEYS = [
251
+ "AUTOBOT_GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT",
252
+ "GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT",
253
+ ] as const;
254
+ const GOOGLE_MEET_DEFAULT_MEETING_KEYS = [
255
+ "AUTOBOT_GOOGLE_MEET_DEFAULT_MEETING",
256
+ "GOOGLE_MEET_DEFAULT_MEETING",
257
+ ] as const;
258
+ const GOOGLE_MEET_PREVIEW_ACK_KEYS = [
259
+ "AUTOBOT_GOOGLE_MEET_PREVIEW_ACK",
260
+ "GOOGLE_MEET_PREVIEW_ACK",
261
+ ] as const;
262
+
263
+ function asRecord(value: unknown): Record<string, unknown> {
264
+ return value && typeof value === "object" && !Array.isArray(value)
265
+ ? (value as Record<string, unknown>)
266
+ : {};
267
+ }
268
+
269
+ function resolveBoolean(value: unknown, fallback: boolean): boolean {
270
+ return typeof value === "boolean" ? value : fallback;
271
+ }
272
+
273
+ function resolveNumber(value: unknown, fallback: number): number {
274
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
275
+ }
276
+
277
+ function resolveOptionalNumber(value: unknown): number | undefined {
278
+ if (typeof value === "number" && Number.isFinite(value)) {
279
+ return value;
280
+ }
281
+ if (typeof value === "string" && value.trim()) {
282
+ const parsed = Number(value);
283
+ return Number.isFinite(parsed) ? parsed : undefined;
284
+ }
285
+ return undefined;
286
+ }
287
+
288
+ function readEnvString(env: NodeJS.ProcessEnv, keys: readonly string[]): string | undefined {
289
+ for (const key of keys) {
290
+ const value = normalizeOptionalString(env[key]);
291
+ if (value) {
292
+ return value;
293
+ }
294
+ }
295
+ return undefined;
296
+ }
297
+
298
+ function normalizeStringAllowEmpty(value: unknown): string | undefined {
299
+ return typeof value === "string" ? value.trim() : undefined;
300
+ }
301
+
302
+ function readEnvBoolean(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean | undefined {
303
+ const normalized = normalizeOptionalLowercaseString(readEnvString(env, keys));
304
+ if (!normalized) {
305
+ return undefined;
306
+ }
307
+ if (["1", "true", "yes", "on"].includes(normalized)) {
308
+ return true;
309
+ }
310
+ if (["0", "false", "no", "off"].includes(normalized)) {
311
+ return false;
312
+ }
313
+ return undefined;
314
+ }
315
+
316
+ function readEnvNumber(env: NodeJS.ProcessEnv, keys: readonly string[]): number | undefined {
317
+ return resolveOptionalNumber(readEnvString(env, keys));
318
+ }
319
+
320
+ function resolveStringArray(value: unknown): string[] | undefined {
321
+ if (!Array.isArray(value)) {
322
+ return undefined;
323
+ }
324
+ const normalized = value
325
+ .map((entry) => normalizeOptionalString(entry))
326
+ .filter((entry): entry is string => Boolean(entry));
327
+ return normalized.length > 0 ? normalized : undefined;
328
+ }
329
+
330
+ function resolveProvidersConfig(value: unknown): Record<string, Record<string, unknown>> {
331
+ const raw = asRecord(value);
332
+ const providers: Record<string, Record<string, unknown>> = {};
333
+ for (const [key, entry] of Object.entries(raw)) {
334
+ const providerId = normalizeOptionalLowercaseString(key);
335
+ if (!providerId) {
336
+ continue;
337
+ }
338
+ providers[providerId] = asRecord(entry);
339
+ }
340
+ return providers;
341
+ }
342
+
343
+ function resolveTransport(value: unknown, fallback: GoogleMeetTransport): GoogleMeetTransport {
344
+ const normalized = normalizeOptionalLowercaseString(value);
345
+ return normalized === "chrome" || normalized === "chrome-node" || normalized === "twilio"
346
+ ? normalized
347
+ : fallback;
348
+ }
349
+
350
+ function resolveMode(value: unknown, fallback: GoogleMeetMode): GoogleMeetMode {
351
+ const normalized = normalizeOptionalLowercaseString(value);
352
+ if (normalized === "realtime") {
353
+ return "agent";
354
+ }
355
+ return normalized === "agent" || normalized === "bidi" || normalized === "transcribe"
356
+ ? normalized
357
+ : fallback;
358
+ }
359
+
360
+ function resolveRealtimeStrategy(
361
+ value: unknown,
362
+ fallback: GoogleMeetRealtimeStrategy,
363
+ ): GoogleMeetRealtimeStrategy {
364
+ const normalized = normalizeOptionalLowercaseString(value);
365
+ return normalized === "agent" || normalized === "bidi" ? normalized : fallback;
366
+ }
367
+
368
+ function resolveChromeAudioFormat(value: unknown): GoogleMeetChromeAudioFormat | undefined {
369
+ const normalized = normalizeOptionalString(value)?.toLowerCase().replaceAll("_", "-");
370
+ switch (normalized) {
371
+ case "pcm16-24khz":
372
+ case "pcm16-24k":
373
+ case "pcm24":
374
+ case "pcm":
375
+ return "pcm16-24khz";
376
+ case "g711-ulaw-8khz":
377
+ case "g711-ulaw-8k":
378
+ case "g711-ulaw":
379
+ case "mulaw":
380
+ case "mu-law":
381
+ return "g711-ulaw-8khz";
382
+ default:
383
+ return undefined;
384
+ }
385
+ }
386
+
387
+ function resolveAudioBufferBytes(value: unknown, fallback: number): number {
388
+ const number = resolveNumber(value, fallback);
389
+ if (!Number.isFinite(number) || number <= 0) {
390
+ return fallback;
391
+ }
392
+ return Math.max(SOX_MIN_BUFFER_BYTES, Math.trunc(number));
393
+ }
394
+
395
+ function defaultAudioInputCommand(
396
+ format: GoogleMeetChromeAudioFormat,
397
+ bufferBytes: number,
398
+ ): string[] {
399
+ return withSoxBuffer(
400
+ format === "g711-ulaw-8khz"
401
+ ? LEGACY_GOOGLE_MEET_AUDIO_INPUT_COMMAND_BASE
402
+ : DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND_BASE,
403
+ bufferBytes,
404
+ );
405
+ }
406
+
407
+ function defaultAudioOutputCommand(
408
+ format: GoogleMeetChromeAudioFormat,
409
+ bufferBytes: number,
410
+ ): string[] {
411
+ return withSoxBuffer(
412
+ format === "g711-ulaw-8khz"
413
+ ? LEGACY_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND_BASE
414
+ : DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND_BASE,
415
+ bufferBytes,
416
+ );
417
+ }
418
+
419
+ export function resolveGoogleMeetConfig(input: unknown): GoogleMeetConfig {
420
+ return resolveGoogleMeetConfigWithEnv(input);
421
+ }
422
+
423
+ export function resolveGoogleMeetConfigWithEnv(
424
+ input: unknown,
425
+ env: NodeJS.ProcessEnv = process.env,
426
+ ): GoogleMeetConfig {
427
+ const raw = asRecord(input);
428
+ const defaults = asRecord(raw.defaults);
429
+ const preview = asRecord(raw.preview);
430
+ const chrome = asRecord(raw.chrome);
431
+ const configuredAudioInputCommand = resolveStringArray(chrome.audioInputCommand);
432
+ const configuredAudioOutputCommand = resolveStringArray(chrome.audioOutputCommand);
433
+ const hasCustomAudioCommand =
434
+ configuredAudioInputCommand !== undefined || configuredAudioOutputCommand !== undefined;
435
+ const audioFormat =
436
+ resolveChromeAudioFormat(chrome.audioFormat) ??
437
+ (hasCustomAudioCommand ? "g711-ulaw-8khz" : DEFAULT_GOOGLE_MEET_CONFIG.chrome.audioFormat);
438
+ const audioBufferBytes = resolveAudioBufferBytes(
439
+ chrome.audioBufferBytes,
440
+ DEFAULT_GOOGLE_MEET_CONFIG.chrome.audioBufferBytes,
441
+ );
442
+ const chromeNode = asRecord(raw.chromeNode);
443
+ const twilio = asRecord(raw.twilio);
444
+ const voiceCall = asRecord(raw.voiceCall);
445
+ const realtime = asRecord(raw.realtime);
446
+ const realtimeProvider = normalizeOptionalString(realtime.provider);
447
+ const resolvedRealtimeProvider = realtimeProvider ?? DEFAULT_GOOGLE_MEET_CONFIG.realtime.provider;
448
+ const oauth = asRecord(raw.oauth);
449
+ const auth = asRecord(raw.auth);
450
+
451
+ return {
452
+ enabled: resolveBoolean(raw.enabled, DEFAULT_GOOGLE_MEET_CONFIG.enabled),
453
+ defaults: {
454
+ meeting:
455
+ normalizeOptionalString(defaults.meeting) ??
456
+ readEnvString(env, GOOGLE_MEET_DEFAULT_MEETING_KEYS),
457
+ },
458
+ preview: {
459
+ enrollmentAcknowledged: resolveBoolean(
460
+ preview.enrollmentAcknowledged,
461
+ readEnvBoolean(env, GOOGLE_MEET_PREVIEW_ACK_KEYS) ??
462
+ DEFAULT_GOOGLE_MEET_CONFIG.preview.enrollmentAcknowledged,
463
+ ),
464
+ },
465
+ defaultTransport: resolveTransport(
466
+ raw.defaultTransport,
467
+ DEFAULT_GOOGLE_MEET_CONFIG.defaultTransport,
468
+ ),
469
+ defaultMode: resolveMode(raw.defaultMode, DEFAULT_GOOGLE_MEET_CONFIG.defaultMode),
470
+ chrome: {
471
+ audioBackend: "blackhole-2ch",
472
+ audioFormat,
473
+ audioBufferBytes,
474
+ launch: resolveBoolean(chrome.launch, DEFAULT_GOOGLE_MEET_CONFIG.chrome.launch),
475
+ browserProfile: normalizeOptionalString(chrome.browserProfile),
476
+ guestName:
477
+ normalizeOptionalString(chrome.guestName) ?? DEFAULT_GOOGLE_MEET_CONFIG.chrome.guestName,
478
+ reuseExistingTab: resolveBoolean(
479
+ chrome.reuseExistingTab,
480
+ DEFAULT_GOOGLE_MEET_CONFIG.chrome.reuseExistingTab,
481
+ ),
482
+ autoJoin: resolveBoolean(chrome.autoJoin, DEFAULT_GOOGLE_MEET_CONFIG.chrome.autoJoin),
483
+ joinTimeoutMs: resolveNumber(
484
+ chrome.joinTimeoutMs,
485
+ DEFAULT_GOOGLE_MEET_CONFIG.chrome.joinTimeoutMs,
486
+ ),
487
+ waitForInCallMs: resolveNumber(
488
+ chrome.waitForInCallMs,
489
+ DEFAULT_GOOGLE_MEET_CONFIG.chrome.waitForInCallMs,
490
+ ),
491
+ audioInputCommand:
492
+ configuredAudioInputCommand ?? defaultAudioInputCommand(audioFormat, audioBufferBytes),
493
+ audioOutputCommand:
494
+ configuredAudioOutputCommand ?? defaultAudioOutputCommand(audioFormat, audioBufferBytes),
495
+ bargeInInputCommand: resolveStringArray(chrome.bargeInInputCommand),
496
+ bargeInRmsThreshold: resolveNumber(
497
+ chrome.bargeInRmsThreshold,
498
+ DEFAULT_GOOGLE_MEET_CONFIG.chrome.bargeInRmsThreshold,
499
+ ),
500
+ bargeInPeakThreshold: resolveNumber(
501
+ chrome.bargeInPeakThreshold,
502
+ DEFAULT_GOOGLE_MEET_CONFIG.chrome.bargeInPeakThreshold,
503
+ ),
504
+ bargeInCooldownMs: resolveNumber(
505
+ chrome.bargeInCooldownMs,
506
+ DEFAULT_GOOGLE_MEET_CONFIG.chrome.bargeInCooldownMs,
507
+ ),
508
+ audioBridgeCommand: resolveStringArray(chrome.audioBridgeCommand),
509
+ audioBridgeHealthCommand: resolveStringArray(chrome.audioBridgeHealthCommand),
510
+ },
511
+ chromeNode: {
512
+ node: normalizeOptionalString(chromeNode.node),
513
+ },
514
+ twilio: {
515
+ defaultDialInNumber: normalizeOptionalString(twilio.defaultDialInNumber),
516
+ defaultPin: normalizeOptionalString(twilio.defaultPin),
517
+ defaultDtmfSequence: normalizeOptionalString(twilio.defaultDtmfSequence),
518
+ },
519
+ voiceCall: {
520
+ enabled: resolveBoolean(voiceCall.enabled, DEFAULT_GOOGLE_MEET_CONFIG.voiceCall.enabled),
521
+ gatewayUrl: normalizeOptionalString(voiceCall.gatewayUrl),
522
+ token: normalizeOptionalString(voiceCall.token),
523
+ requestTimeoutMs: resolveNumber(
524
+ voiceCall.requestTimeoutMs,
525
+ DEFAULT_GOOGLE_MEET_CONFIG.voiceCall.requestTimeoutMs,
526
+ ),
527
+ dtmfDelayMs: resolveNumber(
528
+ voiceCall.dtmfDelayMs,
529
+ DEFAULT_GOOGLE_MEET_CONFIG.voiceCall.dtmfDelayMs,
530
+ ),
531
+ postDtmfSpeechDelayMs: resolveNumber(
532
+ voiceCall.postDtmfSpeechDelayMs,
533
+ DEFAULT_GOOGLE_MEET_CONFIG.voiceCall.postDtmfSpeechDelayMs,
534
+ ),
535
+ introMessage: normalizeOptionalString(voiceCall.introMessage),
536
+ },
537
+ realtime: {
538
+ strategy: resolveRealtimeStrategy(
539
+ realtime.strategy,
540
+ DEFAULT_GOOGLE_MEET_CONFIG.realtime.strategy,
541
+ ),
542
+ provider: resolvedRealtimeProvider,
543
+ transcriptionProvider:
544
+ normalizeOptionalString(realtime.transcriptionProvider) ??
545
+ (realtimeProvider && realtimeProvider !== "google"
546
+ ? resolvedRealtimeProvider
547
+ : DEFAULT_GOOGLE_MEET_CONFIG.realtime.transcriptionProvider),
548
+ voiceProvider: normalizeOptionalString(realtime.voiceProvider),
549
+ model: normalizeOptionalString(realtime.model) ?? DEFAULT_GOOGLE_MEET_CONFIG.realtime.model,
550
+ instructions:
551
+ normalizeOptionalString(realtime.instructions) ??
552
+ DEFAULT_GOOGLE_MEET_CONFIG.realtime.instructions,
553
+ introMessage:
554
+ normalizeStringAllowEmpty(realtime.introMessage) ??
555
+ DEFAULT_GOOGLE_MEET_CONFIG.realtime.introMessage,
556
+ agentId: normalizeOptionalString(realtime.agentId),
557
+ toolPolicy: resolveRealtimeVoiceAgentConsultToolPolicy(
558
+ realtime.toolPolicy,
559
+ DEFAULT_GOOGLE_MEET_CONFIG.realtime.toolPolicy,
560
+ ),
561
+ providers: resolveProvidersConfig(realtime.providers),
562
+ },
563
+ oauth: {
564
+ clientId:
565
+ normalizeOptionalString(oauth.clientId) ??
566
+ normalizeOptionalString(auth.clientId) ??
567
+ readEnvString(env, GOOGLE_MEET_CLIENT_ID_KEYS),
568
+ clientSecret:
569
+ normalizeOptionalString(oauth.clientSecret) ??
570
+ normalizeOptionalString(auth.clientSecret) ??
571
+ readEnvString(env, GOOGLE_MEET_CLIENT_SECRET_KEYS),
572
+ refreshToken:
573
+ normalizeOptionalString(oauth.refreshToken) ??
574
+ readEnvString(env, GOOGLE_MEET_REFRESH_TOKEN_KEYS),
575
+ accessToken:
576
+ normalizeOptionalString(oauth.accessToken) ??
577
+ readEnvString(env, GOOGLE_MEET_ACCESS_TOKEN_KEYS),
578
+ expiresAt:
579
+ resolveOptionalNumber(oauth.expiresAt) ??
580
+ readEnvNumber(env, GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT_KEYS),
581
+ },
582
+ auth: {
583
+ provider: "google-oauth",
584
+ clientId: normalizeOptionalString(auth.clientId),
585
+ clientSecret: normalizeOptionalString(auth.clientSecret),
586
+ tokenPath: normalizeOptionalString(auth.tokenPath),
587
+ },
588
+ };
589
+ }