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.
Step by step
Section titled “Step by step”1. Turn it on
Section titled “1. Turn it on”{ "numerals": true }2. Start a session with it
Section titled “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.
# pip install gladiaio-sdk requestsimport osimport sysimport threadingimport wave
import requestsfrom 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 chunkfor start in range(0, len(audio), step): session.send_audio(audio[start : start + step]) # live audio: send each chunk as it is capturedsession.stop_recording()
ended.wait(timeout=60)sys.exit(1 if errors or not ended.is_set() else 0)// npm install @gladiaio/sdkimport { 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 chunkfor (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 };}Create the session, then stream its audio to the returned url (Live WebSocket):
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
Section titled “3. Read the result”{ "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
Section titled “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
Section titled “The country”Phone numbers and dates follow a country. Set it with region, an ISO 3166-1 alpha-2 code in capitals:
{ "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
Section titled “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": falsethey stay as plain digits. - Numbers said in pieces. “my card is 4970… 1234…” stays in one utterance thanks to the number
hold, on by default. With
turn_router: false, each piece may come as its own final. - Vocabulary is kept. A custom vocabulary phrase made of number words (“Seven Eleven”) stays as you listed it.
- Search and storage. Store
textfor display andtext_rawif you match on spoken words. - With PII redaction, numbers that are personal data (card, phone, account numbers) are redacted after the conversion.
Troubleshooting
Section titled “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 |
| 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 |