@openclaw/google-meet 2026.5.7-beta.1 → 2026.5.9-beta.1
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/dist/{calendar-6EQiwLUb.js → calendar-DXHh6IyF.js} +12 -2
- package/dist/{chrome-create-B0wV2zaj.js → chrome-create-D0XLX4Ed.js} +1 -3
- package/dist/{cli-CP1gp7Wl.js → cli-Dgpp3OTQ.js} +3 -3
- package/dist/{create-mHH3keLH.js → create-C01ezOIH.js} +1 -1
- package/dist/index.js +697 -403
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import { a as isSameMeetUrlForReuse, c as resolveChromeNode, f as endGoogleMeetActiveConference, g as fetchLatestGoogleMeetConferenceRecord, h as fetchGoogleMeetSpace, i as callBrowserProxyOnNode, l as resolveChromeNodeInfo, m as fetchGoogleMeetAttendance, n as isGoogleMeetBrowserManualActionError, o as normalizeMeetUrlForReuse, p as fetchGoogleMeetArtifacts, r as asBrowserTabs, s as readBrowserTab, t as createMeetWithBrowserProxyOnNode, u as buildGoogleMeetPreflightReport } from "./chrome-create-
|
|
2
|
-
import { n as findGoogleMeetCalendarEvent, r as listGoogleMeetCalendarEvents, t as buildGoogleMeetCalendarDayWindow } from "./calendar-
|
|
1
|
+
import { a as isSameMeetUrlForReuse, c as resolveChromeNode, f as endGoogleMeetActiveConference, g as fetchLatestGoogleMeetConferenceRecord, h as fetchGoogleMeetSpace, i as callBrowserProxyOnNode, l as resolveChromeNodeInfo, m as fetchGoogleMeetAttendance, n as isGoogleMeetBrowserManualActionError, o as normalizeMeetUrlForReuse, p as fetchGoogleMeetArtifacts, r as asBrowserTabs, s as readBrowserTab, t as createMeetWithBrowserProxyOnNode, u as buildGoogleMeetPreflightReport } from "./chrome-create-D0XLX4Ed.js";
|
|
2
|
+
import { n as findGoogleMeetCalendarEvent, r as listGoogleMeetCalendarEvents, t as buildGoogleMeetCalendarDayWindow } from "./calendar-DXHh6IyF.js";
|
|
3
3
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
4
4
|
import { ErrorCodes, GatewayClient, callGatewayFromCli, errorShape, startGatewayClientWhenEventLoopReady } from "openclaw/plugin-sdk/gateway-runtime";
|
|
5
5
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
6
6
|
import { normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
9
|
-
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ, REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ, buildRealtimeVoiceAgentConsultWorkingResponse, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createRealtimeVoiceBridgeSession, mulawToPcm, resamplePcm, resolveConfiguredRealtimeVoiceProvider, resolveRealtimeVoiceAgentConsultToolPolicy, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceAgentConsultToolsAllow } from "openclaw/plugin-sdk/realtime-voice";
|
|
9
|
+
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ, REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ, buildRealtimeVoiceAgentConsultWorkingResponse, consultRealtimeVoiceAgent, convertPcmToMulaw8k, createRealtimeVoiceAgentTalkbackQueue, createRealtimeVoiceBridgeSession, createTalkSessionController, extendRealtimeVoiceOutputEchoSuppression, getRealtimeVoiceBridgeEventHealth, getRealtimeVoiceTranscriptHealth, isLikelyRealtimeVoiceAssistantEchoTranscript, mulawToPcm, recordRealtimeVoiceBridgeEvent, recordRealtimeVoiceTranscript, recordTalkObservabilityEvent, resamplePcm, resolveConfiguredRealtimeVoiceProvider, resolveRealtimeVoiceAgentConsultToolPolicy, resolveRealtimeVoiceAgentConsultTools, resolveRealtimeVoiceAgentConsultToolsAllow } from "openclaw/plugin-sdk/realtime-voice";
|
|
10
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
13
13
|
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
|
14
14
|
import { getRealtimeTranscriptionProvider, listRealtimeTranscriptionProviders } from "openclaw/plugin-sdk/realtime-transcription";
|
|
15
|
+
import { sleep } from "openclaw/plugin-sdk/runtime-env";
|
|
15
16
|
import fs from "node:fs";
|
|
16
17
|
import os from "node:os";
|
|
17
18
|
import path from "node:path";
|
|
@@ -131,7 +132,7 @@ const DEFAULT_GOOGLE_MEET_CONFIG = {
|
|
|
131
132
|
voiceCall: {
|
|
132
133
|
enabled: true,
|
|
133
134
|
requestTimeoutMs: 3e4,
|
|
134
|
-
dtmfDelayMs:
|
|
135
|
+
dtmfDelayMs: 12e3,
|
|
135
136
|
postDtmfSpeechDelayMs: 5e3
|
|
136
137
|
},
|
|
137
138
|
realtime: {
|
|
@@ -381,45 +382,89 @@ async function consultOpenClawAgentForGoogleMeet(params) {
|
|
|
381
382
|
extraSystemPrompt: GOOGLE_MEET_CONSULT_SYSTEM_PROMPT
|
|
382
383
|
});
|
|
383
384
|
}
|
|
385
|
+
function handleGoogleMeetRealtimeConsultToolCall(params) {
|
|
386
|
+
const callId = params.event.callId || params.event.itemId;
|
|
387
|
+
if (params.strategy !== "bidi") {
|
|
388
|
+
params.onTalkEvent?.({
|
|
389
|
+
type: "tool.error",
|
|
390
|
+
callId,
|
|
391
|
+
payload: {
|
|
392
|
+
name: params.event.name,
|
|
393
|
+
error: `Tool "${params.event.name}" is only available in bidi realtime strategy`
|
|
394
|
+
},
|
|
395
|
+
final: true
|
|
396
|
+
});
|
|
397
|
+
params.session.submitToolResult(callId, { error: `Tool "${params.event.name}" is only available in bidi realtime strategy` });
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (params.event.name !== GOOGLE_MEET_AGENT_CONSULT_TOOL_NAME) {
|
|
401
|
+
params.onTalkEvent?.({
|
|
402
|
+
type: "tool.error",
|
|
403
|
+
callId,
|
|
404
|
+
payload: {
|
|
405
|
+
name: params.event.name,
|
|
406
|
+
error: `Tool "${params.event.name}" not available`
|
|
407
|
+
},
|
|
408
|
+
final: true
|
|
409
|
+
});
|
|
410
|
+
params.session.submitToolResult(callId, { error: `Tool "${params.event.name}" not available` });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
params.onTalkEvent?.({
|
|
414
|
+
type: "tool.progress",
|
|
415
|
+
callId,
|
|
416
|
+
payload: {
|
|
417
|
+
name: params.event.name,
|
|
418
|
+
status: "working"
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
submitGoogleMeetConsultWorkingResponse(params.session, callId);
|
|
422
|
+
consultOpenClawAgentForGoogleMeet({
|
|
423
|
+
config: params.config,
|
|
424
|
+
fullConfig: params.fullConfig,
|
|
425
|
+
runtime: params.runtime,
|
|
426
|
+
logger: params.logger,
|
|
427
|
+
meetingSessionId: params.meetingSessionId,
|
|
428
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
429
|
+
args: params.event.args,
|
|
430
|
+
transcript: params.transcript
|
|
431
|
+
}).then((result) => {
|
|
432
|
+
params.onTalkEvent?.({
|
|
433
|
+
type: "tool.result",
|
|
434
|
+
callId,
|
|
435
|
+
payload: {
|
|
436
|
+
name: params.event.name,
|
|
437
|
+
result
|
|
438
|
+
},
|
|
439
|
+
final: true
|
|
440
|
+
});
|
|
441
|
+
params.session.submitToolResult(callId, result);
|
|
442
|
+
}).catch((error) => {
|
|
443
|
+
params.onTalkEvent?.({
|
|
444
|
+
type: "tool.error",
|
|
445
|
+
callId,
|
|
446
|
+
payload: {
|
|
447
|
+
name: params.event.name,
|
|
448
|
+
error: formatErrorMessage(error)
|
|
449
|
+
},
|
|
450
|
+
final: true
|
|
451
|
+
});
|
|
452
|
+
params.session.submitToolResult(callId, { error: formatErrorMessage(error) });
|
|
453
|
+
});
|
|
454
|
+
}
|
|
384
455
|
//#endregion
|
|
385
456
|
//#region extensions/google-meet/src/realtime.ts
|
|
386
|
-
|
|
387
|
-
const entry = {
|
|
388
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
389
|
-
role,
|
|
390
|
-
text
|
|
391
|
-
};
|
|
392
|
-
transcript.push(entry);
|
|
393
|
-
if (transcript.length > 40) transcript.splice(0, transcript.length - 40);
|
|
394
|
-
return entry;
|
|
395
|
-
}
|
|
457
|
+
const recordGoogleMeetRealtimeTranscript = recordRealtimeVoiceTranscript;
|
|
396
458
|
function getGoogleMeetRealtimeTranscriptHealth(transcript) {
|
|
397
|
-
|
|
398
|
-
return {
|
|
399
|
-
realtimeTranscriptLines: transcript.length,
|
|
400
|
-
lastRealtimeTranscriptAt: last?.at,
|
|
401
|
-
lastRealtimeTranscriptRole: last?.role,
|
|
402
|
-
lastRealtimeTranscriptText: last?.text,
|
|
403
|
-
recentRealtimeTranscript: transcript.slice(-5)
|
|
404
|
-
};
|
|
459
|
+
return getRealtimeVoiceTranscriptHealth(transcript);
|
|
405
460
|
}
|
|
406
461
|
const GOOGLE_MEET_OUTPUT_ECHO_SUPPRESSION_TAIL_MS = 3e3;
|
|
462
|
+
const GOOGLE_MEET_TRANSCRIPT_ECHO_LOOKBACK_MS = 45e3;
|
|
407
463
|
function recordGoogleMeetRealtimeEvent(events, event) {
|
|
408
|
-
|
|
409
|
-
events.push({
|
|
410
|
-
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
411
|
-
...event
|
|
412
|
-
});
|
|
413
|
-
if (events.length > 40) events.splice(0, events.length - 40);
|
|
464
|
+
recordRealtimeVoiceBridgeEvent(events, event);
|
|
414
465
|
}
|
|
415
466
|
function getGoogleMeetRealtimeEventHealth(events) {
|
|
416
|
-
|
|
417
|
-
return {
|
|
418
|
-
lastRealtimeEventAt: last?.at,
|
|
419
|
-
lastRealtimeEventType: last ? `${last.direction}:${last.type}` : void 0,
|
|
420
|
-
lastRealtimeEventDetail: last?.detail,
|
|
421
|
-
recentRealtimeEvents: events.slice(-10)
|
|
422
|
-
};
|
|
467
|
+
return getRealtimeVoiceBridgeEventHealth(events);
|
|
423
468
|
}
|
|
424
469
|
function splitCommand$1(argv) {
|
|
425
470
|
const [command, ...args] = argv;
|
|
@@ -444,40 +489,19 @@ function readPcm16Stats(audio) {
|
|
|
444
489
|
peak
|
|
445
490
|
};
|
|
446
491
|
}
|
|
447
|
-
function normalizeTranscriptForEchoMatch(text) {
|
|
448
|
-
return text.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter((token) => token.length > 1);
|
|
449
|
-
}
|
|
450
|
-
function hasMeaningfulEchoOverlap(userTokens, assistantTokens) {
|
|
451
|
-
if (userTokens.length < 4 || assistantTokens.length < 4) return false;
|
|
452
|
-
const uniqueUserTokens = [...new Set(userTokens)];
|
|
453
|
-
if (uniqueUserTokens.length < 4) return false;
|
|
454
|
-
const assistantTokenSet = new Set(assistantTokens);
|
|
455
|
-
return uniqueUserTokens.filter((token) => assistantTokenSet.has(token)).length / uniqueUserTokens.length >= .58;
|
|
456
|
-
}
|
|
457
492
|
function isGoogleMeetLikelyAssistantEchoTranscript(params) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (entry.role !== "assistant") return false;
|
|
463
|
-
const at = Date.parse(entry.at);
|
|
464
|
-
return Number.isFinite(at) && nowMs - at <= 45e3;
|
|
465
|
-
}).slice(-6).map((entry) => entry.text).join(" ");
|
|
466
|
-
if (!recentAssistantText.trim()) return false;
|
|
467
|
-
const userNormalized = userTokens.join(" ");
|
|
468
|
-
const assistantTokens = normalizeTranscriptForEchoMatch(recentAssistantText);
|
|
469
|
-
const assistantNormalized = assistantTokens.join(" ");
|
|
470
|
-
return userNormalized.length >= 18 && assistantNormalized.includes(userNormalized) || assistantNormalized.length >= 18 && userNormalized.includes(assistantNormalized) || hasMeaningfulEchoOverlap(userTokens, assistantTokens);
|
|
493
|
+
return isLikelyRealtimeVoiceAssistantEchoTranscript({
|
|
494
|
+
...params,
|
|
495
|
+
lookbackMs: GOOGLE_MEET_TRANSCRIPT_ECHO_LOOKBACK_MS
|
|
496
|
+
});
|
|
471
497
|
}
|
|
472
498
|
function extendGoogleMeetOutputEchoSuppression(params) {
|
|
473
499
|
const bytesPerMs = params.audioFormat === "g711-ulaw-8khz" ? 8 : 48;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
suppressInputUntilMs: Math.max(params.suppressInputUntilMs, playbackEndMs + GOOGLE_MEET_OUTPUT_ECHO_SUPPRESSION_TAIL_MS)
|
|
480
|
-
};
|
|
500
|
+
return extendRealtimeVoiceOutputEchoSuppression({
|
|
501
|
+
...params,
|
|
502
|
+
bytesPerMs,
|
|
503
|
+
tailMs: GOOGLE_MEET_OUTPUT_ECHO_SUPPRESSION_TAIL_MS
|
|
504
|
+
});
|
|
481
505
|
}
|
|
482
506
|
function resolveGoogleMeetRealtimeAudioFormat(config) {
|
|
483
507
|
return config.chrome.audioFormat === "g711-ulaw-8khz" ? REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ : REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ;
|
|
@@ -598,6 +622,9 @@ function formatGoogleMeetAgentTtsResultLog(prefix, result) {
|
|
|
598
622
|
...result.fallbackFrom ? [`fallbackFrom=${formatLogValue(result.fallbackFrom)}`] : []
|
|
599
623
|
].join(" ");
|
|
600
624
|
}
|
|
625
|
+
function formatGoogleMeetTranscriptSummaryLog(prefix, text) {
|
|
626
|
+
return `[google-meet] ${prefix}: chars=${text.length}`;
|
|
627
|
+
}
|
|
601
628
|
function normalizeGoogleMeetTtsPromptText$1(text) {
|
|
602
629
|
const trimmed = text?.trim();
|
|
603
630
|
if (!trimmed) return;
|
|
@@ -605,6 +632,21 @@ function normalizeGoogleMeetTtsPromptText$1(text) {
|
|
|
605
632
|
if (sayExactly) return sayExactly.replace(/^["']|["']$/g, "").trim() || trimmed;
|
|
606
633
|
return trimmed;
|
|
607
634
|
}
|
|
635
|
+
function pushGoogleMeetTalkEvent(events, event, maxEntries = 40) {
|
|
636
|
+
events.push(event);
|
|
637
|
+
if (events.length > maxEntries) events.splice(0, events.length - maxEntries);
|
|
638
|
+
}
|
|
639
|
+
function summarizeGoogleMeetTalkEvents(events) {
|
|
640
|
+
return events.slice(-20).map((event) => ({
|
|
641
|
+
id: event.id,
|
|
642
|
+
type: event.type,
|
|
643
|
+
sessionId: event.sessionId,
|
|
644
|
+
turnId: event.turnId,
|
|
645
|
+
seq: event.seq,
|
|
646
|
+
timestamp: event.timestamp,
|
|
647
|
+
final: event.final
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
608
650
|
async function startCommandAgentAudioBridge(params) {
|
|
609
651
|
const input = splitCommand$1(params.inputCommand);
|
|
610
652
|
const output = splitCommand$1(params.outputCommand);
|
|
@@ -630,9 +672,7 @@ async function startCommandAgentAudioBridge(params) {
|
|
|
630
672
|
let lastSuppressedInputAt;
|
|
631
673
|
let suppressInputUntil = 0;
|
|
632
674
|
let lastOutputPlayableUntilMs = 0;
|
|
633
|
-
let
|
|
634
|
-
let pendingAgentQuestion;
|
|
635
|
-
let agentConsultDebounceTimer;
|
|
675
|
+
let agentTalkback;
|
|
636
676
|
let ttsQueue = Promise.resolve();
|
|
637
677
|
const transcript = [];
|
|
638
678
|
const resolved = resolveGoogleMeetRealtimeTranscriptionProvider({
|
|
@@ -640,6 +680,25 @@ async function startCommandAgentAudioBridge(params) {
|
|
|
640
680
|
fullConfig: params.fullConfig,
|
|
641
681
|
providers: params.providers
|
|
642
682
|
});
|
|
683
|
+
const talk = createTalkSessionController({
|
|
684
|
+
sessionId: `google-meet:${params.meetingSessionId}:agent`,
|
|
685
|
+
mode: "stt-tts",
|
|
686
|
+
transport: "gateway-relay",
|
|
687
|
+
brain: "agent-consult",
|
|
688
|
+
provider: resolved.provider.id,
|
|
689
|
+
turnIdPrefix: `google-meet:${params.meetingSessionId}:turn`
|
|
690
|
+
}, { onEvent: recordTalkObservabilityEvent });
|
|
691
|
+
const recentTalkEvents = [];
|
|
692
|
+
const emitTalkEvent = (input) => pushGoogleMeetTalkEvent(recentTalkEvents, talk.emit(input));
|
|
693
|
+
const ensureTalkTurn = () => {
|
|
694
|
+
const turn = talk.ensureTurn({ payload: { meetingSessionId: params.meetingSessionId } });
|
|
695
|
+
if (turn.event) pushGoogleMeetTalkEvent(recentTalkEvents, turn.event);
|
|
696
|
+
return turn.turnId;
|
|
697
|
+
};
|
|
698
|
+
const endTalkTurn = () => {
|
|
699
|
+
const ended = talk.endTurn({ payload: { meetingSessionId: params.meetingSessionId } });
|
|
700
|
+
if (ended.ok) pushGoogleMeetTalkEvent(recentTalkEvents, ended.event);
|
|
701
|
+
};
|
|
643
702
|
params.logger.info(formatGoogleMeetAgentAudioModelLog({
|
|
644
703
|
provider: resolved.provider,
|
|
645
704
|
providerConfig: resolved.providerConfig,
|
|
@@ -666,15 +725,17 @@ async function startCommandAgentAudioBridge(params) {
|
|
|
666
725
|
const stop = async () => {
|
|
667
726
|
if (stopped) return;
|
|
668
727
|
stopped = true;
|
|
669
|
-
|
|
670
|
-
clearTimeout(agentConsultDebounceTimer);
|
|
671
|
-
agentConsultDebounceTimer = void 0;
|
|
672
|
-
}
|
|
728
|
+
agentTalkback?.close();
|
|
673
729
|
try {
|
|
674
730
|
sttSession?.close();
|
|
675
731
|
} catch (error) {
|
|
676
732
|
params.logger.debug?.(`[google-meet] agent transcription bridge close ignored: ${formatErrorMessage(error)}`);
|
|
677
733
|
}
|
|
734
|
+
emitTalkEvent({
|
|
735
|
+
type: "session.closed",
|
|
736
|
+
final: true,
|
|
737
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
738
|
+
});
|
|
678
739
|
terminateProcess(inputProcess);
|
|
679
740
|
terminateProcess(outputProcess);
|
|
680
741
|
};
|
|
@@ -715,6 +776,14 @@ async function startCommandAgentAudioBridge(params) {
|
|
|
715
776
|
lastOutputPlayableUntilMs = suppression.lastOutputPlayableUntilMs;
|
|
716
777
|
lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
717
778
|
lastOutputBytes += audio.byteLength;
|
|
779
|
+
emitTalkEvent({
|
|
780
|
+
type: "output.audio.delta",
|
|
781
|
+
turnId: ensureTalkTurn(),
|
|
782
|
+
payload: {
|
|
783
|
+
meetingSessionId: params.meetingSessionId,
|
|
784
|
+
bytes: audio.byteLength
|
|
785
|
+
}
|
|
786
|
+
});
|
|
718
787
|
try {
|
|
719
788
|
outputProcess.stdin?.write(audio);
|
|
720
789
|
} catch (error) {
|
|
@@ -727,94 +796,121 @@ async function startCommandAgentAudioBridge(params) {
|
|
|
727
796
|
ttsQueue = ttsQueue.then(async () => {
|
|
728
797
|
if (stopped) return;
|
|
729
798
|
recordGoogleMeetRealtimeTranscript(transcript, "assistant", normalized);
|
|
730
|
-
params.logger.info(
|
|
799
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("agent assistant", normalized));
|
|
800
|
+
const turnId = ensureTalkTurn();
|
|
801
|
+
emitTalkEvent({
|
|
802
|
+
type: "output.text.done",
|
|
803
|
+
turnId,
|
|
804
|
+
final: true,
|
|
805
|
+
payload: {
|
|
806
|
+
meetingSessionId: params.meetingSessionId,
|
|
807
|
+
text: normalized
|
|
808
|
+
}
|
|
809
|
+
});
|
|
731
810
|
const result = await params.runtime.tts.textToSpeechTelephony({
|
|
732
811
|
text: normalized,
|
|
733
812
|
cfg: params.fullConfig
|
|
734
813
|
});
|
|
735
814
|
if (!result.success || !result.audioBuffer || !result.sampleRate) throw new Error(result.error ?? "TTS conversion failed");
|
|
736
815
|
params.logger.info(formatGoogleMeetAgentTtsResultLog("agent", result));
|
|
816
|
+
emitTalkEvent({
|
|
817
|
+
type: "output.audio.started",
|
|
818
|
+
turnId,
|
|
819
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
820
|
+
});
|
|
737
821
|
writeOutputAudio(convertGoogleMeetTtsAudioForBridge(result.audioBuffer, result.sampleRate, params.config, result.outputFormat));
|
|
822
|
+
emitTalkEvent({
|
|
823
|
+
type: "output.audio.done",
|
|
824
|
+
turnId,
|
|
825
|
+
final: true,
|
|
826
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
827
|
+
});
|
|
828
|
+
endTalkTurn();
|
|
738
829
|
}).catch((error) => {
|
|
739
830
|
params.logger.warn(`[google-meet] agent TTS failed: ${formatErrorMessage(error)}`);
|
|
740
831
|
});
|
|
741
832
|
};
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
args: {
|
|
765
|
-
question: currentQuestion,
|
|
766
|
-
responseStyle: "Brief, natural spoken answer for a live meeting."
|
|
767
|
-
},
|
|
768
|
-
transcript
|
|
769
|
-
})).text);
|
|
770
|
-
nextQuestion = pendingAgentQuestion;
|
|
771
|
-
}
|
|
772
|
-
} catch (error) {
|
|
773
|
-
params.logger.warn(`[google-meet] agent consult failed: ${formatErrorMessage(error)}`);
|
|
774
|
-
enqueueSpeakText("I hit an error while checking that. Please try again.");
|
|
775
|
-
} finally {
|
|
776
|
-
agentConsultActive = false;
|
|
777
|
-
const queuedQuestion = pendingAgentQuestion;
|
|
778
|
-
pendingAgentQuestion = void 0;
|
|
779
|
-
if (queuedQuestion && !stopped) runAgentConsultForUserTranscript(queuedQuestion);
|
|
780
|
-
}
|
|
781
|
-
};
|
|
782
|
-
const enqueueAgentConsultForUserTranscript = (question) => {
|
|
783
|
-
const trimmed = question.trim();
|
|
784
|
-
if (!trimmed || stopped) return;
|
|
785
|
-
pendingAgentQuestion = pendingAgentQuestion ? `${pendingAgentQuestion}\n${trimmed}` : trimmed;
|
|
786
|
-
if (agentConsultDebounceTimer) clearTimeout(agentConsultDebounceTimer);
|
|
787
|
-
agentConsultDebounceTimer = setTimeout(() => {
|
|
788
|
-
agentConsultDebounceTimer = void 0;
|
|
789
|
-
const queuedQuestion = pendingAgentQuestion;
|
|
790
|
-
pendingAgentQuestion = void 0;
|
|
791
|
-
if (queuedQuestion && !stopped) runAgentConsultForUserTranscript(queuedQuestion);
|
|
792
|
-
}, 900);
|
|
793
|
-
agentConsultDebounceTimer.unref?.();
|
|
794
|
-
};
|
|
833
|
+
agentTalkback = createRealtimeVoiceAgentTalkbackQueue({
|
|
834
|
+
debounceMs: 900,
|
|
835
|
+
isStopped: () => stopped,
|
|
836
|
+
logger: params.logger,
|
|
837
|
+
logPrefix: "[google-meet] agent",
|
|
838
|
+
responseStyle: "Brief, natural spoken answer for a live meeting.",
|
|
839
|
+
fallbackText: "I hit an error while checking that. Please try again.",
|
|
840
|
+
consult: ({ question, responseStyle }) => consultOpenClawAgentForGoogleMeet({
|
|
841
|
+
config: params.config,
|
|
842
|
+
fullConfig: params.fullConfig,
|
|
843
|
+
runtime: params.runtime,
|
|
844
|
+
logger: params.logger,
|
|
845
|
+
meetingSessionId: params.meetingSessionId,
|
|
846
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
847
|
+
args: {
|
|
848
|
+
question,
|
|
849
|
+
responseStyle
|
|
850
|
+
},
|
|
851
|
+
transcript
|
|
852
|
+
}),
|
|
853
|
+
deliver: enqueueSpeakText
|
|
854
|
+
});
|
|
795
855
|
sttSession = resolved.provider.createSession({
|
|
796
856
|
providerConfig: resolved.providerConfig,
|
|
797
857
|
onTranscript: (text) => {
|
|
798
858
|
const trimmed = text.trim();
|
|
799
859
|
if (!trimmed || stopped) return;
|
|
860
|
+
const turnId = ensureTalkTurn();
|
|
861
|
+
emitTalkEvent({
|
|
862
|
+
type: "input.audio.committed",
|
|
863
|
+
turnId,
|
|
864
|
+
final: true,
|
|
865
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
866
|
+
});
|
|
867
|
+
emitTalkEvent({
|
|
868
|
+
type: "transcript.done",
|
|
869
|
+
turnId,
|
|
870
|
+
final: true,
|
|
871
|
+
payload: {
|
|
872
|
+
meetingSessionId: params.meetingSessionId,
|
|
873
|
+
text: trimmed,
|
|
874
|
+
role: "user"
|
|
875
|
+
}
|
|
876
|
+
});
|
|
800
877
|
recordGoogleMeetRealtimeTranscript(transcript, "user", trimmed);
|
|
801
|
-
params.logger.info(
|
|
878
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("agent user", trimmed));
|
|
802
879
|
if (isGoogleMeetLikelyAssistantEchoTranscript({
|
|
803
880
|
transcript,
|
|
804
881
|
text: trimmed
|
|
805
882
|
})) {
|
|
806
|
-
params.logger.info(
|
|
883
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("agent ignored assistant echo transcript", trimmed));
|
|
807
884
|
return;
|
|
808
885
|
}
|
|
809
|
-
|
|
886
|
+
agentTalkback?.enqueue(trimmed);
|
|
810
887
|
},
|
|
811
888
|
onError: (error) => {
|
|
812
889
|
params.logger.warn(`[google-meet] agent transcription bridge failed: ${formatErrorMessage(error)}`);
|
|
890
|
+
emitTalkEvent({
|
|
891
|
+
type: "session.error",
|
|
892
|
+
final: true,
|
|
893
|
+
payload: {
|
|
894
|
+
meetingSessionId: params.meetingSessionId,
|
|
895
|
+
error: formatErrorMessage(error)
|
|
896
|
+
}
|
|
897
|
+
});
|
|
813
898
|
stop();
|
|
814
899
|
}
|
|
815
900
|
});
|
|
901
|
+
emitTalkEvent({
|
|
902
|
+
type: "session.started",
|
|
903
|
+
payload: {
|
|
904
|
+
meetingSessionId: params.meetingSessionId,
|
|
905
|
+
provider: resolved.provider.id
|
|
906
|
+
}
|
|
907
|
+
});
|
|
816
908
|
await sttSession.connect();
|
|
817
909
|
realtimeReady = true;
|
|
910
|
+
emitTalkEvent({
|
|
911
|
+
type: "session.ready",
|
|
912
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
913
|
+
});
|
|
818
914
|
inputProcess.stdout?.on("data", (chunk) => {
|
|
819
915
|
if (stopped) return;
|
|
820
916
|
const audio = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
@@ -825,6 +921,14 @@ async function startCommandAgentAudioBridge(params) {
|
|
|
825
921
|
}
|
|
826
922
|
lastInputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
827
923
|
lastInputBytes += audio.byteLength;
|
|
924
|
+
emitTalkEvent({
|
|
925
|
+
type: "input.audio.delta",
|
|
926
|
+
turnId: ensureTalkTurn(),
|
|
927
|
+
payload: {
|
|
928
|
+
meetingSessionId: params.meetingSessionId,
|
|
929
|
+
bytes: audio.byteLength
|
|
930
|
+
}
|
|
931
|
+
});
|
|
828
932
|
sttSession?.sendAudio(convertGoogleMeetBridgeAudioForStt(audio, params.config));
|
|
829
933
|
});
|
|
830
934
|
return {
|
|
@@ -844,6 +948,7 @@ async function startCommandAgentAudioBridge(params) {
|
|
|
844
948
|
lastOutputBytes,
|
|
845
949
|
suppressedInputBytes,
|
|
846
950
|
...getGoogleMeetRealtimeTranscriptHealth(transcript),
|
|
951
|
+
recentTalkEvents: summarizeGoogleMeetTalkEvents(recentTalkEvents),
|
|
847
952
|
bridgeClosed: stopped
|
|
848
953
|
}),
|
|
849
954
|
stop
|
|
@@ -879,7 +984,7 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
879
984
|
let lastOutputAtMs = 0;
|
|
880
985
|
let lastOutputPlayableUntilMs = 0;
|
|
881
986
|
let bargeInInputProcess;
|
|
882
|
-
let
|
|
987
|
+
let agentTalkback;
|
|
883
988
|
const suppressInputForOutput = (audio) => {
|
|
884
989
|
const suppression = extendGoogleMeetOutputEchoSuppression({
|
|
885
990
|
audio,
|
|
@@ -912,10 +1017,7 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
912
1017
|
const stop = async () => {
|
|
913
1018
|
if (stopped) return;
|
|
914
1019
|
stopped = true;
|
|
915
|
-
|
|
916
|
-
clearTimeout(agentConsultDebounceTimer);
|
|
917
|
-
agentConsultDebounceTimer = void 0;
|
|
918
|
-
}
|
|
1020
|
+
agentTalkback?.close();
|
|
919
1021
|
try {
|
|
920
1022
|
bridge?.close();
|
|
921
1023
|
} catch (error) {
|
|
@@ -1028,62 +1130,60 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
1028
1130
|
}));
|
|
1029
1131
|
const transcript = [];
|
|
1030
1132
|
const realtimeEvents = [];
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
pendingAgentQuestion = void 0;
|
|
1042
|
-
if (queuedQuestion && !stopped) runAgentConsultForUserTranscript(queuedQuestion);
|
|
1043
|
-
}, 900);
|
|
1044
|
-
agentConsultDebounceTimer.unref?.();
|
|
1133
|
+
const talk = createTalkSessionController({
|
|
1134
|
+
sessionId: `google-meet:${params.meetingSessionId}:command-realtime`,
|
|
1135
|
+
mode: "realtime",
|
|
1136
|
+
transport: "gateway-relay",
|
|
1137
|
+
brain: strategy === "bidi" ? "direct-tools" : "agent-consult",
|
|
1138
|
+
provider: resolved.provider.id
|
|
1139
|
+
}, { onEvent: recordTalkObservabilityEvent });
|
|
1140
|
+
const recentTalkEvents = [];
|
|
1141
|
+
const rememberTalkEvent = (event) => {
|
|
1142
|
+
if (event) pushGoogleMeetTalkEvent(recentTalkEvents, event);
|
|
1045
1143
|
};
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
agentConsultActive = true;
|
|
1054
|
-
let nextQuestion = trimmed;
|
|
1055
|
-
try {
|
|
1056
|
-
while (nextQuestion) {
|
|
1057
|
-
if (stopped) return;
|
|
1058
|
-
const currentQuestion = nextQuestion;
|
|
1059
|
-
pendingAgentQuestion = void 0;
|
|
1060
|
-
params.logger.info(`[google-meet] realtime agent consult: ${currentQuestion}`);
|
|
1061
|
-
const result = await consultOpenClawAgentForGoogleMeet({
|
|
1062
|
-
config: params.config,
|
|
1063
|
-
fullConfig: params.fullConfig,
|
|
1064
|
-
runtime: params.runtime,
|
|
1065
|
-
logger: params.logger,
|
|
1066
|
-
meetingSessionId: params.meetingSessionId,
|
|
1067
|
-
requesterSessionKey: params.requesterSessionKey,
|
|
1068
|
-
args: {
|
|
1069
|
-
question: currentQuestion,
|
|
1070
|
-
responseStyle: "Brief, natural spoken answer for a live meeting."
|
|
1071
|
-
},
|
|
1072
|
-
transcript
|
|
1073
|
-
});
|
|
1074
|
-
if (!stopped && result.text.trim()) bridge?.sendUserMessage(buildGoogleMeetSpeakExactUserMessage(result.text.trim()));
|
|
1075
|
-
nextQuestion = pendingAgentQuestion;
|
|
1076
|
-
}
|
|
1077
|
-
} catch (error) {
|
|
1078
|
-
params.logger.warn(`[google-meet] realtime agent consult failed: ${formatErrorMessage(error)}`);
|
|
1079
|
-
if (!stopped) bridge?.sendUserMessage(buildGoogleMeetSpeakExactUserMessage("I hit an error while checking that. Please try again."));
|
|
1080
|
-
} finally {
|
|
1081
|
-
agentConsultActive = false;
|
|
1082
|
-
const queuedQuestion = pendingAgentQuestion;
|
|
1083
|
-
pendingAgentQuestion = void 0;
|
|
1084
|
-
if (queuedQuestion && !stopped) runAgentConsultForUserTranscript(queuedQuestion);
|
|
1085
|
-
}
|
|
1144
|
+
const emitTalkEvent = (input) => {
|
|
1145
|
+
rememberTalkEvent(talk.emit(input));
|
|
1146
|
+
};
|
|
1147
|
+
const ensureTalkTurn = () => {
|
|
1148
|
+
const turn = talk.ensureTurn({ payload: { meetingSessionId: params.meetingSessionId } });
|
|
1149
|
+
if (turn.event) rememberTalkEvent(turn.event);
|
|
1150
|
+
return turn.turnId;
|
|
1086
1151
|
};
|
|
1152
|
+
const finishOutputAudio = (reason) => {
|
|
1153
|
+
rememberTalkEvent(talk.finishOutputAudio({ payload: { reason } }));
|
|
1154
|
+
};
|
|
1155
|
+
const endTalkTurn = (reason = "completed") => {
|
|
1156
|
+
const ended = talk.endTurn({ payload: { reason } });
|
|
1157
|
+
if (ended.ok) rememberTalkEvent(ended.event);
|
|
1158
|
+
};
|
|
1159
|
+
emitTalkEvent({
|
|
1160
|
+
type: "session.started",
|
|
1161
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
1162
|
+
});
|
|
1163
|
+
agentTalkback = createRealtimeVoiceAgentTalkbackQueue({
|
|
1164
|
+
debounceMs: 900,
|
|
1165
|
+
isStopped: () => stopped,
|
|
1166
|
+
logger: params.logger,
|
|
1167
|
+
logPrefix: "[google-meet] realtime agent",
|
|
1168
|
+
responseStyle: "Brief, natural spoken answer for a live meeting.",
|
|
1169
|
+
fallbackText: "I hit an error while checking that. Please try again.",
|
|
1170
|
+
consult: ({ question, responseStyle }) => consultOpenClawAgentForGoogleMeet({
|
|
1171
|
+
config: params.config,
|
|
1172
|
+
fullConfig: params.fullConfig,
|
|
1173
|
+
runtime: params.runtime,
|
|
1174
|
+
logger: params.logger,
|
|
1175
|
+
meetingSessionId: params.meetingSessionId,
|
|
1176
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
1177
|
+
args: {
|
|
1178
|
+
question,
|
|
1179
|
+
responseStyle
|
|
1180
|
+
},
|
|
1181
|
+
transcript
|
|
1182
|
+
}),
|
|
1183
|
+
deliver: (text) => {
|
|
1184
|
+
bridge?.sendUserMessage(buildGoogleMeetSpeakExactUserMessage(text));
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1087
1187
|
bridge = createRealtimeVoiceBridgeSession({
|
|
1088
1188
|
provider: resolved.provider,
|
|
1089
1189
|
providerConfig: resolved.providerConfig,
|
|
@@ -1097,69 +1197,140 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
1097
1197
|
audioSink: {
|
|
1098
1198
|
isOpen: () => !stopped,
|
|
1099
1199
|
sendAudio: (audio) => {
|
|
1200
|
+
const turnId = ensureTalkTurn();
|
|
1201
|
+
rememberTalkEvent(talk.startOutputAudio({
|
|
1202
|
+
turnId,
|
|
1203
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
1204
|
+
}).event);
|
|
1205
|
+
emitTalkEvent({
|
|
1206
|
+
type: "output.audio.delta",
|
|
1207
|
+
turnId,
|
|
1208
|
+
payload: { byteLength: audio.byteLength }
|
|
1209
|
+
});
|
|
1100
1210
|
lastOutputAtMs = Date.now();
|
|
1101
1211
|
lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1102
1212
|
lastOutputBytes += audio.byteLength;
|
|
1103
1213
|
suppressInputForOutput(audio);
|
|
1104
1214
|
writeOutputAudio(audio);
|
|
1105
1215
|
},
|
|
1106
|
-
clearAudio:
|
|
1216
|
+
clearAudio: () => {
|
|
1217
|
+
clearOutputPlayback();
|
|
1218
|
+
finishOutputAudio("clear");
|
|
1219
|
+
}
|
|
1107
1220
|
},
|
|
1108
1221
|
onTranscript: (role, text, isFinal) => {
|
|
1222
|
+
const turnId = ensureTalkTurn();
|
|
1223
|
+
emitTalkEvent({
|
|
1224
|
+
type: role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta",
|
|
1225
|
+
turnId,
|
|
1226
|
+
payload: role === "assistant" ? { text } : {
|
|
1227
|
+
role,
|
|
1228
|
+
text
|
|
1229
|
+
},
|
|
1230
|
+
final: isFinal
|
|
1231
|
+
});
|
|
1232
|
+
if (role === "user" && isFinal) emitTalkEvent({
|
|
1233
|
+
type: "input.audio.committed",
|
|
1234
|
+
turnId,
|
|
1235
|
+
payload: { meetingSessionId: params.meetingSessionId },
|
|
1236
|
+
final: true
|
|
1237
|
+
});
|
|
1109
1238
|
if (isFinal) {
|
|
1110
1239
|
recordGoogleMeetRealtimeTranscript(transcript, role, text);
|
|
1111
|
-
params.logger.info(`
|
|
1240
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog(`realtime ${role}`, text));
|
|
1112
1241
|
if (role === "user" && strategy === "agent") {
|
|
1113
1242
|
if (isGoogleMeetLikelyAssistantEchoTranscript({
|
|
1114
1243
|
transcript,
|
|
1115
1244
|
text
|
|
1116
1245
|
})) {
|
|
1117
|
-
params.logger.info(
|
|
1246
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("realtime ignored assistant echo transcript", text));
|
|
1118
1247
|
return;
|
|
1119
1248
|
}
|
|
1120
|
-
|
|
1249
|
+
agentTalkback?.enqueue(text);
|
|
1121
1250
|
}
|
|
1122
1251
|
}
|
|
1123
1252
|
},
|
|
1124
1253
|
onEvent: (event) => {
|
|
1125
1254
|
recordGoogleMeetRealtimeEvent(realtimeEvents, event);
|
|
1255
|
+
if (event.type === "input_audio_buffer.speech_started") ensureTalkTurn();
|
|
1256
|
+
else if (event.type === "input_audio_buffer.speech_stopped") {
|
|
1257
|
+
const turnId = talk.activeTurnId;
|
|
1258
|
+
if (!turnId) return;
|
|
1259
|
+
emitTalkEvent({
|
|
1260
|
+
type: "input.audio.committed",
|
|
1261
|
+
turnId,
|
|
1262
|
+
payload: {
|
|
1263
|
+
meetingSessionId: params.meetingSessionId,
|
|
1264
|
+
source: event.type
|
|
1265
|
+
},
|
|
1266
|
+
final: true
|
|
1267
|
+
});
|
|
1268
|
+
} else if (event.type === "response.done") {
|
|
1269
|
+
finishOutputAudio("response.done");
|
|
1270
|
+
endTalkTurn("response.done");
|
|
1271
|
+
} else if (event.type === "error") emitTalkEvent({
|
|
1272
|
+
type: "session.error",
|
|
1273
|
+
payload: { message: event.detail ?? "Realtime provider error" },
|
|
1274
|
+
final: true
|
|
1275
|
+
});
|
|
1126
1276
|
if (event.type === "error" || event.type === "response.done" || event.type === "input_audio_buffer.speech_started" || event.type === "input_audio_buffer.speech_stopped" || event.type === "conversation.item.input_audio_transcription.completed" || event.type === "conversation.item.input_audio_transcription.failed") {
|
|
1127
1277
|
const detail = event.detail ? ` ${event.detail}` : "";
|
|
1128
1278
|
params.logger.info(`[google-meet] realtime ${event.direction}:${event.type}${detail}`);
|
|
1129
1279
|
}
|
|
1130
1280
|
},
|
|
1131
1281
|
onToolCall: (event, session) => {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1282
|
+
emitTalkEvent({
|
|
1283
|
+
type: "tool.call",
|
|
1284
|
+
turnId: ensureTalkTurn(),
|
|
1285
|
+
itemId: event.itemId,
|
|
1286
|
+
callId: event.callId,
|
|
1287
|
+
payload: {
|
|
1288
|
+
name: event.name,
|
|
1289
|
+
args: event.args
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
const turnId = ensureTalkTurn();
|
|
1293
|
+
handleGoogleMeetRealtimeConsultToolCall({
|
|
1294
|
+
strategy,
|
|
1295
|
+
session,
|
|
1296
|
+
event,
|
|
1142
1297
|
config: params.config,
|
|
1143
1298
|
fullConfig: params.fullConfig,
|
|
1144
1299
|
runtime: params.runtime,
|
|
1145
1300
|
logger: params.logger,
|
|
1146
1301
|
meetingSessionId: params.meetingSessionId,
|
|
1147
1302
|
requesterSessionKey: params.requesterSessionKey,
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
session.submitToolResult(event.callId || event.itemId, { error: formatErrorMessage(error) });
|
|
1303
|
+
transcript,
|
|
1304
|
+
onTalkEvent: (input) => emitTalkEvent({
|
|
1305
|
+
...input,
|
|
1306
|
+
turnId: input.turnId ?? turnId
|
|
1307
|
+
})
|
|
1154
1308
|
});
|
|
1155
1309
|
},
|
|
1156
|
-
onError:
|
|
1310
|
+
onError: (error) => {
|
|
1311
|
+
emitTalkEvent({
|
|
1312
|
+
type: "session.error",
|
|
1313
|
+
payload: { message: formatErrorMessage(error) },
|
|
1314
|
+
final: true
|
|
1315
|
+
});
|
|
1316
|
+
fail("realtime voice bridge")(error);
|
|
1317
|
+
},
|
|
1157
1318
|
onClose: (reason) => {
|
|
1158
1319
|
realtimeReady = false;
|
|
1320
|
+
finishOutputAudio(reason);
|
|
1321
|
+
emitTalkEvent({
|
|
1322
|
+
type: "session.closed",
|
|
1323
|
+
payload: { reason },
|
|
1324
|
+
final: true
|
|
1325
|
+
});
|
|
1159
1326
|
if (reason === "error") stop();
|
|
1160
1327
|
},
|
|
1161
1328
|
onReady: () => {
|
|
1162
1329
|
realtimeReady = true;
|
|
1330
|
+
emitTalkEvent({
|
|
1331
|
+
type: "session.ready",
|
|
1332
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
1333
|
+
});
|
|
1163
1334
|
}
|
|
1164
1335
|
});
|
|
1165
1336
|
startHumanBargeInMonitor();
|
|
@@ -1173,6 +1344,11 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
1173
1344
|
}
|
|
1174
1345
|
lastInputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1175
1346
|
lastInputBytes += audio.byteLength;
|
|
1347
|
+
emitTalkEvent({
|
|
1348
|
+
type: "input.audio.delta",
|
|
1349
|
+
turnId: ensureTalkTurn(),
|
|
1350
|
+
payload: { byteLength: audio.byteLength }
|
|
1351
|
+
});
|
|
1176
1352
|
bridge?.sendAudio(Buffer.from(audio));
|
|
1177
1353
|
}
|
|
1178
1354
|
});
|
|
@@ -1197,6 +1373,7 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
1197
1373
|
suppressedInputBytes,
|
|
1198
1374
|
...getGoogleMeetRealtimeTranscriptHealth(transcript),
|
|
1199
1375
|
...getGoogleMeetRealtimeEventHealth(realtimeEvents),
|
|
1376
|
+
recentTalkEvents: summarizeGoogleMeetTalkEvents(recentTalkEvents),
|
|
1200
1377
|
lastClearAt,
|
|
1201
1378
|
clearCount,
|
|
1202
1379
|
bridgeClosed: stopped
|
|
@@ -1244,17 +1421,12 @@ async function startNodeAgentAudioBridge(params) {
|
|
|
1244
1421
|
audioFormat: params.config.chrome.audioFormat
|
|
1245
1422
|
}));
|
|
1246
1423
|
const transcript = [];
|
|
1247
|
-
let
|
|
1248
|
-
let pendingAgentQuestion;
|
|
1249
|
-
let agentConsultDebounceTimer;
|
|
1424
|
+
let agentTalkback;
|
|
1250
1425
|
let ttsQueue = Promise.resolve();
|
|
1251
1426
|
const stop = async () => {
|
|
1252
1427
|
if (stopped) return;
|
|
1253
1428
|
stopped = true;
|
|
1254
|
-
|
|
1255
|
-
clearTimeout(agentConsultDebounceTimer);
|
|
1256
|
-
agentConsultDebounceTimer = void 0;
|
|
1257
|
-
}
|
|
1429
|
+
agentTalkback?.close();
|
|
1258
1430
|
try {
|
|
1259
1431
|
sttSession?.close();
|
|
1260
1432
|
} catch (error) {
|
|
@@ -1303,7 +1475,7 @@ async function startNodeAgentAudioBridge(params) {
|
|
|
1303
1475
|
ttsQueue = ttsQueue.then(async () => {
|
|
1304
1476
|
if (stopped) return;
|
|
1305
1477
|
recordGoogleMeetRealtimeTranscript(transcript, "assistant", normalized);
|
|
1306
|
-
params.logger.info(
|
|
1478
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("node agent assistant", normalized));
|
|
1307
1479
|
const result = await params.runtime.tts.textToSpeechTelephony({
|
|
1308
1480
|
text: normalized,
|
|
1309
1481
|
cfg: params.fullConfig
|
|
@@ -1315,74 +1487,43 @@ async function startNodeAgentAudioBridge(params) {
|
|
|
1315
1487
|
params.logger.warn(`[google-meet] node agent TTS failed: ${formatErrorMessage(error)}`);
|
|
1316
1488
|
});
|
|
1317
1489
|
};
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
args: {
|
|
1341
|
-
question: currentQuestion,
|
|
1342
|
-
responseStyle: "Brief, natural spoken answer for a live meeting."
|
|
1343
|
-
},
|
|
1344
|
-
transcript
|
|
1345
|
-
})).text);
|
|
1346
|
-
nextQuestion = pendingAgentQuestion;
|
|
1347
|
-
}
|
|
1348
|
-
} catch (error) {
|
|
1349
|
-
params.logger.warn(`[google-meet] node agent consult failed: ${formatErrorMessage(error)}`);
|
|
1350
|
-
enqueueSpeakText("I hit an error while checking that. Please try again.");
|
|
1351
|
-
} finally {
|
|
1352
|
-
agentConsultActive = false;
|
|
1353
|
-
const queuedQuestion = pendingAgentQuestion;
|
|
1354
|
-
pendingAgentQuestion = void 0;
|
|
1355
|
-
if (queuedQuestion && !stopped) runAgentConsultForUserTranscript(queuedQuestion);
|
|
1356
|
-
}
|
|
1357
|
-
};
|
|
1358
|
-
const enqueueAgentConsultForUserTranscript = (question) => {
|
|
1359
|
-
const trimmed = question.trim();
|
|
1360
|
-
if (!trimmed || stopped) return;
|
|
1361
|
-
pendingAgentQuestion = pendingAgentQuestion ? `${pendingAgentQuestion}\n${trimmed}` : trimmed;
|
|
1362
|
-
if (agentConsultDebounceTimer) clearTimeout(agentConsultDebounceTimer);
|
|
1363
|
-
agentConsultDebounceTimer = setTimeout(() => {
|
|
1364
|
-
agentConsultDebounceTimer = void 0;
|
|
1365
|
-
const queuedQuestion = pendingAgentQuestion;
|
|
1366
|
-
pendingAgentQuestion = void 0;
|
|
1367
|
-
if (queuedQuestion && !stopped) runAgentConsultForUserTranscript(queuedQuestion);
|
|
1368
|
-
}, 900);
|
|
1369
|
-
agentConsultDebounceTimer.unref?.();
|
|
1370
|
-
};
|
|
1490
|
+
agentTalkback = createRealtimeVoiceAgentTalkbackQueue({
|
|
1491
|
+
debounceMs: 900,
|
|
1492
|
+
isStopped: () => stopped,
|
|
1493
|
+
logger: params.logger,
|
|
1494
|
+
logPrefix: "[google-meet] node agent",
|
|
1495
|
+
responseStyle: "Brief, natural spoken answer for a live meeting.",
|
|
1496
|
+
fallbackText: "I hit an error while checking that. Please try again.",
|
|
1497
|
+
consult: ({ question, responseStyle }) => consultOpenClawAgentForGoogleMeet({
|
|
1498
|
+
config: params.config,
|
|
1499
|
+
fullConfig: params.fullConfig,
|
|
1500
|
+
runtime: params.runtime,
|
|
1501
|
+
logger: params.logger,
|
|
1502
|
+
meetingSessionId: params.meetingSessionId,
|
|
1503
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
1504
|
+
args: {
|
|
1505
|
+
question,
|
|
1506
|
+
responseStyle
|
|
1507
|
+
},
|
|
1508
|
+
transcript
|
|
1509
|
+
}),
|
|
1510
|
+
deliver: enqueueSpeakText
|
|
1511
|
+
});
|
|
1371
1512
|
sttSession = resolved.provider.createSession({
|
|
1372
1513
|
providerConfig: resolved.providerConfig,
|
|
1373
1514
|
onTranscript: (text) => {
|
|
1374
1515
|
const trimmed = text.trim();
|
|
1375
1516
|
if (!trimmed || stopped) return;
|
|
1376
1517
|
recordGoogleMeetRealtimeTranscript(transcript, "user", trimmed);
|
|
1377
|
-
params.logger.info(
|
|
1518
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("node agent user", trimmed));
|
|
1378
1519
|
if (isGoogleMeetLikelyAssistantEchoTranscript({
|
|
1379
1520
|
transcript,
|
|
1380
1521
|
text: trimmed
|
|
1381
1522
|
})) {
|
|
1382
|
-
params.logger.info(
|
|
1523
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("node agent ignored assistant echo transcript", trimmed));
|
|
1383
1524
|
return;
|
|
1384
1525
|
}
|
|
1385
|
-
|
|
1526
|
+
agentTalkback?.enqueue(trimmed);
|
|
1386
1527
|
},
|
|
1387
1528
|
onError: (error) => {
|
|
1388
1529
|
params.logger.warn(`[google-meet] node agent transcription bridge failed: ${formatErrorMessage(error)}`);
|
|
@@ -1482,6 +1623,49 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1482
1623
|
const transcript = [];
|
|
1483
1624
|
const realtimeEvents = [];
|
|
1484
1625
|
const strategy = params.config.realtime.strategy;
|
|
1626
|
+
const talk = createTalkSessionController({
|
|
1627
|
+
sessionId: `google-meet:${params.meetingSessionId}:${params.bridgeId}:node-realtime`,
|
|
1628
|
+
mode: "realtime",
|
|
1629
|
+
transport: "gateway-relay",
|
|
1630
|
+
brain: strategy === "bidi" ? "direct-tools" : "agent-consult",
|
|
1631
|
+
provider: resolved.provider.id
|
|
1632
|
+
}, { onEvent: recordTalkObservabilityEvent });
|
|
1633
|
+
const recentTalkEvents = [];
|
|
1634
|
+
const rememberTalkEvent = (event) => {
|
|
1635
|
+
if (event) pushGoogleMeetTalkEvent(recentTalkEvents, event);
|
|
1636
|
+
};
|
|
1637
|
+
const emitTalkEvent = (input) => {
|
|
1638
|
+
rememberTalkEvent(talk.emit(input));
|
|
1639
|
+
};
|
|
1640
|
+
const ensureTalkTurn = () => {
|
|
1641
|
+
const turn = talk.ensureTurn({ payload: {
|
|
1642
|
+
bridgeId: params.bridgeId,
|
|
1643
|
+
meetingSessionId: params.meetingSessionId
|
|
1644
|
+
} });
|
|
1645
|
+
if (turn.event) rememberTalkEvent(turn.event);
|
|
1646
|
+
return turn.turnId;
|
|
1647
|
+
};
|
|
1648
|
+
const finishOutputAudio = (reason) => {
|
|
1649
|
+
rememberTalkEvent(talk.finishOutputAudio({ payload: {
|
|
1650
|
+
bridgeId: params.bridgeId,
|
|
1651
|
+
reason
|
|
1652
|
+
} }));
|
|
1653
|
+
};
|
|
1654
|
+
const endTalkTurn = (reason = "completed") => {
|
|
1655
|
+
const ended = talk.endTurn({ payload: {
|
|
1656
|
+
bridgeId: params.bridgeId,
|
|
1657
|
+
reason
|
|
1658
|
+
} });
|
|
1659
|
+
if (ended.ok) rememberTalkEvent(ended.event);
|
|
1660
|
+
};
|
|
1661
|
+
emitTalkEvent({
|
|
1662
|
+
type: "session.started",
|
|
1663
|
+
payload: {
|
|
1664
|
+
bridgeId: params.bridgeId,
|
|
1665
|
+
meetingSessionId: params.meetingSessionId,
|
|
1666
|
+
nodeId: params.nodeId
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1485
1669
|
params.logger.info(formatGoogleMeetRealtimeVoiceModelLog({
|
|
1486
1670
|
strategy,
|
|
1487
1671
|
provider: resolved.provider,
|
|
@@ -1489,70 +1673,35 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1489
1673
|
fallbackModel: params.config.realtime.model,
|
|
1490
1674
|
audioFormat: params.config.chrome.audioFormat
|
|
1491
1675
|
}));
|
|
1492
|
-
let
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
agentConsultActive = true;
|
|
1516
|
-
let nextQuestion = trimmed;
|
|
1517
|
-
try {
|
|
1518
|
-
while (nextQuestion) {
|
|
1519
|
-
if (stopped) return;
|
|
1520
|
-
const currentQuestion = nextQuestion;
|
|
1521
|
-
pendingAgentQuestion = void 0;
|
|
1522
|
-
params.logger.info(`[google-meet] node realtime agent consult: ${currentQuestion}`);
|
|
1523
|
-
const result = await consultOpenClawAgentForGoogleMeet({
|
|
1524
|
-
config: params.config,
|
|
1525
|
-
fullConfig: params.fullConfig,
|
|
1526
|
-
runtime: params.runtime,
|
|
1527
|
-
logger: params.logger,
|
|
1528
|
-
meetingSessionId: params.meetingSessionId,
|
|
1529
|
-
requesterSessionKey: params.requesterSessionKey,
|
|
1530
|
-
args: {
|
|
1531
|
-
question: currentQuestion,
|
|
1532
|
-
responseStyle: "Brief, natural spoken answer for a live meeting."
|
|
1533
|
-
},
|
|
1534
|
-
transcript
|
|
1535
|
-
});
|
|
1536
|
-
if (!stopped && result.text.trim()) bridge?.sendUserMessage(buildGoogleMeetSpeakExactUserMessage(result.text.trim()));
|
|
1537
|
-
nextQuestion = pendingAgentQuestion;
|
|
1538
|
-
}
|
|
1539
|
-
} catch (error) {
|
|
1540
|
-
params.logger.warn(`[google-meet] node realtime agent consult failed: ${formatErrorMessage(error)}`);
|
|
1541
|
-
if (!stopped) bridge?.sendUserMessage(buildGoogleMeetSpeakExactUserMessage("I hit an error while checking that. Please try again."));
|
|
1542
|
-
} finally {
|
|
1543
|
-
agentConsultActive = false;
|
|
1544
|
-
const queuedQuestion = pendingAgentQuestion;
|
|
1545
|
-
pendingAgentQuestion = void 0;
|
|
1546
|
-
if (queuedQuestion && !stopped) runAgentConsultForUserTranscript(queuedQuestion);
|
|
1676
|
+
let agentTalkback;
|
|
1677
|
+
agentTalkback = createRealtimeVoiceAgentTalkbackQueue({
|
|
1678
|
+
debounceMs: 900,
|
|
1679
|
+
isStopped: () => stopped,
|
|
1680
|
+
logger: params.logger,
|
|
1681
|
+
logPrefix: "[google-meet] node realtime agent",
|
|
1682
|
+
responseStyle: "Brief, natural spoken answer for a live meeting.",
|
|
1683
|
+
fallbackText: "I hit an error while checking that. Please try again.",
|
|
1684
|
+
consult: ({ question, responseStyle }) => consultOpenClawAgentForGoogleMeet({
|
|
1685
|
+
config: params.config,
|
|
1686
|
+
fullConfig: params.fullConfig,
|
|
1687
|
+
runtime: params.runtime,
|
|
1688
|
+
logger: params.logger,
|
|
1689
|
+
meetingSessionId: params.meetingSessionId,
|
|
1690
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
1691
|
+
args: {
|
|
1692
|
+
question,
|
|
1693
|
+
responseStyle
|
|
1694
|
+
},
|
|
1695
|
+
transcript
|
|
1696
|
+
}),
|
|
1697
|
+
deliver: (text) => {
|
|
1698
|
+
bridge?.sendUserMessage(buildGoogleMeetSpeakExactUserMessage(text));
|
|
1547
1699
|
}
|
|
1548
|
-
};
|
|
1700
|
+
});
|
|
1549
1701
|
const stop = async () => {
|
|
1550
1702
|
if (stopped) return;
|
|
1551
1703
|
stopped = true;
|
|
1552
|
-
|
|
1553
|
-
clearTimeout(agentConsultDebounceTimer);
|
|
1554
|
-
agentConsultDebounceTimer = void 0;
|
|
1555
|
-
}
|
|
1704
|
+
agentTalkback?.close();
|
|
1556
1705
|
try {
|
|
1557
1706
|
bridge?.close();
|
|
1558
1707
|
} catch (error) {
|
|
@@ -1585,6 +1734,16 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1585
1734
|
audioSink: {
|
|
1586
1735
|
isOpen: () => !stopped,
|
|
1587
1736
|
sendAudio: (audio) => {
|
|
1737
|
+
const turnId = ensureTalkTurn();
|
|
1738
|
+
rememberTalkEvent(talk.startOutputAudio({
|
|
1739
|
+
turnId,
|
|
1740
|
+
payload: { bridgeId: params.bridgeId }
|
|
1741
|
+
}).event);
|
|
1742
|
+
emitTalkEvent({
|
|
1743
|
+
type: "output.audio.delta",
|
|
1744
|
+
turnId,
|
|
1745
|
+
payload: { byteLength: audio.byteLength }
|
|
1746
|
+
});
|
|
1588
1747
|
const suppression = extendGoogleMeetOutputEchoSuppression({
|
|
1589
1748
|
audio,
|
|
1590
1749
|
audioFormat: params.config.chrome.audioFormat,
|
|
@@ -1613,6 +1772,7 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1613
1772
|
clearAudio: () => {
|
|
1614
1773
|
lastClearAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1615
1774
|
clearCount += 1;
|
|
1775
|
+
finishOutputAudio("clear");
|
|
1616
1776
|
suppressInputUntil = 0;
|
|
1617
1777
|
lastOutputPlayableUntilMs = 0;
|
|
1618
1778
|
params.runtime.nodes.invoke({
|
|
@@ -1630,63 +1790,119 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1630
1790
|
}
|
|
1631
1791
|
},
|
|
1632
1792
|
onTranscript: (role, text, isFinal) => {
|
|
1793
|
+
const turnId = ensureTalkTurn();
|
|
1794
|
+
emitTalkEvent({
|
|
1795
|
+
type: role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta",
|
|
1796
|
+
turnId,
|
|
1797
|
+
payload: role === "assistant" ? { text } : {
|
|
1798
|
+
role,
|
|
1799
|
+
text
|
|
1800
|
+
},
|
|
1801
|
+
final: isFinal
|
|
1802
|
+
});
|
|
1803
|
+
if (role === "user" && isFinal) emitTalkEvent({
|
|
1804
|
+
type: "input.audio.committed",
|
|
1805
|
+
turnId,
|
|
1806
|
+
payload: { bridgeId: params.bridgeId },
|
|
1807
|
+
final: true
|
|
1808
|
+
});
|
|
1633
1809
|
if (isFinal) {
|
|
1634
1810
|
recordGoogleMeetRealtimeTranscript(transcript, role, text);
|
|
1635
|
-
params.logger.info(`
|
|
1811
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog(`node realtime ${role}`, text));
|
|
1636
1812
|
if (role === "user" && strategy === "agent") {
|
|
1637
1813
|
if (isGoogleMeetLikelyAssistantEchoTranscript({
|
|
1638
1814
|
transcript,
|
|
1639
1815
|
text
|
|
1640
1816
|
})) {
|
|
1641
|
-
params.logger.info(
|
|
1817
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("node realtime ignored assistant echo transcript", text));
|
|
1642
1818
|
return;
|
|
1643
1819
|
}
|
|
1644
|
-
|
|
1820
|
+
agentTalkback?.enqueue(text);
|
|
1645
1821
|
}
|
|
1646
1822
|
}
|
|
1647
1823
|
},
|
|
1648
1824
|
onEvent: (event) => {
|
|
1649
1825
|
recordGoogleMeetRealtimeEvent(realtimeEvents, event);
|
|
1826
|
+
if (event.type === "input_audio_buffer.speech_started") ensureTalkTurn();
|
|
1827
|
+
else if (event.type === "input_audio_buffer.speech_stopped") {
|
|
1828
|
+
const turnId = talk.activeTurnId;
|
|
1829
|
+
if (!turnId) return;
|
|
1830
|
+
emitTalkEvent({
|
|
1831
|
+
type: "input.audio.committed",
|
|
1832
|
+
turnId,
|
|
1833
|
+
payload: {
|
|
1834
|
+
bridgeId: params.bridgeId,
|
|
1835
|
+
source: event.type
|
|
1836
|
+
},
|
|
1837
|
+
final: true
|
|
1838
|
+
});
|
|
1839
|
+
} else if (event.type === "response.done") {
|
|
1840
|
+
finishOutputAudio("response.done");
|
|
1841
|
+
endTalkTurn("response.done");
|
|
1842
|
+
} else if (event.type === "error") emitTalkEvent({
|
|
1843
|
+
type: "session.error",
|
|
1844
|
+
payload: { message: event.detail ?? "Realtime provider error" },
|
|
1845
|
+
final: true
|
|
1846
|
+
});
|
|
1650
1847
|
if (event.type === "error" || event.type === "response.done" || event.type === "input_audio_buffer.speech_started" || event.type === "input_audio_buffer.speech_stopped" || event.type === "conversation.item.input_audio_transcription.completed" || event.type === "conversation.item.input_audio_transcription.failed") {
|
|
1651
1848
|
const detail = event.detail ? ` ${event.detail}` : "";
|
|
1652
1849
|
params.logger.info(`[google-meet] node realtime ${event.direction}:${event.type}${detail}`);
|
|
1653
1850
|
}
|
|
1654
1851
|
},
|
|
1655
1852
|
onToolCall: (event, session) => {
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1853
|
+
emitTalkEvent({
|
|
1854
|
+
type: "tool.call",
|
|
1855
|
+
turnId: ensureTalkTurn(),
|
|
1856
|
+
itemId: event.itemId,
|
|
1857
|
+
callId: event.callId,
|
|
1858
|
+
payload: {
|
|
1859
|
+
name: event.name,
|
|
1860
|
+
args: event.args
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
const turnId = ensureTalkTurn();
|
|
1864
|
+
handleGoogleMeetRealtimeConsultToolCall({
|
|
1865
|
+
strategy,
|
|
1866
|
+
session,
|
|
1867
|
+
event,
|
|
1666
1868
|
config: params.config,
|
|
1667
1869
|
fullConfig: params.fullConfig,
|
|
1668
1870
|
runtime: params.runtime,
|
|
1669
1871
|
logger: params.logger,
|
|
1670
1872
|
meetingSessionId: params.meetingSessionId,
|
|
1671
1873
|
requesterSessionKey: params.requesterSessionKey,
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
session.submitToolResult(event.callId || event.itemId, { error: formatErrorMessage(error) });
|
|
1874
|
+
transcript,
|
|
1875
|
+
onTalkEvent: (input) => emitTalkEvent({
|
|
1876
|
+
...input,
|
|
1877
|
+
turnId: input.turnId ?? turnId
|
|
1878
|
+
})
|
|
1678
1879
|
});
|
|
1679
1880
|
},
|
|
1680
1881
|
onError: (error) => {
|
|
1681
1882
|
params.logger.warn(`[google-meet] node realtime voice bridge failed: ${formatErrorMessage(error)}`);
|
|
1883
|
+
emitTalkEvent({
|
|
1884
|
+
type: "session.error",
|
|
1885
|
+
payload: { message: formatErrorMessage(error) },
|
|
1886
|
+
final: true
|
|
1887
|
+
});
|
|
1682
1888
|
stop();
|
|
1683
1889
|
},
|
|
1684
1890
|
onClose: (reason) => {
|
|
1685
1891
|
realtimeReady = false;
|
|
1892
|
+
finishOutputAudio(reason);
|
|
1893
|
+
emitTalkEvent({
|
|
1894
|
+
type: "session.closed",
|
|
1895
|
+
payload: { reason },
|
|
1896
|
+
final: true
|
|
1897
|
+
});
|
|
1686
1898
|
if (reason === "error") stop();
|
|
1687
1899
|
},
|
|
1688
1900
|
onReady: () => {
|
|
1689
1901
|
realtimeReady = true;
|
|
1902
|
+
emitTalkEvent({
|
|
1903
|
+
type: "session.ready",
|
|
1904
|
+
payload: { bridgeId: params.bridgeId }
|
|
1905
|
+
});
|
|
1690
1906
|
}
|
|
1691
1907
|
});
|
|
1692
1908
|
await bridge.connect();
|
|
@@ -1717,6 +1933,11 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1717
1933
|
}
|
|
1718
1934
|
lastInputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1719
1935
|
lastInputBytes += audio.byteLength;
|
|
1936
|
+
emitTalkEvent({
|
|
1937
|
+
type: "input.audio.delta",
|
|
1938
|
+
turnId: ensureTalkTurn(),
|
|
1939
|
+
payload: { byteLength: audio.byteLength }
|
|
1940
|
+
});
|
|
1720
1941
|
bridge?.sendAudio(audio);
|
|
1721
1942
|
}
|
|
1722
1943
|
if (result.closed === true) await stop();
|
|
@@ -1754,6 +1975,7 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1754
1975
|
suppressedInputBytes,
|
|
1755
1976
|
...getGoogleMeetRealtimeTranscriptHealth(transcript),
|
|
1756
1977
|
...getGoogleMeetRealtimeEventHealth(realtimeEvents),
|
|
1978
|
+
recentTalkEvents: summarizeGoogleMeetTalkEvents(recentTalkEvents),
|
|
1757
1979
|
consecutiveInputErrors,
|
|
1758
1980
|
lastInputError,
|
|
1759
1981
|
clearCount,
|
|
@@ -2289,8 +2511,9 @@ async function openMeetWithBrowserRequest(params) {
|
|
|
2289
2511
|
};
|
|
2290
2512
|
break;
|
|
2291
2513
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2514
|
+
const remainingWaitMs = deadline - Date.now();
|
|
2515
|
+
if (remainingWaitMs > 0) await new Promise((resolve) => setTimeout(resolve, Math.min(750, remainingWaitMs)));
|
|
2516
|
+
} while (Date.now() < deadline);
|
|
2294
2517
|
return {
|
|
2295
2518
|
launched: true,
|
|
2296
2519
|
browser
|
|
@@ -3034,12 +3257,14 @@ function buildMeetDtmfSequence(params) {
|
|
|
3034
3257
|
if (!/^[0-9]+#?$/.test(compactPin)) throw new Error("pin may only contain digits and an optional trailing #");
|
|
3035
3258
|
return compactPin.endsWith("#") ? compactPin : `${compactPin}#`;
|
|
3036
3259
|
}
|
|
3260
|
+
function prefixDtmfWait(sequence, delayMs) {
|
|
3261
|
+
if (!sequence || delayMs <= 0) return sequence;
|
|
3262
|
+
const waitCount = Math.ceil(delayMs / 500);
|
|
3263
|
+
if (waitCount <= 0) return sequence;
|
|
3264
|
+
return `${"w".repeat(waitCount)}${sequence}`;
|
|
3265
|
+
}
|
|
3037
3266
|
//#endregion
|
|
3038
3267
|
//#region extensions/google-meet/src/voice-call-gateway.ts
|
|
3039
|
-
function sleep$1(ms) {
|
|
3040
|
-
if (ms <= 0) return Promise.resolve();
|
|
3041
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3042
|
-
}
|
|
3043
3268
|
async function createConnectedGatewayClient(config) {
|
|
3044
3269
|
let client;
|
|
3045
3270
|
await new Promise((resolve, reject) => {
|
|
@@ -3080,42 +3305,44 @@ async function createConnectedGatewayClient(config) {
|
|
|
3080
3305
|
});
|
|
3081
3306
|
return client;
|
|
3082
3307
|
}
|
|
3308
|
+
function isVoiceCallMissingError(error) {
|
|
3309
|
+
const message = formatErrorMessage(error).toLowerCase();
|
|
3310
|
+
return message.includes("call not found") || message.includes("call is not active");
|
|
3311
|
+
}
|
|
3083
3312
|
async function joinMeetViaVoiceCallGateway(params) {
|
|
3084
3313
|
let client;
|
|
3085
3314
|
try {
|
|
3086
3315
|
client = await createConnectedGatewayClient(params.config);
|
|
3087
|
-
params.logger?.info(`[google-meet] Delegating Twilio join to Voice Call (dtmf=${params.dtmfSequence ? "
|
|
3316
|
+
params.logger?.info(`[google-meet] Delegating Twilio join to Voice Call (dtmf=${params.dtmfSequence ? "pre-connect" : "none"}, intro=${params.message ? "delayed" : "none"})`);
|
|
3088
3317
|
const start = await client.request("voicecall.start", {
|
|
3089
3318
|
to: params.dialInNumber,
|
|
3090
|
-
mode: "conversation"
|
|
3319
|
+
mode: "conversation",
|
|
3320
|
+
...params.dtmfSequence ? { dtmfSequence: params.dtmfSequence } : {},
|
|
3321
|
+
...params.requesterSessionKey ? { requesterSessionKey: params.requesterSessionKey } : {},
|
|
3322
|
+
...params.sessionKey ? { sessionKey: params.sessionKey } : {}
|
|
3091
3323
|
}, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3092
3324
|
if (!start.callId) throw new Error(start.error || "voicecall.start did not return callId");
|
|
3093
3325
|
params.logger?.info(`[google-meet] Voice Call Twilio phone leg started: callId=${start.callId}`);
|
|
3094
|
-
|
|
3095
|
-
if (params.dtmfSequence)
|
|
3096
|
-
const delayMs = params.config.voiceCall.dtmfDelayMs;
|
|
3097
|
-
params.logger?.info(`[google-meet] Waiting ${delayMs}ms before sending Meet DTMF for callId=${start.callId}`);
|
|
3098
|
-
await sleep$1(delayMs);
|
|
3099
|
-
const dtmf = await client.request("voicecall.dtmf", {
|
|
3100
|
-
callId: start.callId,
|
|
3101
|
-
digits: params.dtmfSequence
|
|
3102
|
-
}, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3103
|
-
if (dtmf.success === false) throw new Error(dtmf.error || "voicecall.dtmf failed");
|
|
3104
|
-
dtmfSent = true;
|
|
3105
|
-
params.logger?.info(`[google-meet] Meet DTMF sent after phone leg connected: callId=${start.callId} digits=${params.dtmfSequence.length}`);
|
|
3106
|
-
}
|
|
3326
|
+
const dtmfSent = Boolean(params.dtmfSequence);
|
|
3327
|
+
if (dtmfSent) params.logger?.info(`[google-meet] Meet DTMF queued before realtime connect: callId=${start.callId} digits=${params.dtmfSequence?.length ?? 0}`);
|
|
3107
3328
|
let introSent = false;
|
|
3108
3329
|
if (params.message) {
|
|
3109
3330
|
const delayMs = params.dtmfSequence ? params.config.voiceCall.postDtmfSpeechDelayMs : 0;
|
|
3110
3331
|
if (delayMs > 0) {
|
|
3111
3332
|
params.logger?.info(`[google-meet] Waiting ${delayMs}ms after Meet DTMF before speaking intro for callId=${start.callId}`);
|
|
3112
|
-
await sleep
|
|
3333
|
+
await sleep(delayMs);
|
|
3334
|
+
}
|
|
3335
|
+
let spoken;
|
|
3336
|
+
try {
|
|
3337
|
+
spoken = await client.request("voicecall.speak", {
|
|
3338
|
+
callId: start.callId,
|
|
3339
|
+
allowTwimlFallback: false,
|
|
3340
|
+
message: params.message
|
|
3341
|
+
}, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3342
|
+
} catch (err) {
|
|
3343
|
+
params.logger?.warn?.(`[google-meet] Skipped intro speech because realtime bridge was not ready: ${formatErrorMessage(err)}`);
|
|
3344
|
+
spoken = { success: false };
|
|
3113
3345
|
}
|
|
3114
|
-
const spoken = await client.request("voicecall.speak", {
|
|
3115
|
-
callId: start.callId,
|
|
3116
|
-
allowTwimlFallback: false,
|
|
3117
|
-
message: params.message
|
|
3118
|
-
}, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3119
3346
|
if (spoken.success === false) params.logger?.warn?.(`[google-meet] Skipped intro speech because realtime bridge was not ready: ${spoken.error || "voicecall.speak failed"}`);
|
|
3120
3347
|
else {
|
|
3121
3348
|
introSent = true;
|
|
@@ -3135,7 +3362,20 @@ async function endMeetVoiceCallGatewayCall(params) {
|
|
|
3135
3362
|
let client;
|
|
3136
3363
|
try {
|
|
3137
3364
|
client = await createConnectedGatewayClient(params.config);
|
|
3138
|
-
|
|
3365
|
+
try {
|
|
3366
|
+
await client.request("voicecall.end", { callId: params.callId }, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3367
|
+
} catch (err) {
|
|
3368
|
+
if (!isVoiceCallMissingError(err)) throw err;
|
|
3369
|
+
}
|
|
3370
|
+
} finally {
|
|
3371
|
+
await client?.stopAndWait({ timeoutMs: 1e3 });
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
async function getMeetVoiceCallGatewayCall(params) {
|
|
3375
|
+
let client;
|
|
3376
|
+
try {
|
|
3377
|
+
client = await createConnectedGatewayClient(params.config);
|
|
3378
|
+
return await client.request("voicecall.status", { callId: params.callId }, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3139
3379
|
} finally {
|
|
3140
3380
|
await client?.stopAndWait({ timeoutMs: 1e3 });
|
|
3141
3381
|
}
|
|
@@ -3158,6 +3398,9 @@ async function speakMeetViaVoiceCallGateway(params) {
|
|
|
3158
3398
|
function nowIso() {
|
|
3159
3399
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3160
3400
|
}
|
|
3401
|
+
function buildTwilioVoiceCallSessionKey(meetingSessionId) {
|
|
3402
|
+
return `voice:google-meet:${meetingSessionId}`;
|
|
3403
|
+
}
|
|
3161
3404
|
function normalizeMeetUrl(input) {
|
|
3162
3405
|
const raw = normalizeOptionalString(input);
|
|
3163
3406
|
if (!raw) throw new Error("url required");
|
|
@@ -3200,12 +3443,12 @@ function resolveProbeTimeoutMs(input, fallback) {
|
|
|
3200
3443
|
if (!Number.isFinite(input) || input <= 0) throw new Error("timeoutMs must be a positive number");
|
|
3201
3444
|
return Math.min(Math.trunc(input), 12e4);
|
|
3202
3445
|
}
|
|
3203
|
-
function sleep(ms) {
|
|
3204
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3205
|
-
}
|
|
3206
3446
|
function isManagedChromeBrowserSession(session) {
|
|
3207
3447
|
return Boolean((session.transport === "chrome" || session.transport === "chrome-node") && session.chrome && session.chrome.launched);
|
|
3208
3448
|
}
|
|
3449
|
+
function noteSession(session, note) {
|
|
3450
|
+
session.notes = [...session.notes.filter((item) => item !== note), note];
|
|
3451
|
+
}
|
|
3209
3452
|
function evaluateSpeechReadiness(session) {
|
|
3210
3453
|
if (!isGoogleMeetTalkBackMode$1(session.mode) || !session.chrome) return { ready: true };
|
|
3211
3454
|
if (!isManagedChromeBrowserSession(session)) {
|
|
@@ -3378,15 +3621,20 @@ var GoogleMeetRuntime = class {
|
|
|
3378
3621
|
const url = normalizeMeetUrl(request.url);
|
|
3379
3622
|
const transport = resolveTransport(request.transport, this.params.config);
|
|
3380
3623
|
const mode = resolveMode(request.mode, this.params.config);
|
|
3381
|
-
|
|
3624
|
+
let reusable = this.list().find((session) => session.state === "active" && isSameMeetUrlForReuse(session.url, url) && session.transport === transport && session.mode === mode);
|
|
3625
|
+
if (reusable?.transport === "twilio") {
|
|
3626
|
+
await this.#refreshTwilioVoiceCallStatus(reusable);
|
|
3627
|
+
if (reusable.state !== "active") reusable = void 0;
|
|
3628
|
+
}
|
|
3382
3629
|
const speechInstructions = request.message ?? this.params.config.realtime.introMessage;
|
|
3383
3630
|
if (reusable) {
|
|
3384
3631
|
await this.#refreshBrowserHealthForChromeSession(reusable);
|
|
3385
|
-
reusable
|
|
3632
|
+
noteSession(reusable, "Reused existing active Meet session.");
|
|
3386
3633
|
reusable.updatedAt = nowIso();
|
|
3634
|
+
const spoken = isGoogleMeetTalkBackMode$1(mode) && speechInstructions ? await this.#speakWhenReady(reusable, speechInstructions) : false;
|
|
3387
3635
|
return {
|
|
3388
3636
|
session: reusable,
|
|
3389
|
-
spoken
|
|
3637
|
+
spoken
|
|
3390
3638
|
};
|
|
3391
3639
|
}
|
|
3392
3640
|
const createdAt = nowIso();
|
|
@@ -3444,15 +3692,18 @@ var GoogleMeetRuntime = class {
|
|
|
3444
3692
|
} else {
|
|
3445
3693
|
const dialInNumber = normalizeDialInNumber(request.dialInNumber ?? this.params.config.twilio.defaultDialInNumber);
|
|
3446
3694
|
if (!dialInNumber) throw new Error("Twilio transport requires a Meet dial-in phone number. Google Meet URLs do not include dial-in details; pass dialInNumber with optional pin/dtmfSequence, configure twilio.defaultDialInNumber, or use chrome/chrome-node transport.");
|
|
3447
|
-
const
|
|
3695
|
+
const rawDtmfSequence = buildMeetDtmfSequence({
|
|
3448
3696
|
pin: request.pin ?? this.params.config.twilio.defaultPin,
|
|
3449
3697
|
dtmfSequence: request.dtmfSequence ?? this.params.config.twilio.defaultDtmfSequence
|
|
3450
3698
|
});
|
|
3699
|
+
const dtmfSequence = request.dtmfSequence || this.params.config.twilio.defaultDtmfSequence ? rawDtmfSequence : prefixDtmfWait(rawDtmfSequence, this.params.config.voiceCall.dtmfDelayMs);
|
|
3451
3700
|
const voiceCallResult = this.params.config.voiceCall.enabled ? await joinMeetViaVoiceCallGateway({
|
|
3452
3701
|
config: this.params.config,
|
|
3453
3702
|
dialInNumber,
|
|
3454
3703
|
dtmfSequence,
|
|
3455
3704
|
logger: this.params.logger,
|
|
3705
|
+
...request.requesterSessionKey ? { requesterSessionKey: request.requesterSessionKey } : {},
|
|
3706
|
+
sessionKey: buildTwilioVoiceCallSessionKey(session.id),
|
|
3456
3707
|
message: isGoogleMeetTalkBackMode$1(mode) ? request.message ?? this.params.config.voiceCall.introMessage ?? this.params.config.realtime.introMessage : void 0
|
|
3457
3708
|
}) : void 0;
|
|
3458
3709
|
delegatedTwilioSpoken = Boolean(voiceCallResult?.introSent);
|
|
@@ -3470,7 +3721,7 @@ var GoogleMeetRuntime = class {
|
|
|
3470
3721
|
callId: voiceCallResult.callId
|
|
3471
3722
|
});
|
|
3472
3723
|
});
|
|
3473
|
-
session.notes.push(this.params.config.voiceCall.enabled ? dtmfSequence ? "Twilio transport delegated the phone leg to the voice-call plugin, then
|
|
3724
|
+
session.notes.push(this.params.config.voiceCall.enabled ? dtmfSequence ? "Twilio transport delegated the phone leg to the voice-call plugin, then queued configured DTMF before realtime connect." : "Twilio transport delegated the call to the voice-call plugin without configured DTMF." : "Twilio transport is an explicit dial plan; voice-call delegation is disabled.");
|
|
3474
3725
|
}
|
|
3475
3726
|
} catch (err) {
|
|
3476
3727
|
this.params.logger.warn(`[google-meet] join failed: ${formatErrorMessage(err)}`);
|
|
@@ -3490,7 +3741,12 @@ var GoogleMeetRuntime = class {
|
|
|
3490
3741
|
this.#sessionStops.delete(sessionId);
|
|
3491
3742
|
this.#sessionSpeakers.delete(sessionId);
|
|
3492
3743
|
this.#sessionHealth.delete(sessionId);
|
|
3493
|
-
|
|
3744
|
+
try {
|
|
3745
|
+
await stop();
|
|
3746
|
+
} finally {
|
|
3747
|
+
session.state = "ended";
|
|
3748
|
+
session.updatedAt = nowIso();
|
|
3749
|
+
}
|
|
3494
3750
|
}
|
|
3495
3751
|
session.state = "ended";
|
|
3496
3752
|
session.updatedAt = nowIso();
|
|
@@ -3506,11 +3762,21 @@ var GoogleMeetRuntime = class {
|
|
|
3506
3762
|
spoken: false
|
|
3507
3763
|
};
|
|
3508
3764
|
if (session.transport === "twilio" && session.twilio?.voiceCallId) {
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3765
|
+
try {
|
|
3766
|
+
await speakMeetViaVoiceCallGateway({
|
|
3767
|
+
config: this.params.config,
|
|
3768
|
+
callId: session.twilio.voiceCallId,
|
|
3769
|
+
message: instructions || this.params.config.voiceCall.introMessage || this.params.config.realtime.introMessage || ""
|
|
3770
|
+
});
|
|
3771
|
+
} catch (err) {
|
|
3772
|
+
if (!isVoiceCallMissingError(err)) throw err;
|
|
3773
|
+
this.#markTwilioSessionEnded(session, "Voice Call is no longer active.");
|
|
3774
|
+
return {
|
|
3775
|
+
found: true,
|
|
3776
|
+
spoken: false,
|
|
3777
|
+
session
|
|
3778
|
+
};
|
|
3779
|
+
}
|
|
3514
3780
|
session.twilio.introSent = true;
|
|
3515
3781
|
session.updatedAt = nowIso();
|
|
3516
3782
|
return {
|
|
@@ -3553,7 +3819,7 @@ var GoogleMeetRuntime = class {
|
|
|
3553
3819
|
const waitMs = Math.min(Math.max(0, this.params.config.chrome.waitForInCallMs), Math.max(0, this.params.config.chrome.joinTimeoutMs));
|
|
3554
3820
|
const deadline = Date.now() + waitMs;
|
|
3555
3821
|
while (Date.now() < deadline) {
|
|
3556
|
-
await sleep(250);
|
|
3822
|
+
await sleep(Math.min(250, Math.max(0, deadline - Date.now())));
|
|
3557
3823
|
result = await this.speak(session.id, instructions);
|
|
3558
3824
|
if (result.spoken) return true;
|
|
3559
3825
|
const health = result.session?.chrome?.health;
|
|
@@ -3672,6 +3938,34 @@ var GoogleMeetRuntime = class {
|
|
|
3672
3938
|
});
|
|
3673
3939
|
return;
|
|
3674
3940
|
}
|
|
3941
|
+
if (session.transport === "twilio") {
|
|
3942
|
+
await this.#refreshTwilioVoiceCallStatus(session);
|
|
3943
|
+
return;
|
|
3944
|
+
}
|
|
3945
|
+
this.#refreshSpeechReadiness(session);
|
|
3946
|
+
}
|
|
3947
|
+
#markTwilioSessionEnded(session, reason) {
|
|
3948
|
+
session.state = "ended";
|
|
3949
|
+
session.updatedAt = nowIso();
|
|
3950
|
+
this.#sessionStops.delete(session.id);
|
|
3951
|
+
this.#sessionSpeakers.delete(session.id);
|
|
3952
|
+
this.#sessionHealth.delete(session.id);
|
|
3953
|
+
noteSession(session, reason);
|
|
3954
|
+
}
|
|
3955
|
+
async #refreshTwilioVoiceCallStatus(session) {
|
|
3956
|
+
const callId = session.twilio?.voiceCallId;
|
|
3957
|
+
if (!callId || session.state !== "active") {
|
|
3958
|
+
this.#refreshSpeechReadiness(session);
|
|
3959
|
+
return;
|
|
3960
|
+
}
|
|
3961
|
+
try {
|
|
3962
|
+
if ((await getMeetVoiceCallGatewayCall({
|
|
3963
|
+
config: this.params.config,
|
|
3964
|
+
callId
|
|
3965
|
+
})).found === false) this.#markTwilioSessionEnded(session, "Voice Call is no longer active.");
|
|
3966
|
+
} catch (error) {
|
|
3967
|
+
this.params.logger.debug?.(`[google-meet] voice-call status refresh ignored: ${formatErrorMessage(error)}`);
|
|
3968
|
+
}
|
|
3675
3969
|
this.#refreshSpeechReadiness(session);
|
|
3676
3970
|
}
|
|
3677
3971
|
async #refreshBrowserHealthForChromeSession(session, options = {}) {
|
|
@@ -3897,13 +4191,13 @@ const googleMeetConfigSchema = {
|
|
|
3897
4191
|
advanced: true
|
|
3898
4192
|
},
|
|
3899
4193
|
"voiceCall.dtmfDelayMs": {
|
|
3900
|
-
label: "
|
|
3901
|
-
help: "
|
|
4194
|
+
label: "DTMF Wait Before PIN (ms)",
|
|
4195
|
+
help: "Leading Twilio wait time before playing a PIN-derived Meet DTMF sequence. Increase it if Meet asks for the PIN after DTMF was sent.",
|
|
3902
4196
|
advanced: true
|
|
3903
4197
|
},
|
|
3904
4198
|
"voiceCall.postDtmfSpeechDelayMs": {
|
|
3905
|
-
label: "
|
|
3906
|
-
help: "
|
|
4199
|
+
label: "Post-DTMF Speech Delay (ms)",
|
|
4200
|
+
help: "Delay before requesting the realtime intro greeting after Voice Call starts the Twilio leg.",
|
|
3907
4201
|
advanced: true
|
|
3908
4202
|
},
|
|
3909
4203
|
"voiceCall.introMessage": {
|
|
@@ -4141,10 +4435,10 @@ async function callGoogleMeetGatewayFromTool(params) {
|
|
|
4141
4435
|
}
|
|
4142
4436
|
}
|
|
4143
4437
|
async function createMeetFromParams(params) {
|
|
4144
|
-
return (await import("./create-
|
|
4438
|
+
return (await import("./create-C01ezOIH.js")).createMeetFromParams(params);
|
|
4145
4439
|
}
|
|
4146
4440
|
async function createAndJoinMeetFromParams(params) {
|
|
4147
|
-
return (await import("./create-
|
|
4441
|
+
return (await import("./create-C01ezOIH.js")).createAndJoinMeetFromParams(params);
|
|
4148
4442
|
}
|
|
4149
4443
|
async function resolveGoogleMeetTokenFromParams(config, raw) {
|
|
4150
4444
|
const { resolveGoogleMeetAccessToken } = await import("./oauth-BJwzuzT-.js");
|
|
@@ -4236,7 +4530,7 @@ async function exportGoogleMeetBundleFromParams(config, raw) {
|
|
|
4236
4530
|
lateAfterMinutes: resolved.lateAfterMinutes,
|
|
4237
4531
|
earlyBeforeMinutes: resolved.earlyBeforeMinutes
|
|
4238
4532
|
})]);
|
|
4239
|
-
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await import("./cli-
|
|
4533
|
+
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await import("./cli-Dgpp3OTQ.js");
|
|
4240
4534
|
const calendarId = normalizeOptionalString(raw.calendarId);
|
|
4241
4535
|
const request = {
|
|
4242
4536
|
...resolved.meeting ? { meeting: resolved.meeting } : {},
|
|
@@ -4652,7 +4946,7 @@ var google_meet_default = definePluginEntry({
|
|
|
4652
4946
|
handle: handleGoogleMeetNodeHostCommand
|
|
4653
4947
|
});
|
|
4654
4948
|
api.registerCli(async ({ program }) => {
|
|
4655
|
-
const { registerGoogleMeetCli } = await import("./cli-
|
|
4949
|
+
const { registerGoogleMeetCli } = await import("./cli-Dgpp3OTQ.js");
|
|
4656
4950
|
registerGoogleMeetCli({
|
|
4657
4951
|
program,
|
|
4658
4952
|
config,
|