# Speech-to-text

Buy a transcript of an audio file. The endpoint has the shape of the OpenAI transcriptions API, so an unchanged OpenAI client works. It also reads the JSON form that OpenRouter uses.

Only a deployment that is registered to transcribe can win the job. A chat deployment never wins one.

This job is not the same as [Audio input](https://inference.ai.exchange/docs/audio-input.md). Audio input sends a clip to a chat model that can hear. This job returns the words of the clip.

## Quickstart

```bash
curl https://router.inference.ai.exchange/v1/audio/transcriptions \
  -H "Authorization: Bearer sk-liquid-..." \
  -F model=whisper-1 \
  -F file=@meeting.mp3 \
  -F response_format=verbose_json
```

```python
from openai import OpenAI

client = OpenAI(base_url="https://router.inference.ai.exchange/v1", api_key="sk-liquid-...")

with open("meeting.mp3", "rb") as audio:
    transcript = client.audio.transcriptions.create(model="whisper-1", file=audio)
print(transcript.text)
```

## Find a model

A control works only when an offer for the model declares it. Read the model card before you send `diarize` or `keyterms`.

- `GET /v1/models?kind=transcription` lists the models as cards, with `kind` `audio_transcription` and `modality` `transcription`.
- `parameters` lists `diarize` and `keyterms` when an offer declared them. A name that is not a key is not supported, so omit it.
- `pricing` gives the price of one second of audio.

See [Modalities](https://inference.ai.exchange/docs/modalities.md#read-a-model-card). If no offer declared a control that you sent, the answer is `400 unsupported_parameter`, and `param` names the feature, such as `transcription.diarize`.

## Controls

`diarize` and `keyterms` are offer features. `language`, `timestamp_granularities` and the audio limits are matched against what an offer registered.

### Speakers and key terms

`diarize` and `keyterms` are offer features: `transcription.diarize` and `transcription.keyterms`. Only an offer that declared the feature can win a request that sets the field.

```bash
curl https://router.inference.ai.exchange/v1/audio/transcriptions \
  -H "Authorization: Bearer sk-liquid-..." \
  -F model=<model> \
  -F file=@meeting.mp3 \
  -F response_format=verbose_json \
  -F diarize=true \
  -F 'keyterms[]=Liquid Inference' \
  -F 'keyterms[]=nano-USD'
```

- With `diarize`, each segment has a `speaker`, such as `speaker_0`. Without it, a segment has no `speaker`. `diarize` without `verbose_json` gets `400 invalid_request`.
- With `keyterms`, the transcript spells each term as you gave it. A list of more than 100 terms, or a term over 100 characters, gets `400 invalid_request`.

### Audio by link

Send an `https://` link in `url` and the exchange fetches the file.

```json
{
  "model": "<model>",
  "input_references": [{ "type": "audio", "url": "https://example.com/call.wav" }]
}
```

- The link must use `https` and name a public host. The exchange refuses an IP address, a host in its own network, a port that is not the default port, a login in the link, and a redirect to any of these.
- The file has the same limits as a file that you send: 25 MB and two hours.
- The fetch stops after 20 seconds and three redirects.
- A link that the exchange cannot read gets `400`, and the message says why. It does not repeat the link.

### Audio limits

The exchange reads WAV, FLAC, MP3, Ogg, WebM, M4A and AAC from the bytes of the file. It does not trust the name or the `format` field.

- A file can be 25 MB and two hours at most.
- A file with no readable length gets `400`, because the exchange cannot fix a cap without it.
- An offer can also limit the container, the length (`max_duration_seconds`) and the size (`max_bytes`). A request that breaks a limit does not reach that offer.

## Response

`json` returns the text, the usage and the `exchange` block.

```json
{
  "text": "Welcome to the meeting.",
  "usage": { "type": "duration", "seconds": 4 },
  "exchange": { "...": "..." }
}
```

`verbose_json` adds `language`, `duration` (the seconds that the exchange measured), `segments` and `words`. A segment or a word is present only when you asked for that granularity. A segment has `id`, `start`, `end` and `text`, and a `speaker` with `diarize`.

```json
{ "id": 0, "start": 0.0, "end": 3.4, "text": "Welcome to the meeting.", "speaker": "speaker_0" }
```

`text` returns plain text. Its `exchange` block comes back as JSON in the `x-liquid-exchange` response header.

## Billing

The price is for each second of audio. The exchange reads the length from the file, so the cap is known before dispatch. It is exactly the most that you can pay.

The provider meters the seconds that it worked on. You pay that figure, never more than the cap.

## Request parameters

Send `multipart/form-data` with the audio in `file`, or JSON with the audio in `input_references`.

| Field                     | Type    | Required | Description                                                                                                            |
| ------------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `model`                   | string  | Yes      | The model to buy, or a preset of the transcription kind.                                                               |
| `file`                    | file    | Yes\*    | The audio, in a form body.                                                                                             |
| `input_references`        | array   | Yes\*    | One entry of `type` `audio`: a `data:audio/` URL or an `https://` link in `url`, or base64 in `data` and `media_type`. |
| `response_format`         | string  | No       | `json` (the default), `text` or `verbose_json`. `srt` and `vtt` are not supported.                                     |
| `timestamp_granularities` | array   | No       | `segment`, `word` or both. Only with `verbose_json`. An offer that cannot return them cannot win.                      |
| `diarize`                 | boolean | No       | Label each segment with its speaker. Needs `verbose_json`.                                                             |
| `keyterms`                | array   | No       | Up to 100 words or names, each up to 100 characters, that the transcript must spell as given.                          |
| `language`                | string  | No       | An ISO 639-1 code such as `en`. An offer that declared its languages must list it.                                     |
| `prompt`                  | string  | No       | A hint of vocabulary or style. At most 2,048 characters.                                                               |
| `temperature`             | number  | No       | From 0 to 1.                                                                                                           |
| `exchange`                | object  | No       | Purchasing rules, such as `cap_usd`. See [Purchasing rules](https://inference.ai.exchange/docs/rules.md).                                               |

\* Send `file` or `input_references`. Send one audio entry only.

`stream` is not supported. The transcript comes whole.

## Errors

| Error                       | Cause                                                                                      | Fix                                                             |
| --------------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
| `400`                       | No audio, a file that is too large or too long, or a length that the exchange cannot read. | Send a supported container under 25 MB and two hours.           |
| `400 unsupported_model`     | The model is not a transcription model.                                                    | Pick a model that lists audio input and a transcript output.    |
| `400 unsupported_parameter` | No offer serves your `language`, format or granularity, or none declared a control.        | Read `parameters` on the model card, or `eligibility.excluded`. |

## Next steps

- [Audio input](https://inference.ai.exchange/docs/audio-input.md): send audio to a chat model.
- [Speech](https://inference.ai.exchange/docs/speech.md): turn text into speech.
- [Modalities](https://inference.ai.exchange/docs/modalities.md): the map of all modalities.
