Skip to content

LearnTutorials

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.

By Recaply EditorsAdvanced19 min6 stepsUpdated

A hand holds an old telephone handset as curved sound lines flow toward a small hourglass

What you need

Ink-2 understands English, French, Hindi, Japanese and Spanish, and detects the language on its own.

  1. Step 1: Create an API key

    2 min

    Your server code signs every Ink-2 request with this key.

    1. Go to play.cartesia.ai/keys, sign in and create a key.
    2. Copy it and export it in your terminal, as below.
    Run this
    export 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 with sk_car_admin_, are rejected for speech-to-text.

    Keep the key in your shell profile or a git-ignored .env file. Never put it in shared code or in an app that runs in a browser.

  2. Step 2: Install the Python SDK

    2 min

    The SDK opens the WebSocket for you and hands you each turn event as an object.

    1. Run the command below. The [websockets] extra adds the realtime client.
    Run this
    python3 -m pip install -U "cartesia[websockets]"

    You should see Successfully installed, with cartesia in the list.

    Python says the websockets module is missing?

    You installed plain cartesia. Run the command again with the [websockets] extra, inside the quotes.

  3. Step 3: Prepare a test recording

    3 min

    Realtime Ink-2 takes raw mono samples, and you tell it the encoding and the sample rate.

    1. Record 10 to 20 seconds of speech. Stop mid-sentence once, then carry on.
    2. Convert it with the command below. It makes a mono, 16 kHz, 16-bit file.
    Run this
    ffmpeg -i [YOUR RECORDING] -ac 1 -ar 16000 -c:a pcm_s16le test.wav

    You should see a new test.wav file 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.

  4. Step 4: Stream it and print turn events

    5 min

    Ink-2 reports each turn as it happens, so your agent never needs its own voice activity detector.

    1. Save the script below as turns.py.
    2. Run python3 turns.py test.wav.
    3. Read the events in order: turn.start, growing turn.update lines, then turn.end.
    Your script
    import 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.end line 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.end transcripts, 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.

  5. Step 5: Start replies early on eager end

    4 min

    turn.eager_end fires when the caller might be done, so your LLM can start before the turn closes.

    1. In turns.py, replace the last for event in ws: loop with the one below.
    2. Put your LLM call where the comment says, and cancel it on turn.resume.
    3. Run the script again on your recording with the pause in it.
    Your loop
        draft = 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 = None

    You should see draft reply for: lines, and drop the draft where you paused mid-sentence.

    Your agent answers half a sentence?

    Never speak on turn.eager_end. Speak only on turn.end, and drop the draft if turn.resume arrives first.

  6. Step 6: Tune it for your callers

    3 min

    The defaults suit most agents, but fast chats and slow thinkers need different timing.

    1. Change the websocket(...) call to the one below. These are Cartesia's Responsive settings.
    2. Add your brand and product names to keyterm, so Ink-2 spells them right.
    Your settings
    with 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.end arriving 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.1 and turn_end_timeout_ms=8000.

    Keyterms are set when the connection opens. You can pass up to 100, with 1,200 characters in all.

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

Did this guide work for you?

Sources