Skip to main content
Zuko includes a standalone meeting-bot service (apps/meeting-bot) that automatically joins Google Meet meetings, records audio/video, and streams live transcripts back to the backend.

What it does

  • Joins a Google Meet URL using headless browser automation (Puppeteer)
  • Records the meeting as a WebM file and uploads it to S3
  • Streams live transcripts to the backend in real-time using Deepgram
  • Captures chat messages and responds to @zuko mentions via the AI agent
  • Emits status webhooks throughout the meeting lifecycle
  • Auto-leaves when it’s the only participant remaining

How it works

When the backend calls POST /api/google-meet/join, the meeting-bot:
  1. Launches an ephemeral Fly.io machine (in production) or runs locally (in dev)
  2. Opens Chrome via Puppeteer and navigates to the Meet URL
  3. Injects a browser script that extracts participants and chat via a local WebSocket
  4. Streams audio through FFmpeg to Deepgram for live transcription
  5. Sends transcript chunks (buffered at ~600 chars) to the backend
  6. On meeting end, uploads the recording and full transcript JSON to S3
  7. Emits a completed webhook with the S3 artifact keys

Environment variables

apps/meeting-bot reads:
  • BACKEND_URL: Backend base URL (example: http://localhost:3001)
  • AGENT_TOKEN: Shared token for authenticating requests to the backend
  • DEEPGRAM_API_KEY: Deepgram API key for live transcription
  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_BUCKET_NAME: S3 credentials for uploading recordings and transcripts
  • FLY_API_TOKEN, FLY_APP_NAME: Fly.io credentials for launching worker machines (production only)
  • PUPPETEER_EXECUTABLE_PATH: Path to the Chrome binary
  • BOT_NAME: Display name shown in the meeting (default: Zuko AI)
  • PORT: HTTP server port (default: 8000)
The backend must have the same AGENT_TOKEN configured to validate incoming transcript and callback requests.

Run locally

npx nx run @zuko/meeting-bot:dev

API

Join a meeting

POST /api/google-meet/join Triggers the bot to join a Google Meet.
{
  "meetingId": "clx1234abcd",
  "meetingUrl": "https://meet.google.com/abc-defg-hij",
  "callbackUrl": "https://your-backend/api/meetings/webhook"
}
FieldTypeRequiredDescription
meetingIdstringYesMeeting ID from the database
meetingUrlstringYesFull Google Meet URL
callbackUrlstringNoWebhook URL for status updates
Response
{ "message": "Meeting scheduled" }

Status webhooks

The bot sends POST requests to callbackUrl (or BACKEND_URL/api/meetings/webhook) at each lifecycle stage.
{
  "meetingId": "clx1234abcd",
  "event": "completed",
  "data": {
    "recording": "meetings/clx1234abcd/recording.webm",
    "transcript": "meetings/clx1234abcd/transcript.json"
  }
}
EventDescription
fly-machine-launchedFly worker machine created
in_progressBot successfully joined the meeting
processingMeeting ended; uploading artifacts to S3
completedRecording and transcript uploaded; S3 keys in data
failedAn error occurred
rejectedHost denied the bot entry

Transcript streaming

During the meeting, the bot streams transcript chunks to the backend in real-time. POST /api/meetings/:meetingId/transcript-chunks Headers:
  • x-agent-token: must match AGENT_TOKEN
  • x-user-provider: google-meet
{
  "text": "Let's align on the Q2 roadmap...",
  "sequence": 12,
  "isFinal": true
}
Chunks are buffered at ~600 characters with 100-character overlap to preserve context across boundaries. On meeting end, any remaining buffered text is flushed.

S3 artifacts

After a meeting, two files are written to S3:
FilePathDescription
Recordingmeetings/{meetingId}/recording.webmRaw WebM video; the backend transcodes this to MP4 asynchronously
Transcriptmeetings/{meetingId}/transcript.jsonFull speaker-diarized transcript with timestamps and chat messages

Chat and AI responses

The bot captures all chat messages from the meeting. When a participant sends a message containing @zuko, the bot queries the backend AI agent and posts the response back to the meeting chat.

Deployment

In production, each meeting runs on its own ephemeral Fly.io machine (1 CPU, 2 GB RAM). The machine is launched when a join request arrives and automatically destroyed after cleanup completes.