feat(google-meet): include transcript entries in artifacts

This commit is contained in:
Peter Steinberger
2026-04-25 07:43:44 +01:00
parent 356530598a
commit a983ea61ac
6 changed files with 124 additions and 6 deletions

View File

@@ -73,7 +73,7 @@ Docs: https://docs.openclaw.ai
- Plugins/Google Meet: add a bundled participant plugin with personal Google auth, explicit meeting URL joins, Chrome and Twilio transports, and realtime voice support. (#70765) Thanks @steipete.
- Plugins/Google Meet: default Chrome realtime sessions to OpenAI plus SoX `rec`/`play` audio bridge commands, so the usual setup only needs the plugin enabled and `OPENAI_API_KEY`. Thanks @steipete.
- Plugins/Google Meet: add a `chrome-node` transport so a paired macOS node, such as a Parallels VM, can own Chrome, BlackHole, and SoX while the Gateway machine keeps the agent and model key. Thanks @steipete.
- Plugins/Google Meet: add `googlemeet artifacts` and `googlemeet attendance` commands plus matching tool/gateway actions for conference records, recordings, transcripts, smart notes, and participant sessions. Thanks @steipete.
- Plugins/Google Meet: add `googlemeet artifacts` and `googlemeet attendance` commands plus matching tool/gateway actions for conference records, recordings, transcripts and transcript entries, smart notes, and participant sessions. Thanks @steipete.
- Plugins/Google Meet: add `googlemeet doctor --oauth` so operators can verify OAuth token refresh, Meet space reads, and side-effecting space creation without printing secrets. Thanks @steipete.
- Plugins/Voice Call: expose the shared `openclaw_agent_consult` realtime tool so live phone calls can ask the full OpenClaw agent for deeper/tool-backed answers. Thanks @steipete.
- Plugins/Voice Call: add `voicecall setup` and a dry-run-by-default `voicecall smoke` command so Twilio/provider readiness can be checked before placing a live test call. Thanks @steipete.

View File

@@ -643,11 +643,12 @@ openclaw googlemeet attendance --conference-record conferenceRecords/abc123 --js
```
`artifacts` returns conference record metadata plus participant, recording,
transcript, and smart-note resource metadata when Google exposes it for the
meeting. `attendance` expands participants into participant-session rows with
join/leave timestamps. These commands use the Meet REST API only; transcript or
smart-note document body download is intentionally out of scope because that
requires separate Google Docs/Drive access.
transcript, structured transcript-entry, and smart-note resource metadata when
Google exposes it for the meeting. Use `--no-transcript-entries` to skip
entry lookup for large meetings. `attendance` expands participants into
participant-session rows with join/leave timestamps. These commands use the Meet
REST API only; Google Docs/Drive document body download is intentionally out of
scope because that requires separate Google Docs/Drive access.
Create a fresh Meet space:

View File

@@ -156,6 +156,20 @@ function stubMeetArtifactsApi() {
],
});
}
if (url.pathname === "/v2/conferenceRecords/rec-1/transcripts/t1/entries") {
return jsonResponse({
transcriptEntries: [
{
name: "conferenceRecords/rec-1/transcripts/t1/entries/e1",
participant: "conferenceRecords/rec-1/participants/p1",
text: "Hello from the transcript.",
languageCode: "en-US",
startTime: "2026-04-25T10:01:00Z",
endTime: "2026-04-25T10:01:05Z",
},
],
});
}
if (url.pathname === "/v2/conferenceRecords/rec-1/smartNotes") {
return jsonResponse({
smartNotes: [
@@ -439,6 +453,17 @@ describe("google-meet plugin", () => {
participants: [{ name: "conferenceRecords/rec-1/participants/p1" }],
recordings: [{ name: "conferenceRecords/rec-1/recordings/r1" }],
transcripts: [{ name: "conferenceRecords/rec-1/transcripts/t1" }],
transcriptEntries: [
{
transcript: "conferenceRecords/rec-1/transcripts/t1",
entries: [
{
name: "conferenceRecords/rec-1/transcripts/t1/entries/e1",
text: "Hello from the transcript.",
},
],
},
],
smartNotes: [{ name: "conferenceRecords/rec-1/smartNotes/sn1" }],
},
],
@@ -460,6 +485,12 @@ describe("google-meet plugin", () => {
auditContext: "google-meet.conferenceRecords.smartNotes.list",
}),
);
expect(fetchGuardMocks.fetchWithSsrFGuard).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://meet.googleapis.com/v2/conferenceRecords/rec-1/transcripts/t1/entries?pageSize=2",
auditContext: "google-meet.conferenceRecords.transcripts.entries.list",
}),
);
});
it("lists Meet attendance rows with participant sessions", async () => {
@@ -868,6 +899,12 @@ describe("google-meet plugin", () => {
{
recordings: [{ name: "conferenceRecords/rec-1/recordings/r1" }],
transcripts: [{ name: "conferenceRecords/rec-1/transcripts/t1" }],
transcriptEntries: [
{
transcript: "conferenceRecords/rec-1/transcripts/t1",
entries: [{ text: "Hello from the transcript." }],
},
],
smartNotes: [{ name: "conferenceRecords/rec-1/smartNotes/sn1" }],
},
],

View File

@@ -186,6 +186,9 @@ const GoogleMeetToolSchema = Type.Object({
Type.String({ description: "Meet conferenceRecords/{id} resource name or id" }),
),
pageSize: Type.Optional(Type.Number({ description: "Meet API page size for list actions" })),
includeTranscriptEntries: Type.Optional(
Type.Boolean({ description: "For artifacts, include structured transcript entries" }),
),
accessToken: Type.Optional(Type.String({ description: "Access token override" })),
refreshToken: Type.Optional(Type.String({ description: "Refresh token override" })),
clientId: Type.Optional(Type.String({ description: "OAuth client id override" })),
@@ -271,6 +274,7 @@ async function resolveArtifactQueryFromParams(
meeting,
conferenceRecord,
pageSize: resolveOptionalPositiveInteger(raw.pageSize),
includeTranscriptEntries: raw.includeTranscriptEntries !== false,
};
}
@@ -397,6 +401,7 @@ export default definePluginEntry({
meeting: resolved.meeting,
conferenceRecord: resolved.conferenceRecord,
pageSize: resolved.pageSize,
includeTranscriptEntries: resolved.includeTranscriptEntries,
}),
);
} catch (err) {
@@ -566,6 +571,7 @@ export default definePluginEntry({
meeting: resolved.meeting,
conferenceRecord: resolved.conferenceRecord,
pageSize: resolved.pageSize,
includeTranscriptEntries: resolved.includeTranscriptEntries,
}),
);
}

View File

@@ -51,6 +51,7 @@ type ResolveSpaceOptions = {
type MeetArtifactOptions = ResolveSpaceOptions & {
conferenceRecord?: string;
pageSize?: string;
transcriptEntries?: boolean;
};
type SetupOptions = {
@@ -429,6 +430,7 @@ function resolveArtifactTokenOptions(
accessToken?: string;
expiresAt?: number;
pageSize?: number;
includeTranscriptEntries?: boolean;
} {
const meeting = options.meeting?.trim() || config.defaults.meeting;
const conferenceRecord = options.conferenceRecord?.trim();
@@ -446,6 +448,7 @@ function resolveArtifactTokenOptions(
accessToken: options.accessToken?.trim() || config.oauth.accessToken,
expiresAt: parseOptionalNumber(options.expiresAt) ?? config.oauth.expiresAt,
pageSize: parseOptionalNumber(options.pageSize),
includeTranscriptEntries: options.transcriptEntries !== false,
};
}
@@ -474,6 +477,10 @@ function writeArtifactsSummary(result: GoogleMeetArtifactsResult): void {
writeStdoutLine("participants: %d", entry.participants.length);
writeStdoutLine("recordings: %d", entry.recordings.length);
writeStdoutLine("transcripts: %d", entry.transcripts.length);
writeStdoutLine(
"transcript entries: %d",
entry.transcriptEntries.reduce((count, transcript) => count + transcript.entries.length, 0),
);
writeStdoutLine("smart notes: %d", entry.smartNotes.length);
if (entry.smartNotesError) {
writeStdoutLine("smart notes warning: %s", entry.smartNotesError);
@@ -484,6 +491,15 @@ function writeArtifactsSummary(result: GoogleMeetArtifactsResult): void {
for (const transcript of entry.transcripts) {
writeStdoutLine("- transcript: %s", transcript.name);
}
for (const transcriptEntries of entry.transcriptEntries) {
if (transcriptEntries.entriesError) {
writeStdoutLine(
"- transcript entries warning: %s: %s",
transcriptEntries.transcript,
transcriptEntries.entriesError,
);
}
}
for (const smartNote of entry.smartNotes) {
writeStdoutLine("- smart note: %s", smartNote.name);
}
@@ -840,6 +856,7 @@ export function registerGoogleMeetCli(params: {
.option("--client-secret <secret>", "OAuth client secret override")
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
.option("--page-size <n>", "Max resources per Meet API page")
.option("--no-transcript-entries", "Skip structured transcript entry lookup")
.option("--json", "Print JSON output", false)
.action(async (options: MeetArtifactOptions) => {
const resolved = resolveArtifactTokenOptions(params.config, options);
@@ -849,6 +866,7 @@ export function registerGoogleMeetCli(params: {
meeting: resolved.meeting,
conferenceRecord: resolved.conferenceRecord,
pageSize: resolved.pageSize,
includeTranscriptEntries: resolved.includeTranscriptEntries,
});
if (options.json) {
writeStdoutJson({

View File

@@ -73,6 +73,21 @@ export type GoogleMeetTranscript = {
docsDestination?: Record<string, unknown>;
};
export type GoogleMeetTranscriptEntry = {
name: string;
participant?: string;
text?: string;
languageCode?: string;
startTime?: string;
endTime?: string;
};
export type GoogleMeetTranscriptEntries = {
transcript: string;
entries: GoogleMeetTranscriptEntry[];
entriesError?: string;
};
export type GoogleMeetSmartNote = {
name: string;
startTime?: string;
@@ -85,6 +100,7 @@ export type GoogleMeetArtifactsEntry = {
participants: GoogleMeetParticipant[];
recordings: GoogleMeetRecording[];
transcripts: GoogleMeetTranscript[];
transcriptEntries: GoogleMeetTranscriptEntries[];
smartNotes: GoogleMeetSmartNote[];
smartNotesError?: string;
};
@@ -434,6 +450,21 @@ export async function listGoogleMeetTranscripts(params: {
});
}
export async function listGoogleMeetTranscriptEntries(params: {
accessToken: string;
transcript: string;
pageSize?: number;
}): Promise<GoogleMeetTranscriptEntry[]> {
return listGoogleMeetCollection<GoogleMeetTranscriptEntry>({
accessToken: params.accessToken,
path: `${encodeResourceNameForPath(params.transcript)}/entries`,
collectionKey: "transcriptEntries",
query: { pageSize: params.pageSize },
auditContext: "google-meet.conferenceRecords.transcripts.entries.list",
errorPrefix: "Google Meet conferenceRecords.transcripts.entries.list",
});
}
export async function listGoogleMeetSmartNotes(params: {
accessToken: string;
conferenceRecord: string;
@@ -506,6 +537,7 @@ export async function fetchGoogleMeetArtifacts(params: {
meeting?: string;
conferenceRecord?: string;
pageSize?: number;
includeTranscriptEntries?: boolean;
}): Promise<GoogleMeetArtifactsResult> {
const resolved = await resolveConferenceRecordQuery(params);
const artifacts = await Promise.all(
@@ -537,11 +569,35 @@ export async function fetchGoogleMeetArtifacts(params: {
smartNotesError: getErrorMessage(error),
})),
]);
const transcriptEntries =
params.includeTranscriptEntries === false
? []
: await Promise.all(
transcripts.map(async (transcript) => {
try {
return {
transcript: transcript.name,
entries: await listGoogleMeetTranscriptEntries({
accessToken: params.accessToken,
transcript: transcript.name,
pageSize: params.pageSize,
}),
};
} catch (error) {
return {
transcript: transcript.name,
entries: [],
entriesError: getErrorMessage(error),
};
}
}),
);
return {
conferenceRecord,
participants,
recordings,
transcripts,
transcriptEntries,
smartNotes: smartNotesResult.smartNotes,
...(smartNotesResult.smartNotesError
? { smartNotesError: smartNotesResult.smartNotesError }