Realtime transcription

Realtime transcription turns live audio, such as a microphone, a phone line, or a broadcast, into text while the person is still speaking. This page explains how a session runs from start to finish, how to configure it, how to read its events, and how to trade speed for accuracy.

i
Information
Understand the lifecycle

Understand the lifecycle

A session is one WebSocket connection that goes through four stages, always in the same order. Your application sends the audio, and the API sends the text back while the audio still flows.

Session lifecycle

Read from top to bottom: each arrow is one message, from your application on the left or from the API on the right.

Realtime session lifecycle: connect, then session.created; session.update, then session.updated; input_audio.append and transcription.text.delta, repeated while audio flows; then input_audio.end and transcription.done with the full transcript.

At each stage, your application does the following:

StageYou sendYou receiveWhat your application does
OpenA connection with the model and your API keysession.created, with the default settingsWait for it before you send anything else.
Configuresession.update, with your settingssession.updated, with the settings the session appliesCheck the values. They're fixed from the first audio chunk.
Streaminput_audio.append, one chunk of audio at a timetranscription.text.delta, with each piece of new textAdd each piece of text to the end of the transcript you show.
Finishinput_audio.end, when the audio stopstranscription.done, with the full transcriptSave the full transcript, then close the connection.

If the API rejects a message, it sends an error event at that stage instead. See Common issues.

Connect to a session

Connect to a session

Open a WebSocket connection to the realtime endpoint, with the model in the query string and your API key in the Authorization header:

wss://api.mistral.ai/v1/audio/transcriptions/realtime?model=voxtral-mini-transcribe-realtime-3
Authorization: Bearer $MISTRAL_API_KEY

With the Python or TypeScript SDK, connect() opens the connection and returns once the session is created:

import asyncio
import os

from mistralai.client import Mistral

async def main():
    client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
    async with await client.audio.realtime.connect(
        model="voxtral-mini-transcribe-realtime-3",
    ) as connection:
        print(connection.session.model_dump_json(indent=2))

asyncio.run(main())
Configure the session

Configure the session

Send your settings in a session.update message, before the first audio chunk. The session answers with session.updated, which echoes every setting it applies:

{
  "type": "session.update",
  "session": {
    "audio_format": { "encoding": "pcm_s16le", "sample_rate": 24000 },
    "target_streaming_delay_ms": 480,
    "language": "en",
    "context_bias": ["Northwind", "Quillara", "Alder Creek Design", "Rotterdam"]
  }
}

The following script sends these settings, streams a WAV file in chunks of 100 ms, and prints the transcript as it arrives.

Note

The Voice Agents SDK sends language, context_bias, and turn_detection through the stt config. For raw WebSocket sessions without the SDK, send session.update with websockets in Python and ws in TypeScript.

import asyncio
import base64
import json
import os
import wave

import websockets

MODEL = "voxtral-mini-transcribe-realtime-3"
URL = f"wss://api.mistral.ai/v1/audio/transcriptions/realtime?model={MODEL}"
HEADERS = {"Authorization": f"Bearer {os.environ['MISTRAL_API_KEY']}"}
AUDIO_FILE = "customer_call.wav"  # 16-bit mono PCM WAV

async def send_audio(ws, path):
    with wave.open(path, "rb") as wav:
        while chunk := wav.readframes(wav.getframerate() // 10):
            await ws.send(json.dumps({"type": "input_audio.append", "audio": base64.b64encode(chunk).decode()}))
    await ws.send(json.dumps({"type": "input_audio.end"}))

async def main():
    with wave.open(AUDIO_FILE, "rb") as wav:
        sample_rate = wav.getframerate()

    async with websockets.connect(URL, additional_headers=HEADERS) as ws:
        print(json.loads(await ws.recv())["type"])  # session.created

        await ws.send(json.dumps({
            "type": "session.update",
            "session": {
                "audio_format": {"encoding": "pcm_s16le", "sample_rate": sample_rate},
                "target_streaming_delay_ms": 480,
                "language": "en",
                "context_bias": ["Northwind", "Quillara", "Alder Creek Design", "Rotterdam"],
            },
        }))
        sender = asyncio.create_task(send_audio(ws, AUDIO_FILE))

        async for message in ws:
            event = json.loads(message)
            if event["type"] == "session.updated":
                print("session.updated:", json.dumps(event["session"]))
            elif event["type"] == "transcription.text.delta":
                print(event["text"], end="", flush=True)
            elif event["type"] == "transcription.done":
                print("\ntranscription.done")
                break
            elif event["type"] == "error":
                print("error:", event["error"]["message"])
                break
        sender.cancel()

asyncio.run(main())
Caution

The settings are fixed once audio has started. A session.update sent after the first input_audio.append returns the error Session fields cannot be changed after audio has started. To change a setting, open a new session.

Session fields

Session fields

session.update takes the following fields. Send only the ones you want to change: the others keep the default that session.created reports.

FieldTypeDefaultUse it to
audio_formatObject: encoding, sample_ratepcm_s16le at 16000 HzDescribe the audio you send: 16-bit signed little-endian PCM, mono, at the sample rate of your source. The format is inferred automatically from the PCM payload if you don't set it.
target_streaming_delay_msInteger480 ms (recommended starting point)Trade speed for accuracy: any value from 80 to 1200, or 2400. See Control latency.
languageString, a two-letter ISO 639-1 code such as "en"Not setPin the language of the audio. Leave it out and the model detects the language. See Languages.

| context_bias | Array of strings | [] | Help the model spell names and terms. See Bias context and vocabulary. | | diarize | Boolean | false | Label each piece of text with its speaker. See Diarize speakers. | | speaker_references | Object: a name for each base64 audio clip | Not set | Name known speakers, up to 8 clips of 3 to 15 seconds. Requires diarize. See Diarize speakers. | | turn_detection | Object | Not set | Know when the user starts and finishes speaking. See Turn detection. |

Caution

diarize and turn_detection can't be used in the same session: the session returns the error Diarization is not compatible with turn detection.

Events: receive updates and finish

Events: receive updates and finish

Everything the API sends arrives as a JSON message with a type. Read the type first, then the fields of that event:

EventFieldsSent when
session.createdsessionThe connection opens, with the default settings.
session.updatedsessionAfter each session.update, with the settings the session applies.
transcription.text.deltatextNew text is ready, as the audio flows.
transcription.segmenttext, speaker_id, start, endWith diarize on, instead of transcription.text.delta. See Diarize speakers.
turn.start, turn.eager_end, turn.resume, turn.endtime_s, and end_reason on turn.endWith turn_detection on, at the moments of each turn. See Turn detection.
transcription.donetext, segments, language, model, usageAfter input_audio.end, once all the audio is transcribed.
errorerror.message, error.codeThe API rejects a message or a setting.
Show the text as it arrives

Show the text as it arrives

Each transcription.text.delta adds new text after the text already sent. A delta never corrects earlier text, so your application only appends: there's nothing to replace.

  • Show each delta as soon as it arrives. It's final when you receive it.
  • Append it as is. Deltas carry their own spaces and punctuation, such as " for" or ".", so don't add separators.
  • Save the text of transcription.done as the transcript of record.
Tip

To show fast text first and replace it with more accurate text later, you need two sessions on the same audio. See Variable transcription rate.

Finish the session

Finish the session

When the audio stops, send input_audio.end. The API sends the last deltas, then transcription.done with the full transcript. Close the connection once you've received it.

Control latency

Control latency

The model doesn't write a word as soon as it hears it: it waits to hear what follows, because the next sounds help it pick the right word. target_streaming_delay_ms sets that wait for the session.

  • A shorter delay gets the text out sooner, but less accurately.
  • A longer delay gives the model more context, and more accurate text, but later.

The delay takes any integer from 80 to 1200, or 2400. Any other value returns an error. The Python SDK can set it with update_session(), along with the audio format.

i
Information

For where the delay fits in the wait your users notice, see Latency. Turn events don't wait for the delay: see Turn detection.

Variable transcription rate

Variable transcription rate

Readers see words at once, and the text they keep is the accurate one. Live captions and note-taking benefit most.

Common issues

Common issues

Most issues come back as an error event with a message. These are the ones specific to live sessions:

Error or symptomCauseFix
Session fields cannot be changed after audio has started.A session.update sent after the first audio chunkSend every setting before the first input_audio.append. To change one, open a new session.
target_streaming_delay_ms must be one of the trained valuesA delay outside 80 to 1200, other than 2400Pick a value in the accepted range. See Control latency.
Diarization is not compatible with turn detection.diarize and turn_detection in the same sessionKeep one of them. See Turn detection.
Speaker references require diarization, set diarize=true.speaker_references without diarizeSet diarize to true.
An error that mentions Invalid language alpha2 codeA language that isn't a two-letter ISO 639-1 codeUse a code such as "en" or "fr".
Latency measured on a file doesn't match live useThe file is sent faster than real timeTo measure latency, send each chunk at the pace of the audio, for example 100 ms of audio every 100 ms.

For other errors, see Troubleshooting.

Going further

Going further