# Numbers as digits

**The problem.** Speech models write numbers as words: "one thousand three hundred fifty five dollars twenty eight
cents". Your application needs `$1,355.28` to show an order, fill a form or check a payment. With `numerals`, the
transcript arrives with digits.

**Where it works.** Every model.

> **Use the SDK.** The Gladia SDKs stream the audio, keep it until the API has transcribed it, and reconnect after a network drop
without losing a word. Most integration issues come from hand-written WebSocket clients: start from the SDK
examples below.

## Step by step

### 1. Turn it on

```json
{ "numerals": true }
```

### 2. Start a session with it

Each program streams a 16-bit WAV file and prints what comes back. Put your API key in the `GLADIA_API_KEY` environment variable, then run `python numerals.py meeting.wav`.

**Python SDK**

```python title="numerals.py"
# pip install gladiaio-sdk requests
import os
import sys
import threading
import wave

import requests
from gladiaio_sdk import GladiaClient, LiveV2ConnectSessionOptions

API_URL = os.environ.get("GLADIA_API_URL", "https://eu1.api.gladia.io")
API_KEY = os.environ["GLADIA_API_KEY"]

with wave.open(sys.argv[1]) as wav:  # a 16-bit PCM WAV file
    rate, channels = wav.getframerate(), wav.getnchannels()
    audio = wav.readframes(wav.getnframes())

config = {
    "encoding": "wav/pcm",
    "sample_rate": rate,
    "bit_depth": 16,
    "channels": channels,
    "model": "solaria-3",
    "language_config": {
        "languages": [
            "en"
        ]
    },
    "numerals": True
}

client = GladiaClient(api_key=API_KEY, api_url=API_URL)
# `numerals` is a Gladia Enterprise option the Python SDK does not send yet
# (gladiaio/sdk#61): create the session over HTTP, then let the SDK stream it.
created = requests.post(f"{API_URL}/v2/live", headers={"x-gladia-key": API_KEY}, json=config, timeout=10)
created.raise_for_status()
info = created.json()
session = client.live_v2().connect_session(LiveV2ConnectSessionOptions(id=info["id"], url=info["url"]))
ended, errors = threading.Event(), []

@session.on("message")
def on_message(message):
    if message.type == "transcript" and message.data.is_final:
        print(message.data.utterance.text)

session.on("error", errors.append)
session.once("ended", lambda _: ended.set())

step = rate * channels * 2 // 10  # 100 ms of audio per chunk
for start in range(0, len(audio), step):
    session.send_audio(audio[start : start + step])  # live audio: send each chunk as it is captured
session.stop_recording()

ended.wait(timeout=60)
sys.exit(1 if errors or not ended.is_set() else 0)
```

**JavaScript SDK**

```js title="numerals.mjs"
// npm install @gladiaio/sdk
import { readFileSync } from "node:fs";
import { GladiaClient } from "@gladiaio/sdk";

const { sampleRate, channels, pcm } = readWav(process.argv[2]); // a 16-bit PCM WAV file

const config = {
  encoding: "wav/pcm",
  sample_rate: sampleRate,
  bit_depth: 16,
  channels,
  model: "solaria-3",
  language_config: {
    languages: [
      "en"
    ]
  },
  numerals: true
};

const client = new GladiaClient({
  apiKey: process.env.GLADIA_API_KEY,
  apiUrl: process.env.GLADIA_API_URL ?? "https://eu1.api.gladia.io",
});
// `numerals` is a Gladia Enterprise option not in the SDK's types yet: the SDK sends it as is.
const session = client.liveV2().startSession(config);

session.on("message", (message) => {
  if (message.type === "transcript" && message.data.is_final) {
    const { text, text_raw } = message.data.utterance;
    console.log(text, text_raw ? `(spoken: ${text_raw})` : "");
  }
});
session.on("error", (error) => {
  console.error(error);
  process.exitCode = 1;
});
session.once("ended", () => process.exit());

const step = (sampleRate * channels * 2) / 10; // 100 ms of audio per chunk
for (let start = 0; start < pcm.length; start += step) {
  session.sendAudio(pcm.subarray(start, start + step)); // live audio: send each chunk as it is captured
}
session.stopRecording();

function readWav(path) {
  const file = readFileSync(path);
  let fmt, pcm;
  for (let pos = 12; pos < file.length; ) {
    const id = file.toString("ascii", pos, pos + 4);
    const size = file.readUInt32LE(pos + 4);
    if (id === "fmt ") fmt = { channels: file.readUInt16LE(pos + 10), sampleRate: file.readUInt32LE(pos + 12) };
    if (id === "data") pcm = file.subarray(pos + 8, pos + 8 + size);
    pos += 8 + size + (size % 2);
  }
  return { ...fmt, pcm };
}
```

**HTTP**

Create the session, then stream its audio to the returned `url` ([Live WebSocket](/api-reference/v2/live/websocket/)):

```bash
curl -X POST https://eu1.api.gladia.io/v2/live \
  -H "x-gladia-key: $GLADIA_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "encoding": "wav/pcm",
    "sample_rate": 16000,
    "bit_depth": 16,
    "channels": 1,
    "model": "solaria-3",
    "language_config": {
      "languages": [
        "en"
      ]
    },
    "numerals": true
  }'
```

### 3. Read the result

```json
{
  "type": "transcript",
  "data": {
    "is_final": true,
    "utterance": {
      "text": "The total is $1,355.28 for 42 items.",
      "text_raw": "The total is one thousand three hundred fifty five dollars twenty eight cents for forty two items."
    }
  }
}
```

`text` has the digits; `text_raw` keeps the words as spoken.

| spoken | written |
|---|---|
| "forty two" | `42` |
| "one thousand three hundred fifty five dollars twenty eight cents" | `$1,355.28` |
| "mille trois cent cinquante-cinq euros vingt-huit" (French) | `1 355,28 €` |
| "call me at nine fifteen" | `call me at 9:15` |
| "rendez-vous à neuf heures quinze" (French) | `rendez-vous à 9h15` |
| "um halb zehn" (German) | `um 9:30` |
| "a las nueve y cuarto" (Spanish) | `a las 9:15` |

Clock times are written the way the language writes them, with no extra setting.

## Phone numbers and dates

Phone numbers are written in their country's format and dates the way the language writes them. Nothing to add:
`"numerals": true` is enough wherever the turn router runs (the default), and your messages keep their shape (no
turn-router label is added to the finals).

| spoken | written |
|---|---|
| "zéro six douze trente-quatre cinquante-six soixante-dix-huit" (French) | `06 12 34 56 78` |
| "two one two five five five zero one two three" | `(212) 555-0123` |
| "plus thirty three six twelve thirty four fifty six seventy eight" | `+33 6 12 34 56 78` |
| "march twelfth twenty twenty six" | `March 12, 2026` |
| "march twelfth twenty twenty six" (with `"region": "GB"`) | `12 March 2026` |
| "le douze mars deux mille vingt-six" (French) | `le 12 mars 2026` |
| "am zwölften märz" (German) | `am 12. März` |
| "el primero de mayo" (Spanish) | `el 1 de mayo` |
| "il primo maggio" (Italian) | `il 1º maggio` |

**How it works.** The turn router reads each final transcript and tells what its numbers are: an amount, a phone
number, a date, a clock time, a count. Numerals then writes each one for what it is:

- **a phone number the speaker has finished** is written in the national format of its country. A spoken country
  code ("plus thirty three") gives the international format. A number that no country's numbering plan knows stays
  as digits;
- **a phone number still being dictated** keeps its digits as said, the pauses' commas removed: it is formatted once
  the speaker has given all of it;
- **a date** is written the language's way (English, French, German, Spanish, Italian, Portuguese, Dutch);
- **a clock time, a date, a rank or a phone number** is never written as an amount.

**Without the turn router** (`"turn_router": false`, or a host that does not run it), numerals writes numbers,
clock times and amounts only: phone numbers and dates stay as plain digits. Digits alone cannot tell a phone number from an order number, an
account number or a code, and formatting those as phone numbers would change them.

### The country

Phone numbers and dates follow a country. Set it with `region`, an ISO 3166-1 alpha-2 code in capitals:

```json
{ "numerals": { "region": "CH" } }
```

| `region` | "zéro six douze trente-quatre cinquante-six soixante-dix-huit" |
|---|---|
| omitted (French) | `06 12 34 56 78` |
| `CH` | `061 234 56 78` |

Without `region`, the countries where the language is spoken apply, in this order: English US, GB, CA, AU, IE, NZ,
IN, ZA; French FR, BE, CH, CA, LU; German DE, AT, CH; Spanish ES, MX, AR, CO, CL, PE; Italian IT, CH; Portuguese
PT, BR; Dutch NL, BE. A phone number is formatted for the first of these countries whose numbering plan knows it as
dialled. A `region` that is not two capital letters is refused with a 400.

## Limits and gotchas

- **The conventions of the language spoken.** Amounts take that language's separators and currency position: set
  the language (`language_config.languages`) when you know it, or the detected one applies.
- **Partials too.** Partial transcripts show digits as well, and may change while the number is still being said.
  Phone numbers and dates are formatted in the final transcripts only.
- **Phone numbers and dates need the turn router.** It runs by default; with `"turn_router": false` they stay as
  plain digits.
- **Numbers said in pieces.** "my card is 4970… 1234…" stays in one utterance thanks to the [number
  hold](/live/endpointing/#number-hold), on by default. With `turn_router: false`, each piece may come as its own
  final.
- **Vocabulary is kept.** A [custom vocabulary](/features/custom-vocabulary/) phrase made of number words ("Seven
  Eleven") stays as you listed it.
- **Search and storage.** Store `text` for display and `text_raw` if you match on spoken words.
- **With PII redaction**, numbers that are personal data (card, phone, account numbers) are redacted after the
  conversion.

## Troubleshooting

| symptom | cause | fix |
|---|---|---|
| amounts use the wrong separators | another language was detected | set `language_config.languages` |
| a long number is split over several finals | `turn_router: false`, or a pause over 2 s | keep the number hold on |
| a product name with numbers was converted | it is not in your vocabulary | add it to [custom vocabulary](/features/custom-vocabulary/) |
| a phone number or a date stays as plain digits | the turn router is off (`"turn_router": false`) | remove `"turn_router": false` |
| a phone number is in another country's format | the language's first country was used | set `numerals.region` |
| a phone number stays as digits with the turn router on | no country's numbering plan knows it as dialled, or the speaker had not finished it | check the digits and `numerals.region` |
