@openclaw/google-meet 2026.5.7 → 2026.5.10-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 +704 -408
- 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,122 @@ 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({
|
|
856
|
+
cfg: params.fullConfig,
|
|
796
857
|
providerConfig: resolved.providerConfig,
|
|
797
858
|
onTranscript: (text) => {
|
|
798
859
|
const trimmed = text.trim();
|
|
799
860
|
if (!trimmed || stopped) return;
|
|
861
|
+
const turnId = ensureTalkTurn();
|
|
862
|
+
emitTalkEvent({
|
|
863
|
+
type: "input.audio.committed",
|
|
864
|
+
turnId,
|
|
865
|
+
final: true,
|
|
866
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
867
|
+
});
|
|
868
|
+
emitTalkEvent({
|
|
869
|
+
type: "transcript.done",
|
|
870
|
+
turnId,
|
|
871
|
+
final: true,
|
|
872
|
+
payload: {
|
|
873
|
+
meetingSessionId: params.meetingSessionId,
|
|
874
|
+
text: trimmed,
|
|
875
|
+
role: "user"
|
|
876
|
+
}
|
|
877
|
+
});
|
|
800
878
|
recordGoogleMeetRealtimeTranscript(transcript, "user", trimmed);
|
|
801
|
-
params.logger.info(
|
|
879
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("agent user", trimmed));
|
|
802
880
|
if (isGoogleMeetLikelyAssistantEchoTranscript({
|
|
803
881
|
transcript,
|
|
804
882
|
text: trimmed
|
|
805
883
|
})) {
|
|
806
|
-
params.logger.info(
|
|
884
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("agent ignored assistant echo transcript", trimmed));
|
|
807
885
|
return;
|
|
808
886
|
}
|
|
809
|
-
|
|
887
|
+
agentTalkback?.enqueue(trimmed);
|
|
810
888
|
},
|
|
811
889
|
onError: (error) => {
|
|
812
890
|
params.logger.warn(`[google-meet] agent transcription bridge failed: ${formatErrorMessage(error)}`);
|
|
891
|
+
emitTalkEvent({
|
|
892
|
+
type: "session.error",
|
|
893
|
+
final: true,
|
|
894
|
+
payload: {
|
|
895
|
+
meetingSessionId: params.meetingSessionId,
|
|
896
|
+
error: formatErrorMessage(error)
|
|
897
|
+
}
|
|
898
|
+
});
|
|
813
899
|
stop();
|
|
814
900
|
}
|
|
815
901
|
});
|
|
902
|
+
emitTalkEvent({
|
|
903
|
+
type: "session.started",
|
|
904
|
+
payload: {
|
|
905
|
+
meetingSessionId: params.meetingSessionId,
|
|
906
|
+
provider: resolved.provider.id
|
|
907
|
+
}
|
|
908
|
+
});
|
|
816
909
|
await sttSession.connect();
|
|
817
910
|
realtimeReady = true;
|
|
911
|
+
emitTalkEvent({
|
|
912
|
+
type: "session.ready",
|
|
913
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
914
|
+
});
|
|
818
915
|
inputProcess.stdout?.on("data", (chunk) => {
|
|
819
916
|
if (stopped) return;
|
|
820
917
|
const audio = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
@@ -825,6 +922,14 @@ async function startCommandAgentAudioBridge(params) {
|
|
|
825
922
|
}
|
|
826
923
|
lastInputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
827
924
|
lastInputBytes += audio.byteLength;
|
|
925
|
+
emitTalkEvent({
|
|
926
|
+
type: "input.audio.delta",
|
|
927
|
+
turnId: ensureTalkTurn(),
|
|
928
|
+
payload: {
|
|
929
|
+
meetingSessionId: params.meetingSessionId,
|
|
930
|
+
bytes: audio.byteLength
|
|
931
|
+
}
|
|
932
|
+
});
|
|
828
933
|
sttSession?.sendAudio(convertGoogleMeetBridgeAudioForStt(audio, params.config));
|
|
829
934
|
});
|
|
830
935
|
return {
|
|
@@ -844,6 +949,7 @@ async function startCommandAgentAudioBridge(params) {
|
|
|
844
949
|
lastOutputBytes,
|
|
845
950
|
suppressedInputBytes,
|
|
846
951
|
...getGoogleMeetRealtimeTranscriptHealth(transcript),
|
|
952
|
+
recentTalkEvents: summarizeGoogleMeetTalkEvents(recentTalkEvents),
|
|
847
953
|
bridgeClosed: stopped
|
|
848
954
|
}),
|
|
849
955
|
stop
|
|
@@ -879,7 +985,7 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
879
985
|
let lastOutputAtMs = 0;
|
|
880
986
|
let lastOutputPlayableUntilMs = 0;
|
|
881
987
|
let bargeInInputProcess;
|
|
882
|
-
let
|
|
988
|
+
let agentTalkback;
|
|
883
989
|
const suppressInputForOutput = (audio) => {
|
|
884
990
|
const suppression = extendGoogleMeetOutputEchoSuppression({
|
|
885
991
|
audio,
|
|
@@ -912,10 +1018,7 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
912
1018
|
const stop = async () => {
|
|
913
1019
|
if (stopped) return;
|
|
914
1020
|
stopped = true;
|
|
915
|
-
|
|
916
|
-
clearTimeout(agentConsultDebounceTimer);
|
|
917
|
-
agentConsultDebounceTimer = void 0;
|
|
918
|
-
}
|
|
1021
|
+
agentTalkback?.close();
|
|
919
1022
|
try {
|
|
920
1023
|
bridge?.close();
|
|
921
1024
|
} catch (error) {
|
|
@@ -1028,64 +1131,63 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
1028
1131
|
}));
|
|
1029
1132
|
const transcript = [];
|
|
1030
1133
|
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?.();
|
|
1134
|
+
const talk = createTalkSessionController({
|
|
1135
|
+
sessionId: `google-meet:${params.meetingSessionId}:command-realtime`,
|
|
1136
|
+
mode: "realtime",
|
|
1137
|
+
transport: "gateway-relay",
|
|
1138
|
+
brain: strategy === "bidi" ? "direct-tools" : "agent-consult",
|
|
1139
|
+
provider: resolved.provider.id
|
|
1140
|
+
}, { onEvent: recordTalkObservabilityEvent });
|
|
1141
|
+
const recentTalkEvents = [];
|
|
1142
|
+
const rememberTalkEvent = (event) => {
|
|
1143
|
+
if (event) pushGoogleMeetTalkEvent(recentTalkEvents, event);
|
|
1045
1144
|
};
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
}
|
|
1145
|
+
const emitTalkEvent = (input) => {
|
|
1146
|
+
rememberTalkEvent(talk.emit(input));
|
|
1147
|
+
};
|
|
1148
|
+
const ensureTalkTurn = () => {
|
|
1149
|
+
const turn = talk.ensureTurn({ payload: { meetingSessionId: params.meetingSessionId } });
|
|
1150
|
+
if (turn.event) rememberTalkEvent(turn.event);
|
|
1151
|
+
return turn.turnId;
|
|
1152
|
+
};
|
|
1153
|
+
const finishOutputAudio = (reason) => {
|
|
1154
|
+
rememberTalkEvent(talk.finishOutputAudio({ payload: { reason } }));
|
|
1155
|
+
};
|
|
1156
|
+
const endTalkTurn = (reason = "completed") => {
|
|
1157
|
+
const ended = talk.endTurn({ payload: { reason } });
|
|
1158
|
+
if (ended.ok) rememberTalkEvent(ended.event);
|
|
1086
1159
|
};
|
|
1160
|
+
emitTalkEvent({
|
|
1161
|
+
type: "session.started",
|
|
1162
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
1163
|
+
});
|
|
1164
|
+
agentTalkback = createRealtimeVoiceAgentTalkbackQueue({
|
|
1165
|
+
debounceMs: 900,
|
|
1166
|
+
isStopped: () => stopped,
|
|
1167
|
+
logger: params.logger,
|
|
1168
|
+
logPrefix: "[google-meet] realtime agent",
|
|
1169
|
+
responseStyle: "Brief, natural spoken answer for a live meeting.",
|
|
1170
|
+
fallbackText: "I hit an error while checking that. Please try again.",
|
|
1171
|
+
consult: ({ question, responseStyle }) => consultOpenClawAgentForGoogleMeet({
|
|
1172
|
+
config: params.config,
|
|
1173
|
+
fullConfig: params.fullConfig,
|
|
1174
|
+
runtime: params.runtime,
|
|
1175
|
+
logger: params.logger,
|
|
1176
|
+
meetingSessionId: params.meetingSessionId,
|
|
1177
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
1178
|
+
args: {
|
|
1179
|
+
question,
|
|
1180
|
+
responseStyle
|
|
1181
|
+
},
|
|
1182
|
+
transcript
|
|
1183
|
+
}),
|
|
1184
|
+
deliver: (text) => {
|
|
1185
|
+
bridge?.sendUserMessage(buildGoogleMeetSpeakExactUserMessage(text));
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1087
1188
|
bridge = createRealtimeVoiceBridgeSession({
|
|
1088
1189
|
provider: resolved.provider,
|
|
1190
|
+
cfg: params.fullConfig,
|
|
1089
1191
|
providerConfig: resolved.providerConfig,
|
|
1090
1192
|
audioFormat: resolveGoogleMeetRealtimeAudioFormat(params.config),
|
|
1091
1193
|
instructions: params.config.realtime.instructions,
|
|
@@ -1097,69 +1199,140 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
1097
1199
|
audioSink: {
|
|
1098
1200
|
isOpen: () => !stopped,
|
|
1099
1201
|
sendAudio: (audio) => {
|
|
1202
|
+
const turnId = ensureTalkTurn();
|
|
1203
|
+
rememberTalkEvent(talk.startOutputAudio({
|
|
1204
|
+
turnId,
|
|
1205
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
1206
|
+
}).event);
|
|
1207
|
+
emitTalkEvent({
|
|
1208
|
+
type: "output.audio.delta",
|
|
1209
|
+
turnId,
|
|
1210
|
+
payload: { byteLength: audio.byteLength }
|
|
1211
|
+
});
|
|
1100
1212
|
lastOutputAtMs = Date.now();
|
|
1101
1213
|
lastOutputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1102
1214
|
lastOutputBytes += audio.byteLength;
|
|
1103
1215
|
suppressInputForOutput(audio);
|
|
1104
1216
|
writeOutputAudio(audio);
|
|
1105
1217
|
},
|
|
1106
|
-
clearAudio:
|
|
1218
|
+
clearAudio: () => {
|
|
1219
|
+
clearOutputPlayback();
|
|
1220
|
+
finishOutputAudio("clear");
|
|
1221
|
+
}
|
|
1107
1222
|
},
|
|
1108
1223
|
onTranscript: (role, text, isFinal) => {
|
|
1224
|
+
const turnId = ensureTalkTurn();
|
|
1225
|
+
emitTalkEvent({
|
|
1226
|
+
type: role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta",
|
|
1227
|
+
turnId,
|
|
1228
|
+
payload: role === "assistant" ? { text } : {
|
|
1229
|
+
role,
|
|
1230
|
+
text
|
|
1231
|
+
},
|
|
1232
|
+
final: isFinal
|
|
1233
|
+
});
|
|
1234
|
+
if (role === "user" && isFinal) emitTalkEvent({
|
|
1235
|
+
type: "input.audio.committed",
|
|
1236
|
+
turnId,
|
|
1237
|
+
payload: { meetingSessionId: params.meetingSessionId },
|
|
1238
|
+
final: true
|
|
1239
|
+
});
|
|
1109
1240
|
if (isFinal) {
|
|
1110
1241
|
recordGoogleMeetRealtimeTranscript(transcript, role, text);
|
|
1111
|
-
params.logger.info(`
|
|
1242
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog(`realtime ${role}`, text));
|
|
1112
1243
|
if (role === "user" && strategy === "agent") {
|
|
1113
1244
|
if (isGoogleMeetLikelyAssistantEchoTranscript({
|
|
1114
1245
|
transcript,
|
|
1115
1246
|
text
|
|
1116
1247
|
})) {
|
|
1117
|
-
params.logger.info(
|
|
1248
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("realtime ignored assistant echo transcript", text));
|
|
1118
1249
|
return;
|
|
1119
1250
|
}
|
|
1120
|
-
|
|
1251
|
+
agentTalkback?.enqueue(text);
|
|
1121
1252
|
}
|
|
1122
1253
|
}
|
|
1123
1254
|
},
|
|
1124
1255
|
onEvent: (event) => {
|
|
1125
1256
|
recordGoogleMeetRealtimeEvent(realtimeEvents, event);
|
|
1257
|
+
if (event.type === "input_audio_buffer.speech_started") ensureTalkTurn();
|
|
1258
|
+
else if (event.type === "input_audio_buffer.speech_stopped") {
|
|
1259
|
+
const turnId = talk.activeTurnId;
|
|
1260
|
+
if (!turnId) return;
|
|
1261
|
+
emitTalkEvent({
|
|
1262
|
+
type: "input.audio.committed",
|
|
1263
|
+
turnId,
|
|
1264
|
+
payload: {
|
|
1265
|
+
meetingSessionId: params.meetingSessionId,
|
|
1266
|
+
source: event.type
|
|
1267
|
+
},
|
|
1268
|
+
final: true
|
|
1269
|
+
});
|
|
1270
|
+
} else if (event.type === "response.done") {
|
|
1271
|
+
finishOutputAudio("response.done");
|
|
1272
|
+
endTalkTurn("response.done");
|
|
1273
|
+
} else if (event.type === "error") emitTalkEvent({
|
|
1274
|
+
type: "session.error",
|
|
1275
|
+
payload: { message: event.detail ?? "Realtime provider error" },
|
|
1276
|
+
final: true
|
|
1277
|
+
});
|
|
1126
1278
|
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
1279
|
const detail = event.detail ? ` ${event.detail}` : "";
|
|
1128
1280
|
params.logger.info(`[google-meet] realtime ${event.direction}:${event.type}${detail}`);
|
|
1129
1281
|
}
|
|
1130
1282
|
},
|
|
1131
1283
|
onToolCall: (event, session) => {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1284
|
+
emitTalkEvent({
|
|
1285
|
+
type: "tool.call",
|
|
1286
|
+
turnId: ensureTalkTurn(),
|
|
1287
|
+
itemId: event.itemId,
|
|
1288
|
+
callId: event.callId,
|
|
1289
|
+
payload: {
|
|
1290
|
+
name: event.name,
|
|
1291
|
+
args: event.args
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
const turnId = ensureTalkTurn();
|
|
1295
|
+
handleGoogleMeetRealtimeConsultToolCall({
|
|
1296
|
+
strategy,
|
|
1297
|
+
session,
|
|
1298
|
+
event,
|
|
1142
1299
|
config: params.config,
|
|
1143
1300
|
fullConfig: params.fullConfig,
|
|
1144
1301
|
runtime: params.runtime,
|
|
1145
1302
|
logger: params.logger,
|
|
1146
1303
|
meetingSessionId: params.meetingSessionId,
|
|
1147
1304
|
requesterSessionKey: params.requesterSessionKey,
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1305
|
+
transcript,
|
|
1306
|
+
onTalkEvent: (input) => emitTalkEvent({
|
|
1307
|
+
...input,
|
|
1308
|
+
turnId: input.turnId ?? turnId
|
|
1309
|
+
})
|
|
1310
|
+
});
|
|
1311
|
+
},
|
|
1312
|
+
onError: (error) => {
|
|
1313
|
+
emitTalkEvent({
|
|
1314
|
+
type: "session.error",
|
|
1315
|
+
payload: { message: formatErrorMessage(error) },
|
|
1316
|
+
final: true
|
|
1154
1317
|
});
|
|
1318
|
+
fail("realtime voice bridge")(error);
|
|
1155
1319
|
},
|
|
1156
|
-
onError: fail("realtime voice bridge"),
|
|
1157
1320
|
onClose: (reason) => {
|
|
1158
1321
|
realtimeReady = false;
|
|
1322
|
+
finishOutputAudio(reason);
|
|
1323
|
+
emitTalkEvent({
|
|
1324
|
+
type: "session.closed",
|
|
1325
|
+
payload: { reason },
|
|
1326
|
+
final: true
|
|
1327
|
+
});
|
|
1159
1328
|
if (reason === "error") stop();
|
|
1160
1329
|
},
|
|
1161
1330
|
onReady: () => {
|
|
1162
1331
|
realtimeReady = true;
|
|
1332
|
+
emitTalkEvent({
|
|
1333
|
+
type: "session.ready",
|
|
1334
|
+
payload: { meetingSessionId: params.meetingSessionId }
|
|
1335
|
+
});
|
|
1163
1336
|
}
|
|
1164
1337
|
});
|
|
1165
1338
|
startHumanBargeInMonitor();
|
|
@@ -1173,6 +1346,11 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
1173
1346
|
}
|
|
1174
1347
|
lastInputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1175
1348
|
lastInputBytes += audio.byteLength;
|
|
1349
|
+
emitTalkEvent({
|
|
1350
|
+
type: "input.audio.delta",
|
|
1351
|
+
turnId: ensureTalkTurn(),
|
|
1352
|
+
payload: { byteLength: audio.byteLength }
|
|
1353
|
+
});
|
|
1176
1354
|
bridge?.sendAudio(Buffer.from(audio));
|
|
1177
1355
|
}
|
|
1178
1356
|
});
|
|
@@ -1197,6 +1375,7 @@ async function startCommandRealtimeAudioBridge(params) {
|
|
|
1197
1375
|
suppressedInputBytes,
|
|
1198
1376
|
...getGoogleMeetRealtimeTranscriptHealth(transcript),
|
|
1199
1377
|
...getGoogleMeetRealtimeEventHealth(realtimeEvents),
|
|
1378
|
+
recentTalkEvents: summarizeGoogleMeetTalkEvents(recentTalkEvents),
|
|
1200
1379
|
lastClearAt,
|
|
1201
1380
|
clearCount,
|
|
1202
1381
|
bridgeClosed: stopped
|
|
@@ -1244,17 +1423,12 @@ async function startNodeAgentAudioBridge(params) {
|
|
|
1244
1423
|
audioFormat: params.config.chrome.audioFormat
|
|
1245
1424
|
}));
|
|
1246
1425
|
const transcript = [];
|
|
1247
|
-
let
|
|
1248
|
-
let pendingAgentQuestion;
|
|
1249
|
-
let agentConsultDebounceTimer;
|
|
1426
|
+
let agentTalkback;
|
|
1250
1427
|
let ttsQueue = Promise.resolve();
|
|
1251
1428
|
const stop = async () => {
|
|
1252
1429
|
if (stopped) return;
|
|
1253
1430
|
stopped = true;
|
|
1254
|
-
|
|
1255
|
-
clearTimeout(agentConsultDebounceTimer);
|
|
1256
|
-
agentConsultDebounceTimer = void 0;
|
|
1257
|
-
}
|
|
1431
|
+
agentTalkback?.close();
|
|
1258
1432
|
try {
|
|
1259
1433
|
sttSession?.close();
|
|
1260
1434
|
} catch (error) {
|
|
@@ -1303,7 +1477,7 @@ async function startNodeAgentAudioBridge(params) {
|
|
|
1303
1477
|
ttsQueue = ttsQueue.then(async () => {
|
|
1304
1478
|
if (stopped) return;
|
|
1305
1479
|
recordGoogleMeetRealtimeTranscript(transcript, "assistant", normalized);
|
|
1306
|
-
params.logger.info(
|
|
1480
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("node agent assistant", normalized));
|
|
1307
1481
|
const result = await params.runtime.tts.textToSpeechTelephony({
|
|
1308
1482
|
text: normalized,
|
|
1309
1483
|
cfg: params.fullConfig
|
|
@@ -1315,74 +1489,44 @@ async function startNodeAgentAudioBridge(params) {
|
|
|
1315
1489
|
params.logger.warn(`[google-meet] node agent TTS failed: ${formatErrorMessage(error)}`);
|
|
1316
1490
|
});
|
|
1317
1491
|
};
|
|
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
|
-
};
|
|
1492
|
+
agentTalkback = createRealtimeVoiceAgentTalkbackQueue({
|
|
1493
|
+
debounceMs: 900,
|
|
1494
|
+
isStopped: () => stopped,
|
|
1495
|
+
logger: params.logger,
|
|
1496
|
+
logPrefix: "[google-meet] node agent",
|
|
1497
|
+
responseStyle: "Brief, natural spoken answer for a live meeting.",
|
|
1498
|
+
fallbackText: "I hit an error while checking that. Please try again.",
|
|
1499
|
+
consult: ({ question, responseStyle }) => consultOpenClawAgentForGoogleMeet({
|
|
1500
|
+
config: params.config,
|
|
1501
|
+
fullConfig: params.fullConfig,
|
|
1502
|
+
runtime: params.runtime,
|
|
1503
|
+
logger: params.logger,
|
|
1504
|
+
meetingSessionId: params.meetingSessionId,
|
|
1505
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
1506
|
+
args: {
|
|
1507
|
+
question,
|
|
1508
|
+
responseStyle
|
|
1509
|
+
},
|
|
1510
|
+
transcript
|
|
1511
|
+
}),
|
|
1512
|
+
deliver: enqueueSpeakText
|
|
1513
|
+
});
|
|
1371
1514
|
sttSession = resolved.provider.createSession({
|
|
1515
|
+
cfg: params.fullConfig,
|
|
1372
1516
|
providerConfig: resolved.providerConfig,
|
|
1373
1517
|
onTranscript: (text) => {
|
|
1374
1518
|
const trimmed = text.trim();
|
|
1375
1519
|
if (!trimmed || stopped) return;
|
|
1376
1520
|
recordGoogleMeetRealtimeTranscript(transcript, "user", trimmed);
|
|
1377
|
-
params.logger.info(
|
|
1521
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("node agent user", trimmed));
|
|
1378
1522
|
if (isGoogleMeetLikelyAssistantEchoTranscript({
|
|
1379
1523
|
transcript,
|
|
1380
1524
|
text: trimmed
|
|
1381
1525
|
})) {
|
|
1382
|
-
params.logger.info(
|
|
1526
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("node agent ignored assistant echo transcript", trimmed));
|
|
1383
1527
|
return;
|
|
1384
1528
|
}
|
|
1385
|
-
|
|
1529
|
+
agentTalkback?.enqueue(trimmed);
|
|
1386
1530
|
},
|
|
1387
1531
|
onError: (error) => {
|
|
1388
1532
|
params.logger.warn(`[google-meet] node agent transcription bridge failed: ${formatErrorMessage(error)}`);
|
|
@@ -1482,6 +1626,49 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1482
1626
|
const transcript = [];
|
|
1483
1627
|
const realtimeEvents = [];
|
|
1484
1628
|
const strategy = params.config.realtime.strategy;
|
|
1629
|
+
const talk = createTalkSessionController({
|
|
1630
|
+
sessionId: `google-meet:${params.meetingSessionId}:${params.bridgeId}:node-realtime`,
|
|
1631
|
+
mode: "realtime",
|
|
1632
|
+
transport: "gateway-relay",
|
|
1633
|
+
brain: strategy === "bidi" ? "direct-tools" : "agent-consult",
|
|
1634
|
+
provider: resolved.provider.id
|
|
1635
|
+
}, { onEvent: recordTalkObservabilityEvent });
|
|
1636
|
+
const recentTalkEvents = [];
|
|
1637
|
+
const rememberTalkEvent = (event) => {
|
|
1638
|
+
if (event) pushGoogleMeetTalkEvent(recentTalkEvents, event);
|
|
1639
|
+
};
|
|
1640
|
+
const emitTalkEvent = (input) => {
|
|
1641
|
+
rememberTalkEvent(talk.emit(input));
|
|
1642
|
+
};
|
|
1643
|
+
const ensureTalkTurn = () => {
|
|
1644
|
+
const turn = talk.ensureTurn({ payload: {
|
|
1645
|
+
bridgeId: params.bridgeId,
|
|
1646
|
+
meetingSessionId: params.meetingSessionId
|
|
1647
|
+
} });
|
|
1648
|
+
if (turn.event) rememberTalkEvent(turn.event);
|
|
1649
|
+
return turn.turnId;
|
|
1650
|
+
};
|
|
1651
|
+
const finishOutputAudio = (reason) => {
|
|
1652
|
+
rememberTalkEvent(talk.finishOutputAudio({ payload: {
|
|
1653
|
+
bridgeId: params.bridgeId,
|
|
1654
|
+
reason
|
|
1655
|
+
} }));
|
|
1656
|
+
};
|
|
1657
|
+
const endTalkTurn = (reason = "completed") => {
|
|
1658
|
+
const ended = talk.endTurn({ payload: {
|
|
1659
|
+
bridgeId: params.bridgeId,
|
|
1660
|
+
reason
|
|
1661
|
+
} });
|
|
1662
|
+
if (ended.ok) rememberTalkEvent(ended.event);
|
|
1663
|
+
};
|
|
1664
|
+
emitTalkEvent({
|
|
1665
|
+
type: "session.started",
|
|
1666
|
+
payload: {
|
|
1667
|
+
bridgeId: params.bridgeId,
|
|
1668
|
+
meetingSessionId: params.meetingSessionId,
|
|
1669
|
+
nodeId: params.nodeId
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1485
1672
|
params.logger.info(formatGoogleMeetRealtimeVoiceModelLog({
|
|
1486
1673
|
strategy,
|
|
1487
1674
|
provider: resolved.provider,
|
|
@@ -1489,70 +1676,35 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1489
1676
|
fallbackModel: params.config.realtime.model,
|
|
1490
1677
|
audioFormat: params.config.chrome.audioFormat
|
|
1491
1678
|
}));
|
|
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);
|
|
1679
|
+
let agentTalkback;
|
|
1680
|
+
agentTalkback = createRealtimeVoiceAgentTalkbackQueue({
|
|
1681
|
+
debounceMs: 900,
|
|
1682
|
+
isStopped: () => stopped,
|
|
1683
|
+
logger: params.logger,
|
|
1684
|
+
logPrefix: "[google-meet] node realtime agent",
|
|
1685
|
+
responseStyle: "Brief, natural spoken answer for a live meeting.",
|
|
1686
|
+
fallbackText: "I hit an error while checking that. Please try again.",
|
|
1687
|
+
consult: ({ question, responseStyle }) => consultOpenClawAgentForGoogleMeet({
|
|
1688
|
+
config: params.config,
|
|
1689
|
+
fullConfig: params.fullConfig,
|
|
1690
|
+
runtime: params.runtime,
|
|
1691
|
+
logger: params.logger,
|
|
1692
|
+
meetingSessionId: params.meetingSessionId,
|
|
1693
|
+
requesterSessionKey: params.requesterSessionKey,
|
|
1694
|
+
args: {
|
|
1695
|
+
question,
|
|
1696
|
+
responseStyle
|
|
1697
|
+
},
|
|
1698
|
+
transcript
|
|
1699
|
+
}),
|
|
1700
|
+
deliver: (text) => {
|
|
1701
|
+
bridge?.sendUserMessage(buildGoogleMeetSpeakExactUserMessage(text));
|
|
1547
1702
|
}
|
|
1548
|
-
};
|
|
1703
|
+
});
|
|
1549
1704
|
const stop = async () => {
|
|
1550
1705
|
if (stopped) return;
|
|
1551
1706
|
stopped = true;
|
|
1552
|
-
|
|
1553
|
-
clearTimeout(agentConsultDebounceTimer);
|
|
1554
|
-
agentConsultDebounceTimer = void 0;
|
|
1555
|
-
}
|
|
1707
|
+
agentTalkback?.close();
|
|
1556
1708
|
try {
|
|
1557
1709
|
bridge?.close();
|
|
1558
1710
|
} catch (error) {
|
|
@@ -1574,6 +1726,7 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1574
1726
|
};
|
|
1575
1727
|
bridge = createRealtimeVoiceBridgeSession({
|
|
1576
1728
|
provider: resolved.provider,
|
|
1729
|
+
cfg: params.fullConfig,
|
|
1577
1730
|
providerConfig: resolved.providerConfig,
|
|
1578
1731
|
audioFormat: resolveGoogleMeetRealtimeAudioFormat(params.config),
|
|
1579
1732
|
instructions: params.config.realtime.instructions,
|
|
@@ -1585,6 +1738,16 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1585
1738
|
audioSink: {
|
|
1586
1739
|
isOpen: () => !stopped,
|
|
1587
1740
|
sendAudio: (audio) => {
|
|
1741
|
+
const turnId = ensureTalkTurn();
|
|
1742
|
+
rememberTalkEvent(talk.startOutputAudio({
|
|
1743
|
+
turnId,
|
|
1744
|
+
payload: { bridgeId: params.bridgeId }
|
|
1745
|
+
}).event);
|
|
1746
|
+
emitTalkEvent({
|
|
1747
|
+
type: "output.audio.delta",
|
|
1748
|
+
turnId,
|
|
1749
|
+
payload: { byteLength: audio.byteLength }
|
|
1750
|
+
});
|
|
1588
1751
|
const suppression = extendGoogleMeetOutputEchoSuppression({
|
|
1589
1752
|
audio,
|
|
1590
1753
|
audioFormat: params.config.chrome.audioFormat,
|
|
@@ -1613,6 +1776,7 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1613
1776
|
clearAudio: () => {
|
|
1614
1777
|
lastClearAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1615
1778
|
clearCount += 1;
|
|
1779
|
+
finishOutputAudio("clear");
|
|
1616
1780
|
suppressInputUntil = 0;
|
|
1617
1781
|
lastOutputPlayableUntilMs = 0;
|
|
1618
1782
|
params.runtime.nodes.invoke({
|
|
@@ -1630,63 +1794,119 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1630
1794
|
}
|
|
1631
1795
|
},
|
|
1632
1796
|
onTranscript: (role, text, isFinal) => {
|
|
1797
|
+
const turnId = ensureTalkTurn();
|
|
1798
|
+
emitTalkEvent({
|
|
1799
|
+
type: role === "assistant" ? isFinal ? "output.text.done" : "output.text.delta" : isFinal ? "transcript.done" : "transcript.delta",
|
|
1800
|
+
turnId,
|
|
1801
|
+
payload: role === "assistant" ? { text } : {
|
|
1802
|
+
role,
|
|
1803
|
+
text
|
|
1804
|
+
},
|
|
1805
|
+
final: isFinal
|
|
1806
|
+
});
|
|
1807
|
+
if (role === "user" && isFinal) emitTalkEvent({
|
|
1808
|
+
type: "input.audio.committed",
|
|
1809
|
+
turnId,
|
|
1810
|
+
payload: { bridgeId: params.bridgeId },
|
|
1811
|
+
final: true
|
|
1812
|
+
});
|
|
1633
1813
|
if (isFinal) {
|
|
1634
1814
|
recordGoogleMeetRealtimeTranscript(transcript, role, text);
|
|
1635
|
-
params.logger.info(`
|
|
1815
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog(`node realtime ${role}`, text));
|
|
1636
1816
|
if (role === "user" && strategy === "agent") {
|
|
1637
1817
|
if (isGoogleMeetLikelyAssistantEchoTranscript({
|
|
1638
1818
|
transcript,
|
|
1639
1819
|
text
|
|
1640
1820
|
})) {
|
|
1641
|
-
params.logger.info(
|
|
1821
|
+
params.logger.info(formatGoogleMeetTranscriptSummaryLog("node realtime ignored assistant echo transcript", text));
|
|
1642
1822
|
return;
|
|
1643
1823
|
}
|
|
1644
|
-
|
|
1824
|
+
agentTalkback?.enqueue(text);
|
|
1645
1825
|
}
|
|
1646
1826
|
}
|
|
1647
1827
|
},
|
|
1648
1828
|
onEvent: (event) => {
|
|
1649
1829
|
recordGoogleMeetRealtimeEvent(realtimeEvents, event);
|
|
1830
|
+
if (event.type === "input_audio_buffer.speech_started") ensureTalkTurn();
|
|
1831
|
+
else if (event.type === "input_audio_buffer.speech_stopped") {
|
|
1832
|
+
const turnId = talk.activeTurnId;
|
|
1833
|
+
if (!turnId) return;
|
|
1834
|
+
emitTalkEvent({
|
|
1835
|
+
type: "input.audio.committed",
|
|
1836
|
+
turnId,
|
|
1837
|
+
payload: {
|
|
1838
|
+
bridgeId: params.bridgeId,
|
|
1839
|
+
source: event.type
|
|
1840
|
+
},
|
|
1841
|
+
final: true
|
|
1842
|
+
});
|
|
1843
|
+
} else if (event.type === "response.done") {
|
|
1844
|
+
finishOutputAudio("response.done");
|
|
1845
|
+
endTalkTurn("response.done");
|
|
1846
|
+
} else if (event.type === "error") emitTalkEvent({
|
|
1847
|
+
type: "session.error",
|
|
1848
|
+
payload: { message: event.detail ?? "Realtime provider error" },
|
|
1849
|
+
final: true
|
|
1850
|
+
});
|
|
1650
1851
|
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
1852
|
const detail = event.detail ? ` ${event.detail}` : "";
|
|
1652
1853
|
params.logger.info(`[google-meet] node realtime ${event.direction}:${event.type}${detail}`);
|
|
1653
1854
|
}
|
|
1654
1855
|
},
|
|
1655
1856
|
onToolCall: (event, session) => {
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1857
|
+
emitTalkEvent({
|
|
1858
|
+
type: "tool.call",
|
|
1859
|
+
turnId: ensureTalkTurn(),
|
|
1860
|
+
itemId: event.itemId,
|
|
1861
|
+
callId: event.callId,
|
|
1862
|
+
payload: {
|
|
1863
|
+
name: event.name,
|
|
1864
|
+
args: event.args
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
const turnId = ensureTalkTurn();
|
|
1868
|
+
handleGoogleMeetRealtimeConsultToolCall({
|
|
1869
|
+
strategy,
|
|
1870
|
+
session,
|
|
1871
|
+
event,
|
|
1666
1872
|
config: params.config,
|
|
1667
1873
|
fullConfig: params.fullConfig,
|
|
1668
1874
|
runtime: params.runtime,
|
|
1669
1875
|
logger: params.logger,
|
|
1670
1876
|
meetingSessionId: params.meetingSessionId,
|
|
1671
1877
|
requesterSessionKey: params.requesterSessionKey,
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
session.submitToolResult(event.callId || event.itemId, { error: formatErrorMessage(error) });
|
|
1878
|
+
transcript,
|
|
1879
|
+
onTalkEvent: (input) => emitTalkEvent({
|
|
1880
|
+
...input,
|
|
1881
|
+
turnId: input.turnId ?? turnId
|
|
1882
|
+
})
|
|
1678
1883
|
});
|
|
1679
1884
|
},
|
|
1680
1885
|
onError: (error) => {
|
|
1681
1886
|
params.logger.warn(`[google-meet] node realtime voice bridge failed: ${formatErrorMessage(error)}`);
|
|
1887
|
+
emitTalkEvent({
|
|
1888
|
+
type: "session.error",
|
|
1889
|
+
payload: { message: formatErrorMessage(error) },
|
|
1890
|
+
final: true
|
|
1891
|
+
});
|
|
1682
1892
|
stop();
|
|
1683
1893
|
},
|
|
1684
1894
|
onClose: (reason) => {
|
|
1685
1895
|
realtimeReady = false;
|
|
1896
|
+
finishOutputAudio(reason);
|
|
1897
|
+
emitTalkEvent({
|
|
1898
|
+
type: "session.closed",
|
|
1899
|
+
payload: { reason },
|
|
1900
|
+
final: true
|
|
1901
|
+
});
|
|
1686
1902
|
if (reason === "error") stop();
|
|
1687
1903
|
},
|
|
1688
1904
|
onReady: () => {
|
|
1689
1905
|
realtimeReady = true;
|
|
1906
|
+
emitTalkEvent({
|
|
1907
|
+
type: "session.ready",
|
|
1908
|
+
payload: { bridgeId: params.bridgeId }
|
|
1909
|
+
});
|
|
1690
1910
|
}
|
|
1691
1911
|
});
|
|
1692
1912
|
await bridge.connect();
|
|
@@ -1717,6 +1937,11 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1717
1937
|
}
|
|
1718
1938
|
lastInputAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1719
1939
|
lastInputBytes += audio.byteLength;
|
|
1940
|
+
emitTalkEvent({
|
|
1941
|
+
type: "input.audio.delta",
|
|
1942
|
+
turnId: ensureTalkTurn(),
|
|
1943
|
+
payload: { byteLength: audio.byteLength }
|
|
1944
|
+
});
|
|
1720
1945
|
bridge?.sendAudio(audio);
|
|
1721
1946
|
}
|
|
1722
1947
|
if (result.closed === true) await stop();
|
|
@@ -1754,6 +1979,7 @@ async function startNodeRealtimeAudioBridge(params) {
|
|
|
1754
1979
|
suppressedInputBytes,
|
|
1755
1980
|
...getGoogleMeetRealtimeTranscriptHealth(transcript),
|
|
1756
1981
|
...getGoogleMeetRealtimeEventHealth(realtimeEvents),
|
|
1982
|
+
recentTalkEvents: summarizeGoogleMeetTalkEvents(recentTalkEvents),
|
|
1757
1983
|
consecutiveInputErrors,
|
|
1758
1984
|
lastInputError,
|
|
1759
1985
|
clearCount,
|
|
@@ -2289,8 +2515,9 @@ async function openMeetWithBrowserRequest(params) {
|
|
|
2289
2515
|
};
|
|
2290
2516
|
break;
|
|
2291
2517
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2518
|
+
const remainingWaitMs = deadline - Date.now();
|
|
2519
|
+
if (remainingWaitMs > 0) await new Promise((resolve) => setTimeout(resolve, Math.min(750, remainingWaitMs)));
|
|
2520
|
+
} while (Date.now() < deadline);
|
|
2294
2521
|
return {
|
|
2295
2522
|
launched: true,
|
|
2296
2523
|
browser
|
|
@@ -3034,12 +3261,14 @@ function buildMeetDtmfSequence(params) {
|
|
|
3034
3261
|
if (!/^[0-9]+#?$/.test(compactPin)) throw new Error("pin may only contain digits and an optional trailing #");
|
|
3035
3262
|
return compactPin.endsWith("#") ? compactPin : `${compactPin}#`;
|
|
3036
3263
|
}
|
|
3264
|
+
function prefixDtmfWait(sequence, delayMs) {
|
|
3265
|
+
if (!sequence || delayMs <= 0) return sequence;
|
|
3266
|
+
const waitCount = Math.ceil(delayMs / 500);
|
|
3267
|
+
if (waitCount <= 0) return sequence;
|
|
3268
|
+
return `${"w".repeat(waitCount)}${sequence}`;
|
|
3269
|
+
}
|
|
3037
3270
|
//#endregion
|
|
3038
3271
|
//#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
3272
|
async function createConnectedGatewayClient(config) {
|
|
3044
3273
|
let client;
|
|
3045
3274
|
await new Promise((resolve, reject) => {
|
|
@@ -3080,42 +3309,44 @@ async function createConnectedGatewayClient(config) {
|
|
|
3080
3309
|
});
|
|
3081
3310
|
return client;
|
|
3082
3311
|
}
|
|
3312
|
+
function isVoiceCallMissingError(error) {
|
|
3313
|
+
const message = formatErrorMessage(error).toLowerCase();
|
|
3314
|
+
return message.includes("call not found") || message.includes("call is not active");
|
|
3315
|
+
}
|
|
3083
3316
|
async function joinMeetViaVoiceCallGateway(params) {
|
|
3084
3317
|
let client;
|
|
3085
3318
|
try {
|
|
3086
3319
|
client = await createConnectedGatewayClient(params.config);
|
|
3087
|
-
params.logger?.info(`[google-meet] Delegating Twilio join to Voice Call (dtmf=${params.dtmfSequence ? "
|
|
3320
|
+
params.logger?.info(`[google-meet] Delegating Twilio join to Voice Call (dtmf=${params.dtmfSequence ? "pre-connect" : "none"}, intro=${params.message ? "delayed" : "none"})`);
|
|
3088
3321
|
const start = await client.request("voicecall.start", {
|
|
3089
3322
|
to: params.dialInNumber,
|
|
3090
|
-
mode: "conversation"
|
|
3323
|
+
mode: "conversation",
|
|
3324
|
+
...params.dtmfSequence ? { dtmfSequence: params.dtmfSequence } : {},
|
|
3325
|
+
...params.requesterSessionKey ? { requesterSessionKey: params.requesterSessionKey } : {},
|
|
3326
|
+
...params.sessionKey ? { sessionKey: params.sessionKey } : {}
|
|
3091
3327
|
}, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3092
3328
|
if (!start.callId) throw new Error(start.error || "voicecall.start did not return callId");
|
|
3093
3329
|
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
|
-
}
|
|
3330
|
+
const dtmfSent = Boolean(params.dtmfSequence);
|
|
3331
|
+
if (dtmfSent) params.logger?.info(`[google-meet] Meet DTMF queued before realtime connect: callId=${start.callId} digits=${params.dtmfSequence?.length ?? 0}`);
|
|
3107
3332
|
let introSent = false;
|
|
3108
3333
|
if (params.message) {
|
|
3109
3334
|
const delayMs = params.dtmfSequence ? params.config.voiceCall.postDtmfSpeechDelayMs : 0;
|
|
3110
3335
|
if (delayMs > 0) {
|
|
3111
3336
|
params.logger?.info(`[google-meet] Waiting ${delayMs}ms after Meet DTMF before speaking intro for callId=${start.callId}`);
|
|
3112
|
-
await sleep
|
|
3337
|
+
await sleep(delayMs);
|
|
3338
|
+
}
|
|
3339
|
+
let spoken;
|
|
3340
|
+
try {
|
|
3341
|
+
spoken = await client.request("voicecall.speak", {
|
|
3342
|
+
callId: start.callId,
|
|
3343
|
+
allowTwimlFallback: false,
|
|
3344
|
+
message: params.message
|
|
3345
|
+
}, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3346
|
+
} catch (err) {
|
|
3347
|
+
params.logger?.warn?.(`[google-meet] Skipped intro speech because realtime bridge was not ready: ${formatErrorMessage(err)}`);
|
|
3348
|
+
spoken = { success: false };
|
|
3113
3349
|
}
|
|
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
3350
|
if (spoken.success === false) params.logger?.warn?.(`[google-meet] Skipped intro speech because realtime bridge was not ready: ${spoken.error || "voicecall.speak failed"}`);
|
|
3120
3351
|
else {
|
|
3121
3352
|
introSent = true;
|
|
@@ -3135,7 +3366,20 @@ async function endMeetVoiceCallGatewayCall(params) {
|
|
|
3135
3366
|
let client;
|
|
3136
3367
|
try {
|
|
3137
3368
|
client = await createConnectedGatewayClient(params.config);
|
|
3138
|
-
|
|
3369
|
+
try {
|
|
3370
|
+
await client.request("voicecall.end", { callId: params.callId }, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3371
|
+
} catch (err) {
|
|
3372
|
+
if (!isVoiceCallMissingError(err)) throw err;
|
|
3373
|
+
}
|
|
3374
|
+
} finally {
|
|
3375
|
+
await client?.stopAndWait({ timeoutMs: 1e3 });
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
async function getMeetVoiceCallGatewayCall(params) {
|
|
3379
|
+
let client;
|
|
3380
|
+
try {
|
|
3381
|
+
client = await createConnectedGatewayClient(params.config);
|
|
3382
|
+
return await client.request("voicecall.status", { callId: params.callId }, { timeoutMs: params.config.voiceCall.requestTimeoutMs });
|
|
3139
3383
|
} finally {
|
|
3140
3384
|
await client?.stopAndWait({ timeoutMs: 1e3 });
|
|
3141
3385
|
}
|
|
@@ -3158,6 +3402,9 @@ async function speakMeetViaVoiceCallGateway(params) {
|
|
|
3158
3402
|
function nowIso() {
|
|
3159
3403
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3160
3404
|
}
|
|
3405
|
+
function buildTwilioVoiceCallSessionKey(meetingSessionId) {
|
|
3406
|
+
return `voice:google-meet:${meetingSessionId}`;
|
|
3407
|
+
}
|
|
3161
3408
|
function normalizeMeetUrl(input) {
|
|
3162
3409
|
const raw = normalizeOptionalString(input);
|
|
3163
3410
|
if (!raw) throw new Error("url required");
|
|
@@ -3200,12 +3447,12 @@ function resolveProbeTimeoutMs(input, fallback) {
|
|
|
3200
3447
|
if (!Number.isFinite(input) || input <= 0) throw new Error("timeoutMs must be a positive number");
|
|
3201
3448
|
return Math.min(Math.trunc(input), 12e4);
|
|
3202
3449
|
}
|
|
3203
|
-
function sleep(ms) {
|
|
3204
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3205
|
-
}
|
|
3206
3450
|
function isManagedChromeBrowserSession(session) {
|
|
3207
3451
|
return Boolean((session.transport === "chrome" || session.transport === "chrome-node") && session.chrome && session.chrome.launched);
|
|
3208
3452
|
}
|
|
3453
|
+
function noteSession(session, note) {
|
|
3454
|
+
session.notes = [...session.notes.filter((item) => item !== note), note];
|
|
3455
|
+
}
|
|
3209
3456
|
function evaluateSpeechReadiness(session) {
|
|
3210
3457
|
if (!isGoogleMeetTalkBackMode$1(session.mode) || !session.chrome) return { ready: true };
|
|
3211
3458
|
if (!isManagedChromeBrowserSession(session)) {
|
|
@@ -3378,15 +3625,20 @@ var GoogleMeetRuntime = class {
|
|
|
3378
3625
|
const url = normalizeMeetUrl(request.url);
|
|
3379
3626
|
const transport = resolveTransport(request.transport, this.params.config);
|
|
3380
3627
|
const mode = resolveMode(request.mode, this.params.config);
|
|
3381
|
-
|
|
3628
|
+
let reusable = this.list().find((session) => session.state === "active" && isSameMeetUrlForReuse(session.url, url) && session.transport === transport && session.mode === mode);
|
|
3629
|
+
if (reusable?.transport === "twilio") {
|
|
3630
|
+
await this.#refreshTwilioVoiceCallStatus(reusable);
|
|
3631
|
+
if (reusable.state !== "active") reusable = void 0;
|
|
3632
|
+
}
|
|
3382
3633
|
const speechInstructions = request.message ?? this.params.config.realtime.introMessage;
|
|
3383
3634
|
if (reusable) {
|
|
3384
3635
|
await this.#refreshBrowserHealthForChromeSession(reusable);
|
|
3385
|
-
reusable
|
|
3636
|
+
noteSession(reusable, "Reused existing active Meet session.");
|
|
3386
3637
|
reusable.updatedAt = nowIso();
|
|
3638
|
+
const spoken = isGoogleMeetTalkBackMode$1(mode) && speechInstructions ? await this.#speakWhenReady(reusable, speechInstructions) : false;
|
|
3387
3639
|
return {
|
|
3388
3640
|
session: reusable,
|
|
3389
|
-
spoken
|
|
3641
|
+
spoken
|
|
3390
3642
|
};
|
|
3391
3643
|
}
|
|
3392
3644
|
const createdAt = nowIso();
|
|
@@ -3444,15 +3696,18 @@ var GoogleMeetRuntime = class {
|
|
|
3444
3696
|
} else {
|
|
3445
3697
|
const dialInNumber = normalizeDialInNumber(request.dialInNumber ?? this.params.config.twilio.defaultDialInNumber);
|
|
3446
3698
|
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
|
|
3699
|
+
const rawDtmfSequence = buildMeetDtmfSequence({
|
|
3448
3700
|
pin: request.pin ?? this.params.config.twilio.defaultPin,
|
|
3449
3701
|
dtmfSequence: request.dtmfSequence ?? this.params.config.twilio.defaultDtmfSequence
|
|
3450
3702
|
});
|
|
3703
|
+
const dtmfSequence = request.dtmfSequence || this.params.config.twilio.defaultDtmfSequence ? rawDtmfSequence : prefixDtmfWait(rawDtmfSequence, this.params.config.voiceCall.dtmfDelayMs);
|
|
3451
3704
|
const voiceCallResult = this.params.config.voiceCall.enabled ? await joinMeetViaVoiceCallGateway({
|
|
3452
3705
|
config: this.params.config,
|
|
3453
3706
|
dialInNumber,
|
|
3454
3707
|
dtmfSequence,
|
|
3455
3708
|
logger: this.params.logger,
|
|
3709
|
+
...request.requesterSessionKey ? { requesterSessionKey: request.requesterSessionKey } : {},
|
|
3710
|
+
sessionKey: buildTwilioVoiceCallSessionKey(session.id),
|
|
3456
3711
|
message: isGoogleMeetTalkBackMode$1(mode) ? request.message ?? this.params.config.voiceCall.introMessage ?? this.params.config.realtime.introMessage : void 0
|
|
3457
3712
|
}) : void 0;
|
|
3458
3713
|
delegatedTwilioSpoken = Boolean(voiceCallResult?.introSent);
|
|
@@ -3470,7 +3725,7 @@ var GoogleMeetRuntime = class {
|
|
|
3470
3725
|
callId: voiceCallResult.callId
|
|
3471
3726
|
});
|
|
3472
3727
|
});
|
|
3473
|
-
session.notes.push(this.params.config.voiceCall.enabled ? dtmfSequence ? "Twilio transport delegated the phone leg to the voice-call plugin, then
|
|
3728
|
+
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
3729
|
}
|
|
3475
3730
|
} catch (err) {
|
|
3476
3731
|
this.params.logger.warn(`[google-meet] join failed: ${formatErrorMessage(err)}`);
|
|
@@ -3490,7 +3745,12 @@ var GoogleMeetRuntime = class {
|
|
|
3490
3745
|
this.#sessionStops.delete(sessionId);
|
|
3491
3746
|
this.#sessionSpeakers.delete(sessionId);
|
|
3492
3747
|
this.#sessionHealth.delete(sessionId);
|
|
3493
|
-
|
|
3748
|
+
try {
|
|
3749
|
+
await stop();
|
|
3750
|
+
} finally {
|
|
3751
|
+
session.state = "ended";
|
|
3752
|
+
session.updatedAt = nowIso();
|
|
3753
|
+
}
|
|
3494
3754
|
}
|
|
3495
3755
|
session.state = "ended";
|
|
3496
3756
|
session.updatedAt = nowIso();
|
|
@@ -3506,11 +3766,21 @@ var GoogleMeetRuntime = class {
|
|
|
3506
3766
|
spoken: false
|
|
3507
3767
|
};
|
|
3508
3768
|
if (session.transport === "twilio" && session.twilio?.voiceCallId) {
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3769
|
+
try {
|
|
3770
|
+
await speakMeetViaVoiceCallGateway({
|
|
3771
|
+
config: this.params.config,
|
|
3772
|
+
callId: session.twilio.voiceCallId,
|
|
3773
|
+
message: instructions || this.params.config.voiceCall.introMessage || this.params.config.realtime.introMessage || ""
|
|
3774
|
+
});
|
|
3775
|
+
} catch (err) {
|
|
3776
|
+
if (!isVoiceCallMissingError(err)) throw err;
|
|
3777
|
+
this.#markTwilioSessionEnded(session, "Voice Call is no longer active.");
|
|
3778
|
+
return {
|
|
3779
|
+
found: true,
|
|
3780
|
+
spoken: false,
|
|
3781
|
+
session
|
|
3782
|
+
};
|
|
3783
|
+
}
|
|
3514
3784
|
session.twilio.introSent = true;
|
|
3515
3785
|
session.updatedAt = nowIso();
|
|
3516
3786
|
return {
|
|
@@ -3553,7 +3823,7 @@ var GoogleMeetRuntime = class {
|
|
|
3553
3823
|
const waitMs = Math.min(Math.max(0, this.params.config.chrome.waitForInCallMs), Math.max(0, this.params.config.chrome.joinTimeoutMs));
|
|
3554
3824
|
const deadline = Date.now() + waitMs;
|
|
3555
3825
|
while (Date.now() < deadline) {
|
|
3556
|
-
await sleep(250);
|
|
3826
|
+
await sleep(Math.min(250, Math.max(0, deadline - Date.now())));
|
|
3557
3827
|
result = await this.speak(session.id, instructions);
|
|
3558
3828
|
if (result.spoken) return true;
|
|
3559
3829
|
const health = result.session?.chrome?.health;
|
|
@@ -3565,6 +3835,8 @@ var GoogleMeetRuntime = class {
|
|
|
3565
3835
|
}
|
|
3566
3836
|
async testSpeech(request) {
|
|
3567
3837
|
if (request.mode === "transcribe") throw new Error("test_speech requires mode: agent or bidi; use join mode: transcribe for observe-only sessions.");
|
|
3838
|
+
const requestedMode = request.mode ? resolveMode(request.mode, this.params.config) : void 0;
|
|
3839
|
+
const mode = requestedMode && isGoogleMeetTalkBackMode$1(requestedMode) ? requestedMode : isGoogleMeetTalkBackMode$1(this.params.config.defaultMode) ? this.params.config.defaultMode : "agent";
|
|
3568
3840
|
const url = normalizeMeetUrl(request.url);
|
|
3569
3841
|
const transport = resolveTransport(request.transport, this.params.config);
|
|
3570
3842
|
const beforeSessions = this.list();
|
|
@@ -3574,7 +3846,7 @@ var GoogleMeetRuntime = class {
|
|
|
3574
3846
|
...request,
|
|
3575
3847
|
transport,
|
|
3576
3848
|
url,
|
|
3577
|
-
mode
|
|
3849
|
+
mode,
|
|
3578
3850
|
message: request.message ?? "Say exactly: Google Meet speech test complete."
|
|
3579
3851
|
});
|
|
3580
3852
|
let health = result.session.chrome?.health;
|
|
@@ -3662,16 +3934,40 @@ var GoogleMeetRuntime = class {
|
|
|
3662
3934
|
}
|
|
3663
3935
|
async #refreshStatusHealthForSession(session) {
|
|
3664
3936
|
if (session.transport === "chrome" || session.transport === "chrome-node") {
|
|
3665
|
-
if (session.chrome?.health?.manualActionRequired) {
|
|
3666
|
-
this.#refreshSpeechReadiness(session);
|
|
3667
|
-
return;
|
|
3668
|
-
}
|
|
3669
3937
|
await this.#refreshBrowserHealthForChromeSession(session, {
|
|
3670
3938
|
force: true,
|
|
3671
3939
|
readOnly: true
|
|
3672
3940
|
});
|
|
3673
3941
|
return;
|
|
3674
3942
|
}
|
|
3943
|
+
if (session.transport === "twilio") {
|
|
3944
|
+
await this.#refreshTwilioVoiceCallStatus(session);
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
this.#refreshSpeechReadiness(session);
|
|
3948
|
+
}
|
|
3949
|
+
#markTwilioSessionEnded(session, reason) {
|
|
3950
|
+
session.state = "ended";
|
|
3951
|
+
session.updatedAt = nowIso();
|
|
3952
|
+
this.#sessionStops.delete(session.id);
|
|
3953
|
+
this.#sessionSpeakers.delete(session.id);
|
|
3954
|
+
this.#sessionHealth.delete(session.id);
|
|
3955
|
+
noteSession(session, reason);
|
|
3956
|
+
}
|
|
3957
|
+
async #refreshTwilioVoiceCallStatus(session) {
|
|
3958
|
+
const callId = session.twilio?.voiceCallId;
|
|
3959
|
+
if (!callId || session.state !== "active") {
|
|
3960
|
+
this.#refreshSpeechReadiness(session);
|
|
3961
|
+
return;
|
|
3962
|
+
}
|
|
3963
|
+
try {
|
|
3964
|
+
if ((await getMeetVoiceCallGatewayCall({
|
|
3965
|
+
config: this.params.config,
|
|
3966
|
+
callId
|
|
3967
|
+
})).found === false) this.#markTwilioSessionEnded(session, "Voice Call is no longer active.");
|
|
3968
|
+
} catch (error) {
|
|
3969
|
+
this.params.logger.debug?.(`[google-meet] voice-call status refresh ignored: ${formatErrorMessage(error)}`);
|
|
3970
|
+
}
|
|
3675
3971
|
this.#refreshSpeechReadiness(session);
|
|
3676
3972
|
}
|
|
3677
3973
|
async #refreshBrowserHealthForChromeSession(session, options = {}) {
|
|
@@ -3897,13 +4193,13 @@ const googleMeetConfigSchema = {
|
|
|
3897
4193
|
advanced: true
|
|
3898
4194
|
},
|
|
3899
4195
|
"voiceCall.dtmfDelayMs": {
|
|
3900
|
-
label: "
|
|
3901
|
-
help: "
|
|
4196
|
+
label: "DTMF Wait Before PIN (ms)",
|
|
4197
|
+
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
4198
|
advanced: true
|
|
3903
4199
|
},
|
|
3904
4200
|
"voiceCall.postDtmfSpeechDelayMs": {
|
|
3905
|
-
label: "
|
|
3906
|
-
help: "
|
|
4201
|
+
label: "Post-DTMF Speech Delay (ms)",
|
|
4202
|
+
help: "Delay before requesting the realtime intro greeting after Voice Call starts the Twilio leg.",
|
|
3907
4203
|
advanced: true
|
|
3908
4204
|
},
|
|
3909
4205
|
"voiceCall.introMessage": {
|
|
@@ -4141,10 +4437,10 @@ async function callGoogleMeetGatewayFromTool(params) {
|
|
|
4141
4437
|
}
|
|
4142
4438
|
}
|
|
4143
4439
|
async function createMeetFromParams(params) {
|
|
4144
|
-
return (await import("./create-
|
|
4440
|
+
return (await import("./create-C01ezOIH.js")).createMeetFromParams(params);
|
|
4145
4441
|
}
|
|
4146
4442
|
async function createAndJoinMeetFromParams(params) {
|
|
4147
|
-
return (await import("./create-
|
|
4443
|
+
return (await import("./create-C01ezOIH.js")).createAndJoinMeetFromParams(params);
|
|
4148
4444
|
}
|
|
4149
4445
|
async function resolveGoogleMeetTokenFromParams(config, raw) {
|
|
4150
4446
|
const { resolveGoogleMeetAccessToken } = await import("./oauth-BJwzuzT-.js");
|
|
@@ -4236,7 +4532,7 @@ async function exportGoogleMeetBundleFromParams(config, raw) {
|
|
|
4236
4532
|
lateAfterMinutes: resolved.lateAfterMinutes,
|
|
4237
4533
|
earlyBeforeMinutes: resolved.earlyBeforeMinutes
|
|
4238
4534
|
})]);
|
|
4239
|
-
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await import("./cli-
|
|
4535
|
+
const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await import("./cli-Dgpp3OTQ.js");
|
|
4240
4536
|
const calendarId = normalizeOptionalString(raw.calendarId);
|
|
4241
4537
|
const request = {
|
|
4242
4538
|
...resolved.meeting ? { meeting: resolved.meeting } : {},
|
|
@@ -4652,7 +4948,7 @@ var google_meet_default = definePluginEntry({
|
|
|
4652
4948
|
handle: handleGoogleMeetNodeHostCommand
|
|
4653
4949
|
});
|
|
4654
4950
|
api.registerCli(async ({ program }) => {
|
|
4655
|
-
const { registerGoogleMeetCli } = await import("./cli-
|
|
4951
|
+
const { registerGoogleMeetCli } = await import("./cli-Dgpp3OTQ.js");
|
|
4656
4952
|
registerGoogleMeetCli({
|
|
4657
4953
|
program,
|
|
4658
4954
|
config,
|