Skip to main content

Bambara Text Normalizer

Text Normalization & ASR Evaluation Framework for Bambara (Bamanankan)

Installation • Normalization • ASR Evaluation • Modes • CLI • Linguistics • References

PyPI version


Purpose

This tool serves two complementary purposes for Bambara language processing:

Purpose Description
Text Normalization Standardize Bambara text for any downstream NLP task (TTS, MT, NER, etc.)
ASR Evaluation Fair WER/CER computation that accounts for valid orthographic variation

Installation

# Core library only
pip install bambara-text-normalizer

# Install with CLI support
pip install "bambara-text-normalizer[cli]"

# Install everything
pip install "bambara-text-normalizer[all]"
pip install git+https://github.com/sudoping01/bambara-text-normalization.git

Text Normalization

from bambara_normalizer import normalize

# Default: expand contractions
normalize("⁠Ne k’a ma ko ayi")           
normalize("⁠K’ale t’a fɛ k’a kɛ")    
normalize("⁠K’i k’i janto i yɛrɛ la")        


# Contract mode: collapse expanded forms
normalize("Ne ko a ma ko ayi", mode="contract")    
normalize("Ko ale tɛ a fɛ ka o kɛ", mode="contract")    
normalize("Ko i ka i janto i yɛrɛ la", mode="contract")    

# Preserve mode: don't touch contractions
normalize("K’i k’i janto i yɛrɛ la", mode="preserve")     

Custom Settings

from bambara_normalizer import normalize

# Full control over normalization
text = normalize(
    "Ka na son k’o k’a la",
    mode="expand",                      # "expand" | "contract" | "preserve"
    preserve_tones=False,               
    normalize_legacy_orthography=True, 
    lowercase=True,                     
    remove_punctuation=False,           
    normalize_whitespace=True,         
    normalize_apostrophes=True,         
    normalize_special_chars=True,    
    expand_dates = False,
    expand_measurements=False, 
    expand_numbers=False,  
    expand_times=False,            
    expand_arithmetic=False,
    remove_diacritics_except_tones=False,  
    handle_french_loanwords=True,   
    strip_repetitions=False,       
    normalize_compounds=True, 
)

Using BambaraNormalizer Class

For repeated normalization with consistent settings:

from bambara_normalizer import BambaraNormalizer, BambaraNormalizerConfig


config = BambaraNormalizerConfig(contraction_mode="expand") # change it to "contract" or preserve
normalizer = BambaraNormalizer(config)
normalizer("A y'a fɔ")      
normalizer("k'a la")   

# Contraction mode
config = BambaraNormalizerConfig(contraction_mode="contract")
normalizer = BambaraNormalizer(config)
normalizer("bɛ a fɔ")     
normalizer("ka a ta")     

Predefined Configuration Presets

from bambara_normalizer import BambaraNormalizer, BambaraNormalizerConfig

# For WER evaluation (aggressive normalization, removes tones)
normalizer = BambaraNormalizer(BambaraNormalizerConfig.for_wer_evaluation())

# For WER with contract mode
normalizer = BambaraNormalizer(BambaraNormalizerConfig.for_wer_evaluation(mode="contract"))

# For CER evaluation
normalizer = BambaraNormalizer(BambaraNormalizerConfig.for_cer_evaluation())

# Preserve tone marks
normalizer = BambaraNormalizer(BambaraNormalizerConfig.preserving_tones())

# Minimal normalization (only essential fixes)
normalizer = BambaraNormalizer(BambaraNormalizerConfig.minimal())

Number Normalization

The normalizer supports bidirectional number conversion between digits and Bambara words (TN/ITN).

With Normalizer

from bambara_normalizer import normalize

normalize("A ye 100 sɔrɔ", expand_numbers=True)   # => "a ye kɛmɛ sɔrɔ"
normalize("A ye 100 sɔrɔ", expand_numbers=False)  # => "a ye 100 sɔrɔ"

# WER preset has expand_numbers=True by default
normalize("A ye 5 ta", preset="wer")  # => "a ye duuru ta"

Digits to Words (Text Normalization)

from bambara_normalizer import number_to_bambara, normalize_numbers_in_text


number_to_bambara(5)        # => "duuru"
number_to_bambara(123)      # => "kɛmɛ ni mugan ni saba"
number_to_bambara(1000)     # => "waa kelen"
number_to_bambara(5.3)      # => "duuru tomi saba"

# In text
normalize_numbers_in_text("A ye 5 wari di")      # => "A ye duuru wari di"
normalize_numbers_in_text("Mɔgɔ 100 nana")       # => "Mɔgɔ kɛmɛ nana"
normalize_numbers_in_text("N ye shekɛ 1000 sɔrɔ") # => "N ye shekɛ waa kelen sɔrɔ"

Words to Digits (Inverse Text Normalization)

from bambara_normalizer import bambara_to_number, denormalize_numbers_in_text

bambara_to_number("duuru")                    # => 5
bambara_to_number("kɛmɛ ni mugan ni saba")    # => 123
bambara_to_number("duuru tomi saba")          # => 5.3

# In text
denormalize_numbers_in_text("A ye duuru di a ma")  # => "A ye 5 di a ma"
denormalize_numbers_in_text("Mɔgɔ kɛmɛ nana")      # => "Mɔgɔ 100 nana"

Number Vocabulary

Value Bambara Value Bambara
0 fu 10 tan
1 kelen 20 mugan
2 fila 30 bi saba
3 saba 40 bi naani
4 naani 50 bi duuru
5 duuru 100 kɛmɛ
6 wɔɔrɔ 1000 waa
7 wolonwula 1,000,000 miliyɔn
8 seegin decimal tomi
9 kɔnɔntɔn connector ni


Date Normalization

The normalizer supports bidirectional date conversion between standard formats and Bambara expressions (TN/ITN).

With Normalizer

from bambara_normalizer import normalize

normalize("A bɛ na 13-10-2024 la", expand_dates=True)   # ==> "a bɛ na oktɔburu kalo tile tan ni saba san baa fila ni mugan ni naani la"
normalize("A bɛ na 13-10-2024 la", expand_dates=False)  # => "a bɛ na 13-10-2024 la"

# WER preset has expand_dates=True by default
normalize("A bɛ na 25-01-2008 la", preset="wer")  # => "a bɛ na zanwuye kalo tile mugan ni duuru san baa fila ni seegin la"

Date to Bambara (Text Normalization)

from bambara_normalizer import date_to_bambara, format_date_bambara, normalize_dates_in_text
from datetime import date

# Single dates
date_to_bambara(2024, 10, 13)      # => "Oktɔburu kalo tile tan ni saba san baa fila ni mugan ni naani"
date_to_bambara(2008, 1, 25)       # => "Zanwuye kalo tile mugan ni duuru san baa fila ni seegin"

# Without "kalo" (month), which is said by default
date_to_bambara(2024, 10, 13, include_kalo=False)  # => "Oktɔburu tile tan ni saba san ..."

# With day of week
date_to_bambara(2024, 10, 13, include_day_of_week=True)  # => "Kari Oktɔburu kalo tile ..." (Sunday)

# From date object or string
format_date_bambara(date(2024, 10, 13))  # => "Oktɔburu kalo tile tan ni saba san ..."
format_date_bambara("13-10-2024")        # => "Oktɔburu kalo tile tan ni saba san ..."

# In text
normalize_dates_in_text("A bɛ na 13-10-2024 la")  # => "A bɛ na Oktɔburu kalo tile tan ni saba san baa fila ni mugan ni naani la"

Bambara to Date (Inverse Text Normalization)

from bambara_normalizer import bambara_to_date

bambara_to_date("Oktɔburu tile tan ni saba san baa fila ni mugan ni naani")
# => datetime.date(2024, 10, 13)

bambara_to_date("Zanwuye tile mugan ni duuru san baa fila ni seegin")
# => datetime.date(2008, 1, 25)

Date Format

Bambara dates follow this structure:

[Month] kalo tile [day] san [year]

Example: 13-10-2024 => Oktɔburu kalo tile tan ni saba san baa fila ni mugan ni naani

Literal translation: "October month day thirteen year two thousand twenty-four"

kalo ("month") is said by default. To leave it out, pass include_kalo=False to the date functions, to normalize(), or to BambaraNormalizerConfig.

A year is said with baa, not waa. Besides full dates, a four-digit year written after san is read as a year: san 2026 => san baa fila ni mugan ni wɔɔrɔ. A year with no san before it (2026) stays a plain number. Limitation: san is also the verb "buy", so a price after it (A ye a san 2500 la) is read with baa; a currency after the digits (san 2500 FCFA) keeps it a price.

Days of the Week

Day Bambara Day Bambara
Monday Tɛnɛn Friday Juma
Tuesday Tarata Saturday Sibiri
Wednesday Araba Sunday Kari
Thursday Alamisa

Months of the Year

Month Bambara Month Bambara
January Zanwuye July Zuluye
February Feburuye August Uti
March Marsi September Sɛtanburu
April Awirili October Oktɔburu
May Mɛ November Nɔwanburu
June Zuwen December Desanburu

Time Normalization

The normalizer supports bidirectional time and duration conversion between standard formats and Bambara expressions (TN/ITN).

With Normalizer

from bambara_normalizer import normalize

normalize("A nana 7:30 la", expand_times=True)   # => "a nana nɛgɛ kanɲɛ wolonwula ni sanga bi saba la"
normalize("A nana 7:30 la", expand_times=False)  # => "a nana 7:30 la"

# WER preset has expand_times=True by default
normalize("A nana 13:50 la", preset="wer")  # => "a nana nɛgɛ kanɲɛ tan ni saba ni sanga bi duuru la"

Clock Time to Bambara (Text Normalization)

from bambara_normalizer import time_to_bambara, format_time_bambara, normalize_times_in_text
from datetime import time

# Clock times
time_to_bambara(1, 0)       # => "Nɛgɛ kanɲɛ kelen"
time_to_bambara(1, 5)       # => "Nɛgɛ kanɲɛ kelen ni sanga duuru"
time_to_bambara(7, 30)      # => "Nɛgɛ kanɲɛ wolonwula  ni sanga bi saba"
time_to_bambara(13, 50)     # => "Nɛgɛ kanɲɛ tan ni saba ni sanga bi duuru"

# From time object or string
format_time_bambara(time(7, 30))  # => "Nɛgɛ kanɲɛ wolonwula ni sanga bi saba"
format_time_bambara("13:50")      # => "Nɛgɛ kanɲɛ tan ni sab ni sanga bi duuru"

# In text
normalize_times_in_text("A nana 7:30 la")  # => "A nana nɛgɛ kanɲɛ wolonwula ni sanga bi saba la"

Bambara to Clock Time (Inverse Text Normalization)

from bambara_normalizer import bambara_to_time

bambara_to_time("Nɛgɛ kanɲɛ kelen")
# => datetime.time(1, 0)

bambara_to_time("Nɛgɛ kanɲɛ wolonwula ni sanga bi saba")
# => datetime.time(7, 30)

Duration to Bambara

from bambara_normalizer import duration_to_bambara, format_duration_bambara

# Durations
duration_to_bambara(minutes=30)                      # => "miniti bi saba"
duration_to_bambara(hours=1, minutes=30)             # => "lɛrɛ kelen ni miniti bi saba"
duration_to_bambara(hours=1, minutes=30, seconds=10) # => "lɛrɛ kelen ni miniti bi saba ni segɔni tan"

# From string format
format_duration_bambara("30min")      # => "miniti bi saba"
format_duration_bambara("1h30min")    # => "lɛrɛ kelen ni miniti bi saba"
format_duration_bambara("1h30min10s") # => "lɛrɛ kelen ni miniti bi saba ni segɔni tan"

Bambara to Duration (Inverse Text Normalization)

from bambara_normalizer import bambara_to_duration

bambara_to_duration("miniti bi saba")
# => (0, 30, 0)  # (hours, minutes, seconds)

bambara_to_duration("lɛrɛ kelen ni miniti bi saba")
# => (1, 30, 0)

bambara_to_duration("lɛrɛ kelen ni miniti bi saba ni segɔni tan")
# => (1, 30, 10)

Time Format

Clock time follows this structure:

Nɛgɛ kanɲɛ [hour] ( ni sanga [minutes])

Example: 7:30 => Nɛgɛ kanɲɛ wolonwula ni sanga bi saba

Literal translation: "Clock needle seven passed with minute thirty"

Duration follows this structure:

(lɛrɛ [hours] ni) (miniti [minutes] ni) (segɔni [seconds])

Clock time or duration: the convention

Bambara does not mark whether 8h is a time of day or a length of time: Nin kɛ 2h can mean "do it at 2" or "do it in two hours". The normalizer cannot read the meaning of the sentence, and the surrounding words are no reliable cue, so it follows a convention on how the hour is written:

Written Read as Example
8h, 8 h, 8h00, 10h30, 14:30 clock time 8h → Nɛgɛ kanɲɛ seegin
24h00, 24:00 clock time (end of day) Nɛgɛ kanɲɛ mugan ni naani
an hour no clock shows duration 48h → lɛrɛ bi naani ni seegin, 25h30 → lɛrɛ mugan ni duuru ni miniti bi saba
unit written as a word duration 8 heures, 8 hrs, 30 minutes, 20 mn, 1h30min

A clock time counts minutes with sanga; a duration counts them with miniti. An impossible time written with a colon (25:30) is left as it is, because a colon also writes scores, ratios and verse numbers.

A bare hour is a clock time because that is how it is written in French-style text: in human-translated Malian news, a bare Xh was a time of day in 7 of 9 cases (à 4h du matin, vers 8h), and both durations were 72 h.

Limitation: a sentence that means a duration but writes a bare hour, such as A sɛgɛnna 2h ("he was tired for 2 hours"), is read as a clock time. Write the duration with a unit word (2 heures) to get lɛrɛ.

Spelling: the clock word is written Nɛgɛ kanɲɛ. Text written with the earlier spelling Nɛgɛ kaɲɛ still converts back to a time.

Example: 1h30min10s => lɛrɛ kelen ni miniti bi saba ni segɔni tan


Measurement Normalization

The normalizer supports bidirectional measurement conversion between standard units and Bambara expressions (TN/ITN).

With Normalizer

from bambara_normalizer import normalize

normalize("A ye 5 kg san", expand_measurements=True)   # => "a ye kilogaramu duuru san"
normalize("A ye 5 kg san", expand_measurements=False)  # => "a ye 5 kg san"

# WER preset has expand_measurements=True by default
normalize("So in bɛ 100 m", preset="wer")  # => "so in bɛ mɛtɛrɛ kɛmɛ"

Measurement to Bambara (Text Normalization)

from bambara_normalizer import measurement_to_bambara, format_measurement_bambara, normalize_measurements_in_text

# Weight
measurement_to_bambara(5, "kg")      # => "kilogaramu duuru"
measurement_to_bambara(100, "g")     # => "garamu kɛmɛ"

# Length
measurement_to_bambara(10, "km")     # => "kilomɛtɛrɛ tan"
measurement_to_bambara(100, "m")     # => "mɛtɛrɛ kɛmɛ"
measurement_to_bambara(50, "cm")     # => "santimɛtɛrɛ bi duuru"

# Volume
measurement_to_bambara(2, "L")       # => "litiri fila"
measurement_to_bambara(500, "mL")    # => "mililitiri kɛmɛ duuru"

# Area
measurement_to_bambara(3, "ha")      # => "ɛkitari saba"
measurement_to_bambara(100, "m²")    # => "mɛtɛrɛ kare kɛmɛ"

# Decimal values
measurement_to_bambara(2.5, "L")     # => "litiri fila tomi duuru"

# From string format
format_measurement_bambara("5kg")    # => "kilogaramu duuru"
format_measurement_bambara("100 m")  # => "mɛtɛrɛ kɛmɛ"

# In text
normalize_measurements_in_text("A ye 5 kg san")  # => "A ye kilogaramu duuru san"
normalize_measurements_in_text("So in bɛ 100 m") # => "So in bɛ mɛtɛrɛ kɛmɛ"

Bambara to Measurement (Inverse Text Normalization)

from bambara_normalizer import bambara_to_measurement, denormalize_measurements_in_text

bambara_to_measurement("kilogaramu duuru")
# => (5, 'kg')

bambara_to_measurement("mɛtɛrɛ kɛmɛ")
# => (100, 'm')

bambara_to_measurement("litiri fila tomi duuru")
# => (2.5, 'L')

# In text
denormalize_measurements_in_text("A ye kilogaramu duuru san")
# => "A ye 5 kg san"

Measurement Format

Measurements follow this structure:

[unit] [number]

Example: 5 kg => kilogaramu duuru

Literal translation: "kilogram five"

Supported Units

Weight

Unit Abbreviation Bambara
Kilogram kg kilogaramu
Gram g garamu
Milligram mg miligaramu
Ton t tɔni

Length

Unit Abbreviation Bambara
Kilometer km kilomɛtɛrɛ
Meter m mɛtɛrɛ
Centimeter cm santimɛtɛrɛ
Millimeter mm milimɛtɛrɛ

Volume

Unit Abbreviation Bambara
Liter L litiri
Milliliter mL mililitiri

Area

Unit Abbreviation Bambara
Hectare ha ɛkitari
Square meter m² mɛtɛrɛ kare

Arithmetic Normalization

Operators are read the way they are spoken, so a TTS voice does not have to guess at +, -, ×, ÷ and =.

With Normalizer

from bambara_normalizer import normalize

normalize("A ko 2 + 2 = 4", expand_arithmetic=True)   # => "a ko fila fara fila kan o ye naani ye"
normalize("A ko 2 + 2 = 4", expand_arithmetic=False)  # => "a ko 2 + 2 = 4"

# WER preset has expand_arithmetic=True by default
normalize("A ko 5 × 3", preset="wer")  # => "a ko duuru siɲɛ saba"

Arithmetic to Bambara (Text Normalization)

from bambara_normalizer import arithmetic_to_bambara, format_arithmetic_bambara, normalize_arithmetic_in_text

arithmetic_to_bambara(2, "+", 2)             # => "fila fara fila kan"
arithmetic_to_bambara(8, "-", 5)             # => "duuru bɔ seegin na"
arithmetic_to_bambara(5, "×", 3)             # => "duuru siɲɛ saba"
arithmetic_to_bambara(10, "÷", 2)            # => "tan tila fila la"
arithmetic_to_bambara(2, "+", 2, result=4)   # => "fila fara fila kan, o ye naani ye"

# From string format
format_arithmetic_bambara("2+2")             # => "fila fara fila kan"
format_arithmetic_bambara("2 + 2 = 4")       # => "fila fara fila kan, o ye naani ye"

# In text
normalize_arithmetic_in_text("A ko 2 + 2 = 4")
# => "A ko fila fara fila kan, o ye naani ye"

Operators

Operation Symbols Bambara Example
Addition + fara ... kan 2 + 2 => fila fara fila kan
Subtraction - − bɔ ... la 8 - 5 => duuru bɔ seegin na
Multiplication × * x siɲɛ 5 × 3 => duuru siɲɛ saba
Division ÷ / tila ... la 10 / 2 => tan tila fila la
Result = o ye ... ye 2 + 2 = 4 => fila fara fila kan, o ye naani ye

Subtraction takes the subtrahend out of the total, so the operands are spoken in the reverse of the written order: 8 - 5 is "five taken from eight". The closing postposition la surfaces as na after a nasal — seegin na but fila la.

An x only multiplies when it stands alone between spaces, so no word is ever cut in half: 5 x 3 is an operation, 5x3 is left as it is.

What is left alone

Normalization fails open: anything that is not a complete two-operand operation is returned untouched.

normalize_arithmetic_in_text("Telefɔni 20-22-33-44")  # => "Telefɔni 20-22-33-44"   (three or more operands)
normalize_arithmetic_in_text("A wolola 2020-2021 la") # => "A wolola 2020-2021 la"  (year range)
normalize_arithmetic_in_text("sugu la 10/2025")       # => "sugu la 10/2025"        (month and year)

The year guard only covers the tight form. Write the spaces when you mean the operation: 2021 - 2020 is subtraction.


Currency Normalization

A money amount and its currency are read as one phrase: the currency name, wari (money), then the amount. Currency expansion is opt-in: when it is off, 5 000 FCFA reads as before, the number followed by FCFA.

With Normalizer

from bambara_normalizer import normalize, normalize_numeric_expressions

normalize("A ye 100 000 FCFA sara", expand_currency=True, expand_numbers=True)  # => "a ye sefa wari waa mugan sara"
normalize_numeric_expressions("A ye 100 000 FCFA sara.", currency=True)       # => "A ye sefa wari waa mugan sara."

Currency to Bambara (Text Normalization)

from bambara_normalizer import currency_to_bambara, normalize_currency_in_text

currency_to_bambara("20 000", "FCFA")  # => "sefa wari waa naani"
currency_to_bambara(20, "$")           # => "dolari wari mugan"

# In text: the currency may follow or precede the amount
normalize_currency_in_text("A ye 1 100 000 F sara.")  # => "A ye sefa wari miliyɔn kelen ni waa mugan sara."
normalize_currency_in_text("$20 ani €15,50")         # => "dolari wari mugan ani ero wari tan ni duuru tomi duuru"

CFA is counted in dɔrɔmɛ

Spoken Bambara counts CFA francs below a million in dɔrɔmɛ, the five-franc coin: that part of the amount is divided by five. Millions keep their face value. The dɔrɔmɛ count is said in three pieces: hundreds of thousands and the remaining thousands each take waa, and the part below a thousand is joined with ani. When that last part is under 1 000 FCFA (fewer than two hundred dɔrɔmɛ, kɛmɛ fila), the coin is named.

FCFA dɔrɔmɛ Spoken
200 40 sefa wari dɔrɔmɛ bi naani
500 100 sefa wari dɔrɔmɛ kɛmɛ
20 000 4 000 sefa wari waa naani
100 000 20 000 sefa wari waa mugan
600 000 120 000 sefa wari waa kɛmɛ ni waa mugan
601 000 120 200 sefa wari waa kɛmɛ ni waa mugan ani kɛmɛ fila
620 250 124 050 sefa wari waa kɛmɛ ni waa mugan ni naani ani dɔrɔmɛ bi duuru
625 000 125 000 sefa wari waa kɛmɛ ni waa mugan ni duuru
1 000 000 1 million + 0 sefa wari miliyɔn kelen
1 500 000 1 million + 100 000 sefa wari miliyɔn kelen ni waa kɛmɛ
2 000 500 2 million + 100 sefa wari miliyɔn fila ani dɔrɔmɛ kɛmɛ

An amount that is not a multiple of five is never rounded: 7 FCFA is sefa wari dɔrɔmɛ kelen tomi naani (1.4 dɔrɔmɛ). An amount already written in dɔrɔmɛ is not divided again (500 dɔrɔmɛ => dɔrɔmɛ kɛmɛ duuru). Other currencies keep their face value.

Supported Currencies

Currency Written forms Bambara
CFA franc FCFA F CFA F.CFA CFA XOF F franc(s) franc(s) CFA sefa wari (below a million: amount ÷ 5)
Euro € EUR euro(s) ero wari
US dollar $ USD dollar(s) dolari wari
Pound sterling £ GBP livuru wari
dɔrɔmɛ dɔrɔmɛ dɔrɔmɛ

Symbols and ISO codes may also come before the amount ($20, USD 20). Only a capital F is read as the franc, and a currency glued to a word is not matched (5 Fanta). An unknown currency fails open: its amount is read as a plain number.


Range Normalization

A dash between two times or two years is read as a range: both ends, joined by fo (until).

from bambara_normalizer import normalize_ranges_in_text

normalize_ranges_in_text("Butiki bɛ dayɛlɛ 8h-17h")
# => "Butiki bɛ dayɛlɛ Nɛgɛ kanɲɛ seegin fo Nɛgɛ kanɲɛ tan ni wolonwula"

normalize_ranges_in_text("2020-2025")
# => "waa fila ni mugan fo waa fila ni mugan ni duuru"
Written Read as a range
8h-10h, 8h - 10h, 14:00-16:00, 9h-12h30 yes: times are not subtracted
2020-2025, 2020 - 2025, 2026–2027 yes: two increasing years
3-1, 8 - 5, 2025 - 2020 no: the dash may be a minus sign, so the reading is unchanged

Each end keeps the reading it has alone. Ranges are expanded by default in normalize_numeric_expressions (ranges=False turns it off) and, in the normalizer, whenever times are expanded.


Phone Number Normalization

A Malian phone number has eight digits. However it is written, it is read the same way: pair by pair, with a pause between pairs, after the country code when there is one.

from bambara_normalizer import phone_number_to_bambara, normalize_phone_numbers_in_text

phone_number_to_bambara("76 12 34 56")
# => "bi wolonwula ni wɔɔrɔ, tan ni fila, bi saba ni naani, bi duuru ni wɔɔrɔ"

phone_number_to_bambara("+22376123456")
# => "kɛmɛ fila ni mugan ni saba, bi wolonwula ni wɔɔrɔ, tan ni fila, bi saba ni naani, bi duuru ni wɔɔrɔ"

normalize_phone_numbers_in_text("20.06.11.00")
# => "mugan, fu wɔɔrɔ, tan ni kelen, fu fu"
Written Read as a phone number
76 12 34 56, 76-12-34-56, 76.12.34.56 yes: four pairs sharing one separator
+223 76 12 34 56, +22376123456, 00223 76 12 34 56, +223 76 123 456 yes, after kɛmɛ fila ni mugan ni saba: with the country code, any grouping
76123456, 50000000, 76 123 456 no: without a country code, unbroken digits and groups of three are an amount
76 12 34 56 FCFA, $76 12 34 56, 20 30 40 50 kg no: a currency or unit next to the digits makes them an amount
12345678, 12 34 56 78 no: outside the numbering plan (a Malian number starts with 2 to 9)

Eight digits alone can be an amount or a phone number (80000000), and nothing in the digits says which, so this is a convention on how the number is written. Limitation: a phone number written unbroken without its country code (76123456) is read as an amount; write it in pairs or with +223.

A pair starting with 0 is said digit by digit (06 => fu wɔɔrɔ). Phone numbers are expanded by default in normalize_numeric_expressions (phones=False turns it off) and, in the normalizer, whenever numbers are expanded.


Numeric Expansion (dates, ranges, times, arithmetic, phone numbers, measurements, numbers)

Dates, ranges, times, arithmetic, phone numbers, measurements and bare numbers all compete for the same digits. The normalizer resolves them in a single classification pass: every numeric span of the text is claimed by exactly one kind, and only then is anything rewritten. Expansion therefore does not depend on the order the expanders run in.

Precedence runs from the most specific shape to the most generic:

date > range > time > arithmetic > currency > phone > measurement > number
from bambara_normalizer import find_numeric_spans, normalize_numeric_expressions

text = "Ne taara sugu la 24-12-2025 la, 10:45 waati, ne ye tulu 6 l san, wari 12 + 8 = 20."

[(s.source, s.kind) for s in find_numeric_spans(text)]
# => [('24-12-2025', 'date'), ('10:45', 'time'), ('6 l', 'measurement'), ('12 + 8 = 20', 'arithmetic')]

normalize_numeric_expressions(text)
# => "Ne taara sugu la Desanburu kalo tile mugan ni naani san baa fila ni mugan ni duuru la, ..."

A span keeps its kind whether or not that kind is enabled, so partial configurations are safe: disabling one expander never lets another one mis-read its digits.

# expand_numbers=True, expand_dates=False: the date is left alone, not split into three numbers
normalize_numeric_expressions("A bɛ 24-12-2025 la ni 3", dates=False)
# => "A bɛ 24-12-2025 la ni saba"

normalize("A bɛ 24-12-2025 la ni 3", expand_numbers=True, expand_dates=False, remove_punctuation=False)
# => "a bɛ 24-12-2025 la ni saba"

The single-kind functions (normalize_dates_in_text, normalize_times_in_text, normalize_arithmetic_in_text, normalize_currency_in_text, normalize_ranges_in_text, normalize_phone_numbers_in_text, normalize_measurements_in_text, normalize_numbers_in_text) each expand only their own shape and are unaware of the others — normalize_numbers_in_text will happily expand the digits of a date. Use normalize_numeric_expressions (or the normalizer, which calls it) for mixed text.

Not resolved by precedence: genuinely ambiguous shapes. 10-45 could be a date fragment or a ratio, and 24-12-2025 is read as DD-MM-YYYY, never MM-DD-YYYY. Precedence makes the choice deterministic and documented, not correct in every context.

ASR Evaluation Framework

Quick Evaluation

from bambara_normalizer import evaluate


result = evaluate(
    reference="B'a fɔ ka taa",
    hypothesis="bɛ a fɔ ka taa"

)

print(f"WER: {result.wer:.2%}")  
print(f"CER: {result.cer:.2%}")
print(f"MER: {result.mer:.2%}")

Evaluator with Mode Selection

from bambara_normalizer import evaluate


result = evaluate(
    reference="k'a ta", 
    hypothesis="ka a ta"
    mode="expand" # contract | preserve 
    )
print(f"WER: {result.wer:.2%}")   

Flexible Configuration

For full control use the evalution class and define the normalization configuration:

from bambara_normalizer import (
    BambaraNormalizer, 
    BambaraNormalizerConfig, 
    BambaraEvaluator
)

# Define custom normalizer: same then the config we did upside
config = BambaraNormalizerConfig(
    contraction_mode="contract",
    preserve_tones=False,
    lowercase=True,
    remove_punctuation=True,
    normalize_legacy_orthography=True,
)


evaluator = BambaraEvaluator(config=config)


result = evaluator.evaluate(
    reference="K'a fɔ́!",
    hypothesis="ka a fo"
)
print(f"WER: {result.wer:.2%}")

Batch Evaluation

from bambara_normalizer import BambaraEvaluator

evaluator = BambaraEvaluator(mode="contract")

references = ["k'a ta", "b'a fɔ", "n'a ma"]
hypotheses = ["ka a ta", "bɛ a fɔ", "na a ma"]

aggregate, individual = evaluator.evaluate_batch(references, hypotheses)

print(f"Overall WER: {aggregate.wer:.2%}")
for i, result in enumerate(individual):
    print(f"  [{i}] WER: {result.wer:.2%}")

Available Metrics

Metric Method Description
WER evaluator.wer(ref, hyp) Word Error Rate
CER evaluator.cer(ref, hyp) Character Error Rate
MER evaluator.mer(ref, hyp) Match Error Rate
WIL evaluator.wil(ref, hyp) Word Information Lost
WIP evaluator.wip(ref, hyp) Word Information Preserved
DER result.der Diacritic Error Rate (tone accuracy)

Contraction Modes

Version 2.0 introduces three contraction modes to handle bidirectional Bambara orthography:

Mode Direction When to Use
expand b'a => bɛ a Default. Full linguistic analysis with k'/n' disambiguation
contract bɛ a => b'a Simpler, more forgiving. No disambiguation ambiguity
preserve No change Debugging, or when you want raw comparison

Why Contract Mode Matters

Expansion is complex the contraction k' can expand to three different words:

Contraction Possible Expansions Meaning
k'a ka a infinitive marker
k'a kɛ a verb "to do"
k'a ko a verb "to say"

The normalizer uses context to disambiguate, but some cases are genuinely ambiguous.

Contraction is simple all variants collapse to the same form:

ka a  ─┐
kɛ a  ─┼─>  k'a
ko a  ─┘

Contraction Mappings

Expanded Contracted Function
bɛ + vowel b' Affirmative imperfective
tɛ + vowel t' Negative imperfective
ye + vowel y' Perfective marker
ni + vowel n' Conjunction
na + vowel n' Verb "come"
ka + vowel k' Infinitive marker
kɛ + vowel k' Verb "to do"
ko + vowel k' Verb "to say"

Command Line Interface

The CLI is built with Typer and Rich: --help is fully formatted, evaluation metrics come back as tables, and results printed to a terminal are framed. Piped or redirected output is always plain text, so the tool composes with other commands; --plain forces plain text on a terminal too.

Basic Usage

# default mode is expand
bambara-normalize "B'a fɔ́"
# Output: bɛ a fɔ

# Contract mode
bambara-normalize --mode contract "bɛ a fɔ"
# Output: b'a fɔ

# Preserve mode
bambara-normalize --mode preserve "B'a fɔ"
# Output: b'a fɔ

With Presets

# WER preset (aggressive normalization)
bambara-normalize --preset wer "K'a fɔ́!"
# Output: ka a fɔ

# WER preset with contract mode
bambara-normalize --preset wer --mode contract "Ka a fɔ"
# Output: k'a fɔ

# CER preset
bambara-normalize --preset cer "B'a fɔ"

File Evaluation

# Evaluate reference vs hypothesis files
bambara-normalize --evaluate reference.txt hypothesis.txt

# With contract mode
bambara-normalize --evaluate --mode contract ref.txt hyp.txt

# Output detailed metrics
bambara-normalize --evaluate --detailed ref.txt hyp.txt

Batch Processing

# Process file line by line
bambara-normalize --input corpus.txt --output normalized.txt

# With specific mode
bambara-normalize --input corpus.txt --output normalized.txt --mode contract

# Read from stdin
echo "B'a fɔ́" | bambara-normalize

Options

Option Description
-m, --mode expand (default), contract, preserve
-p, --preset standard (default), wer, cer, minimal, preserving_tones
-f, --input Input file, one utterance per line (alias: --file)
-o, --output Write results to a file instead of stdout
-e, --evaluate Treat the two arguments as reference and hypothesis files
--detailed With --evaluate, add a per-utterance table
--preserve-tones Keep tone marks
--expand-numbers Expand digits to Bambara words
--expand-arithmetic Read arithmetic expressions aloud (2 + 2 = 4)
--debug Print the intermediate normalization steps to stderr
--plain Unstyled output, even on a terminal
-v, --version Print the version
-h, --help Show help

Linguistic Decisions

Why Normalize?

Bambara orthography allows variation. For the same spoken utterance:

  • Annotator A writes: k'a ta
  • Annotator B writes: ka a ta

Both are correct. Without normalization, we penalize models for human writing inconsistencies, not recognition errors.

n' Disambiguation

Pattern Expansion Meaning
n' + pronoun + ma na Verb "to come"
n' + other ni Conjunction (default)

Examples:

  • n'a ma => na a ma (come to him)
  • n'a ta => ni a ta (if he takes)

k' Disambiguation Rules

Applied in priority order (derived from Daba grammar):

Priority Pattern Result Example
1 k' + pronoun + ma + X + ye kɛ k'a ma hɛrɛ ye => kɛ a ma hɛrɛ ye
2 k' + pronoun + ma + speech marker ko k'anw ma ko => ko anw ma ko
3 k' + pronoun + postposition kɛ k'a la => kɛ a la
4 k' + pronoun + clause marker ko k'an ka ta => ko an ka ta
5 Default ka k'a ta => ka a ta

Postpositions: la, na, ye, fɛ, kɔnɔ, kɔ, kɔrɔ, kan, kun, ɲɛ, bolo

Clause markers: ka, kana, bɛ, tɛ, bɛna, tɛna, tun, mana

Legacy Orthography Conversion

Legacy Modern Notes
è ɛ Pre-standard spelling
ò ɔ Pre-standard spelling
ny ɲ Digraph => single character
ng ŋ Digraph => single character
ñ ɲ Spanish/Senegalese variant

Known Limitations

Inherent Linguistic Ambiguity

The ye Problem

The word ye has five grammatical functions:

Function Example Meaning
Postposition à fɔ́ ń yé say it to me
Perfective ù ye ɲɔ̀ gòsi they have beaten
Copula ò yé kɔ̀nɔ yé it is a bird
Verb "see" ka a ye to see it
Imperative á' yé nà! come! (plural)

This creates genuine ambiguity for k'a ye:

Interpretation Expansion Meaning
Postposition kɛ a ye do it for him
Verb "see" ka a ye to see it

Default behavior: The normalizer chooses kɛ a ye (postposition is more frequent).

Solution: Use mode="contract" for ASR evaluation to avoid disambiguation penalties:

evaluator = BambaraEvaluator(mode="contract")
# Both "kɛ a ye" and "ka a ye" => "k'a ye" 

Scope

The normalizer uses local context (1-3 word lookahead). It does not:

  • Parse full sentence structure
  • Use dictionary/lexicon for POS tagging
  • Consider discourse-level context

Utility Functions

from bambara_normalizer import (
    is_contraction,
    can_contract,
    find_contractions,
    find_contractable_sequences,
    compare_normalization_modes,
    analyze_text,
    is_bambara_vowel,
    get_tone,
    remove_tones,
    number_to_bambara,
    bambara_to_number,
    normalize_numbers_in_text,
    denormalize_numbers_in_text,
    is_number_word,
    
    bambara_to_date,
    bambara_to_day_of_week,
    bambara_to_month,
    date_to_bambara,
    day_of_week_to_bambara,
    denormalize_dates_in_text,
    format_date_bambara,
    is_bambara_day,
    is_bambara_month,
    month_to_bambara,
    normalize_dates_in_text,

    time_to_bambara,
    bambara_to_time,
    format_time_bambara,
    duration_to_bambara,
    bambara_to_duration,
    format_duration_bambara,
    normalize_times_in_text,
    is_time_word,

    measurement_to_bambara,
    bambara_to_measurement,
    format_measurement_bambara,
    normalize_measurements_in_text,
    denormalize_measurements_in_text,
    is_measurement_word,
    get_unit_category,
)


is_contraction("b'a")                   
is_contraction("bɛ")                     
can_contract("bɛ a")                      

# Find patterns in text
find_contractions("B'a fɔ k'a ta")       # ["b'", "k'"]
find_contractable_sequences("bɛ a fɔ")   # [('bɛ', 'a')]

# Compare modes side-by-side
compare_normalization_modes("b'a fɔ")
# {'original': "b'a fɔ", 'expand': 'bɛ a fɔ', 'contract': "b'a fɔ", 'preserve': "b'a fɔ"}

# Full text analysis
analyze_text("B'a fɔ k'a la")
# {'word_count': 4, 'contractions_found': ["b'", "k'"], 'has_tone_marks': False, ...}

# Tone handling
get_tone("fɔ́")                           # "high"
remove_tones("fɔ́ bɛ̀")                    # "fɔ bɛ"

# Number conversion: digits => Bambara words
number_to_bambara(5)                     # "duuru"
number_to_bambara(23)                    # "mugan ni saba"
number_to_bambara(100)                   # "kɛmɛ"
number_to_bambara(123)                   # "kɛmɛ ni mugan ni saba"
number_to_bambara(1000)                  # "waa kelen"
number_to_bambara(5.3)                   # "duuru tomi saba"

# Number conversion: Bambara words => digits
bambara_to_number("duuru")               # 5
bambara_to_number("mugan ni saba")       # 23
bambara_to_number("kɛmɛ")                # 100
bambara_to_number("waa kelen")           # 1000
bambara_to_number("duuru tomi saba")     # 5.3

# Number normalization in text
normalize_numbers_in_text("A ye 5 di")       # "A ye duuru  di"
normalize_numbers_in_text("Mɔgɔ 100 nana")        # "Mɔgɔ kɛmɛ nana"
normalize_numbers_in_text("A be san 25 bɔ")       # "A be san mugan ni duuru bɔ"

# Inverse: Bambara words => digits in text
denormalize_numbers_in_text("A ye duuru di")  # "A ye 5  di"
denormalize_numbers_in_text("Mɔgɔ kɛmɛ nana")      # "Mɔgɔ 100 nana"

# Check if word is a number word
is_number_word("duuru")                  # True
is_number_word("kɛmɛ")                   # True
is_number_word("fɔ")                     # False


# Date conversion: dates => Bambara
date_to_bambara(2024, 10, 13)            # "Oktɔburu kalo tile tan ni saba san baa fila ni mugan ni naani"
format_date_bambara("13-10-2024")        # Same as above

# Date conversion: Bambara => dates
bambara_to_date("Oktɔburu tile tan ni saba san baa fila ni mugan ni naani")  # datetime.date(2024, 10, 13)

# Day/Month helpers
day_of_week_to_bambara(0)                # "Tɛnɛn" (Monday)
day_of_week_to_bambara(6)                # "Kari" (Sunday)
month_to_bambara(10)                     # "Oktɔburu"
bambara_to_month("Oktɔburu")             # 10

# Date normalization in text
normalize_dates_in_text("A bɛ na 13-10-2024 la")  # "A bɛ na Oktɔburu kalo tile ... la"

# Check if word is date-related
is_bambara_month("Oktɔburu")             # True
is_bambara_day("Juma")                   # True


# Time conversion: clock times → Bambara
time_to_bambara(1, 0)                    # "Nɛgɛ kanɲɛ kelen"
time_to_bambara(7, 30)                   # "Nɛgɛ kanɲɛ wolonwula ni sanga bi saba"
format_time_bambara("13:50")             # "Nɛgɛ kanɲɛ tan ni saba ni sanga bi duuru"

# Time conversion: Bambara → clock times
bambara_to_time("Nɛgɛ kanɲɛ wolonwula ni sanga bi saba")  # datetime.time(7, 30)

# Duration conversion: durations → Bambara
duration_to_bambara(minutes=30)          # "miniti bi saba"
duration_to_bambara(hours=1, minutes=30) # "lɛrɛ kelen ni miniti bi saba"
format_duration_bambara("1h30min10s")    # "lɛrɛ kelen ni miniti bi saba ni segɔni tan"

# Duration conversion: Bambara → durations
bambara_to_duration("lɛrɛ kelen ni miniti bi saba")  # (1, 30, 0)

# Time normalization in text
normalize_times_in_text("A nana 7:30 la")  # "A nana Nɛgɛ kanɲɛ wolonwula ... la"

# Check if word is time-related
is_time_word("lɛrɛ")                      # True
is_time_word("miniti")                    # True
is_time_word("segɔni")                    # True



# Measurement conversion: units => Bambara
measurement_to_bambara(5, "kg")          # "kilogaramu duuru"
measurement_to_bambara(100, "m")         # "mɛtɛrɛ kɛmɛ"
measurement_to_bambara(2.5, "L")         # "litiri fila tomi duuru"
format_measurement_bambara("5kg")        # "kilogaramu duuru"

# Measurement conversion: Bambara => units
bambara_to_measurement("kilogaramu duuru")   # (5, 'kg')
bambara_to_measurement("mɛtɛrɛ kɛmɛ")        # (100, 'm')

# Measurement normalization in text
normalize_measurements_in_text("A ye 5 kg san")      # "A ye kilogaramu duuru san"
denormalize_measurements_in_text("kilogaramu duuru") # "5 kg"

# Check if word is measurement-related
is_measurement_word("kilogaramu")        # True
is_measurement_word("mɛtɛrɛ")            # True
get_unit_category("kg")                  # "weight"
get_unit_category("m")                   # "length"

Evaluation Metrics

Metric Description Range
WER Word Error Rate 0.0 – ∞
CER Character Error Rate 0.0 – ∞
MER Match Error Rate 0.0 – 1.0
WIL Word Information Lost 0.0 – 1.0
WIP Word Information Preserved 0.0 – 1.0
DER Diacritic Error Rate (tone accuracy) 0.0 – ∞

References

Linguistic Resources

Standards

  • UNESCO Bamako Meeting (1966)
  • Niamey African Reference Alphabet (1978)

Tools

  • jiwer ASR evaluation metrics

MALIBA-AI

Metadata

Release files for bambara-text-normalizer 2.0.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 bambara-text-normalizer 2.0.0
File Size Uploaded Provenance Actions
bambara_text_normalizer-2.0.0.tar.gz 107.7 kB Without provenance Details for bambara_text_normalizer-2.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for bambara-text-normalizer 2.0.0
File Interpreter ABI Platform Actions
bambara_text_normalizer-2.0.0-py3-none-any.whl Python 3 none any Details for bambara_text_normalizer-2.0.0-py3-none-any.whl

Total release size: 177.8 kB

Release files / bambara_text_normalizer-2.0.0.tar.gz

Download URL bambara_text_normalizer-2.0.0.tar.gz
Size 107.7 kB
Tags Source
SHA-256 checksum
How to use checksums
6f8d51066a8c3499410bb1aee550e3befb25abb574dfe6b213109f955fc057c7
BLAKE2b-256 checksum
How to use checksums
96d69bcc7bca8426e51e9a41f01bd5385da532efc4e65312531a8af247991f75
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / bambara_text_normalizer-2.0.0-py3-none-any.whl

Download URL bambara_text_normalizer-2.0.0-py3-none-any.whl
Size 70.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
232c6749690711436c7c9039b59f3e0073432018fe3bd624723a795ac659e9ef
BLAKE2b-256 checksum
How to use checksums
9b608cddab8ea3bc9061d34b33c688f8291070dc06394296da1b4d285192a6e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for bambara-text-normalizer 2.0.0

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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