-
-
The antechamber: explainer video plus a launch button that arms once the room is warm.
-
Three rooms, one mechanism: courtroom cross-exams (EN/ES), B2B discovery calls (EN/DE), defenses, and your own uploaded case.
-
Session start: the committee speaks first. Pressure dial from cooperative to hostile, a whisper coach on standby, rubric at zero.
-
Live cross-examination of an uploaded thesis. The rubric sidebar scores every exchange with cited sources while you speak.
-
The debrief: an AMTA-scale score, two cited moments, your next rep, a downloadable transcript, and the session's real token cost.
Inspiration
Roughly 1 percent of U.S. federal civil cases reach trial, a number from published federal judiciary statistics that legal scholars have called the vanishing trial. The result is an entire generation of litigators for whom trial is mostly a theoretical concept. In-person trial-skills programs run about $2,695 for four days, partners have no time to spar, and so junior associates rehearse their first real cross-examination alone, at night, against nobody.
Associates don't need another lecture. They need reps.
What it does
The Stand is a live-voice sparring room. Pick a fictional case file, click "Take the stand.", and cross-examine a witness who answers in real time with native audio, evades in character, and escalates when you turn up the pressure dial. Interrupt him mid-sentence and he stops and comes back at you, audibly irritated. Nothing is pre-rendered.
While you argue, a rubric sidebar scores every exchange within seconds. Each score line cites a published source: FRE 611(c) for a leading question, an AMTA ballot line for witness control. The scorer structurally cannot invent a citation, because it can only choose from the source ids in the active case's rubric. When it is unsure, it says "unscored: ambiguous" instead of guessing. Ending the session produces a debrief: an AMTA-scale score, the two moments worth replaying with their citations, one practice focus, and the session's real token cost.
The room is a collaborative partner, not just an examiner:
- Whisper mode (opt-in): when you're stuck, it quietly suggests your next question, like counsel leaning over at your table.
- Shift the pressure: type a topic mid-session and the witness steers there on the very next answer.
- Reverse mode: switch chairs. The AI runs the pitch and the sidebar names every technique it uses on you.
Six curated fictional cases ship across three rooms: the Courtroom (two civil cross-examinations plus a Spanish-speaking deposition witness, a real litigation scenario, not a translation feature), the Boardroom (B2B discovery calls in English and German), and Your Own Case (a dissertation defense with a two-register committee). Bring Your Own Case lets you upload a PDF in defense or sales mode, and the generated attack lines must each cite a real page or section of your document; a goal without a citation is dropped, an empty or unreadable file is refused before the model is ever called. Legal mode deliberately does not accept uploads: client confidentiality is a product decision, not an afterthought. Uploads are private to the browser that made them and deletable.
The Stand trains technique. It does not give legal advice. All case files, witnesses, and facts are fictional.
How we built it
Python 3.12 on google-adk==2.8.0, deployed on Google Cloud. Architecture diagram: docs/architecture.svg in the repo.
- WitnessAgent runs on
gemini-3.1-flash-live-preview(bidirectional native audio; barge-in comes from the Live API's voice activity detection). A per-case factory builds the agent from a YAML case file: persona, three escalation levels, rubric. The instruction is assembled dynamically from session state via an ADKInstructionProvider, and a deterministicbefore_model_callbackguard backstops character fidelity against prompt injection and legal-advice requests. - The pressure dial and refocus ride the open
LiveRequestQueueas[STAGE DIRECTION]content turns, because a Live connection receives its system instruction exactly once at connect time. Measured, not assumed. - RubricScorer and DebriefAgent are separate
gemini-3.7-flashstructured-output calls (the critic/judge pattern from adk-samples' llm-auditor), fired off the audio path so scoring latency never touches the conversation. The response schema only admits citation ids present in the case rubric. - The server is a custom FastAPI WebSocket app driving
Runner.run_live()directly, per adk-docs' documented custom bidi-streaming pattern, with a small JSON protocol (audio, transcript, score, dial, whisper, debrief). The frontend is vanilla JS, no framework, no build step. - Eval is the product, not an afterthought. An
AgentEvaluatorsuite grades judge agents that reuse the production system prompts, using ADK's rubric-based criteria. On top of that,eval/eval_sets/live_audio_witnessruns WitnessAgent itself over a Live/bidi connection against ADK 2.8.0'sLlmAudioUserSimulatorConfig, a TTS-simulated novice examiner, and the resulting eval run plays back real per-turn audio in adk web's Evals tab. The suite's last run printsOVERALL: PASSper criterion. - Deploy: Cloud Run (
--timeout=3600 --memory=1Gi, key in Secret Manager, min-instances=1 while judging is open), Firestore for sessions and uploaded cases, and a static Firebase Hosting antechamber that warms the service during the landing page's own read time so a juror never feels a cold start.
Challenges we ran into
- The Live transcription stream is not a clean delta feed. At turn end it re-sends the entire turn as one lightly revised chunk; a captured example differed from the 612-character accumulated turn by exactly one inserted word at 0.9976 similarity. Naive appends printed every turn twice, on screen and into the scorer. The fix took instrumentation, a four-case reconciliation function, and a
replaceflag in the client protocol, all pinned by regression tests on the captured chunks. - A dead Web Audio graph. Mic capture was wired through an unconnected gain node, so zero microphone bytes ever reached the model, and the Live API's 1008 idle closes were only the symptom. It surfaced in the first real human microphone test, was reproduced adversarially in a headless browser, and fixed with a zero-gain node into the real destination.
- The debrief that never arrived. Closing the
LiveRequestQueuedoes not terminate therun_live()generator, so the shutdowngatherwaited forever. Fixed by sequencing the shutdown: await upstream, close the queue, grace period, cancel. Debrief now measured at 5 to 8 seconds after "End session." on the deployed service. - A model that fills a vacuum. An empty upload produced a complete invented defense case, committee and all, presented under our citation rubric. That is the one failure a cite-or-refuse product cannot survive, so a substance gate now refuses thin or unreadable documents before the model is called, and only ever refuses on positive evidence.
What did not work
- Runtime model fallback for the Live witness: ADK Python 2.8.0 has no routing primitive for Live connections (
RoutedLlmis TypeScript-only and experimental), so the fallback is a documented one-line model swap instead of a fragile reconnect loop. adk evalcannot replay a bidi Live session throughAgentEvaluator, so the judges are evaluated through turn-based doubles that import the production prompts, and the Live path gets its own audio-simulator eval set.- A "watch a replayed session" entry point was built, then withdrawn from the product: the replay contains only the AI's half of a session, and a monologue misrepresents a sparring product. The recorded bundle stays in the repo as engineering evidence.
Accomplishments that we're proud of
- A live voice product that a human has verified end to end on the deployed URL: real microphone, real interruption, three reproducible sessions, debrief delivered.
- A scoring system that cannot invent its sources. The single rubric criterion for which no published line exists (composure under adversarial questioning, in the defense rubric) is labeled uncited in the case file rather than dressed up.
- One of the first products built on ADK 2.8's live evaluation stack as a core feature: a TTS user simulator cross-examining our witness over real audio, machine-checked, with playable evidence.
- Honest cost telemetry: real token counts from
usage_metadatain every debrief, with dollar estimates only where Google publishes token pricing.
What we learned
- Measure, don't assume. Twice this build, correct-looking code was innocent: the "audio keeps playing" bug was a layout overflow burying the stop button, proven by hit tests, and a missing log line turned out to be a log level, not a smoking gun.
- The Live API has a transcript contract you discover empirically. Instrumentation first, patches second.
- Honesty is an architecture decision: grounding gates, citation allowlists, and "unscored" states have to be built, not promised.
What's next
The room is a config file: persona, escalation ladder, and rubric in one YAML. Next: case packs for more professions (incident reviews, funding pitches, medical consultations), session history across visits, audio-clip debrief moments, and multi-voice committees.
Built With
- fastapi
- firebase-hosting
- gemini
- google-adk
- google-cloud-firestore
- google-cloud-run
- google-cloud-secret-manager
- javascript
- python
- web-audio-api
- websockets
- yaml

Log in or sign up for Devpost to join the conversation.