Polish Whisper Normalizer
Polish port of OpenAI Whisper normalizers — preserves
ąćęłńóśźż, normalizes numbers, time, currency, dates and declensions via Morfeusz2.
English
Features
| Area | Example | Normalized |
|---|---|---|
| Diacritics | Żółć! |
żółć (kept) / zolc with remove_diacritics=True |
| Cardinal / ordinal | sto dwadzieścia trzy, dwudziestu pięciu, pierwszego → 1 |
123, 25, 1 (ordinal dots dropped) |
| Numeral separation | 10, 500, 90 vs dwa tysiące dwadzieścia trzy |
10 500 90 vs 2023 (punctuation/composition aware) |
| Time | piąta trzydzieści, wpół do ósmej, o piątej, piąta rano, od piątej do szóstej |
5:30, 7:30, o 5:00, 5:00 rano, od 5:00 do 6:00 |
Time with minut |
dziesięć minut po piątej, za dwadzieścia minut ósma |
5:10, 7:40 |
| Fractions | jedna trzecia, trzy czwarte |
1/3, 3/4 (keeps /, idempotent) |
| Half | pół litra, półtora, dwa i pół |
0.5 litra, 1.5, 2.5 |
| Currency | pięć złotych, €10, pięć złotówek |
5 zł, 10 €, 5 zł |
| Percent | pięć procentów |
5% |
| Ordinal multipliers | tysiąc dziewięćsetny |
1900 |
| Dates | 5 maja → 05.05 · piątego maja → 05.05 · piątego maja 2026 → 05.05.2026 · piątego maja roku dwa tysiące dwudziestego szóstego → 05.05.2026 |
DD.MM / DD.MM.YYYY zero-padded, conditional (maja alone stays maja, avoids Maja→5) |
| Geographic guard | na północ stays, jest północ → jest 0:00 |
no false 0:00 for north |
Pipeline (text.py): lower → brackets/parens/ignore → r.→roku → sentence periods/ellipsis → time (Morfeusz) → decimal ,→. → numeral boundaries → remove_symbols(keep=".:/%$€£¢+-") → numbers (Morfeusz) → months (conditional, Morfeusz) → dates → ordinal-dot cleanup. Configurable via PolishTextNormalizer(date_format=...). Diacritic-less ASR (czterdziesci, piec, wpol) handled via utils + Morfeusz.generate.
Installation
uv sync # dev + morfeusz2
uv pip install -e . # editable
# or from PyPI
uv pip install polish-whisper-normalizer
# with jiwer extra
uv pip install "polish-whisper-normalizer[jiwer]"
Requires Python >=3.10, regex, morfeusz2.
Quickstart
from polish_whisper_normalizer import PolishTextNormalizer, BasicTextNormalizer
n = PolishTextNormalizer()
n("Było piętnaście po piątej, minus dziesięć stopni.")
# → "było 5:15 -10 stopni"
n("Spotkanie dwudziestego pierwszego maja o piętnastej trzydzieści.")
# → "spotkanie 21.05 o 15:30"
n("piątego maja roku dwa tysiące dwudziestego szóstego")
# → "05.05.2026" # uniform, no trailing "r"
n("5 maja") # digit day works too
# → "05.05"
n("pięć złotówek, pięć procentów, pół litra, jedna trzecia")
# → "5 zł 5% 0.5 litra 1/3"
# custom date format
PolishTextNormalizer(date_format="%Y-%m-%d")("piątego maja 2026")
# → "2026-05-05"
PolishTextNormalizer(date_format="{day}/{month}/{year}")("piątego maja 2026")
# → "5/5/2026"
# diacritics
BasicTextNormalizer()("Żółć!") # → "żółć"
BasicTextNormalizer(remove_diacritics=True)("Żółć!") # → "zolc"
# diacritic-less numbers also work
PolishTextNormalizer()("trzysta czterdziesci osiem") # → "348"
Jiwer — WER with Polish normalization
uv pip install "polish-whisper-normalizer[jiwer]"
# or
uv sync --extra jiwer
Without normalization jiwer.wer penalizes piątego maja 2026 vs 05.05.2026 as 100% error. With PolishTransform they match:
import jiwer
from polish_whisper_normalizer.jiwer import wer, PolishTransform, polish_transform
# helper (recommended) — normalizes both sides with PolishTextNormalizer
wer("piątego maja 2026", "05.05.2026") # → 0.0
wer("o piątej", "o 5:00") # → 0.0
wer("pięć złotówek", "5 zł") # → 0.0
# raw jiwer would be 1.0
jiwer.wer("piątego maja 2026", "05.05.2026") # → 1.0
# via jiwer transforms (for pipelines)
jiwer.wer("piątego maja 2026", "05.05.2026",
reference_transform=polish_transform,
hypothesis_transform=polish_transform) # → 0.0
# custom date_format is forwarded
wer("piątego maja 2026", "2026-05-05", date_format="%Y-%m-%d") # → 0.0
# manual Compose
tr = jiwer.Compose([PolishTransform(date_format="%Y-%m-%d"), jiwer.RemoveMultipleSpaces(), jiwer.Strip(), jiwer.ReduceToListOfListOfWords()])
jiwer.wer("piątego maja 2026", "2026-05-05",
reference_transform=tr, hypothesis_transform=tr) # → 0.0
PolishTransform wraps PolishTextNormalizer (date_format kwarg supported) and returns str; polish_transform is the ready Compose ending with ReduceToListOfListOfWords required by jiwer.wer.
Architecture
src/polish_whisper_normalizer/
basic.py # Whisper basic normalizer, diacritics-aware
lemmatizer.py # PolishLemmatizer – thin Morfeusz2 wrapper (analyse/generate)
utils.py # strip_diacritics, with_ascii_variants – single place for ASCII fallback
numbers.py # PolishNumberNormalizer – cardinals/ordinals/currency/percent
time.py # PolishTimeNormalizer – HH:MM, wpół/za/po, geographic guard
text.py # PolishTextNormalizer – full pipeline: time → numbers → months → dates
polish.py # compatibility re-export shim
jiwer.py # PolishTransform / wer helper
Declension is never re-implemented: base nominative lexicons are stored, all declined forms are resolved via PolishLemmatizer.analyse and generate (Morfeusz2). ASCII-folded variants are derived once via utils and Morfeusz.generate, so diacritic-less ASR needs no duplicated dictionaries.
Components
from polish_whisper_normalizer import PolishNumberNormalizer, PolishTimeNormalizer, PolishLemmatizer
PolishNumberNormalizer()("dwudziestu pięciu złotych") # → "25 zł"
PolishTimeNormalizer()("wpół do ósmej") # → "7:30"
PolishLemmatizer().analyse("dwudziestu") # → [("dwadzieścia","num")]
| Class | Description |
|---|---|
PolishTextNormalizer(date_format="{day:02d}.{month:02d}.{year}") |
Full pipeline, date_format supports str.format ({day}, {month}, {year}) and strftime (%d.%m.%Y) |
PolishNumberNormalizer |
Words → digits, currency/percent/decimals/signs/ordinals |
PolishTimeNormalizer |
Spoken time → HH:MM, guards o 5. stronie vs o 5:00, na północ vs 0:00 |
PolishLemmatizer |
Morfeusz2 wrapper |
BasicTextNormalizer |
Whisper basic but diacritics-aware |
API Docs
Full API: neonfeline.github.io/polish-whisper-normalizer (mkdocs serve locally).
Development
uv sync --group dev
uv run pytest -q # 600+ tests
uv run mypy src # strict, py.typed
uv run ruff check src tests && uv run ruff format --check src tests
uv build
mkdocs serve # strict
py.typed+mypy --strict(warn_unused_ignores=false)ruff+pre-commit+ GitHub Actions (ci.yml: lint → mypy → pytest --cov → build)- Validated on BIGOS v2 + PELCRA (2397 samples, ~5% number words)
License
MIT — see LICENSE (inherits Whisper MIT for basic.py).
Polski
Funkcje
| Obszar | Przykład | Po normalizacji |
|---|---|---|
| Znaki diakrytyczne | Żółć! |
żółć (zachowane) |
| Liczebniki | sto dwadzieścia trzy, pierwszego |
123, 1. |
| Czas | piąta trzydzieści, o piątej, od piątej do szóstej |
5:30, o 5:00, od 5:00 do 6:00 |
| Daty | 5 maja → 05.05 · piątego maja 2026 → 05.05.2026 |
DD.MM / DD.MM.RRRR, warunkowo (maja samo → maja) |
| Waluta / procent / ułamki | pięć złotówek, procentów, 1/3, pół litra |
5 zł, 5%, 1/3, 0.5 litra |
Potok: lower → czas → liczby → miesiące (warunkowo) → daty.
Instalacja i użycie (PL)
uv sync # dev + morfeusz2
uv pip install polish-whisper-normalizer # z PyPI
uv pip install "polish-whisper-normalizer[jiwer]" # z jiwer
from polish_whisper_normalizer import PolishTextNormalizer
n = PolishTextNormalizer() # date_format="{day:02d}.{month:02d}.{year}" domyślnie
n("piątego maja roku dwa tysiące dwudziestego szóstego")
# → "05.05.2026" # jednolicie, bez "r"
n("5 maja") # też działa
# → "05.05"
# własny format daty
PolishTextNormalizer(date_format="%Y-%m-%d")("piątego maja 2026")
# → "2026-05-05"
# bez znaków diakrytycznych też działa
n("trzysta czterdziesci osiem")
# → "348"
maja jako imię Maja zostaje maja (nie 5), na północ nie staje się 0:00.
Jiwer — WER po polsku
uv pip install "polish-whisper-normalizer[jiwer]"
from polish_whisper_normalizer.jiwer import wer
wer("piątego maja 2026", "05.05.2026") # → 0.0 (bez normalizacji 1.0)
wer("o piątej", "o 5:00") # → 0.0
Rozwój / testy
Jak wyżej — uv run pytest, mypy, ruff, mkdocs serve.
Metadata
Release files for polish-whisper-normalizer 0.1.13
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| polish_whisper_normalizer-0.1.13.tar.gz | 28.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| polish_whisper_normalizer-0.1.13-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 59.9 kB
Release files / polish_whisper_normalizer-0.1.13.tar.gz
| Download URL | polish_whisper_normalizer-0.1.13.tar.gz |
|---|---|
| Size | 28.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e6ec0f8eb705da2abb426579cddcb8bca35849839c5ccc72812f3ebf3dfffb9a
|
|
BLAKE2b-256 checksum How to use checksums |
343140d23075d0a6d118c311182dbef8ebac229beae11d26d80774f37dfe9fb6
|
| 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 28, 2026.
Transparency logRelease files / polish_whisper_normalizer-0.1.13-py3-none-any.whl
| Download URL | polish_whisper_normalizer-0.1.13-py3-none-any.whl |
|---|---|
| Size | 31.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5315d44bfcd925b2f704e60de6ec5c1feafbf52f878b52f17d6b555dbd7e1957
|
|
BLAKE2b-256 checksum How to use checksums |
6066386b945c06af2934444170a2ba4ae2b84baa607d9017798b8aa4b279fcb7
|
| 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 28, 2026.
Transparency log