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

# Voice Design

> Create a voice from a written description, audition it, and convert it into a voice you can use everywhere

Voice Design creates natural, realistic voices from a text description.
Describe the character you want for your voice agent or product, and Gradium
generates a few candidates. You listen, then convert the one you like into a
permanent voice. From then on it works like any other `voice_id`: REST,
WebSocket and Speech-to-Speech. No reference audio is needed.

<Card title="Have a recording instead?" icon="microphone" href="/guides/voices/custom-voices">
  Custom voices clone a speaker from a short audio sample.
</Card>

## How it works

<CardGroup cols={2}>
  <Card title="1. Generate" icon="wand-magic-sparkles">
    Send a description, get candidate voices back in seconds.
  </Card>

  <Card title="2. Listen" icon="headphones">
    Audition each candidate on a short line of Text-to-Speech.
  </Card>

  <Card title="3. Convert" icon="bookmark">
    Promote your pick to a permanent `voice_id`.
  </Card>

  <Card title="4. Use it" icon="waveform-lines">
    Text-to-Speech, streaming, and Speech-to-Speech all accept it.
  </Card>
</CardGroup>

Describe the voice in one or two sentences, up to 500 characters, in English,
French, Spanish, Portuguese or German. Gradium expands the description into a
fuller specification and samples 1 to 5 complete voices from it, ready in a few
seconds. Audition each on a short line, then convert the one you want. Until you
convert it, a candidate cannot be used in production.

Two things to know. The model samples a new voice on every request, even with a
fixed `seed`, so if you like a candidate, convert it: re-running the request will
not bring it back. And the candidates from one request are variations on one
character. For a different character, change the description.

## Access

Base URL `https://api.gradium.ai/api`, API key in the `x-api-key` header.

| Call                                | Endpoint                                            |
| :---------------------------------- | :-------------------------------------------------- |
| Generate candidates                 | `POST /voice-generator/generate`                    |
| Check readiness, or list candidates | `GET /voice-generator/embeddings`                   |
| Audition a candidate                | `POST /post/speech/tts`                             |
| Convert a candidate                 | `POST /voices/from-embedding`                       |
| Delete a candidate                  | `DELETE /voice-generator/embeddings/{embedding_id}` |

## Quickstart

The steps run as one sequence, carrying ids in shell variables or Python names.

<Steps>
  <Step title="Generate candidates">
    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST https://api.gradium.ai/api/voice-generator/generate \
        -H "x-api-key: $GRADIUM_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "prompt": "A British female voice, 20 to 30, glossy and confident, with girly chatter, high pitch, fast pacing, high energy and bright sparkling resonance. Ideal for a friendly receptionist or assistant.",
          "language": "en",
          "n_samples": 3
        }' > candidates.json

      CAND0=$(jq -r '.embeddings[0].embedding_id' candidates.json)
      CAND1=$(jq -r '.embeddings[1].embedding_id' candidates.json)
      ```

      ```python Python theme={null}
      import os
      import requests

      API_KEY = os.environ["GRADIUM_API_KEY"]
      BASE = "https://api.gradium.ai/api"

      resp = requests.post(
          f"{BASE}/voice-generator/generate",
          json={
              "prompt": (
                  "A British female voice, 20 to 30, glossy and confident, with girly "
                  "chatter, high pitch, fast pacing, high energy and bright sparkling "
                  "resonance. Ideal for a friendly receptionist or assistant."
              ),
              "language": "en",
              "n_samples": 3,
          },
          headers={"x-api-key": API_KEY},
      )
      resp.raise_for_status()
      candidates = [e["embedding_id"] for e in resp.json()["embeddings"]]
      ```
    </CodeGroup>

    ```json 201 Created theme={null}
    {
      "embeddings": [
        { "embedding_id": "vox_emb_njsiEgpj5NjHKdZc", "ready": false, "expires_at": "2026-10-03T11:00:00Z" },
        { "embedding_id": "vox_emb_39vb1XVZpnSF5Rt4", "ready": false, "expires_at": "2026-10-03T11:00:00Z" },
        { "embedding_id": "vox_emb_99mdAvteTpRBRBdW", "ready": false, "expires_at": "2026-10-03T11:00:00Z" }
      ]
    }
    ```

    Generation runs in the background, so the ids come back with `ready: false`.

    <Note>
      Every request mints new ids. Store them. To recover ids you did not store,
      list your candidates with `GET /voice-generator/embeddings`.
    </Note>
  </Step>

  <Step title="Wait for them to be ready">
    <CodeGroup>
      ```bash cURL theme={null}
      until curl -s -H "x-api-key: $GRADIUM_API_KEY" \
          "https://api.gradium.ai/api/voice-generator/embeddings?embedding_id=$CAND0" \
          | jq -e '.embeddings[0].ready' > /dev/null; do
        sleep 2
      done
      ```

      ```python Python theme={null}
      import time

      def wait_until_ready(embedding_id, timeout_s=120):
          deadline = time.monotonic() + timeout_s
          while time.monotonic() < deadline:
              resp = requests.get(
                  f"{BASE}/voice-generator/embeddings",
                  params={"embedding_id": embedding_id},
                  headers={"x-api-key": API_KEY},
              )
              resp.raise_for_status()
              found = resp.json()["embeddings"]
              if found and found[0]["ready"]:
                  return found[0]
              time.sleep(2)
          raise TimeoutError(embedding_id)

      for embedding_id in candidates:
          wait_until_ready(embedding_id)
      ```
    </CodeGroup>

    ```json 200 OK theme={null}
    {
      "embeddings": [{
        "embedding_id": "vox_emb_njsiEgpj5NjHKdZc",
        "ready": true,
        "prompt": "A British female voice, 20 to 30, ...",
        "language": "en",
        "created_at": "2026-09-03T11:00:00.968734",
        "expires_at": "2026-10-03T11:00:00.959626"
      }]
    }
    ```

    Three candidates typically take three to five seconds. Bound the loop at
    about two minutes and offer a retry.

    An unknown id returns `200` with an empty `embeddings` list, not `404`, so
    check the list before indexing into it. Timestamps are UTC with or without a
    trailing `Z`.
  </Step>

  <Step title="Listen to a candidate">
    Pass the candidate id as `voice_id` on the normal Text-to-Speech endpoint. The
    `vox_emb_` prefix is how the API tells a candidate from a converted voice.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST https://api.gradium.ai/api/post/speech/tts \
        -H "x-api-key: $GRADIUM_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{
          \"text\": \"Hi there, thanks so much for calling! How can I help you today?\",
          \"voice_id\": \"$CAND0\",
          \"model_name\": \"default\",
          \"output_format\": \"wav\",
          \"only_audio\": true
        }" --output candidate-0.wav
      ```

      ```python Python theme={null}
      resp = requests.post(
          f"{BASE}/post/speech/tts",
          json={
              "text": "Hi there, thanks so much for calling! How can I help you today?",
              "voice_id": candidates[0],
              "model_name": "default",
              "output_format": "wav",
              "only_audio": True,
          },
          headers={"x-api-key": API_KEY},
      )
      resp.raise_for_status()
      with open("candidate-0.wav", "wb") as f:
          f.write(resp.content)
      ```
    </CodeGroup>

    The response body is the audio, 48 kHz mono. Repeat for the other candidates.

    Candidates have three restrictions that converted voices do not:

    * Text is capped at 100 characters (`400 input text too long`).
    * REST only. The TTS WebSocket and Speech-to-Speech reject candidate ids with error `1011`.
    * A candidate that is not ready, unknown, or from another account returns `404 Embedding not found`.

    <Tip>
      Audition with the `model_name` and `json_config` you will ship, so the voice
      you approve is the voice you get.
    </Tip>
  </Step>

  <Step title="Convert the candidate into a voice">
    A candidate is a draft, deleted 30 days after generation. Converting it
    creates a permanent voice in your library.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST https://api.gradium.ai/api/voices/from-embedding \
        -H "x-api-key: $GRADIUM_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{
          \"voxium_embedding_id\": \"$CAND0\",
          \"name\": \"Receptionist EN\",
          \"description\": \"Front desk and appointment booking\"
        }" > voice.json

      VOICE=$(jq -r '.uid' voice.json)
      ```

      ```python Python theme={null}
      resp = requests.post(
          f"{BASE}/voices/from-embedding",
          json={
              "voxium_embedding_id": candidates[0],
              "name": "Receptionist EN",
              "description": "Front desk and appointment booking",
          },
          headers={"x-api-key": API_KEY},
      )
      resp.raise_for_status()
      voice_id = resp.json()["uid"]
      ```
    </CodeGroup>

    ```json 201 Created theme={null}
    {
      "uid": "b0ntuVzgFdUGoSPc",
      "name": "Receptionist EN",
      "description": "Front desk and appointment booking",
      "filename": "vox_emb_njsiEgpj5NjHKdZc",
      "start_s": 0.0,
      "is_catalog": false,
      "is_pro_clone": false,
      "language": "en",
      "tags": []
    }
    ```

    `uid` is your permanent `voice_id`. Store this one. It is the same value as
    `voxium_embedding_id` in the request and `voice_id` everywhere else.

    * Converting is free. The voice uses one custom-voice slot, shared with [voice clones](/guides/voices/custom-voices). Over the allowance: `409 Custom voice limit reached`.
    * It clears the candidate's expiry.
    * Converting the same candidate twice: `409 A voice was already created from this embedding: <voice_id>`. Store the candidate-to-voice mapping yourself rather than parsing this.
    * Not ready or unknown: `409 Embedding is not ready yet`.
  </Step>

  <Step title="Use the voice">
    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST https://api.gradium.ai/api/post/speech/tts \
        -H "x-api-key: $GRADIUM_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{
          \"text\": \"Good morning, and welcome to Northgate! Do you have an appointment with us today, or shall I get you booked in?\",
          \"voice_id\": \"$VOICE\",
          \"model_name\": \"default\",
          \"output_format\": \"wav\",
          \"only_audio\": true
        }" --output line-0.wav
      ```

      ```python Python theme={null}
      resp = requests.post(
          f"{BASE}/post/speech/tts",
          json={
              "text": (
                  "Good morning, and welcome to Northgate! Do you have an appointment "
                  "with us today, or shall I get you booked in?"
              ),
              "voice_id": voice_id,
              "model_name": "default",
              "output_format": "wav",
              "only_audio": True,
          },
          headers={"x-api-key": API_KEY},
      )
      resp.raise_for_status()
      with open("line-0.wav", "wb") as f:
          f.write(resp.content)
      ```
    </CodeGroup>

    No length cap here. The same `voice_id` works with the
    [Python SDK](/guides/text-to-speech), the
    [TTS WebSocket](/api-reference/endpoint/tts-websocket) and
    [Speech-to-Speech](/guides/speech-to-speech).
  </Step>

  <Step title="Clean up">
    Candidates you do not convert are removed after 30 days. To remove one sooner:

    ```bash cURL theme={null}
    curl -s -X DELETE \
      -H "x-api-key: $GRADIUM_API_KEY" \
      "https://api.gradium.ai/api/voice-generator/embeddings/$CAND1"
    ```

    Safe at any time: a converted voice holds its own copy. An id that is already
    gone returns `404`.
  </Step>
</Steps>

## Complete example

The whole flow in one file. Set `GRADIUM_API_KEY` and run it. It creates a
real voice in your account.

<CodeGroup>
  ```python voice_design.py theme={null}
  import os
  import time

  import requests

  BASE_URL = "https://api.gradium.ai/api"
  HEADERS = {"x-api-key": os.environ["GRADIUM_API_KEY"]}

  DESCRIPTION = (
      "A British female voice, 20 to 30, glossy and confident, with high pitch, "
      "fast pacing, high energy and bright sparkling resonance. Ideal for a "
      "friendly receptionist or assistant."
  )
  AUDITION_LINE = "Hi there, thanks so much for calling Northgate! How can I help you today?"


  def check(resp):
      if not resp.ok:
          raise RuntimeError(f"HTTP {resp.status_code}: {resp.text}")
      return resp


  def generate(prompt, language="en", n_samples=3, json_config=None):
      body = {"prompt": prompt, "language": language, "n_samples": n_samples}
      if json_config:
          body["json_config"] = json_config
      resp = check(requests.post(f"{BASE_URL}/voice-generator/generate", headers=HEADERS, json=body))
      return [c["embedding_id"] for c in resp.json()["embeddings"]]


  def wait_until_ready(candidate_ids, timeout_s=120.0):
      deadline = time.monotonic() + timeout_s
      pending = set(candidate_ids)
      while pending:
          for candidate_id in sorted(pending):
              resp = check(requests.get(
                  f"{BASE_URL}/voice-generator/embeddings",
                  headers=HEADERS,
                  params={"embedding_id": candidate_id},
              ))
              embeddings = resp.json()["embeddings"]
              if not embeddings:
                  raise RuntimeError(f"candidate {candidate_id} not found")
              if embeddings[0]["ready"]:
                  pending.discard(candidate_id)
          if pending:
              if time.monotonic() > deadline:
                  raise TimeoutError(f"still not ready: {sorted(pending)}")
              time.sleep(2.0)


  def synthesise(voice_id, text, path):
      resp = check(requests.post(
          f"{BASE_URL}/post/speech/tts",
          headers=HEADERS,
          json={"text": text, "voice_id": voice_id, "output_format": "wav", "only_audio": True},
      ))
      with open(path, "wb") as f:
          f.write(resp.content)


  def convert(candidate_id, name, description=None):
      resp = check(requests.post(
          f"{BASE_URL}/voices/from-embedding",
          headers=HEADERS,
          json={"voxium_embedding_id": candidate_id, "name": name, "description": description},
      ))
      return resp.json()["uid"]


  def delete_candidate(candidate_id):
      requests.delete(f"{BASE_URL}/voice-generator/embeddings/{candidate_id}", headers=HEADERS)


  if __name__ == "__main__":
      print("[1/5] generating candidates")
      candidate_ids = generate(DESCRIPTION, language="en", n_samples=3)

      print("[2/5] waiting until ready")
      wait_until_ready(candidate_ids)

      print("[3/5] auditioning")
      for i, candidate_id in enumerate(candidate_ids):
          synthesise(candidate_id, AUDITION_LINE, f"candidate-{i}.wav")
          print(f"      candidate-{i}.wav  {candidate_id}")

      # Listen to the files, then pick one. This example keeps the first.
      chosen = candidate_ids[0]

      print("[4/5] converting the chosen candidate into a voice")
      voice_id = convert(chosen, "Receptionist EN", "Front desk and appointment booking")
      print(f"      voice_id={voice_id}")

      print("[5/5] synthesising with the new voice")
      synthesise(
          voice_id,
          "Good morning, and welcome to Northgate! Do you have an appointment with us "
          "today, or shall I get you booked in?",
          "welcome.wav",
      )

      for candidate_id in candidate_ids:
          if candidate_id != chosen:
              delete_candidate(candidate_id)
      print("done")
  ```

  ```javascript voice_design.mjs theme={null}
  import { writeFile } from "node:fs/promises";

  const BASE_URL = "https://api.gradium.ai/api";
  const HEADERS = {
    "x-api-key": process.env.GRADIUM_API_KEY,
    "Content-Type": "application/json",
  };

  const DESCRIPTION =
    "A British female voice, 20 to 30, glossy and confident, with high pitch, " +
    "fast pacing, high energy and bright sparkling resonance. Ideal for a " +
    "friendly receptionist or assistant.";
  const AUDITION_LINE =
    "Hi there, thanks so much for calling Northgate! How can I help you today?";

  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  async function check(resp) {
    if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
    return resp;
  }

  async function generate(prompt, { language = "en", nSamples = 3, jsonConfig } = {}) {
    const body = { prompt, language, n_samples: nSamples };
    if (jsonConfig) body.json_config = jsonConfig;
    const resp = await check(
      await fetch(`${BASE_URL}/voice-generator/generate`, {
        method: "POST",
        headers: HEADERS,
        body: JSON.stringify(body),
      }),
    );
    return (await resp.json()).embeddings.map((c) => c.embedding_id);
  }

  async function waitUntilReady(ids, timeoutMs = 120_000) {
    const deadline = Date.now() + timeoutMs;
    const pending = new Set(ids);
    while (pending.size > 0) {
      for (const id of [...pending]) {
        const resp = await check(
          await fetch(`${BASE_URL}/voice-generator/embeddings?embedding_id=${id}`, {
            headers: HEADERS,
          }),
        );
        const { embeddings } = await resp.json();
        if (embeddings.length === 0) throw new Error(`candidate ${id} not found`);
        if (embeddings[0].ready) pending.delete(id);
      }
      if (pending.size > 0) {
        if (Date.now() > deadline) throw new Error(`still not ready: ${[...pending]}`);
        await sleep(2000);
      }
    }
  }

  async function synthesise(voiceId, text, path) {
    const resp = await check(
      await fetch(`${BASE_URL}/post/speech/tts`, {
        method: "POST",
        headers: HEADERS,
        body: JSON.stringify({ text, voice_id: voiceId, output_format: "wav", only_audio: true }),
      }),
    );
    await writeFile(path, Buffer.from(await resp.arrayBuffer()));
  }

  async function convert(candidateId, name, description) {
    const resp = await check(
      await fetch(`${BASE_URL}/voices/from-embedding`, {
        method: "POST",
        headers: HEADERS,
        body: JSON.stringify({ voxium_embedding_id: candidateId, name, description }),
      }),
    );
    return (await resp.json()).uid;
  }

  async function deleteCandidate(candidateId) {
    await fetch(`${BASE_URL}/voice-generator/embeddings/${candidateId}`, {
      method: "DELETE",
      headers: HEADERS,
    });
  }

  console.log("[1/5] generating candidates");
  const candidateIds = await generate(DESCRIPTION, { language: "en", nSamples: 3 });

  console.log("[2/5] waiting until ready");
  await waitUntilReady(candidateIds);

  console.log("[3/5] auditioning");
  for (const [i, candidateId] of candidateIds.entries()) {
    await synthesise(candidateId, AUDITION_LINE, `candidate-${i}.wav`);
    console.log(`      candidate-${i}.wav  ${candidateId}`);
  }

  // Listen to the files, then pick one. This example keeps the first.
  const chosen = candidateIds[0];

  console.log("[4/5] converting the chosen candidate into a voice");
  const voiceId = await convert(chosen, "Receptionist EN", "Front desk and appointment booking");
  console.log(`      voice_id=${voiceId}`);

  console.log("[5/5] synthesising with the new voice");
  await synthesise(
    voiceId,
    "Good morning, and welcome to Northgate! Do you have an appointment with us " +
      "today, or shall I get you booked in?",
    "welcome.wav",
  );

  for (const candidateId of candidateIds) {
    if (candidateId !== chosen) await deleteCandidate(candidateId);
  }
  console.log("done");
  ```
</CodeGroup>

<Note>
  Audio is streamed, so WAV headers carry placeholder lengths. Readers that trust the header, such as Python's `wave`, report a wrong duration. Rewrite the RIFF and `data` sizes after saving if that matters to you.
</Note>

## Writing a description

The description is the only input the model has. It responds to the attributes a
casting brief would carry, and naming more of them gives a tighter result.

| Attribute            | Examples                                                      |
| -------------------- | ------------------------------------------------------------- |
| Gender               | female, male, androgynous                                     |
| Age                  | 20 to 30, middle aged, elderly                                |
| Accent or origin     | British, Southern US, Parisian, Brazilian                     |
| Pitch                | high pitch, deep, mid range                                   |
| Pace                 | fast pacing, measured, unhurried                              |
| Energy               | high energy, calm, subdued                                    |
| Timbre and resonance | glossy, gravelly, breathy, bright sparkling resonance, warm   |
| Register and manner  | confident, girly chatter, formal, conspiratorial              |
| Intended use         | a friendly receptionist, an audiobook narrator, a news anchor |

End with the intended use. It steers delivery and register, not only the colour
of the voice.

> A British female voice, 20 to 30, glossy and confident, with girly
> chatter, high pitch, fast pacing, high energy and bright sparkling
> resonance. Ideal for a friendly receptionist or assistant.

### Rules of thumb

* **Use the space.** Up to 500 characters, all usable. Full sentences beat a list of adjectives.
* **Describe the voice, not the script.** Words to be spoken go in the audition line.
* **Set `language` to the language the voice will speak.** It shapes the accent and the delivery, not only the words. Regional accents still go in the description: "Bristolian" or "Parisian" has to be written out.
* **Concrete beats evaluative.** "Low pitch, slow pacing, gravelly" gives the model more than "a great narrator voice".

### Examples

| Voice       | Language         | Made for            |
| :---------- | :--------------- | :------------------ |
| **Pirate**  | English, Bristol | Character narration |
| **Fionn**   | English, Ireland | Customer service    |
| **Freya**   | English, GB      | Receptionist        |
| **Desmond** | English, US      | Long-form narration |

The descriptions as sent:

**Pirate**

> A gruff Bristolian English male pirate voice, 45 to 60, for game and character
> narration: weathered low pitch, gravelly timbre with heavy vocal fry, strong
> projection, at a steady, unhurried pace, boisterous and commanding energy.

**Fionn**

> An Irish English male voice, 40 to 55, for customer service: calm and crisp,
> with mid-low pitch, steady natural pacing, medium energy and warm rounded
> resonance. Ideal for reassuring walkthroughs, empathic de-escalation and
> complex IT support.

**Freya**

> A British female voice, 20 to 30, glossy and confident, with girly chatter,
> high pitch, fast pacing, high energy and bright sparkling resonance. Ideal for
> a friendly receptionist or assistant.

**Desmond**

> An American English male voice, 55 to 65: clean, deliberate and precise, with
> low pitch, slow pacing and low-to-mid energy, resonant timbre and a gentle
> low-to-high flow. Ideal for projecting academic authority.

### Iterating

Edit the attribute that is off rather than rewriting the description.

| Candidates sound...          | Change                                                  |
| ---------------------------- | ------------------------------------------------------- |
| Too young or old             | An explicit range: "40 to 55" rather than "middle aged" |
| Too fast or slow             | "unhurried", "measured" or "fast pacing"                |
| Flat                         | An energy word and a manner word                        |
| Too polished                 | A timbre word: "gravelly", "breathy", "weathered"       |
| Right voice, wrong delivery  | An intended-use clause at the end                       |
| Inconsistent with each other | A higher [`cfg_scale`](#cfg_scale)                      |

### Building this into a product

If you expose voice design to your own users, prefill a form over the
attributes above and compose the sentence from it, with a free-text box as the
escape hatch. Show the 500 character limit. Offer "keep this one" as the way to
hold onto a voice, and make regenerate send a new description rather than more
samples.

## Generation settings

Generation settings shape the voice and go in `json_config` on
`POST /voice-generator/generate`. [Synthesis settings](#synthesis-settings)
shape each utterance and go in `json_config` on the TTS request. Each has a
guidance knob, and they differ: `cfg_scale` is how literally the voice follows
your description, `cfg_coef` is how closely each utterance sticks to the voice.

All generation settings are optional. The defaults are tuned, so send only the
keys you have a reason to change.

```json theme={null}
{
  "prompt": "A calm Irish male voice, 40 to 55, warm and steady, for customer support.",
  "language": "en",
  "n_samples": 3,
  "json_config": { "cfg_scale": 8.0, "steps": 16, "seed": 42, "utmos_score": 3.5 }
}
```

| Key           | Range           | Default                      | Effect                                                                              |
| ------------- | --------------- | ---------------------------- | ----------------------------------------------------------------------------------- |
| `cfg_scale`   | `1.0` to `20.0` | `5.0`                        | How closely the voice follows the description. Higher is more literal, less varied. |
| `steps`       | `1` to `128`    | `16`                         | Sampling steps. More costs time for little gain.                                    |
| `seed`        | integer         | unset                        | Fixes the noise draw. Does not make generation reproducible.                        |
| `utmos_score` | `1.0` to `5.0`  | `3.1` (`en`), `3.0` (others) | Target recording quality. Higher is cleaner and more studio-like.                   |

### `cfg_scale`

* **`5.0`**, the default, when exploring with 3 to 5 candidates and you want them to differ.
* **`8.0` to `12.0`** when candidates drift from the description, or when generating one candidate at a time. Conversational designers that generate one candidate per turn typically use `10.0`.
* **Above `12.0`** the voice follows the description ever more literally at the cost of naturalness, and candidates converge on each other.

`cfg_scale` cannot add an attribute the description does not name. Fix the
[description](#writing-a-description) first.

### `seed`

The description is expanded before sampling and that expansion varies per
request, so the same `prompt` and `seed` still give a different voice.

### `utmos_score`

Conditions the recording, not the speaker: the same character at a higher score
sounds captured in a cleaner room on better equipment. Go toward `4.0` for a
studio sound, lower when a rough quality is part of the character.

### The `json_config` allow-list

<Warning>
  Only the four keys above are recognised. **An unknown key or out-of-range value is not rejected.** The request returns `201` and the candidates never become `ready`. Bound your polling loop and treat a timeout as a bad request.

  The usual causes: synthesis settings such as `temp` or `cfg_coef` sent at generation time, or `cfg_scale` and `steps` outside their ranges.
</Warning>

## Synthesis settings

Auditioning a candidate or synthesising with a converted voice is an ordinary
TTS request, so everything in [Voice Settings](/guides/voice-settings) applies.
Both `default` and `gradium-tts-beta` accept candidates and converted voices.

```json theme={null}
{
  "text": "Hi there, thanks so much for calling Northgate!",
  "voice_id": "vox_emb_njsiEgpj5NjHKdZc",
  "output_format": "wav",
  "only_audio": true,
  "json_config": { "temp": 0.5, "cfg_coef": 2.5 }
}
```

### Temperature (`temp`)

`0.0` to `1.4`, default `0.7`.

* **Audition at the default.** `0.0` makes a lively voice sound flatter than it will in production.
* **`0.3` to `0.5`** for scripted, high-volume output where takes must match: IVR prompts, fixed announcements.
* **Default or slightly above** for conversational agents and narration.

### Voice similarity (`cfg_coef`)

`1.0` to `4.0`, default `2.0`.

* **`2.0`** for most designed voices.
* **Toward `3.0`** when the voice drifts on long or emotional passages, or a designed trait such as a strong accent softens in production.
* **Above `3.0`** artefacts become likely.

`cfg_coef` cannot add a trait the voice lacks. That fix is at generation time: a
sharper description or higher [`cfg_scale`](#cfg_scale), then a new candidate.

## Supported tags

The models read plain text. Two inline tags are recognised, on candidates and
converted voices alike. **Anything else is spoken aloud as text.**

| Tag                     | Effect                                                                                      |
| ----------------------- | ------------------------------------------------------------------------------------------- |
| `<break time="1.5s" />` | A pause of `0.1` to `2.0` seconds. Surround the tag with spaces.                            |
| `<flush>`               | Emit audio for all text received so far. Only meaningful when streaming over the WebSocket. |

On a candidate, tags count toward the 100-character limit.

### Not supported

SSML is not interpreted. `<speak><prosody rate="slow">Hello there.</prosody></speak>`
produces a voice saying "speak prosody rate slow, hello there, slash prosody,
slash speak". Use these instead:

| Instead of                            | Use                                                                                                                     |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `<prosody rate>`                      | `padding_bonus`, see [Speed control](/guides/voice-settings#speed-control)                                              |
| `<prosody pitch>`, `<prosody volume>` | The description. Pitch and energy are attributes of the voice.                                                          |
| `<emphasis>`                          | Punctuation and sentence structure                                                                                      |
| `<say-as>`, `<sub>`, `<phoneme>`      | [`rewrite_rules`](/guides/text-rewriting) or a [pronunciation dictionary](/api-reference/endpoint/create-pronunciation) |
| `<voice>`                             | A separate request with a different `voice_id`                                                                          |

Markdown and HTML are read as text too. Strip formatting before sending.

## Candidate lifecycle

|                      |                                                                                                                                                                                                    |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Retention**        | Candidates are removed 30 days after generation. Converting one clears its expiry.                                                                                                                 |
| **Accumulation**     | Generating never evicts earlier candidates. Holding them is free.                                                                                                                                  |
| **Cleanup**          | `DELETE /voice-generator/embeddings/{embedding_id}`. Always safe: a converted voice holds its own copy. Unknown id: `404`.                                                                         |
| **Deleting a voice** | `DELETE /voices/{voice_id}` leaves the candidate intact, with no expiry. It can be converted again.                                                                                                |
| **Lost an id**       | `GET /voice-generator/embeddings` lists every candidate, newest first, with its description. Page with `skip` and `limit` (1 to 1000, default 100). `expires_at: null` marks converted candidates. |

## Limits

| Limit                  | Value                                    |
| ---------------------- | ---------------------------------------- |
| Description            | 1 to 500 characters                      |
| Candidates per request | 1 to 5, default 1                        |
| Audition text          | 100 characters, REST only                |
| Languages              | `en`, `fr`, `es`, `pt`, `de`             |
| Candidate retention    | 30 days, cleared once converted          |
| Converted voices       | Count against the custom-voice allowance |

## Errors

Errors are `{"detail": "..."}`, except `422`, which carries a list of field errors.

| Status | Where            | Meaning                                                                               |
| ------ | ---------------- | ------------------------------------------------------------------------------------- |
| `400`  | Audition         | Text over 100 characters                                                              |
| `401`  | Any              | Invalid or expired API key                                                            |
| `404`  | Audition, delete | Unknown, not ready, or another account's id                                           |
| `409`  | Convert          | Already converted, not ready, or custom-voice allowance reached. `detail` says which. |
| `422`  | Generate, list   | Invalid `language`, `n_samples`, `prompt` or `limit`                                  |

A candidate that never becomes ready is almost always a `json_config` value out
of range or a key the API does not recognise. See [the
allow-list](#the-json_config-allow-list).

## Next steps

<CardGroup cols={2}>
  <Card title="Manage Voices" icon="sliders" href="/guides/voices/manage-voices">
    List, update, and delete the voices you convert.
  </Card>

  <Card title="Voice Settings" icon="sliders-up" href="/guides/voice-settings">
    Speed, temperature and other synthesis options.
  </Card>

  <Card title="Streaming Text-to-Speech" icon="waveform-lines" href="/guides/text-to-speech">
    Use your new voice on the low-latency WebSocket.
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/endpoint/generate-voice">
    The Voice Design endpoints in full.
  </Card>
</CardGroup>
