Full code on GitHub
A FastAPI server, a browser page with a live transcript and event log, and a terminal probe that streams a sample caller through it.
What you’ll build
- A Python server that holds one Dialog-RSN-1 connection per browser tab and forwards the caller’s audio to it.
- Streaming speech: Cartesia speaks each reply from its first few words, while Dialog-RSN-1 is still writing it.
- Barge-in: talk over the agent and it stops as soon as Dialog-RSN-1 confirms it’s you, and the model learns which words you heard.
- A browser page with a microphone picker, voice and turn-taking settings, a live transcript with per-turn latency, and a raw event log.
Why this shape
A typical browser voice agent chains three services on its server:- A speech-to-text service transcribes the caller and sends an end-of-turn event when it thinks they’ve finished.
- The server appends the transcript to its own
messageslist and calls an LLM. - It sends the reply to a TTS service and forwards the audio to the page.
Prerequisites
- Python 3.12 and uv.
- A Dialog-RSN-1 API key. Sign up or log in at studio.poly.ai and create one in the API Keys tab. The quickstart walks through it.
- A Cartesia API key. The free tier works.
- Chrome, Edge or Safari, and headphones if you’ll use the microphone.
Run it
http://127.0.0.1:3000. Click Start conversation and talk. Or click Play the sample caller: a recorded caller asks about a trip to Lisbon, then talks over the answer, so you see a barge-in without a microphone.
To check the whole pipeline from a terminal, leave the server running and stream the same sample through it. The probe counts from audio_end_ms, where Dialog-RSN-1 marks the end of speech, about half a second after the caller’s last word:
Step 1: Open one socket and configure the session
Each browser tab gets its ownCall, which opens a Dialog-RSN-1 WebSocket with the key in a header. The browser never sees the key.
config.DIALOG_REALTIME_URL defaults to wss://api.studio.poly.ai/v1/realtime. Set DIALOG_REALTIME_URL in .env to point somewhere else.
Then it sends one session.update:
output_modalities: ["text"]. This agent speaks through its own Cartesia stream, so it asks Dialog-RSN-1 for text. Sessions start with["audio"], where the server speaks the reply itself, so set this or the caller hears two voices.rate: 24000. The page records at 24 kHz, and Cartesia returns 24 kHz, so the page never resamples.create_response: true. When Dialog-RSN-1 confirms the caller has finished, it starts the reply itself. You don’t sendresponse.create.
session.updated before it sends any audio, so Dialog-RSN-1 reads every frame at the right rate.
Step 2: Forward the caller’s audio
The page sends the microphone as binary PCM16, 100 ms per frame. The server wraps each frame ininput_audio_buffer.append and passes it on:
messages and starts an LLM call. Here, the next thing your server hears about is the reply.
Step 3: Stream the reply into speech
Dialog-RSN-1 sends the reply asresponse.output_text.delta events. Each delta goes straight to Cartesia:
cartesia_tts.py keeps one Cartesia WebSocket open for the whole call. Each response gets its own Cartesia context, named after the response_id. Text goes in with continue: true, so Cartesia keeps the rhythm of one sentence across many small pieces. Two details matter:
- Send whole words. A delta can end mid-word.
push()holds the text after the last space until the word is complete. - Lower
max_buffer_delay_ms. Cartesia waits up to this long for more text before it speaks. Its default, 3 seconds, favours prosody. The demo sets 250 ms. Dialog-RSN-1 streams text faster than that in our runs, so the limit costs little.
Step 4: Stop talking when the caller does
When the caller starts speaking, Dialog-RSN-1 sendsinput_audio_buffer.speech_started. It has already checked that the sound is the caller and not noise, so the server acts on it at once:
Step 5: Tell Dialog-RSN-1 what the caller heard
Only the page knows what reached the speakers. Its player worklet counts the samples it has played and reports every 250 ms. The server turns those counts into Dialog-RSN-1 playback reports:spoken_text comes from Cartesia’s word timestamps: every word whose audio had started by played_ms.
These reports do two things:
- They make the barge-in check follow the real speaker. Dialog-RSN-1 checks that an interruption comes from the caller for as long as it thinks the reply is audible. Without reports, it guesses that window from a typical speaking rate of 15 characters a second, counted from the reply’s first words. With them, the window lasts exactly as long as your speaker plays, so a cough or an “mm-hm” three seconds into a slow reply still gets the check.
- They record where the caller cut in. Dialog-RSN-1 cuts the reply in its history back to
spoken_text, so the model knows what the caller missed. In the sample, the agent had written its whole reply, but the caller heard only up to “The Alfama neighborhood”. The page strikes through the words the caller never heard.
Tune turn-taking in plain language
A chained pipeline tunes the end of the turn with numbers, such as a confidence threshold and a timeout. Dialog-RSN-1 has no timing knobs. The model reads each pause in context and gives a verdict: still going, or finished. The realtime schema’ssilence_duration_ms and threshold are accepted and ignored, and idle_timeout_ms is rejected with unsupported_parameter.
You can tell it about your callers instead. The page’s Turn-taking menu picks one extra line for the instructions:
What the latency looks like
The page and the probe time every turn fromaudio_end_ms on speech_stopped, the point in the caller’s audio where Dialog-RSN-1 places the end of speech. Over twenty-two turns against us-1 production, on three days:
The
audio_end_ms mark sits about 500 ms after the caller’s last word, so the caller waits about half a second longer than the table shows. First audio reaches them about 0.9 to 1.9 s after they stop talking. It’s first text plus Cartesia’s own time to first audio, which ran about 130 to 430 ms.
Troubleshooting
Where to go next
- The streaming text-to-speech guide covers the same streaming and barge-in pattern with ElevenLabs, in a terminal.
- The prompt guide covers turn-taking instructions in depth.
- The API reference lists every event this demo uses, including the
poly.output_audio.*playback reports.

