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

# Phoneme control

> Set how Coda pronounces brand names, people's names, and domain terms with a custom lexicon over HTTP, WebSocket, or gRPC. The guide also covers how to write X-SAMPA pronunciations and fix a rejected request.

Send a `customLexicon` list with a Coda request, and Coda speaks each listed word or phrase with the X-SAMPA pronunciation you supply. The lexicon works in English, German, Spanish, French, Italian, and Portuguese. Send it over HTTP, WebSocket, or gRPC.

Each lexicon entry pairs a spelling with the pronunciation that Coda uses for it:

```json theme={null}
"customLexicon": [
  { "spelling": "Aoife", "pronunciation": "\" i . f @" },
  { "spelling": "kubectl", "pronunciation": "\" k u b . k @ n . % t r\\ oU l" }
]
```

## Draft pronunciations with an AI assistant

An AI assistant, such as Claude or ChatGPT, can draft pronunciations from your language's phoneme table. Give it this prompt, with your language, its page from [Supported languages](#supported-languages), and your words in place of the English examples:

```text theme={null}
Write Rime custom lexicon entries for these words, as spoken in English (US):
Aoife, kubectl, Velkora

Use only the X-SAMPA phonemes on this page, and follow its rules:
https://docs.rime.ai/platform/phoneme-control/en.md
Also follow the format rules in "Write a pronunciation" on this page:
https://docs.rime.ai/platform/phoneme-control.md

Return only a JSON array of {"spelling": ..., "pronunciation": ...} objects.
```

A draft gets close, but it is not always right. Send it, and fix any entry that the API rejects with [Fix a rejected request](#fix-a-rejected-request). Then listen to each word. If a word still sounds wrong, contact [support@rime.ai](mailto:support@rime.ai).

## Before you start

Keep these points in mind:

* **Coda only.** Mist v3 rejects a request that carries a custom lexicon.
* **Send it to the Coda API.** Use `https://coda.api.rime.ai/` for HTTP, `wss://coda.api.rime.ai/ws` for WebSocket, or `coda.api.rime.ai:443` for gRPC.
* **Six languages.** Coda accepts a lexicon in the six [supported languages](#supported-languages). A lexicon on an Arabic, Dutch, Hindi, or Japanese request fails with `400`.
* **One bad entry fails the whole request.** The API checks every entry before synthesis starts and names each ill-formed one. It never falls back to the default pronunciation.
* **At most 500 entries per request.**
* **The API stores nothing.** Send the lexicon with every request, or once per [WebSocket](#websocket) connection.
* **Coda follows a pronunciation closely.** A pronunciation that sounds natural in the request's language works best. Listen to the result before you ship it.

## Supported languages

The API checks each pronunciation against the phoneme inventory of the request language, so a string that one language accepts can fail in another.

| Language | Tag | Stress marks | Phoneme inventory |
| :- | :- | :- | :- |
| English (US) | `en` | One primary stress | [English](/platform/phoneme-control/en.md) |
| German (Germany) | `de` | One primary stress | [German](/platform/phoneme-control/de.md) |
| Spanish (Latin America) | `es` | One primary stress | [Spanish](/platform/phoneme-control/es.md) |
| French (France) | `fr` | None | [French](/platform/phoneme-control/fr.md) |
| Italian (Italy) | `it` | One primary stress | [Italian](/platform/phoneme-control/it.md) |
| Portuguese (Brazil) | `pt` | One primary stress | [Portuguese](/platform/phoneme-control/pt.md) |

## Write a pronunciation

Use these rules to check a draft or to write a pronunciation yourself. A pronunciation is a single word written in [X-SAMPA](https://en.wikipedia.org/wiki/X-SAMPA), a plain-ASCII encoding of the International Phonetic Alphabet. Separate every phoneme and every mark with a space. The API reads an unspaced string such as `h@lo` as one unknown phoneme and rejects it.

| Mark | Meaning |
| :- | :- |
| `.` | Syllable boundary |
| `"` | Primary stress. Place it at the start of the stressed syllable. |
| `%` | Secondary stress. Place it like `"`. |

For example, English `hello` is `h E . " l oU`: two syllables, with primary stress on the second.

A pronunciation must also follow these rules:

* **Use only the language's phonemes.** Each language page lists them. Diphthongs such as English `aI` and `oU` are single phonemes.
* **Mark exactly one primary stress**, with `"` at the start of a syllable. French has no lexical stress, so a French pronunciation carries no `"` or `%`.
* **Give each syllable one vowel.** Separate adjacent vowels with `.`: the API rejects two full vowels in one syllable.
* **Do not stress a reduced vowel** such as English `@` when a full vowel in the word could carry the stress.
* **Start each syllable with a consonant cluster the language allows.** English accepts `" t r\ i` but rejects `" S b eI`, because no English syllable begins with `S b`.

The invented brand name `Velkora` shows how the inventory and stress rule change with the language:

| Language | Pronunciation |
| :- | :- |
| English | `v E l . " k oU . r\ @` |
| German | `v E l . " k o: . R a` |
| Spanish | `b e l . " k o . 4 a` |
| French | `v E l . k O . R a` |
| Italian | `v e l . " k O . r a` |
| Portuguese | `v e w . " k O . 4 6` |

> [!TIP]
>
> Escape `"` and `\` inside a JSON string. Write English `r\` as `"r\\"` and primary stress as `\"`. English `Velkora` becomes `"v E l . \" k oU . r\\ @"`.

## How entries match your text

Spellings match your text by these rules:

* **Whole words and phrases, ignoring case.** `Aoife` matches `aoife` and `AOIFE`, but not the `aoife` inside `aoifestudio`. Write a phrase such as `San Jose` with one space between its words. The phrase then matches those words with one or more spaces between them, but not across a tab or line break.
* **Spellings match the words Coda reads.** The API expands abbreviations, numbers, and symbols before it looks for your entries. So write each spelling the way Coda reads it: `Doctor` for `Dr.`, and `forty-two` for `42`. A written form such as `Dr.`, `AT&T`, `C++`, or `42` never matches, and the request still succeeds. The [Text Normalization API](/api-reference/other/textnorm.md) returns the words that Coda reads.
* **Entries still apply inside URLs and email addresses.** Coda reads `www.velkora.com` as a web address and speaks `velkora` the way your entry says.
* **Every occurrence.** Coda uses the pronunciation each time the spelling appears.
* **The longest spelling wins.** If you list both `San` and `San Jose`, `San Jose` takes the phrase and `San` covers the other occurrences.
* **The last duplicate wins.** If you list a spelling twice, the API uses the later entry.
* **Coda speaks a phrase as one word.** Write its pronunciation with one primary stress: `San Jose` is `% s { n . h oU . " z eI`.

## Send a custom lexicon

Every transport carries the same list of `{spelling, pronunciation}` entries: `customLexicon` in JSON and `custom_lexicon` in the Protobuf schema. Set the request language to a [supported language](#supported-languages) and choose a voice that speaks it. The language field is `lang` over HTTP and `language` over WebSocket and gRPC.

### HTTP

This request synthesizes one sentence with the `kubectl` entry and writes the audio to `output.wav`:

```bash theme={null}
curl --fail-with-body https://coda.api.rime.ai/ \
  -H "Authorization: Bearer $RIME_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: audio/wav' \
  -d '{
    "text": "Run kubectl apply on the cluster.",
    "speaker": "astra",
    "lang": "en",
    "customLexicon": [
      { "spelling": "kubectl", "pronunciation": "\" k u b . k @ n . % t r\\ oU l" }
    ]
  }' \
  -o output.wav
```

Play `output.wav` and listen for "kube control" where the text says `kubectl`. To hear the difference, run the request again without `customLexicon`. If the API rejects the request, curl exits with an error and `output.wav` holds the JSON error body. [Fix a rejected request](#fix-a-rejected-request) explains each message.

### WebSocket

A WebSocket context takes its lexicon from `start.customLexicon`. To apply one lexicon to every context on a connection, send it in `config.defaults.customLexicon`. You can send `config` only once, before the first `start`. A non-empty `customLexicon` in a `start` replaces the connection default for that context.

Install the `ws` package with `npm install ws`. Save this example as `lexicon.mjs`, and run it with Node.js 22 or later. It sends your API key in the upgrade header, sets the lexicon in `config`, and writes `output.wav` after the API sends `done`:

```javascript theme={null}
import { writeFileSync } from "node:fs";
import WebSocket from "ws";

const socket = new WebSocket("wss://coda.api.rime.ai/ws", "rime.v1.json", {
  headers: { Authorization: `Bearer ${process.env.RIME_API_KEY}` },
});
const chunks = [];
const send = (message) => socket.send(JSON.stringify(message));

socket.on("open", () => {
  send({ config: { defaults: {
    customLexicon: [
      { spelling: "kubectl", pronunciation: '" k u b . k @ n . % t r\\ oU l' },
    ],
  } } });
});

socket.on("unexpected-response", (_request, response) => {
  console.error(`Upgrade failed with HTTP ${response.statusCode}`);
  process.exitCode = 1;
});

socket.on("message", (data) => {
  const message = JSON.parse(data);
  if (message.error) {
    console.error(message.error);
    process.exitCode = 1;
    socket.close();
    return;
  }
  if (message.ready) {
    send({ contextId: "example", start: {
      text: "Run kubectl apply on the cluster.",
      speaker: "astra", language: "en",
      audioParameters: { audioFormat: "audio/wav" },
    } });
  }
  if (message.contextId !== "example") return;
  if (message.audio) chunks.push(Buffer.from(message.audio, "base64"));
  if (message.done) {
    writeFileSync("output.wav", Buffer.concat(chunks));
    socket.close();
  }
});
```

The API checks the connection-level lexicon against each context's language when the context starts. A context that fails a lexicon check gets an `error` event, and the connection stays open. Keep each `config` and `start` message under the [64 KiB message limit](/api-reference/coda/websocket-v1.md#limits-and-connection-health). A larger message closes the socket without an `error` event. See the [Coda WebSocket API](/api-reference/coda/websocket-v1.md#run-a-synthesis-context) for the full context lifecycle.

### gRPC

Add `customLexicon` to the `SynthesisRequest` for `Synthesize`, or to the `header` for `SynthesizeStreaming`. Use the schema from the [`rime-api`](https://pypi.org/project/rime-api/) Python package, version 0.3.0 or later. Its `schema` directory holds the [Rime schema](/docs/on-prem/protocols.md#grpc) and the Google protos it imports. These commands install the package and print the directory:

```bash theme={null}
python3 -m pip install "rime-api>=0.3.0"
python3 -c "import pathlib, rime_api; print(pathlib.Path(rime_api.__file__).parent / 'schema')"
```

This command sends your API key as `authorization` metadata and prints the streamed response messages with base64 `audio` fields. Replace `./rime-protos` with the `schema` path:

```bash theme={null}
grpcurl \
  -H "authorization: Bearer $RIME_API_KEY" \
  -import-path ./rime-protos \
  -proto rime/text_to_speech.proto \
  -d '{
    "text": "Run kubectl apply on the cluster.",
    "speaker": "astra",
    "language": "en",
    "audioParameters": { "audioFormat": "audio/wav" },
    "customLexicon": [
      { "spelling": "kubectl", "pronunciation": "\" k u b . k @ n . % t r\\ oU l" }
    ]
  }' \
  coda.api.rime.ai:443 rime.TextToSpeech/Synthesize
```

A generated client sets the repeated `custom_lexicon` field with `PronunciationEntry` messages. To call a self-hosted Coda engine, send the same request to the engine's gRPC address, as [Protocols](/docs/on-prem/protocols.md#grpc) shows.

## Fix a rejected request

A rejected request produces no audio. Over HTTP, the API returns `400` with `kind` set to `invalid_input`. Over a WebSocket, the context gets an `error` event with the same `kind` and message. Over gRPC, the call fails with `INVALID_ARGUMENT` and the same message. The message names each ill-formed entry, starting with `custom-lexicon entry`, and lists its failed checks.

```json theme={null}
{
  "error": {
    "kind": "invalid_input",
    "message": "Invalid argument: stage 'validate_pronunciation' failed: custom-lexicon entry \"hello\": \"h E . l oU\" is not well-formed (no-primary-stress)",
    "requestId": "..."
  }
}
```

The last row of this table applies only to a WebSocket connection:

| Message or check | Cause | Fix |
| :- | :- | :- |
| `custom lexicon is not supported by this model` | The request went to a Mist model. | Send it to Coda. |
| `custom lexicon is not supported for language "ja"` | The request's language does not accept a custom lexicon. | Remove the lexicon, or use a [supported language](#supported-languages). |
| `custom lexicon has 640 entries; the maximum is 500` | Too many entries. | Send only the entries this request's text needs. |
| `unknown-phone` | A token is not in the language's inventory, often because spaces are missing. | Use the language page's phonemes, with a space between every token. |
| `no-primary-stress`, `multi-primary-stress` | Zero or several `"` marks. | Mark exactly one primary stress. |
| `unexpected-stress` | A French pronunciation contains `"` or `%`. | Remove the stress marks. |
| `mid-stress`, `end-stress` | A stress mark is not at the start of a syllable. | Put the mark before the syllable's first phoneme. |
| `addtl-boundary` | A leading, trailing, or doubled `.`. | Put `.` only between syllables. |
| `no-nucleus`, `multi-nuclei`, `invalid-nucleus` | A syllable has no vowel or more than one. | Give each syllable one vowel phoneme, and add `.` between adjacent vowels. |
| `multi-stress` | One syllable has two stress marks. | Keep one. |
| `stressed-reduced` | A reduced vowel such as `@` carries the stress. | Move the stress to a full vowel. |
| `unknown-onset` | The syllable starts with a cluster the language does not allow. | Move the boundary to split the cluster across two syllables. |
| `invalid-coda`, `dup-consonant` | The syllable ends in a sequence the language does not allow, or repeats a consonant. | Move the boundary, or remove the repeated consonant. |
| `empty-spelling` | An entry has an empty `spelling`. | Give the entry a spelling, or remove it. |
| `config must be sent at most once, before the first start` | A second `config` on the same connection. This `error` event has no `contextId`. | Send later lexicons in `start`. |

## Related

* [Pronunciation control](/platform/pronunciation-control.md): check dictionary coverage, or ask Rime to add a word


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