Skip to main content

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

https://github.com/user-attachments/assets/25319385-ca21-443c-9d35-9399c5276ff3

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.

v0.3.0 structured normalization and temporal context

v0.3.0 adds structured results and deterministic temporal enrichment without changing the frozen scorer or the ordinary readable normalization path. One inference produces three views:

from datetime import datetime

from premove_itn import NormalizationContext, PremoveITN

itn = PremoveITN.from_pretrained()
result = itn.normalize_structured(
    "schedule it for tomorrow",
    context=NormalizationContext(
        reference_datetime=datetime(2026, 9, 20, 12, 0),
    ),
)

print(result.text)
# schedule it for tomorrow
print(result.resolved_text)
# schedule it for 2026-09-21
print(result.spans[0].resolved_value)
# 2026-09-21

normalize() returns readable normalized text, normalize_resolved() returns the resolved text view, and normalize_structured() returns both views plus immutable spans with exact source and normalized offsets. Context is always caller supplied. It is never inferred from the system clock, locale, or timezone.

The temporal resolver supports relative dates, bounded day/week offsets, calendar-week weekdays, named dates, weekday-qualified dates, and numeric dates. See the Python API for the complete contract.

v0.2.0 contextual model update

v0.2.0 improves contextual disambiguation between identifiers and times, reaching 10/10 on a targeted comparison where v0.1.0 reached 2/10. For example:

send someone to desk six forty after lunch
→ send someone to desk 640 after lunch

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 210

See the complete targeted comparison for the outputs, methodology, and limitations. Release-level changes are summarized in the changelog.

For reproducible deployments, pin the package version:

pip install premove-itn==0.3.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 twenty → 120
Digit sequences zero eight two zero six three → 082063
Times four thirty → 04:30
Dates march fifth twenty twenty four → march 5 2024
Money twenty dollars → $20
Decimals one point five → 1.5
Measurements two hundred meters → 200 m
Ordinals the eighth → 8th
Phone numbers eight one four two three one four → 814-2314
Email and URLs support at example dot com → support@example.com
Versions and identifiers v three dot one dot nine → v3.1.9
Punctuation comma → ,
Abbreviations doctor → dr.

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.3.0 package support claims.

Documentation

Model, citation, and license

Model and weights

The current public package release is v0.3.0. It uses the unchanged, frozen v0.2.0 model artifact because the v0.3.0 changes are in the structured runtime and deterministic temporal resolver:

The public inference artifact is premove-ai/premove-itn, model release v0.2.0, at the immutable commit e42a6ad5f58d3fde9cb6cf1f81f7fe40b9d99526. PremoveITN.from_pretrained() uses that model 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/.

Metadata

Release files for premove-itn 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for premove-itn 0.3.0
File Size Uploaded
premove_itn-0.3.0.tar.gz 91.2 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for premove-itn 0.3.0
File
premove_itn-0.3.0-cp313-cp313-manylinux_2_28_x86_64.whl CPython 3.13 CPython 3.13 Linux glibc 2.28+ x86-64 Details
premove_itn-0.3.0-cp313-cp313-macosx_11_0_arm64.whl CPython 3.13 CPython 3.13 macOS 11.0+ ARM64 Details
premove_itn-0.3.0-cp312-cp312-manylinux_2_28_x86_64.whl CPython 3.12 CPython 3.12 Linux glibc 2.28+ x86-64 Details
premove_itn-0.3.0-cp312-cp312-macosx_11_0_arm64.whl CPython 3.12 CPython 3.12 macOS 11.0+ ARM64 Details
premove_itn-0.3.0-cp311-cp311-manylinux_2_28_x86_64.whl CPython 3.11 CPython 3.11 Linux glibc 2.28+ x86-64 Details
premove_itn-0.3.0-cp311-cp311-macosx_11_0_arm64.whl CPython 3.11 CPython 3.11 macOS 11.0+ ARM64 Details

Total release size: 4.1 MB

Release files / premove_itn-0.3.0.tar.gz

Download URL premove_itn-0.3.0.tar.gz
Size 91.2 kB
Tags Source
SHA-256 checksum
How to use checksums
e68d1484f9d5ea511cae8d9de868f82975186aed1ff52b51e07cdd347cebe944
BLAKE2b-256 checksum
How to use checksums
1eb152e2aeb2c3b208a234c85ac60ab56d54977fc7f7375864e6868fa8ad8a82
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / premove_itn-0.3.0-cp313-cp313-manylinux_2_28_x86_64.whl

Download URL premove_itn-0.3.0-cp313-cp313-manylinux_2_28_x86_64.whl
Size 714.2 kB
Tags CPython 3.13 Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
1b7a8138d548a3533345abc67f291f9c0bc5fea296dd4145feb016b1fd1595a6
BLAKE2b-256 checksum
How to use checksums
6eab37bcf6aee94a9b7fd5b5d8a3db3615cf6108b2194caffd13a4efc708a960
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / premove_itn-0.3.0-cp313-cp313-macosx_11_0_arm64.whl

Download URL premove_itn-0.3.0-cp313-cp313-macosx_11_0_arm64.whl
Size 627.3 kB
Tags CPython 3.13 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
37ee94d4f8173b55e22b6888e85ff3a53d6052d39c6b52786df8f6707b64b86a
BLAKE2b-256 checksum
How to use checksums
340e10ebcdddb5be35759d276d3ba1553158a51abb0805a32c7074a88a3913c7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / premove_itn-0.3.0-cp312-cp312-manylinux_2_28_x86_64.whl

Download URL premove_itn-0.3.0-cp312-cp312-manylinux_2_28_x86_64.whl
Size 714.2 kB
Tags CPython 3.12 Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
2cf7b44d048e0943d585eb2ea2e3af3b774020eafca9f4a7251402c74b6e091f
BLAKE2b-256 checksum
How to use checksums
ae9b60b19f1e2fd6c4f562e3567d8ae04aa1696bdf49511e940a4ae7be984f66
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / premove_itn-0.3.0-cp312-cp312-macosx_11_0_arm64.whl

Download URL premove_itn-0.3.0-cp312-cp312-macosx_11_0_arm64.whl
Size 627.1 kB
Tags CPython 3.12 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
0f2d8a5d6b21f72b63b72142e2b75c8877284c798a52f5a8bee2b6f1b1db8b1c
BLAKE2b-256 checksum
How to use checksums
93adc24789efa78ac6cd488666b67d41980896a77d2ce615c714b30a8eae24a8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / premove_itn-0.3.0-cp311-cp311-manylinux_2_28_x86_64.whl

Download URL premove_itn-0.3.0-cp311-cp311-manylinux_2_28_x86_64.whl
Size 716.9 kB
Tags CPython 3.11 Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
dd338cdf1ddc24dc4d48fdd2669ff54731a09c144c49e1c8c89f5af2720e615f
BLAKE2b-256 checksum
How to use checksums
725da5c7495e7cacfe24798644b5285721209923d60e6b317dd385b50effd0e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / premove_itn-0.3.0-cp311-cp311-macosx_11_0_arm64.whl

Download URL premove_itn-0.3.0-cp311-cp311-macosx_11_0_arm64.whl
Size 631.7 kB
Tags CPython 3.11 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
ca6f3cfb63621b5a699a2fde5d22c72158ae4ced03cd1b0fd8afa22164696e18
BLAKE2b-256 checksum
How to use checksums
014e66ad342a5f91b9e9b63c0cf5580f9d7aa69383236b72da158fe8c7a31ff3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

7 release files

0.2.0

7 release files

0.1.0

7 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page