Case Study · Voice AI Lab

Building a voice agent platform, end to end

Voice AI Lab is a working voice agent that runs entirely in the browser: you speak, it thinks, and it answers out loud in under a second. Under the hood that is a full three-stage voice pipeline — speech recognition (ASR) to hear you, a language model as the agent’s brain, and speech synthesis (TTS) to answer — wired together in real time. The agent is the focus, and around that loop I built the scaffolding a real deployment needs — agent management with prompt versioning, document-grounded answers, per-turn latency telemetry, and an evaluation framework that scores every agent response. Built solo, as a proof of concept, on free-tier APIs.

Cartesia ASR + TTSGeminiSupabase + pgvectorNext.jsWeb Audio API

The Core Decision

visibility over hops

The industry is moving toward realtime voice-to-voice models — audio in, audio out, one opaque stream. They are fast and natural, but they collapse the layers: there is no transcript to inspect mid-flight, no text layer to attach rules or tool logic to, and no way to intercept a bad answer before it is spoken. Evaluating them means simulating actual audio conversations, which is expensive and slow.

I deliberately went the other way: a three-stage cascading pipeline — speech-to-text, a language model, text-to-speech — connected by streams. At every stage there is something I can read, measure, guard, and test. Tool calls happen in text. Latency is measured per stage. Every answer is evaluable after the fact.

The cost of that choice is latency: three hops instead of one. Most of the engineering in this project is about buying that latency back.

01

Listen

Streaming speech-to-text (Cartesia ink) with server-side turn detection — the service decides when you have finished speaking.

02

Think

Gemini streams its reply token by token, optionally grounded in documents you have uploaded to the agent.

03

Speak

Cartesia sonic turns each finished sentence into audio over a persistent WebSocket, scheduled gaplessly in the browser.

Key Decisions

and what influenced them

Latency: engineered back, not accepted

A voice agent lives or dies by one number: the pause between when you stop talking and when it starts. Three choices keep that pause under a second. The text-to-speech connection stays open for the whole session, because reconnecting on every turn costs 150 to 300 milliseconds of audible silence. The agent starts speaking after its first complete sentence instead of waiting for the full reply — a small buffer between the model and the speech engine flushes only on sentence boundaries, because feeding half-sentences to a speech engine breaks the intonation and sounds robotic. And audio chunks are scheduled back-to-back on the browser’s audio clock, so sentences join without gaps.

Every turn logs four timings — speech to transcript, transcript to first model token, first sentence to first audio, and end to end — into Postgres, with daily median and 95th-percentile views. A latency regression shows up as a number, not a feeling.

Interruptions that feel human, not clipped

The hardest moment in a spoken conversation is the interruption — when you start talking before the agent has finished. Cutting its audio dead mid-word sounds like a glitch, so instead the agent fades to silence over a fraction of a second, the way a person trails off when you jump in. The stop reads as intentional rather than broken.

And rather than snapping straight into the new answer after that abrupt stop, it does what people do when they are cut off — a brief “okay” or “sure” in its own voice while the real reply is still forming behind it. That acknowledgment is synthesized once per session and slotted seamlessly in front of the first sentence, sharing the same gapless audio clock, so there is no pause and no overlap. This only happens on an interruption — on an ordinary turn a filler would sound odd. Neither touch changes when the agent stops or starts; they only change how those transitions sound, which is most of what makes a voice feel human.

Free-tier quotas as a design constraint

The whole proof of concept runs on free API tiers, so hitting a quota is a when, not an if. Eight Gemini models sit in a priority chain tracked in the database. A single atomic Postgres function picks the best available model and increments its usage counter in one step, so two simultaneous requests can never double-spend the same quota. When a model runs out for the day, the system slides to the next one instead of failing.

The influence here was simple: a demo that dies mid-conversation is worse than one that quietly gets a little slower.

Two languages, two listening strategies

English uses the speech vendor’s server-side turn detection. Hindi isn’t supported by that mode, so the browser runs a small voice-activity model locally (Silero VAD compiled to WebAssembly) to detect when you’ve spoken, then sends each speech segment for transcription. Both paths sit behind one shared interface, so the rest of the app never knows which is active.

The decision driver: build to what each service actually supports, instead of forcing one elegant path that only works for one language.

Tool calls without a tool API

The chat pipeline streams plain text — there is no native function-calling. So “tools” are sentinel tags like [END_CALL] and [TRANSFER_TO_HUMAN] that the agent is prompted to emit. The client strips them before they are ever spoken or shown, and acts on them: the call ends only after the goodbye has finished playing, and a tag that arrives split across two streamed tokens is held back until it completes.

This is exactly the kind of behaviour that is nearly impossible to test in a speech-to-speech model. Here it lives in text, so the evals can check it.

Answers grounded in documents

Agents can answer from uploaded documents. Retrieval runs two searches in parallel — semantic similarity over vector embeddings, and classic keyword search — then fuses the two rankings, because each catches what the other misses: keyword search nails exact names and codes, semantic search handles paraphrases. A lightweight model re-ranks the shortlist before it reaches the agent, and if that re-ranking step fails, the system degrades to the fused order rather than erroring.

Evaluating the Agent Like a Product

golden tasks · judges · guardrails

Shipping a voice agent without an evaluation loop means every prompt change is a guess. The eval framework treats each agent like a product with acceptance criteria:

Golden task suites

Fixed question-and-expected-answer pairs per agent, re-run after every prompt change. They are regression tests for behaviour: if an edit breaks something that used to work, a suite catches it.

Judge where needed, rules where possible

A model acting as judge scores correctness (35%), tone (20%), and instruction-following (20%). Speech-suitability (15%) and conciseness (10%) are scored by plain rules — no model call where a regex will do.

Suites define their own pass bar

Each suite sets its tone guidance, instruction rules, and thresholds — by default an answer passes at 70 overall with no dimension below 50. The floor exists so a weighted average can never hide one catastrophic dimension behind four good ones.

Guardrails and failure labels

Every answer is checked for leaked personal data, refusals, empty responses, and formatting that would sound wrong read aloud. Failures are clustered by label — wrong answer, hallucination, refusal, leaked info — so I learn why things fail, not just that they fail.

Prompt versioning

Every system-prompt edit becomes a new version, and every conversation records which version was live. A regression traces back to an exact change.

By the Numbers

e2e

<1s

Time to First Audio

End of speech → agent starts talking, on typical hardware

ms

150–300

Reconnect Cost Avoided

One persistent TTS WebSocket per session instead of per turn

models

8

Fallback Chain

Atomic quota rotation across Gemini models in Postgres

dims

5

Eval Dimensions

Weighted LLM-judge + rule-based scores per answer

per turn

4

Latency Metrics

Per-stage timings with daily p50/p95 SQL views

voices

2×5

Languages × Voices

English and Hindi, five curated voices each

What This Isn’t (Yet)

The line between a demo and production

Auth is a name gate, not real authentication, and the database policies are wide open — fine for a lab, not for users. The evals score text, not audio: a full-fidelity voice evaluation eventually needs audio-in, audio-out simulation with varied accents, interruptions, and background noise. And the guardrails run at eval time, not live before the agent speaks.

Knowing exactly where that line sits — and saying so — is half the job.

Hear it for yourself

The lab is live — start a conversation with an agent.