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

# Voice cloning

> Instant clone from one 10-30 s clip, pro clone from a 10+ minute dataset, localize into a new language

Create a voice your organization owns in three ways: instant clone from a single clip, pro clone from a multi-file dataset, or localization of an existing voice into another language. A cloned voice is used in TTS requests like any other voice. Asynchronous operations return a `job_id`; poll `GET /jobs/:id` until the job settles.

## Instant clone

`POST /voices/clone` accepts the audio in one of three forms:

* **multipart/form-data** — one file field plus the text fields below.
* **Raw body** — `Content-Type: audio/*` or `application/octet-stream`; text fields go in query params.
* **JSON** — `{"blob_id": "..."}` referencing audio already stored with your organization.

Send exactly one clip per request; zero or several return 400 `missing_clip`. Use 10–30 seconds of clean speech — 10 seconds is the working minimum. Caps: **25 MB per clip** (413) and **250 MB of stored audio per organization** (413 `blob_quota_exceeded`).

| Field        | Type   | Constraint                                 | Default      |
| ------------ | ------ | ------------------------------------------ | ------------ |
| `name`       | string | Required — 400 `missing_name` without it   | —            |
| `mode`       | string | `stability` or `similarity`                | `similarity` |
| `language`   | string | ISO code                                   | none         |
| `transcript` | string | Transcript of the clip, ≤20,000 characters | none         |
| `model_id`   | string | Model to prepare the voice for             | `omnivoice`  |

<CodeGroup>
  ```python Python theme={null}
  import requests

  resp = requests.post(
      "https://api.withhopper.com/voices/clone",
      headers={"Authorization": "Bearer sk_hopper_..."},
      files={"file": open("clip.wav", "rb")},
      data={"name": "Support agent", "language": "en"},
  )
  body = resp.json()
  # 201 -> body is the Voice, ready now; 202 -> {"job_id", "voice_id", "status": "queued"}
  ```

  ```typescript TypeScript theme={null}
  import { openAsBlob } from "node:fs";

  const form = new FormData();
  form.append("file", await openAsBlob("clip.wav"), "clip.wav");
  form.append("name", "Support agent");
  form.append("language", "en");

  const resp = await fetch("https://api.withhopper.com/voices/clone", {
    method: "POST",
    headers: { Authorization: "Bearer sk_hopper_..." },
    body: form,
  });
  const body = await resp.json();
  // 201 -> body is the Voice, ready now; 202 -> {"job_id", "voice_id", "status": "queued"}
  ```

  ```bash cURL theme={null}
  curl https://api.withhopper.com/voices/clone \
    -H "Authorization: Bearer sk_hopper_..." \
    -F "file=@clip.wav" \
    -F "name=Support agent" \
    -F "language=en"
  ```
</CodeGroup>

Two success shapes:

* **201** — the clone ran synchronously; the body is the finished [Voice](/tts/voices#the-voice-object), usable in TTS immediately.
* **202** — `{"job_id": "...", "voice_id": "...", "status": "queued"}`. The voice row exists now under the returned `voice_id`; its profile attaches when the job succeeds.

## Pro clone

`POST /voices/clone/pro` builds a higher-fidelity voice from a dataset. Provide **10+ minutes of audio across files**, 25 MB per file. Input forms: multipart with multiple file fields, a raw audio body, or JSON `{"blob_ids": ["..."]}`. `name` is required. There is no `mode` field.

Pro clones never complete synchronously. The endpoint always returns **202** `{"job_id": "...", "status": "..."}`.

## Localize

`POST /voices/:id/localize` derives a new voice in another language.

Body: `{"target_language": "..."}` (required — 400 without it) plus optional `model_id`. Returns **202** `{"job_id": "...", "status": "..."}`.

The result is a new voice your organization owns, with `source: "localized"` and `parent_voice_id` pointing at the original. Featured platform voices can be localized; the original is untouched.

## Polling jobs

`GET /jobs/:id` returns the state of any clone or localize job in your organization; unknown ids return 404.

| Field                      | Values                                                                                          |
| -------------------------- | ----------------------------------------------------------------------------------------------- |
| `id`                       | job id                                                                                          |
| `kind`                     | `instant_clone`, `pro_clone`, `localize`                                                        |
| `status`                   | `queued`, `running`, `succeeded`, `failed`                                                      |
| `created_at`, `updated_at` | timestamps                                                                                      |
| `result`                   | on success: `{"voice_id": "...", "profile": {model_id, profile_kind, profile_version, status}}` |
| `error`                    | on failure, e.g. `model_offline` when no upstream is configured                                 |

Poll until the status leaves `queued`/`running`:

```python theme={null}
import time, requests

headers = {"Authorization": "Bearer sk_hopper_..."}
url = f"https://api.withhopper.com/jobs/{job_id}"

job = requests.get(url, headers=headers).json()
while job["status"] in ("queued", "running"):
    time.sleep(2)
    job = requests.get(url, headers=headers).json()

if job["status"] == "succeeded":
    voice_id = job["result"]["voice_id"]  # ready for /tts/bytes
```
