Every utterance boundary (1.5 seconds of silence) triggers a Gemini extraction session.Why multi-turn sessions matter (convex/claimExtraction.ts:78-96):Stanzo maintains the full conversation history with Gemini for each debate. This allows the model to:
Resolve context: Understand “that number” refers to a statistic from 2 minutes ago
Track continuity: Know when a speaker is elaborating vs. making a new claim
function buildSystemPrompt(speakerA: string, speakerB: string): string { return `You are a factual claim extractor for a live debate between ${speakerA} (speaker 0) and ${speakerB} (speaker 1).Each turn, I provide a new transcript segment. You have the full conversation history.Rules:- ONLY extract claims from the NEW segment in my latest message- Do NOT re-extract claims from previous turns- Extract specific, verifiable factual claims (statistics, dates, named facts, causal claims)- Extract the factual core when mixed with opinion- Ignore purely opinion/prediction/subjective statements- Use context to resolve pronouns and referencesOutput: JSONL, one object per line:- speaker: 0 for ${speakerA}, 1 for ${speakerB}- claimText: concise factual claim- originalTranscriptExcerpt: quote from the new segmentIf no factual claims, output: NO_CLAIMSNo markdown, no explanation, no array brackets.`}
export const extract = internalAction({ handler: async (ctx, { debateId }) => { // Get unprocessed transcript chunks const chunks = await ctx.runQuery( internal.transcriptChunks.getUnprocessed, { debateId } ) if (chunks.length === 0) return null // Mark as processed BEFORE calling LLM to prevent race conditions await ctx.runMutation(internal.transcriptChunks.markProcessed, { chunkIds: chunks.map((c) => c._id), }) // Load existing conversation history from extractionSessions table const session = await ctx.runQuery( internal.extractionSessions.getByDebate, { debateId } ) const existingMessages = session?.messages ?? [] // Build new user message from chunks const newUserMessage = chunks .map((c) => `[${speakerNames[c.speaker]}]: ${c.text}`) .join("\n") const messages = [ ...existingMessages, { role: "user", content: newUserMessage }, ] // Stream claims from Gemini await streamClaims(apiKey, systemPrompt, messages, async (claim) => { // Save each claim as it's parsed await ctx.runMutation(internal.claims.saveClaim, { debateId, speaker: claim.speaker, claimText: claim.claimText, originalTranscriptExcerpt: claim.originalTranscriptExcerpt, }) }) // Persist updated conversation history await ctx.runMutation(internal.extractionSessions.upsert, { debateId, messages: [...messages, { role: "model", content: result }], }) },})
JSONL streaming (convex/claimExtraction.ts:30-75):Claims are parsed line-by-line from Gemini’s response, so they appear in the UI incrementally:
for await (const chunk of stream) { buffer += chunk.text // Process complete lines while ((newlineIdx = buffer.indexOf("\n")) !== -1) { const line = buffer.slice(0, newlineIdx).trim() buffer = buffer.slice(newlineIdx + 1) const claim = parseClaim(line) // Parse JSON from line if (claim) await onClaim(claim) // Save to database immediately }}
Every time a claim is saved with pending status, Convex triggers a scheduled action to fact-check it with Perplexity.Fact-check flow (convex/factCheck.ts:93-131):
Convex powers the UI with reactive subscriptions. When a claim’s status changes in the database, the React component re-renders automatically.Query subscriptions (src/app/debates/new/page.tsx:20-23):
When claims updates (e.g., a claim goes from pending → true), React automatically re-renders the ClaimsSidebar component with the new data. No polling required.
Decoupling extraction from fact-checking prevents slow Perplexity calls from blocking Gemini. If a claim takes 10 seconds to verify, it shouldn’t delay extraction of the next utterance.Convex’s scheduler runs fact-checks in parallel, so multiple claims are verified simultaneously.
Transcription latency: ~500ms from speech to transcript appearing in UI (Deepgram’s nova-3 model)Claim extraction latency: 1-3 seconds after utterance boundary (depends on Gemini response time and conversation length)Fact-check latency: 3-10 seconds per claim (Perplexity searches the web and evaluates sources)UI update latency: 100ms from database write to React re-render (Convex reactive subscriptions)
Total time from spoken word to verified claim: 5-15 seconds depending on claim complexity and API response times.