Skip to content

Latest commit

 

History

171 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Premove ITN — open-source, context-aware inverse text normalization for conversational voice-agent transcripts

PyPI CI License Model weights

Open-source, context-aware inverse text normalization for conversational voice-agent transcripts, with open weights.

Premove ITN converts spoken ASR output into canonical written forms for phone numbers, email addresses, identifiers, dates, times, money, measurements, and alphanumeric codes.

Read how I ended up building Premove ITN →

Demo

Demo.mp4

The ambiguity

System Output
Input the room code is one oh five
Expected the room code is 105
Premove ITN the room code is 105
NVIDIA Thutmose the room code is 1 oh 5
text-processing-rs the room code is 01:05

The spoken form is ambiguous. Context tells us that one oh five is an identifier, not a time.

These are the retained outputs for row va6_collision_0013 in the frozen evaluation.

Results

On our frozen benchmark, Premove ITN reaches 99.50% semantic accuracy on the voice-agent subset, compared with 68.25% for text-processing-rs and 67.00% for NVIDIA Thutmose.

Backend Voice-agent semantic Overall semantic Mean warm latency
Premove ITN 99.50% (398/400) 89.70% 56.49 ms
NVIDIA Thutmose 67.00% (268/400) 59.39% 15.98 ms
text-processing-rs 68.25% (273/400) 55.79% 0.14 ms

The benchmark is a frozen, balanced synthetic stress suite with 1,500 rows and includes a dedicated 400-row voice-agent subset. It was held out from both training and checkpoint selection, so these scores come from unseen evaluation data. Semantic accuracy checks whether the structured value is correct while allowing approved formatting differences. Latency is warm, sequential batch-one inference on an Apple M4 MacBook Air; every backend received transcript text only. This is a synthetic stress benchmark, not a sample of live production traffic.

Full report · Detailed results · Reproduce the benchmark

Quick start

Install from PyPI:

pip install premove-itn

To update an existing installation to the current library and its matching model release:

python -m pip install --upgrade premove-itn
from premove_itn import PremoveITN

itn = PremoveITN.from_pretrained()

print(itn.normalize("the room code is one oh five"))
# the room code is 105

from_pretrained() downloads the current frozen model weights from premove-ai/premove-itn on first use and caches them locally. The package pins the model to the release revision, so upgrading the package automatically selects the matching model snapshot on the next initialization. Existing cached release snapshots remain available for pinned deployments.

Create one PremoveITN instance and reuse it across requests. Model loading is expensive; warm normalization calls are much faster.

Targeted release comparison

We also checked these 10 contextual TIME-versus-identifier transcripts against both releases. The v0.1.0 output matched the intended result on 2/10 rows; the v0.2.0 output matched all 10/10 rows. marks a v0.1.0 inaccuracy and marks an output that matches the intended result. This is a targeted smoke set, not a frozen benchmark. The queue-position phrase is excluded because its identifier/time labels were semantically inconsistent.

Transcript v0.1.0 output v0.2.0 output Correction
send someone to desk six forty after lunch send someone to desk 06:40 after lunch send someone to desk 640 after lunch desk 06:40desk 640
we can start the demo around nine twenty we can start the demo around 09:20 we can start the demo around 09:20 No change; 09:20 is correct
they moved me into cabin eight fifteen today they moved me into cabin 08:15 today they moved me into cabin 815 today cabin 08:15cabin 815
schedule the technician for eleven forty five and send them to unit two ten schedule the technician for 11:45 and send them to unit 02:10 schedule the technician for 11:45 and send them to unit 210 unit 02:10unit 210
I need bus four twenty but I won't get there until four twenty I need bus 04:20 but I won't get there until 04:20 I need bus 420 but I won't get there until 04:20 bus 04:20bus 420; arrival time remains 04:20
our table is seven thirty and dinner starts at eight ten our table is 07:30 and dinner starts at 08:10 our table is 07:30 and dinner starts at 08:10 No change; both times are correct
try locker twelve oh six, I'll be there at twelve oh six try locker 1206, I'll be there at 1206 try locker 1206, I'll be there at 12:06 Arrival 120612:06
our table number is seven thirty and dinner starts at eight ten our table number is 07:30 and dinner starts at 08:10 our table number is 730 and dinner starts at 08:10 table number 07:30table number 730
the courier marked package twenty one forty and said he'd arrive around twenty one forty the courier marked package 21:40 and said he'd arrive around 21:40 the courier marked package 2140 and said he'd arrive around 21:40 package 21:40package 2140; arrival time remains 21:40
take elevator three twelve, then meet me downstairs at three twelve take elevator 03:12, then meet me downstairs at 03:12 take elevator 312, then meet me downstairs at 03:12 elevator 03:12elevator 312; meeting time remains 03:12

For reproducible deployments, pin the package version:

pip install premove-itn==0.2.0

Why this exists

I ran into this problem while building a voice agent. Deepgram gave me transcripts in spoken form, but the tools behind the agent needed normalized values. A person can read one hundred twenty three and know it means 123; an API usually cannot.

My first solution was text-processing-rs. It was extremely fast and handled straightforward normalization well. But some spoken forms are impossible to normalize correctly without the sentence around them. one oh five might mean 105, 1:05, or something else entirely. Rules can generate plausible answers, but they cannot always know which one the speaker meant.

That sent me looking for context-aware ITN. I tried NVIDIA Thutmose, but on the structured values I cared about for tool calls, I still found surprisingly simple failures.

Premove ITN came from a different idea: do not ask the model to perform the entire normalization. Generate the valid written forms first, then train the model only to decide which one fits the context. An exact decoder handles the rest.

How Premove ITN works

Premove ITN splits normalization into three steps: generate, score, decode.

Simplified example:

input
enter the digits four one seven two zero one two
                         │
                         ▼
1. GENERATE

Python enumerates spans; Rust generates valid written forms.

[four]                               → 4
[four one]                           → 41 | 04:01
[one seven two]                      → 172 | 17:02
...
[four one seven two zero one two]    → 4172012 | 417-2012
                         │
                         ▼
2. SCORE

DeBERTa encodes the transcript once.

Each candidate gets one score from:
contextual source span + candidate kind labels + proposed replacement
                         │
                         ▼
3. DECODE

Candidates can overlap, so Premove ITN uses exact dynamic programming
to find the highest-scoring compatible path through the transcript.
                         │
                         ▼
output
enter the digits 4172012

The rules decide what can be written. The model decides what fits the context. The decoder decides which edits can coexist.

Supported forms

Premove ITN covers English structured values commonly needed by voice agents:

Form Example
Numbers one hundred twenty120
Digit sequences zero eight two zero six three082063
Times four thirty04:30
Dates march fifth twenty twenty fourmarch 5 2024
Money twenty dollars$20
Decimals one point five1.5
Measurements two hundred meters200 m
Ordinals the eighth8th
Phone numbers eight one four two three one four814-2314
Email and URLs support at example dot comsupport@example.com
Versions and identifiers v three dot one dot ninev3.1.9
Punctuation comma,
Abbreviations doctordr.

Not supported

Premove ITN currently targets English structured text. It does not provide first-class normalization for non-English speech, street addresses, free-form rewriting, or arbitrary application-specific formats.

Source text is preserved wherever no candidate is selected.

See the full candidate coverage for the exact forms supported by each realizer.

Limitations

  • English only.
  • Normalization is bounded by the deterministic candidates generated by the Rust realizers. The scorer cannot select a written form that was not generated.
  • Inputs longer than 512 DeBERTa encoder tokens are rejected rather than silently truncated.
  • A 435.6M-parameter DeBERTa-v3-large contextual scorer.
  • About a 1.6 GB first model download.
  • Multi-second model initialization.
  • A custom candidate-scoring architecture; it is not a generic AutoModel.from_pretrained() model.
  • A synthetic stress benchmark, not observed live-traffic accuracy.
  • CUDA, Windows, macOS Intel, Linux ARM64, and other accelerators are not validated v0.2.0 support claims.

Documentation

Model, citation, and license

Model and weights

The current public release is v0.2.0:

The public inference artifact is premove-ai/premove-itn, release v0.2.0, at the immutable commit e42a6ad5f58d3fde9cb6cf1f81f7fe40b9d99526. PremoveITN.from_pretrained() uses that commit by default and verifies the resolved revision, release metadata, base model, and model-file digest before inference.

The artifact is inference-only. It excludes optimizer state, scheduler state, training counters, training data, and evaluation rows. See the artifact release record and training provenance.

The released scorer was trained on 418,000 examples across general text normalization, conversational adaptation, and structured-value adaptation. The frozen evaluation was excluded from training and checkpoint selection; see the training provenance.

Citation

The contextual scorer uses microsoft/deberta-v3-large at a pinned revision as its encoder backbone. Premove ITN adds a custom candidate scorer and exact decoder around the DeBERTaV3 architecture.

License and attribution

Premove ITN source code and model weights are MIT licensed. The contextual scorer uses microsoft/deberta-v3-large at a pinned revision as its encoder backbone. Candidate scoring and exact decoding are Premove ITN-specific.

The Rust realization layer uses text-processing-rs, which is Apache-2.0 licensed. Required third-party licenses and notices are in THIRD_PARTY_NOTICES.md and LICENSES/.

Releases

Packages

Contributors

Languages