What you’ll build
A browser page that captures microphone audio, streams it to Dialog-RSN-1 over a WebSocket, and renders the reply as text while it’s still arriving. You’ll usegetUserMedia for the microphone, AudioWorklet to reach the raw samples, and a WebSocket for the wire protocol. There’s no SDK to install and no build step.
Text replies only. This guide shows the reply on the page, so it asks for text with
output_modalities: ["text"] and stops there. Set the field explicitly: a session that leaves it out speaks its replies, and a page listening for text deltas then shows nothing. For a spoken reply, leave the default and play the server’s audio, or feed the accumulated text to a text-to-speech provider of your own.Connecting from the browser
Your API key from studio.poly.ai is a bearer credential, and a browser tab is a poor place to keep one: a visitor can open dev tools and read it out of your JavaScript. Don’t connect from the browser towss://api.studio.poly.ai/v1/realtime with the key embedded in client code.
Put a small relay in front of it: a same-origin WebSocket endpoint on your own server that holds the real key, opens the upstream connection to Dialog-RSN-1, and pipes frames through in both directions. The browser talks to your relay and nothing else.
connect
relay.js, that also serves the page. If you write your own, set a User-Agent on the upstream connection. The API refuses a handshake without one with a bare 403, and Node’s ws package sends none by default.
The code below assumes that relay exists and connects to it:
connect.js
Capturing microphone audio
The browser gives you microphone audio as floating-point samples; Dialog-RSN-1 wants base64-encoded 16-bit PCM. Do that conversion in anAudioWorkletProcessor, which runs off the main thread, so encoding audio doesn’t compete with rendering the page.
pcm-worklet.js
capture.js
AudioContext sample rate from the hardware, 48 kHz on most machines, where Dialog-RSN-1’s native rate is 16 kHz. Rather than resample in the client, declare your true rate with the poly_input_rate extension field on session.update and let the server resample on the way in:
session.update
AudioContext reports in audioContext.sampleRate. Hardcoding 48000 is wrong on hardware that runs at 44.1 kHz: the server then resamples from a rate your audio was never at, and hears it about 9% fast. You can read the rate before the user clicks anything. A context created on page load starts suspended, but its sampleRate is already set. Call audioContext.resume() first thing in the click handler, before you wait on the microphone: Safari only lets a context start during the click itself. The full example below does this.
Streaming audio and handling server VAD
Once you’ve wired up capture,input_audio_buffer.append frames flow without a break. The server decides when a turn starts and ends. Voice activity detection proposes a boundary and Dialog-RSN-1 judges it. speech_stopped always waits for Dialog-RSN-1’s judgment that the caller has finished. speech_started arrives as soon as the detector hears sound, unless a reply is playing, when Dialog-RSN-1 confirms the caller is talking first. A sound it then judges to be noise is dropped, so a speech_started can arrive with no speech_stopped after it. The server tells you with these events:
vad.js
response.done with status: "cancelled" that response.cancel produces, with status_details.reason set to "turn_detected" rather than "client_cancelled".
Reconstructing the streamed reply
The reply arrives as a burst of small events instead of one block of text. Accumulateresponse.output_text.delta by the item_id it belongs to, and treat response.done as the signal the turn is over:
reply.js
item_id keeps each reply separate. Only one response runs at a time and each carries one text item, so deltas never interleave, but a new reply’s deltas start a new entry instead of appending to the last one.
Keep the error branch. Every failure on this API arrives as an error event and leaves the connection open, as the quickstart explains. Without the branch, a rejected session.update looks like a page that connects and never answers.
Voice output is out of scope here. With
output_modalities: ["text"] the server sends no audio events. For a spoken reply, either drop the output_modalities line and play response.output_audio.delta (base64 PCM16 at 24 kHz) through Web Audio, or hand the accumulated text from renderFinal to a text-to-speech provider of your own.Full example
Three files: the page, its script, and thepcm-worklet.js from Capturing microphone audio saved next to them.
Full code on GitHub
These files plus the relay, ready to clone and run with
npm start.index.html
app.js
Next steps
Overview
Realtime streaming explained, plus how turn-taking, generation, and prompting fit together.
Dialog-RSN-1 reference
Every event, the session object, function calling, and error codes.
Python from scratch
The same protocol, built server-side with plain
websockets.Streaming to TTS
Where the accumulated text from
renderFinal actually goes: barge-in, unspeakable text, and playback reports.
