> For the full page index, fetch https://echonova.ai/llms.txt

# Custom terminology

Send your own names, products and acronyms with a transcription and get the transcript back spelled your way, with every correction listed, near misses to review, and the same terms on a live stream.

Speech to Text writes what it hears. A bank name, a product or an acronym
often comes out spelled the way it sounds: `امريتس ان بي دي` instead of
`Emirates NBD`, or `kay why see` instead of `KYC`. Send your terms in the
`terminology` field and the result carries a corrected copy of the
transcript, with every change listed.

The words as heard are never lost. `transcription` stays exactly as it was
transcribed; the corrected text is in `terminology.corrected`.

## When to use it

- Your callers say brand, product or place names that a general model
  spells its own way.
- You need acronyms written as letters: `KYC`, `IBAN`, `SWIFT`.
- The same name is said in Arabic and English, and you want one spelling.

Terminology does not change what the model hears. It corrects the text
after transcription, so a word the model missed entirely cannot be added.

## Quick example

Send `terminology` as a JSON string beside `file` and `language`. It works
on `POST /v1/speech-to-text/sync` and `POST /v1/speech-to-text`.

```bash
curl -X POST "https://api.echonova.ai/v1/speech-to-text/sync" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@call.mp3;type=audio/mpeg" \
  -F "language=ar" \
  -F 'terminology={"keywords": ["KYC", {"term": "Emirates NBD", "variants": ["امريتس ان بي دي"]}]}'
```

```python
import json
import requests

terminology = {
    "keywords": [
        "KYC",
        {"term": "Emirates NBD", "variants": ["امريتس ان بي دي"]},
        {"term": "SWIFT", "context_words": ["transfer", "تحويل"]},
    ]
}

with open("call.mp3", "rb") as audio:
    response = requests.post(
        "https://api.echonova.ai/v1/speech-to-text/sync",
        headers={"Authorization": "Bearer YOUR_API_KEY"},
        files={"file": ("call.mp3", audio, "audio/mpeg")},
        data={"language": "ar", "terminology": json.dumps(terminology)},
    )

data = response.json()["data"]
print(data["transcription"])              # as heard
print(data["terminology"]["corrected"])   # spelled your way
```

The response adds a `terminology` object:

```json
{
  "transcription": "حولت المبلغ عبر السويفت إلى امريتس ان بي دي أمس.",
  "terminology": {
    "raw": "حولت المبلغ عبر السويفت إلى امريتس ان بي دي أمس.",
    "corrected": "حولت المبلغ عبر الـSWIFT إلى Emirates NBD أمس.",
    "edits": [
      { "start": 16, "end": 23, "original": "السويفت", "replacement": "الـSWIFT", "stage": "phonetic", "score": 1 },
      { "start": 28, "end": 43, "original": "امريتس ان بي دي", "replacement": "Emirates NBD", "stage": "exact", "score": 1 }
    ],
    "nearMisses": [],
    "warnings": [],
    "mapperStatus": "applied",
    "mapperVersion": "3.0.0"
  }
}
```

With the asynchronous endpoint, the same object is on the job from
`GET /v1/speech-to-text/jobs/{jobId}` once it completes, and in the
`job.completed` webhook.

## Keyword fields

`terminology` is `{"keywords": [...]}`. Each keyword is a plain string, or
an object when you want more control.

| Field           | Type                          | What it does                                                                                                                                      |
| --------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `term`          | string, required              | The spelling you want in the output. A plain string keyword is the same as `{"term": "..."}`.                                                     |
| `variants`      | list of strings, up to 10     | Ways the term is misheard that you have seen. Each one is matched exactly.                                                                        |
| `blocked`       | list of strings, up to 20     | Phrases that must never become this term, for example `swift delivery` for `SWIFT`.                                                               |
| `context_words` | list of strings, up to 20     | Words that, when one appears within eight words, let a less certain match apply.                                                                  |
| `needs_context` | boolean                       | `true` applies the term only next to a context word or another keyword. Leave it out and it is decided for you when the term is an everyday word. |
| `output_script` | `latin`, `arabic`, `as_typed` | The script to write the term in. The term is always written as you typed it.                                                                      |

Terms, variants and other phrases are up to 100 characters each. Fields not
in this table are ignored.

## How matching works

Matching ignores case, Arabic hamza forms (`أ` `إ` `آ`), `ى` and `ة`, and
diacritics. A match is always whole words and never crosses a comma or the
end of a sentence.

| `stage`    | What matched                                                                                           |
| ---------- | ------------------------------------------------------------------------------------------------------ |
| `exact`    | The term itself, one of your variants, or an acronym spelled out (`kay why see`, `كي واي سي` → `KYC`). |
| `phonetic` | Words that sound the same as the term, in either script.                                               |
| `fuzzy`    | Words spelled close to the term.                                                                       |

`score` runs from 0 to 1. Exact and phonetic matches apply. A fuzzy match
applies when it is close enough, or when it is a little less close but one
of your `context_words`, or another keyword, is nearby. Short acronyms such
as `IT` or `API` match exactly only, so they never replace an everyday word.

Arabic prefixes stay on the word: `السويفت` becomes `الـSWIFT`, and
`بالكي واي سي` becomes `بالـKYC`. Digits, e-mail addresses and URLs are
never changed.

## Read the result

| Field           | What it holds                                                                                         |
| --------------- | ----------------------------------------------------------------------------------------------------- |
| `raw`           | The transcript as heard: the same text as `transcription`.                                            |
| `corrected`     | `raw` with your terms applied. Outside the edits it is identical to `raw`.                            |
| `edits`         | Every replacement, in order. `start` and `end` are character offsets in `raw`, end-exclusive.         |
| `nearMisses`    | Close matches that were not applied. See below.                                                       |
| `warnings`      | Anything that limited the mapping, each with a `code` and a `detail`.                                 |
| `mapperStatus`  | `applied`, `no_keywords`, or `skipped` when the mapping could not run. `corrected` then equals `raw`. |
| `mapperVersion` | The version of the matcher that answered.                                                             |

Use the offsets to highlight corrections in your own interface:
`raw.slice(edit.start, edit.end)` is always `edit.original`.

Without the `terminology` field the response has no `terminology` key at
all, and the endpoint behaves exactly as before.

### Downloads stay as heard

`GET /v1/speech-to-text/jobs/{jobId}/export` builds TXT, SRT and JSON from
`transcription` and its timed segments, so downloads keep the words as
heard. Take `terminology.corrected` from the job for the corrected text.

## Near misses

A near miss is a match the service was not sure enough to apply, for
example `اونلاين بانكنق` for `Online Banking` at a score of 0.81. It is
listed in `nearMisses` with the words heard, the `candidate` term and a
`score`, and the transcript is left unchanged there.

Nothing is remembered between requests. To have a near miss fixed next
time, show it to a person, and when they confirm it, add the heard words to
that term's `variants` in the list you send.

## Live streams

On `wss://api.echonova.ai/v1/speech-to-text/stream`, send a `terminology`
message at any time. It applies to every segment after it:

```json
{
  "type": "terminology",
  "terminology": { "keywords": ["KYC", { "term": "Emirates NBD", "variants": ["امريتس ان بي دي"] }] }
}
```

The server answers `{"type": "terminology.set", "active": true}`, and each
`transcript` message then carries a `terminology` object for its segment,
with offsets into that segment's `text`. Send it again to replace the list,
or with `"terminology": null` to clear it.

A list that is not valid gets an `error` message with code
`validation_failed` and is ignored; the session continues with the terms it
had. The message must fit in one 64 KiB frame, which holds a few hundred
keywords.

## Limits

| Limit                     | Value                                                      |
| ------------------------- | ---------------------------------------------------------- |
| Keywords                  | 500 per request                                            |
| Field size                | 256 KB on the file endpoints; one 64 KiB frame on a stream |
| Term, variant, phrase     | 100 characters                                             |
| Variants                  | 10 per keyword                                             |
| Blocked and context words | 20 each per keyword                                        |

Terminology is free: the request costs the same with or without it.

## Errors

A `terminology` field that cannot be read answers `400` before anything is
transcribed or charged.

| Status | `errorMessage`                                                                                                                            | Fix                                                 |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `400`  | `terminology must be a JSON object with a keywords list`                                                                                  | Send a JSON string such as `{"keywords": ["KYC"]}`. |
| `400`  | `each terminology keyword needs a term of 1 to 100 characters`                                                                            | Give every keyword a term of 1 to 100 characters.   |
| `400`  | `terminology variants, blocked and context_words must be lists of text of up to 100 characters; at most 10 variants and 20 of the others` | Send lists of short phrases within the limits.      |
| `400`  | `terminology needs_context must be true or false, and output_script latin, arabic or as_typed`                                            | Fix the value, or leave the field out.              |
| `400`  | `terminology allows at most 500 keywords`                                                                                                 | Send the terms this audio is likely to contain.     |
| `400`  | `terminology must be at most 256 KB`                                                                                                      | Send a shorter list.                                |

If the matching itself fails or takes too long, the transcription still
completes: `mapperStatus` is `skipped`, `corrected` equals `raw`, and
`warnings` says why.

## FAQ

### Does terminology make transcription slower?

Barely. A list of a few thousand keywords on a five-minute call adds well
under a second.

### Can I store a list and refer to it by name?

No. Send the full list with every request. Keep it in your own system and
add confirmed near misses to it over time.

### Why did a term not apply?

Check `nearMisses` first: the match may have been too uncertain. Then add
the heard words as a variant, or add `context_words` so a close match can
apply when they are nearby.

### Is the Speech to Text playground the same feature?

Yes. The playground's Terminology option sends the same field and shows the
corrected transcript with each change highlighted.

## Related

- [Speech to Text formats and output](/docs/developers/stt-formats-and-output)
- [Live transcription](/docs/developers/stt-streaming)
- [Transcribe audio synchronously](/docs/reference/stt-transcribe-sync)
- [Stream speech to text](/docs/reference/stt-stream)
- [Errors](/docs/developers/errors)
