# speech.nineonefour.org — how to use the speech API

Text-to-speech with Fish-Speech Drama 3 on this server's CPUs. Every request (except this page)
needs an API key with the `speech` grant. Only HTTPS is accepted.

**Speech is slow.** The model runs on the CPU at about 2 tokens per second: a few short lines
take about 10 minutes. Thus every request is a job: send it, then ask for its status.

## 1. Get a key

The operator of this server gives you a key. It looks like `mfk_<id>_<secret>`. The same key can
also have grants for files (files.nineonefour.org) and images (images.nineonefour.org). A key
without the `speech` grant gets `403`. An expired or revoked key gets `401`.

Keep the key secret. Do not put it in a URL or in a git repository.

## 2. Send the key

```
Authorization: Bearer mfk_...
X-Api-Key: mfk_...
```

## 3. Requests

| Request | Does |
|---|---|
| `POST /v1/jobs` | queue one text and get a job id at once (`202`) |
| `GET /v1/jobs` | your jobs |
| `GET /v1/jobs/{id}` | the job status: `queued` (with `position`), `running`, `done` or `error` |
| `GET /v1/jobs/{id}/audio` | the audio of a finished job (`409` until it is done) |
| `DELETE /v1/jobs/{id}` | remove a finished job and its audio |

Only the key that made a job can see it. The server does one job at a time, in the order they
came in. A finished job is kept for one hour.

**Cold start:** the model (about 78 GB) is not kept in memory when nobody uses it. The first job
after 5 minutes with no work loads it first (about 5 minutes), then it speaks.

## 4. Body of `POST /v1/jobs`

| Field | Default | Notes |
|---|---|---|
| `text` | required | the text to speak, up to 5000 characters |
| `format` | `wav` | `wav`, `mp3`, `opus` or `pcm` |
| `seed` | random | the same seed and text give the same voice |
| `single_chunk` | `false` | `true`: speak the whole text in one pass (better flow, more memory) |
| `chunk_length` | 200 | 100–1000; text is cut into chunks of about this size |
| `normalize` | `true` | write numbers as words (English and Chinese) |
| `max_new_tokens` | model limit | stop after this many tokens per chunk |
| `top_p` | 0.7 | 0.1–1.0 |
| `temperature` | 0.7 | 0.1–1.0 |

Other fields get `400`. Voice cloning from reference audio is not available through this API.

## 5. Examples

```sh
KEY=mfk_...
API=https://speech.nineonefour.org/v1

curl -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
     -d '{"text":"Hello. This voice was made on a CPU.","format":"mp3"}' $API/jobs
# -> {"id":"9f2c…","status":"queued","position":0,…}

curl -H "Authorization: Bearer $KEY" $API/jobs/9f2c…
# -> {"status":"done","seconds":512.4,"audio_url":"/v1/jobs/9f2c…/audio",…}

curl -H "Authorization: Bearer $KEY" -o hello.mp3 $API/jobs/9f2c…/audio
```

Python:

```python
import time, requests
api = "https://speech.nineonefour.org/v1"
h = {"Authorization": f"Bearer {key}"}
job = requests.post(f"{api}/jobs", headers=h, json={"text": "Hello there.", "format": "wav"}).json()
while (s := requests.get(f"{api}/jobs/{job['id']}", headers=h).json())["status"] in ("queued", "running"):
    time.sleep(30)
open("hello.wav", "wb").write(requests.get(f"{api}/jobs/{job['id']}/audio", headers=h).content)
```

## 6. Errors

Errors are JSON with a `detail` text.

| Code | Meaning |
|---|---|
| `400` | Bad body (the `detail` tells why), or plain HTTP was used |
| `401` | No key, or the key is unknown, expired or revoked |
| `403` | Your key does not have the `speech` grant |
| `404` | Unknown job, or the job expired |
| `409` | The job is not done, or it failed (the `detail` tells why) |
| `413` | The body is larger than 64 KB |
| `429` | Too many jobs wait; try again later |
