From a983ea61ac326c3a1103b1891a243ff232644b2f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 25 Apr 2026 07:43:44 +0100 Subject: [PATCH] feat(google-meet): include transcript entries in artifacts --- CHANGELOG.md | 2 +- docs/plugins/google-meet.md | 11 +++--- extensions/google-meet/index.test.ts | 37 ++++++++++++++++++ extensions/google-meet/index.ts | 6 +++ extensions/google-meet/src/cli.ts | 18 +++++++++ extensions/google-meet/src/meet.ts | 56 ++++++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d559830031..6eb53bc55d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/plugins/google-meet.md b/docs/plugins/google-meet.md index 96d342740fe..53c419c6b92 100644 --- a/docs/plugins/google-meet.md +++ b/docs/plugins/google-meet.md @@ -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: diff --git a/extensions/google-meet/index.test.ts b/extensions/google-meet/index.test.ts index c5be3528d2a..66ef97d6adb 100644 --- a/extensions/google-meet/index.test.ts +++ b/extensions/google-meet/index.test.ts @@ -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" }], }, ], diff --git a/extensions/google-meet/index.ts b/extensions/google-meet/index.ts index 10a668ae0f5..d875dccfaa0 100644 --- a/extensions/google-meet/index.ts +++ b/extensions/google-meet/index.ts @@ -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, }), ); } diff --git a/extensions/google-meet/src/cli.ts b/extensions/google-meet/src/cli.ts index c2c6b7c7ba7..afc4b77f0a7 100644 --- a/extensions/google-meet/src/cli.ts +++ b/extensions/google-meet/src/cli.ts @@ -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 ", "OAuth client secret override") .option("--expires-at ", "Cached access token expiry as unix epoch milliseconds") .option("--page-size ", "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({ diff --git a/extensions/google-meet/src/meet.ts b/extensions/google-meet/src/meet.ts index acf1ba9af88..8cace830bfd 100644 --- a/extensions/google-meet/src/meet.ts +++ b/extensions/google-meet/src/meet.ts @@ -73,6 +73,21 @@ export type GoogleMeetTranscript = { docsDestination?: Record; }; +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 { + return listGoogleMeetCollection({ + 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 { 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 }