> ## 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 Enhance

> Clean up a voice you already have, without changing its language or who is speaking

Voice Enhance takes an existing voice, a flagship voice, a clone or a designed
voice, and produces cleaner candidates of the same speaker in the same
language. A clone made from a noisy recording comes back with the background
noise reduced and a quality target applied; the speaker, the language and the
delivery stay. You audition the candidates, convert the one you like, and it
works like any other `voice_id`: REST, WebSocket and Speech-to-Speech.

<Warning>
  Voice Enhance is in beta. Output quality will keep improving; audition every
  candidate against its source before you convert it.
</Warning>

<CardGroup cols={2}>
  <Card title="Starting from a recording?" icon="microphone" href="/guides/voices/custom-voices">
    Clone the speaker first, then enhance the clone.
  </Card>

  <Card title="Starting from a description?" icon="wand-magic-sparkles" href="/guides/voices/voice-design">
    Voice Design creates a new voice from words. Enhance cleans up a voice you already have.
  </Card>
</CardGroup>

## How it works

<CardGroup cols={2}>
  <Card title="1. Pick a source" icon="user">
    A flagship voice, one of your clones, or a Voice Design candidate.
  </Card>

  <Card title="2. Enhance" icon="sparkles">
    One request, no other settings. Candidates are ready in about twenty seconds.
  </Card>

  <Card title="3. Listen" icon="headphones">
    Audition each candidate against the source on the same line of Text-to-Speech.
  </Card>

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

The source is never modified. Each request creates `n_samples` new
candidates, drafts with a `vox_emb_` id, exactly like Voice Design candidates:
you audition them on `POST /post/speech/tts`, convert them with
`POST /voices/from-embedding`, and they expire after 30 days unless converted.

## Access

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

| Call                                | Endpoint                                            |
| :---------------------------------- | :-------------------------------------------------- |
| Enhance a voice                     | `POST /voice-generator/enhance`                     |
| 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 example enhances a French clone. Replace `$SOURCE` with any voice id or
candidate id you own, or a flagship voice id.

<Steps>
  <Step title="Enhance the voice">
    <CodeGroup>
      ```bash cURL theme={null}
      curl -s -X POST https://api.gradium.ai/api/voice-generator/enhance \
        -H "x-api-key: $GRADIUM_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{
          \"src_voice\": \"$SOURCE\",
          \"n_samples\": 2
        }" > candidates.json

      CAND0=$(jq -r '.embeddings[0].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"
      SOURCE = os.environ["SOURCE"]  # a voice id or a vox_emb_ candidate id

      resp = requests.post(
          f"{BASE}/voice-generator/enhance",
          json={"src_voice": SOURCE, "n_samples": 2},
          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_XeBcXDgoigTfL9Cv", "ready": false, "expires_at": "2026-10-21T13:03:52Z" },
        { "embedding_id": "vox_emb_ni8lehVHTb35siXI", "ready": false, "expires_at": "2026-10-21T13:03:53Z" }
      ]
    }
    ```

    | Field       |          |                                                                                                   |
    | :---------- | :------- | :------------------------------------------------------------------------------------------------ |
    | `src_voice` | required | A voice id (your clone, a converted candidate, or a flagship voice) or a `vox_emb_` candidate id. |
    | `n_samples` | optional | 1 to 5 candidates, default 1.                                                                     |

    There is nothing else to set: the language comes from the source voice, and
    so does the speaker. The request is checked before anything is queued, so
    an unknown source or a source that cannot be enhanced fails here with
    `404` or `409` and nothing is billed.
  </Step>

  <Step title="Wait for the candidates">
    <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_XeBcXDgoigTfL9Cv",
        "ready": true,
        "kind": "enhance",
        "language": "fr",
        "created_at": "2026-09-21T13:03:52.591128",
        "expires_at": "2026-10-21T13:03:52.495534",
        "enhance_config": {
          "src_voice": "H5oin0KHqRTAxaeL",
          "src_language": "fr"
        },
        "prompt": null
      }]
    }
    ```

    Enhanced candidates are typically ready in fifteen to twenty seconds. In
    the listing, `kind` is `enhance` and `enhance_config` holds the source id
    and its language. `language` is the source voice's language, which the
    candidate keeps. `prompt` is empty for an enhanced candidate.
  </Step>

  <Step title="Listen to a candidate">
    Pass the candidate id as `voice_id` on the Text-to-Speech endpoint, with
    text in the voice's language. Synthesise the same line with the source
    voice too: the difference between the two files is what Enhance did.

    <CodeGroup>
      ```bash cURL theme={null}
      for VOICE in "$CAND0" "$SOURCE"; do
        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\": \"Bonjour, c'est Constance. Même voix, un peu plus propre, j'espère.\",
            \"voice_id\": \"$VOICE\",
            \"model_name\": \"default\",
            \"output_format\": \"wav\",
            \"only_audio\": true
          }" --output "$VOICE.wav"
      done
      ```

      ```python Python theme={null}
      LINE = "Bonjour, c'est Constance. Même voix, un peu plus propre, j'espère."

      for voice_id, path in [(candidates[0], "candidate-0.wav"), (SOURCE, "source.wav")]:
          resp = requests.post(
              f"{BASE}/post/speech/tts",
              json={
                  "text": LINE,
                  "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(path, "wb") as f:
              f.write(resp.content)
      ```
    </CodeGroup>

    Candidates keep the same restrictions as Voice Design candidates: REST
    only, capped audition text, and an opaque `404 Embedding not found` until
    ready.
  </Step>

  <Step title="Convert the candidate into a voice">
    <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\": \"Constance (enhanced)\",
          \"description\": \"French support voice, enhanced\"
        }" > 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": "Constance (enhanced)",
              "description": "French support voice, enhanced",
          },
          headers={"x-api-key": API_KEY},
      )
      resp.raise_for_status()
      voice_id = resp.json()["uid"]
      ```
    </CodeGroup>

    ```json 201 Created theme={null}
    {
      "uid": "w0F73K2fcf3X2Vcn",
      "name": "Constance (enhanced)",
      "description": "French support voice, enhanced",
      "filename": "vox_emb_XeBcXDgoigTfL9Cv",
      "start_s": 0.0,
      "is_catalog": false,
      "is_pro_clone": false,
      "language": "fr",
      "tags": []
    }
    ```

    `uid` is your permanent `voice_id`. Store it. The new voice keeps the
    source's `language`. Converting is free, uses one custom-voice slot, and
    clears the candidate's expiry. Candidates from one request are variations:
    if you like one, convert it rather than re-running the request. The source
    voice is untouched, so you can keep both or delete the source once you are
    happy with the enhanced one.
  </Step>
</Steps>

## Complete example

Set `GRADIUM_API_KEY` and `SOURCE`, then run it. It creates a real voice in
your account.

```python voice_enhance.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"]}
SOURCE = os.environ["SOURCE"]
LINE = "Hi, this is your assistant. How can I help you today?"  # in the source voice's language
N_SAMPLES = 3


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


def enhance(src_voice, n_samples=1):
    resp = check(requests.post(
        f"{BASE_URL}/voice-generator/enhance",
        headers=HEADERS,
        json={"src_voice": src_voice, "n_samples": n_samples},
    ))
    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"]


if __name__ == "__main__":
    print("[1/4] enhancing")
    candidates = enhance(SOURCE, N_SAMPLES)

    print("[2/4] waiting until ready")
    wait_until_ready(candidates)

    print("[3/4] auditioning")
    synthesise(SOURCE, LINE, "source.wav")  # the same line without enhancement
    for i, candidate_id in enumerate(candidates):
        synthesise(candidate_id, LINE, f"candidate-{i}.wav")
        print(f"      candidate-{i}.wav vs source.wav")

    # Listen, then convert the one you keep. This example keeps the first.
    print("[4/4] converting")
    voice_id = convert(candidates[0], "Assistant (enhanced)", f"Enhanced from {SOURCE}")
    print(f"      voice_id={voice_id}")
    print("done")
```

## Sources you can enhance

| Source                                    | Works                     | Notes                                                                                                                                                                                                                                                                                                 |
| :---------------------------------------- | :------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Flagship voice                            | Yes                       | Any id from [Flagship Voices](/guides/voices/flagship-voices).                                                                                                                                                                                                                                        |
| Instant clone                             | Yes, if it has a language | Clones created without a `language` return `409 The source voice has no language`. Set it once with [`PUT /voices/{voice_id}`](/guides/voices/manage-voices#update-a-voice) and retry. The language must be the one spoken in the recording: a wrong tag is accepted and quietly degrades the result. |
| Voice Design candidate or converted voice | Yes                       | Use the `vox_emb_` id or the converted `voice_id`.                                                                                                                                                                                                                                                    |
| Enhanced candidate                        | Yes                       | The candidate must be ready (`409` otherwise).                                                                                                                                                                                                                                                        |
| Pro clone                                 | No                        | `409 This voice has no embedding the voice generator can edit.`                                                                                                                                                                                                                                       |
| Another account's voice                   | No                        | Opaque `404`.                                                                                                                                                                                                                                                                                         |

## Tips from testing

* **Compare against the source reading the same text.** Enhance changes how
  the voice sounds, not what it says or in which language. Put the source and
  the candidate side by side in your review UI, on the same line.
* **Start with a clone.** Flagship voices are produced from clean recordings
  already; the clone you wish sounded cleaner is the natural source.
* **Candidates vary.** Five samples from one request, or five separate
  requests, give five slightly different results. Convert the one you like;
  do not expect a re-run to reproduce it.
* **Keep the source.** Converting a candidate does not replace the source
  voice. Switch your `voice_id` over once you have listened, and delete the
  old one when you no longer need it.
* **Budget the wait.** Enhanced candidates take fifteen to twenty seconds,
  longer than Voice Design candidates. Poll rather than block a request on it.

## Candidate lifecycle

Enhanced candidates follow the [Voice Design candidate
lifecycle](/guides/voices/voice-design#candidate-lifecycle): 30 days retention,
cleared on conversion, `DELETE /voice-generator/embeddings/{embedding_id}` to
remove one early. Deleting the source voice or candidate after the `201` never
breaks the request: each candidate holds its own copy of the source.

## Limits

| Limit                  | Value                                                       |
| ---------------------- | ----------------------------------------------------------- |
| Candidates per request | 1 to 5, default 1                                           |
| Languages              | The source voice's language: `en`, `fr`, `es`, `pt` or `de` |
| Audition text          | REST only, same cap as Voice Design candidates              |
| 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                                                                                                         |
| ------ | -------- | --------------------------------------------------------------------------------------------------------------- |
| `401`  | Any      | Invalid or expired API key                                                                                      |
| `404`  | Enhance  | Unknown source, or another account's voice or candidate                                                         |
| `404`  | Audition | Candidate not ready, unknown, or deleted                                                                        |
| `409`  | Enhance  | Source has no language, source is a candidate that is not ready, or source is a pro clone. `detail` says which. |
| `409`  | Convert  | Already converted, not ready, or custom-voice allowance reached                                                 |
| `422`  | Enhance  | Invalid `n_samples`, or an empty `src_voice`                                                                    |
| `503`  | Enhance  | Source audio could not be copied, or no voice generator is available. Retry.                                    |

## Next steps

<CardGroup cols={2}>
  <Card title="Custom Voices" icon="microphone" href="/guides/voices/custom-voices">
    Clone a speaker, with a language, so the clone can be enhanced.
  </Card>

  <Card title="Voice Design" icon="wand-magic-sparkles" href="/guides/voices/voice-design">
    Create a voice from a description instead.
  </Card>

  <Card title="Manage Voices" icon="sliders" href="/guides/voices/manage-voices">
    Set a language on an older clone, list and delete voices.
  </Card>

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