Skip to main content

dv_normalizer — written Dhivehi to spoken Dhivehi

Converts written Dhivehi (Thaana script) into the spoken form: numbers, dates, times, money, percentages, abbreviations, URLs and the literary sentence endings of news prose (…އެވެ) become the words a Dhivehi speaker would actually say. Built as the text-normalization frontend for Dhivehi text-to-speech, and usable on its own.

ލިޔެފައިވާ ދިވެހި ބަސް ވާހަކަދައްކާ ދިވެހި ބަހަށް ބަދަލުކުރާ ނިޒާމު

What it's for:

  • Dhivehi text normalization — written to spoken Dhivehi conversion of numbers, dates, money, percentages, and more, in the Thaana script
  • Dhivehi number to words conversion (cardinals, ordinals, fractions, decimals, years)
  • The text-normalization frontend for a Dhivehi TTS (text-to-speech) pipeline, or standalone use in any Maldivian language NLP tool

Installation

pip install dv-normalize

Quick start

from dv_normalize import normalize

normalize("ވަކި ލާރިން ވެސް 232.23 ލާރި ހޯދައެވެ")
# 'ވަކި ލާރިން ވެސް ދުވިސައްތަ ތިރީސް ދޭއް ޕޮއިންޓް ތޭވީސް ލާރި ހޯދައި'

normalize("ޑރ. އިބްރާހިމް 14:30 ގައި އައި")
# 'ޑޮކްޓަރު އިބްރާހިމް ސާދަ ގަޑި ތިރީސް ގައި އައި'

normalize("ކ.އަތޮޅު ވިލިނގިލިން 120 ކިލޯ މީޓަރު")
# 'ކާފު އަތޮޅު ވިލިނގިލިން އެއް ސަތޭކަ ވިހި ކިލޯ މީޓަރު'

normalize("ފޭސް2ގެ")
# 'ފޭސް ދޭއް ގެ'

normalize("އިތުރު މައުލޫމާތު: https://example.com/news/123 ބައްލަވާ")
# 'އިތުރު މައުލޫމާތު: https://example.com/news/123 ބައްލަވާ'  (URL passes through untouched)

For repeated use, hold onto a Normalizer instance:

from dv_normalize import Normalizer, NormalizerConfig

n = Normalizer(NormalizerConfig(keep_punctuation=False))
n("ހެލޯ، ދުނިޔެ")  # → 'ހެލޯ ދުނިޔެ'

# strict=True raises ValueError if any digit survives normalization; on
# well-formed input it's a no-op safety net.
ns = Normalizer(NormalizerConfig(strict=True))
ns("232 ލާރި")  # → 'ދުވިސައްތަ ތިރީސް ދެ ލާރި'

How it works

A four-stage pipeline: tokenize (split text into words, punctuation and whole-passthrough spans like URLs) → classify (a priority-ranked rules engine tags each token with a semiotic class — CARDINAL, DATE, MONEY, …) → verbalize (each class's own module renders the spoken-form words) → post-process (sentence-ending rewriting, spacing, quote/punctuation fixups). The number and date rules were checked against a 2-million-plus- sentence news + majlis corpus; the 214 sentence-ending rules were reviewed by a native speaker one family at a time against real corpus sentences and cross-checked against ~440,000 majlis speech sentences.

What it handles

Class Example input Example output
Cardinal 232 ދުވިސައްތަ ތިރީސް ދޭއް
Comma-grouped 104,880 އެއް ލައްކަ ހަތަރުހާސް އަށް ސަތޭކަ އައްޑިހަ (single cardinal)
Per-digit identifier 9982711 ނުވައެއް ނުވައެއް އަށެއް ދޭއް ހަތެއް އެކެއް އެކެއް (7+ digits)
Decimal 232.23 ދުވިސައްތަ ތިރީސް ދޭއް ޕޮއިންޓް ތޭވީސް
Year 2024 ދެހާސް ސައްވީސް
Year range 1982 - 2024 ނަވާރަސަތޭކަ އައްޑިހަ ދޭއް އިން ދެހާސް ސައްވީސް އާ ހަމައަށް
Date 12 ފެބުރުވަރީ 1988 ބާރަ ފެބުރުވަރީ ނަވާރަސަތޭކަ އައްޑިހަ އަށް
Time 14:30 ސާދަ ގަޑި ތިރީސް
Ordinal 11ވަނަ އެގާރަ ވަނަ
Fraction 1/2 ދެބައިކުޅަ އެއްބައި
Mixed fraction 1 1/2 އެކެއް ދެބައިކުޅަ އެއްބައި
Percent 25% ފަންސަވީސް ޕަސެންޓު
Percent, prefix pair %12-%13 ބާރަ ޕަސެންޓު-ތޭރަ ޕަސެންޓު (both values read)
Percent, glued punct (50%) (ފަންސާސް ޕަސެންޓު)
Oblique/reference 2024/3 ދެހާސް ސައްވީސް އޮބްލިކް ތިން ވަނަ
Oblique, abbreviated އޖ/21/2019 އަލިފު ޖަވިޔަނި އޮބްލިކް އެކާވީސް އޮބްލިކް ދެހާސް ނަވާރަ ވަނަ
Score 3-2, 0-0, 5-0 ތިނެއް ދޭ, ސުމެއްސުމެއް, ފަހެއްސުމެއް (draw/shutout forms)
Money 41,800 ރ ސާޅީސް އެއްހާސް އަށް ސަތޭކަ ރުފިޔާ
Money, silent marks -/7000ރ, 500/- ހަތްހާސް ރުފިޔާ, ފަސް ސަތޭކަ ރުފިޔާ
Phone (+country) +960 9611929 ޕްލަސް ނުވައެއް ހައެއް ސުމެއް ނުވައެއް ... (every digit)
Plus sign +2.88, 50+ ޕްލަސް ދޭއް ޕޮއިންޓް އައްޑިހަ އަށެއް, ފަންސާސް ޕްލަސް
Age (40އ) (ސާޅީސް އަހަރު)
Abbreviation ޑރ. ޑޮކްޓަރު (real abbreviations only)
Atoll code ގދ., ހއ. ގާފު ދާލު, ހާ އަލިފު (per-letter spelling)
Compound abbrev ސ.ޢ.ވ. ޞައްލަﷲ ޢަލައިހި ވަސައްލަމް
Calendar marker 2026 މ., 1447 ހ. ދެހާސް ސައްބީސް މީލާދީ, ... ހިޖުރީ
Latin acronym MNDF އެމް އެން ޑީ އެފް (2-4 letter all-caps, always spelled)
URL / e-mail / handle / hashtag https://example.com/x?y=1 unchanged (passthrough, digits inside never read aloud)
Sentence ending ނުކެރުނެވެ ނުކެރުނު
Sentence ending ދުވަހެވެ ދުވަސް
Sentence ending, before a closing quote މަޖުބޫރުވާނެއެވެ". މަޖުބޫރުވާނެ".

The classifier is priority-ranked, so more specific patterns (calendar markers, multi-letter compound abbreviations, year ranges, the digit-anchored symbol rules) shadow the generic ones. Tokens that don't match any rule pass through unchanged.

Sentence-ending coverage: 214 reviewed rewrite rules handle the literary noun/verb ending families found across the news + majlis corpora (formal -އެވެ/-ެވެ endings collapsed to their spoken form). The rules were validated against 437,000 majlis speech sentences — majlis transcripts are themselves largely spoken-register, so a rule that also matches the majlis corpus's own usage is strong evidence it is producing a real spoken form rather than an invented one.

Coverage

Measured against a 1.79M-sentence news corpus and a 399k-sentence majlis (parliament) corpus.

  • Full-corpus baseline, before the 1.0 ending-rules and symbol-handling work: news 96.87% of 1,794,272 sentences fully normalized, majlis 99.44% of 399,088 sentences.
  • Final 6,000-document sample (3,000 news articles + 3,000 majlis turns) for this release, measured two ways:
    • Raw (every sentence in the sample, Dhivehi or not): news 98.18%, majlis 99.93% fully normalized.
    • Dhivehi-content (sentences that contain at least some Thaana text; a sentence whose only non-Dhivehi content is a passed-through URL still counts as handled): news 99.86%, majlis 99.93%.
    • Zero sentences in either sample left a digit unspoken.

The raw-sample residue is overwhelmingly English-language prose and web-scraping boilerplate (menus, bylines, "read more" links) that isn't Dhivehi text to begin with, so it's outside what a Dhivehi normalizer is meant to convert. The smaller Dhivehi-content residue is mostly English words embedded inside otherwise-Dhivehi sentences (left as-is per unknown_latin), street-address-style slashes, and Arabic-script Quranic or hadith citations.

Ending alignment: of literary-register endings found in the news sample, 99.9% are rewritten by some rule at all, and 91.5% are rewritten to a form that also occurs, verbatim, as a genuine spoken-register ending somewhere in the majlis corpus. That second number is a deliberately conservative metric — a rewritten ending that happens not to appear in the majlis sample isn't necessarily wrong, it just wasn't independently confirmed against spoken-register evidence.

Known issues

Measured state of 1.0.0 (see Coverage for how the numbers were taken): 99.86% of Dhivehi-content news sentences and 99.93% of majlis sentences come out fully spoken, no digit is ever left unread, and 91.5% of rewritten sentence endings are attested verbatim in spoken-register majlis speech. In a human review of 28 random news articles read end to end, 20 needed no change and 8 had corrections; every one of those corrections is now encoded as a rule. What remains:

  • Definite vs. indefinite nouns. Literary Xއެވެ (definite, "it is X") and Xެކެވެ (indefinite, "an X") are handled separately, but a few legacy overrides still blur them: ދައްކައެވެ reads ދައްކަ while the spaced ދައްކަ އެވެ reads ދައްކައި. Awaiting a native-speaker ruling.
  • ގެ after a number is always read as the genitive particle, so a number before ގެ meaning "house" inflects wrongly (ގޭ is not produced for ގެ އެވެ either).
  • Ablative ން after a number is not inflected: 5 ން ފެށިގެން reads ފަސް ން rather than the spoken ފަހަކުން.
  • Durations in mm:ss after a hour count keep the colon: 3 ގަޑި 30:04 މިނިޓު reads ތިން ގަޑި ތިރީސް:ހަތަރު މިނިޓު.
  • Percentages: negative percents, the fraction separator in comma-decimal mode, and 50% އަށްވުރެ (no dative inflection) are read more literally than a speaker would.
  • Units such as kg, km, GB have no Dhivehi lexicon yet and follow unknown_latin; Roman numerals are spelled letter by letter.
  • English words inside Dhivehi sentences are left as written by default (unknown_latin="passthrough"); there is no transliteration.
  • Score draws like 2-2 use the linking form ދޭދޭ; other scores read both numbers in full (3-2 → ތިނެއް ދޭއް).
  • Arabic-script quotations (Quran, hadith) pass through untouched.

Report a wrong reading with the written sentence and the form you would say.

Configuration

NormalizerConfig(
    dialect="spoken",            # only option for now
    unknown_latin="passthrough", # "passthrough" | "drop" | "spell"
    decimal_separator="auto",    # "auto" | "dot" | "comma"
    time_system="auto",          # "auto" | "12" | "24"
    currency_default="MVR",
    keep_punctuation=True,
    diagnostic=False,
    strict=False,
)
  • dialect="spoken" — the only option today; reserved for a future literary/written-register mode.
  • unknown_latin — controls plain Latin words only, never acronyms and never URLs: 2-4 letter all-caps runs (MNDF) are always spelled via Thaana letter names regardless of this setting, and URLs/e-mails/handles/ hashtags always pass through untouched regardless of this setting (even "drop" never removes a URL — only ordinary Latin words like STORE). "passthrough" (default) leaves other Latin words unchanged; "drop" removes them (a Latin-only input can yield an empty string); "spell" spells them out letter-by-letter.
  • decimal_separator — "auto" and "dot" both use . as the decimal mark and ,/، as thousands separators. "comma" swaps that: , is the decimal mark (1,5 → އެކެއް ޕޮއިންޓް ފަހެއް), except a chain of ≥2 three-digit , groups is still read as thousands grouping (1,234,567 stays one cardinal, not a decimal).
  • time_system — "auto" and "24" read the hour as-is (14:30 → ސާދަ ގަޑި ތިރީސް). "12" prefixes a day-period word and reduces the hour to a 12-hour clock value (14:30 → މެންދުރުފަހު ދެ ގަޑި ތިރީސް).
  • currency_default — documented as reserved; only "MVR" (ރ) is wired today, so changing it currently has no effect.
  • keep_punctuation — when False, punctuation tokens are dropped from the output instead of being kept verbatim.
  • diagnostic — internal flag used by Normalizer.trace(); leave this False when calling normalize()/Normalizer.__call__ directly.
  • strict — when True, raises ValueError if any ASCII or Arabic-Indic digit survives in the final output instead of silently passing it through. Digits inside a passed-through URL/e-mail/handle token are exempt from this check — strict=True does not raise on https://example.com/12345, since that digit was never meant to be read aloud in the first place.

Diagnostic mode

Normalizer.trace(text) returns the classified token list instead of joined text. Useful for debugging which rule fired:

for tok in Normalizer().trace("ޑރ. އިބްރާހިމް 2024ގައި"):
    print(tok.cls, tok.text, tok.spoken, tok.fields)

License

MIT — see LICENSE.

Release files for dv-normalizer 1.0.1

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

Source distribution (sdist)

Source distribution for dv-normalizer 1.0.1
File Size Uploaded
dv_normalizer-1.0.1.tar.gz 78.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dv-normalizer 1.0.1
File Interpreter ABI Platform
dv_normalizer-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 167.9 kB

Release files / dv_normalizer-1.0.1.tar.gz

Download URL dv_normalizer-1.0.1.tar.gz
Size 78.3 kB
Tags Source
SHA-256 checksum
How to use checksums
6a8ee524eb10fbf277c32cd413c75b4bafe7a1f9684eedfc43f9082dabd233f6
BLAKE2b-256 checksum
How to use checksums
e1d6629b3495c1011e42ff77b76cca107c75d3993fd4ab485f43a972c7bf81a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.18

Release files / dv_normalizer-1.0.1-py3-none-any.whl

Download URL dv_normalizer-1.0.1-py3-none-any.whl
Size 89.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4b644f55194930037351545ccc18373ebb82cf848e2745e03cbaac82f1336849
BLAKE2b-256 checksum
How to use checksums
d90de9d2ce9e391d5a307d044a766c50ff52b0f40e56da608b6f042154d9a4e5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.18

Release history Release notifications | RSS feed

1.0.3

2 release files

1.0.2

2 release files

This release

1.0.1 This release

2 release files

1.0.0

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

1 release file

0.1.4

1 release file

0.1.3

2 release files

0.1.2

2 release files

0.1.0

2 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