Add turn detection to a voice agent with Ink-2
Stream speech to Cartesia's Ink-2 in Python and use its turn events to know when a caller has stopped talking, in 20 minutes.

What you need
Ink-2 understands English, French, Hindi, Japanese and Spanish, and detects the language on its own.
Step 1: Create an API key
2 minYour server code signs every Ink-2 request with this key.
- Go to play.cartesia.ai/keys, sign in and create a key.
- Copy it and export it in your terminal, as below.
Run thisexport CARTESIA_API_KEY=[YOUR API KEY] echo $CARTESIA_API_KEY | cut -c1-7
You should see
sk_car_, the start of a standard key.Requests fail with an auth error later?
Use a standard key that starts with
sk_car_. Admin keys, which start withsk_car_admin_, are rejected for speech-to-text.Keep the key in your shell profile or a git-ignored
.envfile. Never put it in shared code or in an app that runs in a browser.Step 2: Install the Python SDK
2 minThe SDK opens the WebSocket for you and hands you each turn event as an object.
- Run the command below. The
[websockets]extra adds the realtime client.
Run thispython3 -m pip install -U "cartesia[websockets]"
You should see
Successfully installed, withcartesiain the list.Python says the websockets module is missing?
You installed plain
cartesia. Run the command again with the[websockets]extra, inside the quotes.- Run the command below. The
Step 3: Prepare a test recording
3 minRealtime Ink-2 takes raw mono samples, and you tell it the encoding and the sample rate.
- Record 10 to 20 seconds of speech. Stop mid-sentence once, then carry on.
- Convert it with the command below. It makes a mono, 16 kHz, 16-bit file.
Run thisffmpeg -i [YOUR RECORDING] -ac 1 -ar 16000 -c:a pcm_s16le test.wav
You should see a new
test.wavfile next to your recording.The transcript later comes out as nonsense?
The encoding or sample rate doesn't match the audio, and Ink-2 may not raise an error. Convert the file again with the command above.
Step 4: Stream it and print turn events
5 minInk-2 reports each turn as it happens, so your agent never needs its own voice activity detector.
- Save the script below as
turns.py. - Run
python3 turns.py test.wav. - Read the events in order:
turn.start, growingturn.updatelines, thenturn.end.
Your scriptimport os, sys, time, wave from cartesia import Cartesia client = Cartesia(api_key=os.getenv("CARTESIA_API_KEY")) with wave.open(sys.argv[1], "rb") as wf: rate = wf.getframerate() chunks = [] while True: data = wf.readframes(rate // 10) # 100 ms of audio if not data: break chunks.append(data) with client.stt.auto_finalize.websocket( model="ink-2", encoding="pcm_s16le", sample_rate=rate, ) as ws: for chunk in chunks: ws.send_raw(chunk) time.sleep(0.1) # send at real-time speed ws.send({"type": "close"}) # flush the last audio for event in ws: if event.type == "error": print("error:", event.message) else: print(f"{event.type:15} {getattr(event, 'transcript', '')}")You should see a
turn.endline with the full text of each turn you spoke.Words repeat in your saved transcript?
Each event carries the whole turn so far. Keep only the
turn.endtranscripts, and join them with a space.Send audio in chunks of about 100 ms, at real-time speed. Pushing a whole file at once can overload the server and return an error.
- Save the script below as
Step 5: Start replies early on eager end
4 minturn.eager_endfires when the caller might be done, so your LLM can start before the turn closes.- In
turns.py, replace the lastfor event in ws:loop with the one below. - Put your LLM call where the comment says, and cancel it on
turn.resume. - Run the script again on your recording with the pause in it.
Your loopdraft = None for event in ws: if event.type == "turn.start": print("caller is talking: stop any reply audio") elif event.type == "turn.eager_end": draft = event.transcript # start your LLM call here print("draft reply for:", draft) elif event.type == "turn.resume": draft = None # cancel the early LLM call print("caller kept talking: drop the draft") elif event.type == "turn.end": print("final:", event.transcript, "| use draft:", draft is not None) draft = NoneYou should see
draft reply for:lines, anddrop the draftwhere you paused mid-sentence.Your agent answers half a sentence?
Never speak on
turn.eager_end. Speak only onturn.end, and drop the draft ifturn.resumearrives first.- In
Step 6: Tune it for your callers
3 minThe defaults suit most agents, but fast chats and slow thinkers need different timing.
- Change the
websocket(...)call to the one below. These are Cartesia's Responsive settings. - Add your brand and product names to
keyterm, so Ink-2 spells them right.
Your settingswith client.stt.auto_finalize.websocket( model="ink-2", encoding="pcm_s16le", sample_rate=rate, turn_start_threshold=0.7, turn_eager_end_threshold=0.6, turn_end_threshold=0.4, turn_end_timeout_ms=4500, keyterm=["[YOUR BRAND]", "[A PRODUCT NAME]"], ) as ws:You should see
turn.endarriving sooner after short pauses, and your terms spelled the way you wrote them.It cuts people off while they think?
Use the Patient settings instead:
turn_start_threshold=0.8,turn_eager_end_threshold=0.3,turn_end_threshold=0.1andturn_end_timeout_ms=8000.Keyterms are set when the connection opens. You can pass up to 100, with 1,200 characters in all.
- Change the
You get
A Python script that streams speech to Ink-2 and tells your agent when a caller starts, might be done, carries on or finishes
Try next
Did this guide work for you?
Thanks. It helps us fix the steps.
Sources
0 of 6Next: Create an API key


