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

# Forced alignment

> Send audio and its transcript, and get the start and end time of every word.

> [!NOTE]
>
> **Beta.** Forced alignment takes a recording and the text that is spoken in it, and returns a timestamp for every word. Use it for subtitles and captions, karaoke-style highlighting, audiobook read-along, and cutting audio at word boundaries.

You already have the transcript, so nothing is transcribed: Chariot only works out *when* each word is said. Send up to 90 seconds of audio to [`POST /v1/forced-alignment`](https://docs.chariot.in/api-reference/endpoint/forced-alignment-create) and the timings come back in the same response, in seconds from the start of the audio, one entry per word of your text, in order.

## Align a recording

Send `multipart/form-data` with the audio `file`, the transcript as `text`, and the `language` code. Create an API key in the [Dashboard](https://platform.chariot.in) and keep it on your server.

```bash cURL theme={null}
curl --fail-with-body https://api.chariot.in/v1/forced-alignment \
  -H "chariotai-api-key: $CHARIOT_API_KEY" \
  -F "file=@greeting.wav" \
  -F "text=नमस्ते, आप कैसे हैं?" \
  -F "language=hi"
```

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

with open("greeting.wav", "rb") as audio:
    response = requests.post(
        "https://api.chariot.in/v1/forced-alignment",
        headers={"chariotai-api-key": os.environ["CHARIOT_API_KEY"]},
        files={"file": ("greeting.wav", audio, "audio/wav")},
        data={"text": "नमस्ते, आप कैसे हैं?", "language": "hi"},
    )

response.raise_for_status()
for word in response.json()["words"]:
    print(f'{word["start"]:7.2f} {word["end"]:7.2f}  {word["text"]}')
```

```javascript JavaScript (Node.js) theme={null}
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append("file", new Blob([await readFile("greeting.wav")], { type: "audio/wav" }), "greeting.wav");
form.append("text", "नमस्ते, आप कैसे हैं?");
form.append("language", "hi");

const response = await fetch("https://api.chariot.in/v1/forced-alignment", {
  method: "POST",
  headers: { "chariotai-api-key": process.env.CHARIOT_API_KEY },
  body: form,
});
if (!response.ok) {
  const body = await response.json();
  throw new Error(body.message ?? body.detail);
}
const { words } = await response.json();
console.log(words);
```

The response:

```json theme={null}
{
  "alignment_id": "6f1c2d9e-3b7a-4f0e-9a51-2c8d4e7b1a90",
  "status": "completed",
  "language": "hi",
  "duration_seconds": 1.83,
  "credits_used": 2,
  "loss": 0.06,
  "words": [
    { "text": "नमस्ते,", "start": 0.12, "end": 0.58, "loss": 0.04 },
    { "text": "आप", "start": 0.66, "end": 0.81, "loss": 0.06 },
    { "text": "कैसे", "start": 0.81, "end": 1.1, "loss": 0.05 },
    { "text": "हैं?", "start": 1.1, "end": 1.42, "loss": 0.09 }
  ]
}
```

## What you get back

**words** `array`

One entry per whitespace-separated word of your `text`, in the same order, so you can map results back by index. `text` is the word exactly as you sent it, punctuation included. A word that is only punctuation (for example a lone `-`) gets `start` equal to `end`, at the end of the word before it.

**words\[].loss** `number | null`

How unsure the aligner is about this word. Lower is better. A word with a much higher loss than the words around it usually means the audio does not say it: a typo in the transcript, a skipped or repeated word, or noise. Use it to flag lines for review.

**loss** `number | null`

The average loss over the transcript. A high value for the whole file usually means the transcript or the language does not match the audio.

**credits\_used** `integer`

What the request cost. See [Pricing](#pricing).

Character-level timings are not returned yet.

If your workspace uses [data retention](https://docs.chariot.in/guides/resources/data-retention.md), the alignment also has `content_expires_at`. After it, `words`, `text` and `filename` read as `null` when you fetch the alignment again, `has_audio` is `false`, and the audio endpoint returns `410`.

## Write a good transcript

* Send the words that are spoken, as plain text. Speaker labels, timestamps, and markup are aligned as if they were words.
* Every word in your text gets a timing, even if the audio skips it, so make sure the text matches what is said. Check words with a high `loss` first.
* Write numbers and abbreviations the way they are spoken.
* Send the language that is spoken.

## Languages

Pass an ISO 639-1 code as `language`. The default is `hi`. [`GET /v1/forced-alignment/options`](https://docs.chariot.in/api-reference/endpoint/forced-alignment-options) returns the current list.

| Code | Language | Code | Language | Code | Language |
| - | - | - | - | - | - |
| `hi` | Hindi | `kn` | Kannada | `pa` | Punjabi |
| `en` | English | `ml` | Malayalam | `sa` | Sanskrit |
| `bn` | Bengali | `mr` | Marathi | `ta` | Tamil |
| `gu` | Gujarati | `or` | Odia | `te` | Telugu |
| `ur` | Urdu | | | | |

Any other language is refused with a `422`.

> [!NOTE]
>
> Accuracy is highest for Hindi. The other languages are in beta.

## Limits

| | Limit |
| - | - |
| Audio duration | 0.1 to 90 seconds |
| File size | 50 MB |
| Transcript | 5,000 characters |
| Formats | WAV, MP3, FLAC, OGG, M4A, AAC, WebM |

Audio longer than 90 seconds, or shorter than 0.1 seconds, is refused with a `422` before you are charged. Split longer recordings and align each part. An upload well over 50 MB is refused with a `413` while it is still uploading; a file just over 50 MB is refused with a `422`. Neither is charged.

## Pricing

Forced alignment costs **1 credit per second of audio**, rounded up per request, for every language. A 90-second clip, the longest supported, costs 90 credits. Chariot measures the duration from the decoded audio; that measured duration is what you are billed for.

The cost is charged before alignment starts. If the alignment fails, the credits are refunded and `credits_used` becomes `0`. See [Credits and pricing](https://docs.chariot.in/guides/resources/credits.md).

## Errors

| Status | Meaning |
| - | - |
| `402` | Not enough credits for this audio. The message says how many it needs. |
| `403` | Forced alignment is not included in your plan. |
| `413` | The upload is well over 50 MB. Refused while it is still uploading, before you are charged. |
| `422` | The file is just over 50 MB, the audio could not be read or is too long or too short, the transcript is empty, too long or has no words that can be aligned, or the language is not supported. Not charged. |
| `429` | Too many requests at once for your workspace. Retry after a short wait. |
| `500` | Something went wrong on Chariot's side. The credits were refunded. |
| `503`, `504` | The aligner is unavailable or timed out, and the credits were refunded. Retry later. |

Errors use the standard [error format](https://docs.chariot.in/guides/resources/errors.md), except `413`, whose body is `{"detail": "Request is too large: the file limit is 50 MB"}`. Read `message`, and fall back to `detail`.

## Past alignments

Every charged alignment is recorded, failed ones included. Requests refused before the charge (`402`, `403`, `413`, `429` and input errors) are not. Chariot keeps the audio file and the transcript you send with it, under your workspace's [data retention](https://docs.chariot.in/guides/resources/data-retention.md).

* [`GET /v1/forced-alignment/{alignment_id}`](https://docs.chariot.in/api-reference/endpoint/forced-alignment-get) returns one alignment with its transcript as `text`, and its `words` if it completed.
* [`GET /v1/forced-alignment/{alignment_id}/audio`](https://docs.chariot.in/api-reference/endpoint/forced-alignment-audio) returns a short-lived signed link (about 15 minutes) to the audio you sent, in the format you uploaded. Pass `redirect=true` to be redirected to it. The audio is stored just after the alignment is created, and `has_audio` on the alignment says when it is available. The endpoint returns `404` when the audio is not stored (or not stored yet), and `410` once the content has expired.
* Reuse a link until its `url_expires_at` instead of asking again. The audio endpoint allows 10 link requests per alignment per minute, and 600 per workspace per minute. Over either, it returns `429`.
* [`GET /v1/forced-alignment`](https://docs.chariot.in/api-reference/endpoint/forced-alignment-list) lists your workspace's alignments, newest first, without their transcripts or words; pass `next_before` from one page as `before` to get the next.

A failed alignment's `error.code` is one of:

| `error.code` | Meaning |
| - | - |
| `transcript_unalignable` | The transcript has no words that can be aligned. |
| `audio_too_short` | The audio is too short to align. |
| `aligner_unavailable` | The aligner could not process the request. |
| `aligner_timeout` | The aligner took too long. |
| `interrupted` | The request was interrupted on Chariot's side. |
| `internal_error` | Something went wrong on Chariot's side. |

Every failed alignment is refunded in full, and its `credits_used` reads `0`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.