> ## Documentation Index
> Fetch the complete documentation index at: https://assemblyai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Voice cloning

> Clone a voice from a short recording in the AssemblyAI dashboard, speak with it from the SDKs or the API, and keep its consent current.

Voice cloning makes a voice from a short recording of one person speaking. You create and manage clones in the [voice catalog](https://www.assemblyai.com/dashboard/text-to-speech/voices) of the AssemblyAI dashboard, and the API speaks with them like any other voice: pass the voice's ID in `voice_id`. The frames, audio formats and limits are those of an [AssemblyAI voice](https://assemblyai.com/docs/tts/voices.md).

## Create a voice

In the voice catalog, choose **Create voice clone**, then record or upload a sample:

* **One person, speaking the voice's language.** Pick the language in the dialog and have the speaker read in it: a speaker reading English into a voice set to French is cloned on the wrong sounds.
* **Ten to thirty seconds of speech.** The dashboard uses up to thirty seconds of a recording under two minutes long, and shows when a recording is long enough. Read the script it offers, calm or excited, or your own text at a natural pace.
* **Consent.** You attest that the speaker consented to the clone, and your account must have accepted the biometric data terms on the dashboard's [data controls](https://www.assemblyai.com/dashboard/settings/data-controls) page.

The new voice gets an ID, a UUID, and a slug you can choose when you create it and not after. Either names the voice in `voice_id`; send a slug in lowercase. **Copy voice ID** on the voice copies the ID.

## Speak with it

Name the voice in `voice_id` and leave out `language`. The programs below read your API key and the voice's ID from the environment, open a session with the voice, print what `Begin` resolved and speak one sentence; the cURL tab does the same as one HTTP request. Set the two variables first:

```bash theme={null}
export ASSEMBLYAI_API_KEY=<your-key>
export ASSEMBLYAI_VOICE_ID=<your-voice-id>
```

**Python SDK**

```python theme={null}
import os

import assemblyai as aai

config = aai.TTSConfig(voice_id=os.environ["ASSEMBLYAI_VOICE_ID"])

with aai.TTSSession(config, api_key=os.environ["ASSEMBLYAI_API_KEY"]) as session:
    c = session.begin.configuration
    print(f"voice_id {c['voice_id']}, resolved {c['resolved_voice']['voice_id']}")
    print(f"language {c['language']}, model {c['model']}")
    print(f"voice expires at {c['voice_expires_at']}")

    with open("cloned.pcm", "wb") as out:
        for chunk in session.speak("Hi, thanks for calling. How can I help you today?"):
            out.write(chunk)  # raw 24 kHz 16-bit mono PCM
```

A voice the session cannot use raises `aai.TTSError` from the `with`, with the refusal's `error_type` as `error_code`, such as `voice_not_found`, and the close code in `body["close_code"]`.

**JavaScript SDK**

```javascript theme={null}
import { createWriteStream } from "node:fs";
import { AssemblyAI } from "assemblyai";

const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY });
const session = client.tts.session({ voiceId: process.env.ASSEMBLYAI_VOICE_ID });

const { configuration: c } = await session.connect();
console.log(`voice_id ${c.voice_id}, resolved ${c.resolved_voice.voice_id}`);
console.log(`language ${c.language}, model ${c.model}`);
console.log(`voice expires at ${c.voice_expires_at}`);

const out = createWriteStream("cloned.pcm");
for await (const chunk of session.speak("Hi, thanks for calling. How can I help you today?")) {
  out.write(chunk); // raw 24 kHz 16-bit mono PCM
}
out.end();
await session.close();
```

A voice the session cannot use rejects `connect()` with a `TextToSpeechError`, with the refusal's `error_type` as `errorCode`, such as `voice_not_found`, and the close code in `closeCode`.

**cURL**

The [HTTP API](https://assemblyai.com/docs/tts/api-reference/http-api/overview.md) takes the voice in the body field `voice_id`:

```bash theme={null}
curl -sS --fail-with-body -D headers.txt https://tts.us.assemblyai.com/v1/speech \
  -H "Authorization: $ASSEMBLYAI_API_KEY" \
  -d '{"text": "Hi, thanks for calling. How can I help you today?", "voice_id": "'"$ASSEMBLYAI_VOICE_ID"'", "container": "wav"}' \
  -o cloned.wav
```

Among the headers saved in `headers.txt`, `x-tts-voice-id` is the `voice_id` you sent, `x-tts-language` the voice's language and `x-tts-voice-expires-at` its expiry; the SDKs' `synthesize()` and `stream()` expose them on the result. On the WebSocket URL, the voice is the query parameter `voice_id=<your-voice-id>`.

For an English voice, the output looks like this:

```text theme={null}
voice_id 01a139bf-78a0-7d18-9a4e-6c0f2b8d1e37, resolved 01a139bf-78a0-7d18-9a4e-6c0f2b8d1e37
language english, model assemblyai-tts-1
voice expires at 1797154212
```

`Begin.configuration.voice_id` is the value you sent, and `resolved_voice.voice_id` is the voice's UUID, also when you sent its slug. `voice_expires_at` is when the voice's [consent period](#consent-period) ends. The other keys a cloned voice adds to `Begin` are in [Message frames](https://assemblyai.com/docs/tts/message-frames.md#begin). On the [OpenAI-compatible route](https://assemblyai.com/docs/tts/openai-compatibility.md#cloned-voices), the voice goes in `voice` as `{"id": "<voice ID or slug>"}`.

### Language and model

A cloned voice speaks the language of its recording, and nothing else. Leave `language` out and the session speaks that language, which `Begin.configuration.language` shows; the default of `english` does not apply. A `language` that is not the voice's own closes the session with `3006` before `Begin`, `error_type` `invalid_parameter` and `param` `language`, and the text says to omit it.

Leave `model_id` out as well: the session runs on the default model of the voice's language, `assemblyai-tts-1` today, and `Begin.configuration.model` names it. A voice is prepared for each model it speaks on, which the errors call a pairing; see [Errors](#errors) for a voice that is not ready yet.

### Data zones

A cloned voice belongs to the [data zone](https://assemblyai.com/docs/tts/endpoints-and-data-zones.md) it was created in, and only that zone's host can use it: a voice of the US data zone on `tts.us.assemblyai.com`, a voice of the EU data zone on `tts.eu.assemblyai.com`. On the other zone's host the voice is not found. Set the SDK's host accordingly: `aai.settings.tts_base_url` in Python, `ttsBaseUrl` on the JavaScript client.

## Consent period

A cloned voice can be used for 60 days after its consent was last given, until `voice_expires_at`, a Unix timestamp in seconds that `Begin`, the HTTP response headers and the SDK results carry. After that, sessions and requests with the voice are refused with `voice_expired`. An expired voice cannot be renewed or used again: create the voice again from a new consented recording, which gives it a new ID. Renew a voice you want to keep before it expires.

### Renew consent

The voice's owner renews its biometric consent, which starts the 60 days again, by setting `extend_biometric_consent` to `true` on a session or request that uses the voice. Setting it attests renewed consent on behalf of your account, so send it only when your account can make that attestation. Only the account that owns the voice may set it, with its API key or a temporary token minted with that key; another account is refused with `not_owner`.

**Python SDK**

```python theme={null}
renew = aai.TTSConfig(voice_id=os.environ["ASSEMBLYAI_VOICE_ID"], extend_biometric_consent=True)

with aai.TTSSession(renew) as session:  # a session
    for chunk in session.speak("Consent renewed."):
        play(chunk)
print(session.termination.voice_expires_at)  # the expiry after the renewal

response = aai.TextToSpeech().synthesize("Consent renewed.", renew)  # or an HTTP request
print(response.voice_expires_at)
```

**JavaScript SDK**

```javascript theme={null}
const session = client.tts.session({ voiceId: process.env.ASSEMBLYAI_VOICE_ID, extendBiometricConsent: true }); // a session
await session.connect();
for await (const chunk of session.speak("Consent renewed.")) play(chunk);
const summary = await session.close();
console.log(summary?.voice_expires_at); // the expiry after the renewal

const response = await client.tts.synthesize({ // or an HTTP request
  text: "Consent renewed.",
  voice_id: process.env.ASSEMBLYAI_VOICE_ID,
  extend_biometric_consent: true,
});
console.log(response.voiceExpiresAt);
```

**cURL**

```bash theme={null}
curl -sS --fail-with-body -D headers.txt https://tts.us.assemblyai.com/v1/speech \
  -H "Authorization: $ASSEMBLYAI_API_KEY" \
  -d '{"text": "Consent renewed.", "voice_id": "'"$ASSEMBLYAI_VOICE_ID"'", "extend_biometric_consent": true, "stream": false}' \
  -o renewed.pcm
grep -i x-tts-voice-expires-at headers.txt
```

On the WebSocket URL, add `extend_biometric_consent=true` to the query string.

The renewal runs once the session or request has been accepted, and the new expiry shows in `Termination.voice_expires_at` and in `X-TTS-Voice-Expires-At` on a buffered HTTP response; a streamed response's headers leave with the first audio and still show the old expiry. A renewal within a day of the last one leaves the expiry as it is, and `Begin.configuration.extend_biometric_consent` echoes the flag without promising that the expiry moved. Compare the expiry before and after when you need to know.

## Errors

A voice the session cannot use is refused before `Begin`, with an `Error` frame in its place; over HTTP the same refusals are `422` answers with the same codes, except where noted, and the SDKs raise them as the tabs above describe. Branch on the code, not the text:

* **`voice_not_found`** (close `3012`): no voice with this ID or slug that your account can use on this host. It does not exist, was deleted, belongs to another account, or belongs to the other [data zone](#data-zones). Check the ID and the host.
* **`voice_expired`** (close `3012`): the [consent period](#consent-period) ended. Create the voice again.
* **`voice_blocked`** (close `3012`): the voice is blocked. Contact support.
* **`voice_not_paired`** (close `3014`): the voice is not ready on the session's model, usually because it was just created. Try again shortly; if it keeps failing, check the voice in the dashboard. The texts of this refusal are in [Error handling](https://assemblyai.com/docs/tts/error-handling.md#the-texts-of-3014).
* **`not_owner`** (close `3006`): `extend_biometric_consent` was set on a voice another account owns. Leave it out.
* **`biometric_terms_not_accepted`** (close `3006`, `403` over HTTP): your account has not accepted the biometric data terms. Accept them on the dashboard's data controls page.
* **`invalid_parameter` on `language`** (close `3006`): the `language` is not the voice's own. Leave it out.
* **`model_not_found`**, **`model_retired`** (close `3006`, `3014`) and **`feature_not_enabled`** (close `3006`, `400` over HTTP): a `model_id` the region does not know, a retired model, or a host where cloned voices are not available. Leave `model_id` out, and connect to `tts.us.assemblyai.com` or `tts.eu.assemblyai.com`.

None of these is retryable unchanged, except `voice_not_paired` once the voice is ready. A session refused for its voice still counts toward your account's [new sessions per minute](https://assemblyai.com/docs/tts/limits.md). [Error handling](https://assemblyai.com/docs/tts/error-handling.md) has every close code and what to do about it.

## Related

* [Voices](https://assemblyai.com/docs/tts/voices.md): the voice catalog and the AssemblyAI voices to start with.
* [WebSocket API basics](https://assemblyai.com/docs/tts/connect-parameters.md): how `voice_id` and `extend_biometric_consent` are checked at connect.
* [Error handling](https://assemblyai.com/docs/tts/error-handling.md): every close code and what to do about it.
* [OpenAI compatibility](https://assemblyai.com/docs/tts/openai-compatibility.md#cloned-voices): a cloned voice through the OpenAI SDKs.
* [Cloud endpoints and data residency](https://assemblyai.com/docs/tts/endpoints-and-data-zones.md): the US and EU hosts.
