Skip to content

feat: add Saturn Exa; a real-time AI meeting copilot using Smallest AI STT and Exa#23

Open
malikaa-27 wants to merge 6 commits into
smallest-inc:mainfrom
malikaa-27:feat/saturn-meeting-copilot
Open

feat: add Saturn Exa; a real-time AI meeting copilot using Smallest AI STT and Exa#23
malikaa-27 wants to merge 6 commits into
smallest-inc:mainfrom
malikaa-27:feat/saturn-meeting-copilot

Conversation

@malikaa-27

@malikaa-27 malikaa-27 commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Saturn — AI Meeting Copilot

Saturn is a full-stack real-time meeting intelligence app built with Next.js that uses Smallest AI Pulse STT for audio transcription.

What it does

  • Live transcription — captures mic audio and transcribes via Smallest AI Pulse STT
  • Auto question detection — detects questions in conversation and triggers Exa semantic web search automatically
  • AI insights — Claude synthesizes clean answers shown in a real-time side panel
  • Push-to-talk search — hold Tab and speak to search anything directly
  • Meeting summary — Claude generates full notes (summary, decisions, action items) when the meeting ends
  • Google Meet Chrome extension — reads live captions or tab audio to capture all participants, opens Saturn as a Chrome side panel alongside Meet

Stack

Layer Tech
STT Smallest AI Pulse
Web search Exa semantic search
AI summaries Anthropic Claude

Setup

cd speech-to-text/saturn-meeting-copilot
cp .env.example .env.local   # add your API keys here
npm install
npm run dev

API Keys needed

Key Service
SMALLEST_API_KEY Smallest AI Pulse STT
EXA_API_KEY Exa web search
ANTHROPIC_API_KEY Claude summaries

EntelligenceAI PR Summary

This PR introduces Saturn Meeting Copilot, a complete Next.js 16 application and companion Google Meet Chrome extension providing real-time AI-powered meeting assistance using Smallest.ai, Exa, and Anthropic Claude.

  • API Routes: /api/transcribe (Smallest.ai STT), /api/synthesize (Waves TTS streaming), /api/research (Exa + Claude insight generation), /api/summary (Claude meeting summary), /api/transcript/push (Chrome extension segment ingestion with CORS), /api/transcript/stream (SSE live broadcast)
  • Chrome Extension: Manifest V3 extension for Google Meet with dual capture modes — Caption Mode (MutationObserver on CC DOM) and Audio Mode (WebAudio tab+mic mix → chunked STT), background service worker for tabCapture, popup with health-check polling, and iframe-based side panel
  • Hooks: useGoogleMeetTranscript (SSE + AudioWorklet mic pipeline with WAV encoding, silence detection, and Web Speech API fallback), useExaBot (automatic question detection and research pipeline), useDirectSearch (push-to-talk Tab key recording and search)
  • State Management: Zustand useMeetingStore managing full meeting lifecycle, transcript segments, AI insights, action items, topics, summary, voice/STT settings, and credit balance
  • Services: transcriptionService (Smallest.ai Pulse STT), voiceService (Waves TTS), exaSearchService (Exa semantic search), researchAgent (question detection + runResearch + buildInsight)
  • UI Components: TranscriptPanel, InsightPanel, TopicTimeline, MeetingSummaryModal, TopBar, BottomBar, AudioWaveform, ExaBotStatus; shadcn/ui primitives (Button, Badge, Progress, ScrollArea, Separator, Tooltip) on @base-ui/react
  • Infrastructure: TranscriptBridge EventEmitter singleton on globalThis for cross-route pub/sub, demo simulator with word-by-word typing playback, Tailwind v4 with oklch design tokens, next.config.ts with CORS and X-Frame-Options: ALLOWALL

Confidence Score: 1/5 - Blocking Issues

  • Two critical security vulnerabilities: unauthenticated POST endpoint allowing arbitrary transcript injection to all SSE clients, and unsanitized href rendering enabling XSS via javascript: URLs
  • Multiple high-severity bugs including hardcoded localhost URL making the extension non-functional in any real environment, unstoppable speech recognition instances causing resource leaks, and EventSource with no error handling causing silent infinite reconnection loops
  • Duplicate code across multiple files suggests poor code organization and maintenance risk, compounding the existing functional bugs
  • The combination of security holes and fundamental functional bugs (extension won't work outside localhost, error swallowing in critical loops) makes this PR unsafe to merge without significant fixes
Files requiring special attention
  • speech-to-text/saturn-meeting-copilot/app/api/transcript/push/route.ts
  • speech-to-text/saturn-meeting-copilot/components/insights/InsightPanel.tsx
  • speech-to-text/saturn-meeting-copilot/extensions/google-meet/content.js
  • speech-to-text/saturn-meeting-copilot/hooks/useGoogleMeetTranscript.ts
  • speech-to-text/saturn-meeting-copilot/extensions/google-meet/background.js

Confidence Score: 2/5 - Changes Needed

  • The push endpoint has no authentication, allowing any external actor to inject arbitrary transcript data to all SSE clients — a clear security vulnerability
  • Direct rendering of source.url as an href without sanitization enables XSS via javascript: URLs — another security issue
  • The background.js has an unresolved correctness issue around sender.tab handling
  • Multiple unresolved issues spanning security (auth bypass, XSS), correctness (broken CSS class), and code quality (duplication) suggest the PR needs meaningful fixes before merge
Files requiring special attention
  • speech-to-text/saturn-meeting-copilot/app/api/transcript/push/route.ts
  • speech-to-text/saturn-meeting-copilot/components/insights/InsightPanel.tsx
  • speech-to-text/saturn-meeting-copilot/extensions/google-meet/background.js
  • speech-to-text/saturn-meeting-copilot/components/ui/button.tsx

Saturn is a real-time AI meeting intelligence app built with Next.js.
It transcribes your microphone via Smallest AI Pulse STT, detects
questions automatically, searches Exa for answers, and generates full
meeting notes with Claude when the meeting ends.

Features:
- Live transcription via Smallest AI Pulse STT (5s rolling chunks)
- Auto question detection + Exa semantic web search
- Claude-powered insight summaries shown in a side panel
- Push-to-talk Tab search for manual queries
- End-of-meeting summary: decisions, action items, topics
- Chrome extension for Google Meet (captions + tab audio)
- Responsive layout — works as a Chrome side panel alongside Meet
@vercel

vercel Bot commented Mar 13, 2026

Copy link
Copy Markdown

@malikaa-27 is attempting to deploy a commit to the developer-2074's projects Team on Vercel.

A member of the Team first needs to authorize it.

@malikaa-27 malikaa-27 changed the title feat: add Saturn — real-time AI meeting copilot using Smallest AI STT feat: add Saturn Exa; a real-time AI meeting copilot using Smallest AI STT and Exa Mar 13, 2026
Comment on lines +23 to +50
export async function POST(req: NextRequest) {
try {
const { text, segments } = await req.json();

if (!text || typeof text !== "string") {
return NextResponse.json({ error: "text is required" }, { status: 400, headers: CORS });
}

const rawSpeaker: string | undefined = segments?.[0]?.speaker;
const speaker = rawSpeaker ?? "You";

const segment: TranscriptSegment = {
id: `seg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
speaker,
speakerColor: speakerColor(speaker),
text: text.trim(),
timestamp: new Date().toLocaleTimeString("en-US", { hour12: false }),
isQuestion: text.trimEnd().endsWith("?"),
isHighlighted: false,
words: [],
};

transcriptBridge.push(segment);

return NextResponse.json({ ok: true }, { headers: CORS });
} catch {
return NextResponse.json({ error: "Invalid request" }, { status: 400, headers: CORS });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: The POST endpoint has no authentication or API key validation — any external actor can push arbitrary transcript segments to all connected SSE clients by calling this endpoint directly, enabling data injection attacks.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In file `speech-to-text/saturn-meeting-copilot/app/api/transcript/push/route.ts`, the POST handler at line 23 accepts requests from anyone (CORS is set to `*`). Add authentication/authorization — for example, verify a shared secret or bearer token in the request headers before processing the body. Without this, any caller can inject transcript segments into all active SSE sessions.

Comment on lines +248 to +250
)}

{/* Sources */}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: source.urlis rendered directly as anhrefwithout sanitization — ajavascript:` URL would execute arbitrary script when clicked, enabling XSS.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In speech-to-text/saturn-meeting-copilot/components/insights/InsightPanel.tsx around line 248-250, the `source.url` value is used directly as an anchor `href` without validation. This allows `javascript:` protocol URLs to execute XSS. Add a check before rendering: only allow URLs that start with `http://` or `https://`, otherwise fall back to `#` or omit the href. Example fix: `href={/^https?:\/\//i.test(source.url) ? source.url : '#'}`.

Comment on lines +18 to +19
{ targetTabId: sender.tab.id },
(streamId) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: sender.tab

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In speech-to-text/saturn-meeting-copilot/extensions/google-meet/background.js, lines 18-19, `sender.tab` can be undefined when a message is sent from a non-tab context. Add a guard before accessing `sender.tab.id`: check `if (!sender.tab)` and call `sendResponse({ error: 'No tab context available' }); return;` before the `chrome.tabCapture.getMediaStreamId` call.

{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: The class [a]:hover:bg-primary/80 uses an arbitrary variant selector [a] which targets an ancestor <a> element — this is almost certainly a typo for hover:bg-primary/80, meaning the default button never shows a hover background change when used as a standalone button.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In file `speech-to-text/saturn-meeting-copilot/components/ui/button.tsx`, line 13, the `default` variant class string contains `[a]:hover:bg-primary/80`. This is an arbitrary Tailwind variant that only applies when the element is a descendant of an `<a>` tag, which is likely unintended. Replace `[a]:hover:bg-primary/80` with `hover:bg-primary/80` so the hover style applies to all default buttons.

const segment: TranscriptSegment = {
id: `seg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
speaker,
speakerColor: speakerColor(speaker),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate Code: ⚠️ Duplicate Code Detected (Similarity: 100%)

This function speakerColor duplicates existing code.

📍 Original Location:

speech-to-text/saturn-meeting-copilot/hooks/useGoogleMeetTranscript.ts:25-29

Function: speakerColor

💡 Recommendation:
Extract speakerColor and the SPEAKER_COLORS constant into a shared utility module (e.g., lib/speakerColor.ts) and import it in both transcript/push/route.ts and hooks/useGoogleMeetTranscript.ts.

Consider importing and reusing the existing function instead of duplicating the logic.

@abhishekmishragithub

Copy link
Copy Markdown
Collaborator

copied from slack chat:

the pr is good overall but i have few nitpicks/suggestions:

recommended fixs:
  - app/api/research/route.ts line 3 comment says "summa rizes with OpenAI" but should be claude?
  - duplicated speakerColor() and makeSegment() across push/route.ts and useGoogleMeetTranscript.ts — extract to a shared lib/segment.ts  ---> this is optional, but just for best practice i am sharing. you can leave as it is.
  - metadata title in layout.tsx is "Saturn -- Saturn for Meetings"  should be "Saturn -- AI Meeting Intelligence"
  - switch from claude-opus-4-6 to claude-sonnet-4-20250514 in research and summary routes — faster, cheaper, better
  for developers trying the demo
  - add a short "Production Notes" section at the bottom of the README: "This demo uses open CORS and no auth on API  routes (required for the Chrome extension on localhost). For production, add authentication, restrict CORS origins,
   and update SATURN_URL in the extension."

also, let's keep it in speech-to-text/ since the core loop is Pulse STT. We'll add an  integrations/exa/README that links here. it will help us to be discoverable from Exa's docs too.

you can use the following for the integrations/exa/README.md

Exa + Smallest AI

  Integrations combining Exa semantic web search with Smallest AI's voice and audio APIs.

  Projects

  Saturn — AI Meeting Copilot

  Real-time meeting intelligence app that transcribes audio with Smallest AI Pulse STT, detects questions in
  conversation, and automatically searches the web via Exa to surface relevant answers — all summarized by Claude in
  a live side panel.

  Stack: Next.js, Smallest AI Pulse STT, Exa Search, Anthropic Claude

  Features:
  - Live transcription via Pulse STT
  - Auto question detection → Exa semantic search
  - AI-synthesized answers in real-time
  - Push-to-talk manual search
  - Meeting summary generation
  - Google Meet Chrome extension

  View project →

view project  should have an  url linked to the stt folder where you have added the exa cookbook

rest is good to go.

abhishekmishragithub and others added 4 commits March 18, 2026 19:05
- Fix comment in research route: "OpenAI" → "Claude"
- Fix metadata title: "Saturn for Meetings" → "AI Meeting Intelligence"
- Switch model from claude-opus-4-6 to claude-sonnet-4-20250514 in research and summary routes
- Add Production Notes section to README
- Add integrations/exa/README.md linking to Saturn project
- Switch modal background from bg-zinc-950 to bg-white with proper text colors
- Add empty state messages for Key Decisions and Action Items sections
- Guard all AudioContext.close() calls with state !== "closed" check
- Replace regex detectQuestion + isResearchWorthy with a single Claude
  Haiku call (extractResearchQuery) that decides AND extracts the query
- Add sliding window (last 5 segments per speaker) so chunked speech
  is combined before evaluation, catching questions split across chunks
- Fix race condition: only mark window IDs after confirmed research,
  not before — prevents in-flight skip from blocking concurrent questions
- Add removeInsight to meetingStore for cleaning up skipped placeholders
- Add SOCIAL_PATTERNS regex as a cheap pre-filter for obvious non-research text
@entelligence-ai-pr-reviews

Copy link
Copy Markdown


Confidence Score: 2/5 - Changes Needed

  • There is an unresolved XSS vulnerability in InsightPanel.tsx where source.url is rendered directly as an href without sanitization, allowing javascript: URLs to execute arbitrary scripts — this is a genuine security issue, not a style nit.
  • The background.js issue with sender.tab suggests a potential correctness bug in the Chrome extension's message handling that could cause runtime errors.
  • The button.tsx arbitrary variant selector is likely a typo that would cause broken hover styling, though lower severity than the security issue.
  • All three unresolved comments represent real bugs or security vulnerabilities rather than cosmetic issues, collectively requiring fixes before merge.
  • 1 previous unresolved comment(s) likely resolved in latest diff (score-only signal; thread status unchanged)
Files requiring special attention
  • speech-to-text/saturn-meeting-copilot/components/insights/InsightPanel.tsx
  • speech-to-text/saturn-meeting-copilot/extensions/google-meet/background.js
  • speech-to-text/saturn-meeting-copilot/components/ui/button.tsx

Comment on lines +17 to +26
};

export async function OPTIONS() {
return new NextResponse(null, { status: 204, headers: CORS });
}

export async function POST(req: NextRequest) {
try {
const { text, segments } = await req.json();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: 🔐 The PUSH_TOKEN / x-saturn-token authentication guard has been entirely removed, meaning this endpoint now accepts transcript pushes from any unauthenticated caller on the internet — combined with Access-Control-Allow-Origin: *, any website or script can inject arbitrary transcript segments into active sessions.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In file `speech-to-text/saturn-meeting-copilot/app/api/transcript/push/route.ts`, lines 17-26, the diff removed the SATURN_PUSH_TOKEN-based authentication guard that previously validated the `x-saturn-token` request header before allowing transcript pushes. Restore the token check so that the POST handler rejects requests without a valid token:

1. Re-add `const PUSH_TOKEN = process.env.SATURN_PUSH_TOKEN;` before the OPTIONS handler.
2. Re-add `"x-saturn-token"` to `Access-Control-Allow-Headers` in the CORS object.
3. Re-add the guard at the top of the POST handler body (before the try block):
```ts
if (PUSH_TOKEN && req.headers.get("x-saturn-token") !== PUSH_TOKEN) {
  return NextResponse.json({ error: "Unauthorized" }, { status: 401, headers: CORS });
}

This ensures only callers with the correct shared secret can push transcript segments.


</details>
<!-- ai_prompt_end -->

@entelligence-ai-pr-reviews

Copy link
Copy Markdown

Confidence Score: 1/5 - Blocking Issues

Not safe to merge — this PR introduces a critical unauthenticated endpoint in app/api/transcript/push/route.ts where the PUSH_TOKEN/x-saturn-token authentication guard was entirely removed alongside Access-Control-Allow-Origin: *, allowing any internet actor to inject arbitrary transcript data into all connected SSE clients. Beyond the security regression, the PR also silently drops diarize=true from the transcriptionService.ts URL, which will collapse all speaker-attributed output into a single-segment fallback, breaking the core diarization feature the copilot is built around. While the PR achieves meaningful improvements — multi-language support, configurable research result counts, a new settings UI, and cleaner pipelines — the combination of a critical auth removal, credit deduction before confirmed success in useExaBot.ts, speaker attribution loss in useGoogleMeetTranscript.ts, and unresolved XSS via unsanitized source.url in InsightPanel.tsx makes this unacceptable to merge in its current state.

Key Findings:

  • The PUSH_TOKEN authentication guard was removed from route.ts, and combined with Access-Control-Allow-Origin: *, this allows any unauthenticated caller — including malicious websites — to push arbitrary transcript segments to all connected clients; this is a critical security vulnerability introduced by this PR (and flagged as unresolved in a prior review).
  • In transcriptionService.ts, the diarize=true query parameter was dropped from the API URL, meaning data.utterances will likely be empty and all multi-speaker output silently collapses to the single-speaker fallback, functionally breaking the diarization feature.
  • In useExaBot.ts, store.updateCreditBalance(-2) is now called before the try block with no compensating refund in catch, so users lose credits even on failed API calls — a regression from the prior logic where credits were only deducted on confirmed success.
  • In useGoogleMeetTranscript.ts, the refactored chunk handling attributes all speakers in a multi-speaker segment to only segments?.[0]?.speaker, silently discarding all other speakers' labels; additionally, removing state !== 'closed' guard and webSpeechRef.current?.abort() introduces an unhandled promise rejection and a microphone leak on cleanup.
Files requiring special attention
  • speech-to-text/saturn-meeting-copilot/app/api/transcript/push/route.ts
  • speech-to-text/saturn-meeting-copilot/services/transcriptionService.ts
  • speech-to-text/saturn-meeting-copilot/hooks/useExaBot.ts
  • speech-to-text/saturn-meeting-copilot/hooks/useGoogleMeetTranscript.ts
  • speech-to-text/saturn-meeting-copilot/components/insights/InsightPanel.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants