Before you begin
-
An API key. Copy it from API Keys and set it once:
-
The SDK. Skip this if you only want the
curlcommand. Text to speech is in the Python SDK from release 1.8.0 and in the JavaScript SDK from 4.42.0.- Python SDK
- JavaScript SDK
Generate your first audio
One request, one text, the whole audio back as a WAV file. The SDKs call the HTTP API, and the cURL tab shows the same request without an SDK. Save the program ashello.py or hello.mjs; the .mjs extension makes it an ES module, which the import and the top-level await need.
- Python SDK
- JavaScript SDK
- cURL
python hello.py or node hello.mjs, and hello.wav plays in any audio player. The SDKs print this:
- Only the text is required.
voice_idnames Audrey here by her ID. Leave it out and you getaai-jane, the default voice, in English at 24 kHz 16-bit PCM. Each voice speaks the language it was recorded in; Voices explains how to name one, find its ID and set the language, and the samevoice_idtakes a voice you cloned in the dashboard. - The response says what it ran with. The voice, model, language, encoding and sample rate come back as response headers, which the SDKs expose on the result. Read the sample rate from there rather than from your own request when you write a header or set up a player.
- The duration varies between runs. Synthesis is not deterministic, so the same sentence can come back a few hundred milliseconds longer or shorter. Billing counts characters, not seconds.
- The JavaScript SDK uses two spellings.
synthesize()andstream()take the API’s own field names,voice_idandsample_rate, whilesession()takes camelCase options,voiceIdandsampleRate. Results are camelCase, and theBeginandTerminationframes keep their wire names.
tts.eu.assemblyai.com instead. See Cloud endpoints and data residency.
Speak a conversation over one session
A voice agent speaks many short texts, one per turn, and needs the first audio of each as fast as possible. The WebSocket API is built for that: one connection stays open for the whole conversation, each sentence is a request on it, and the audio of a request streams back while the request is still being spoken. Opening a connection per sentence pays the handshake every time and quickly hits the per-account limit on new sessions per minute. The programs below open a session, speak a four-sentence reply through it and write the raw audio to a file. Save them asreply.py or reply.mjs.
- Python SDK
- JavaScript SDK
python reply.py or node reply.mjs, then play the file. It is raw PCM with no header, so tell the player its format: with SoX, with ffmpeg, or by converting it to a WAV file any player opens.
- The server speaks first. Connecting waits for the
Beginframe, which echoes the configuration the session actually got, with the sessionidto quote in support requests. A rejected key or parameter, an unknownvoice_idincluded, arrives as anErrorframe in its place: the Pythonwithand the JavaScriptconnect()raise it. See Error handling. - Each sentence is one request.
speak()sends the text inGenerateframes, concludes the request with aFlushand yields the audio frames until the request’sFlushDone. The iterator ends with the request, which is what lets the loop count bytes per sentence. See Message frames. - The audio has no header. A session streams raw codec bytes, 16-bit PCM here, so the file will not play if you hand it to an
<audio>element or aBlob. Write a WAV header yourself, or decode the samples and play them as they arrive. Chunk sizes vary and are not aligned to samples, so carry an odd trailing byte over to the next chunk. See Audio formats on WebSocket API basics. - Leaving the session ends it cleanly. Exiting the Python block and awaiting
close()in JavaScript sendTerminate, which speaks any text not yet flushed, and wait for theTerminationsummary: the seconds of audio and the characters of text that reached synthesis.
keep_alive() (keepAlive() in JavaScript) now and then. Keep the number of requests you have sent but not yet read bounded: the server accepts text faster than it speaks it, but only so far ahead. The SDKs refuse a text that would overrun that on its own, and a session that keeps sending far ahead of its speech is closed. See Limits.
Stream text in as an LLM writes it
Text that arrives a token at a time does not have to wait for the end of the sentence.generate() appends text to the current request, flush() concludes it and returns its flush_id, and audio() yields the request’s audio. The server starts speaking once it holds whole sentences, before the flush. cancel() is the barge-in: it stops the request in flight and drops anything queued.
The fragments below continue the programs above: the imports and config in Python, client in JavaScript. You supply the three functions they call: llm_tokens() yields the text as your LLM produces it, user_started_talking() says whether to interrupt, and play() takes a chunk of audio. An LLM client that streams asynchronously pairs with the asyncio twin, aai.AsyncTTSSession, whose methods are awaited.
- Python SDK
- JavaScript SDK
Good followed by morning. is spoken as Goodmorning., so keep the space at the edge of each piece. Requests can be pipelined, a flush() before the previous request’s audio has been read, and their audio comes back in order. See Generate and Cancel.
Using the WebSocket API directly
Not using an SDK? Connect towss://tts.us.assemblyai.com/v1/ws with any WebSocket library, with your API key in the Authorization header and the voice and audio options as query parameters, and handle the frames yourself. The program below needs websockets 14 or newer (pip install -U websockets). Saved as ws.py and run with python ws.py, it reads Begin, sends one Generate and a Flush, collects the Audio frames until FlushDone, then terminates and writes a WAV file.
Audio.audiois base64. EachAudioframe is JSON with the codec bytes base64-encoded, under theflush_idof the request it belongs to.flush_idstarts at0, eachFlushtakes the next id, and ids skip one after aCancel, so read them from the frames rather than counting.WordBoundariesis on by default on the wire. A frame with one entry per word, itsstartandendin milliseconds and aconfidence, follows eachFlushDone. Ignore any frame type you do not handle; the protocol may gain frames. The SDKs turn word boundaries off unless you ask for them. See Word boundaries.
Error frame, for example on a network drop, the async for loop raises websockets.exceptions.ConnectionClosedError. Error handling lists the close codes you can see either way, and the WebSocket API reference every query parameter.
Where to next
- Optimizing accuracy and latency: the session, text and format habits that get the first audio of each reply out sooner and read as you meant it.
- Choose an interface: the SDKs, the HTTP API, the WebSocket API and the OpenAI-compatible route, and which fits how your text arrives.
- Authenticate with a temporary token: open a session from a browser or an app without shipping your API key.
- Voices: AssemblyAI’s voices and the languages they speak.
- HTTP API basics: streamed and buffered responses, MP3 and telephony formats, errors and limits.
- LiveKit: wire Streaming TTS into a LiveKit voice agent.
- WebSocket API basics and Message frames: authentication, the connect sequence, and every frame in the protocol.
- Limits: text, session and rate limits, and how each one reports a breach.
Need help?
Contact support@assemblyai.com or open a support ticket. Include the sessionid from Begin or the x-request-id of an HTTP response.