urdu-text-eval 1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: urdu-text-eval
3
+ Version: 1.0
4
+ Summary: Urdu text evaluation & benchmarking library with configurable orthographic normalization (WER, CER, chrF, BLEU).
5
+ Author: urdu-text-eval
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/local/urdu-text-eval
8
+ Keywords: urdu,evaluation,benchmarking,wer,cer,chrf,bleu,nlp,ocr,asr,transliteration,text-evaluation
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: jiwer>=3.0.0
12
+ Requires-Dist: sacrebleu>=2.4.0
13
+ Requires-Dist: rapidfuzz>=3.0.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == "dev"
16
+
17
+ # urdu-text-eval
18
+
19
+ [![PyPI Version](https://img.shields.io/pypi/v/urdu-text-eval.svg)](https://pypi.org/project/urdu-text-eval/)
20
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
21
+ [![Python Version](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/downloads/)
22
+
23
+ **urdu-text-eval** is a Python benchmarking library for **Urdu text evaluation** (OCR, ASR, transliteration, machine translation, LLM text generation, and text processing).
24
+
25
+ It computes standard NLP metrics (**WER, CER, chrF, BLEU, Exact Match, Character Similarity**) with configurable **Urdu orthographic and taxonomy normalization** (handling Arabic lookalike codepoints, zabar/zeer/pesh diacritics, punctuation, numbers, and presentation forms).
26
+
27
+ ---
28
+
29
+ ## Key Features
30
+
31
+ - **Standard Metrics**: Word Error Rate (WER), Character Error Rate (CER), Word/Char Accuracy, chrF, BLEU, Exact Match, and Levenshtein Character Similarity.
32
+ - **Urdu Taxonomy Normalization**: Standardizes Arabic variants (`ك` → `ک`, `ي` → `ی`, `ه` → `ہ`), presentation forms (`ﺍ`, `ﺎ`, `ﺐ`), and combined characters (`ا`+`ٓ` → `آ`).
33
+ - **Fine-Grained Diacritic Controls**: Selective removal of اعراب (**Zabar** `َ`, **Zeer** `ِ`, **Pesh** `ُ`, **Tanween** `ً ٌ ٍ`, **Shadda** `ّ`, **Sukun** `ْ`, Maddah, and Hamza marks).
34
+ - **Flexible Input Formats**: Evaluates simple list-of-dicts `[{"actual": "...", "pred": "..."}]` with support for common key aliases (`ref`, `target`, `gold`, `Urdu`, `hyp`, `prediction`, `output`).
35
+ - **Dual Reporting**: Always reports both **taxonomy-normalized** (primary) and **raw surface** metrics side-by-side.
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ Install via pip:
42
+
43
+ ```bash
44
+ pip install urdu-text-eval
45
+ ```
46
+
47
+ *(Dependencies: `jiwer`, `sacrebleu`, `rapidfuzz`)*
48
+
49
+ ---
50
+
51
+ ## Quick Start
52
+
53
+ ```python
54
+ from urdu_text_eval import evaluate
55
+
56
+ pairs = [
57
+ {"actual": "کیا یہ ہے؟", "pred": "كيا يہ ہے؟"},
58
+ {"actual": "آہ جو دل سے", "pred": "آه جو دل سے"},
59
+ ]
60
+
61
+ result = evaluate(pairs)
62
+
63
+ print(result["summary"])
64
+ print(f"WER: {result['wer']:.4f} | CER: {result['cer']:.4f} | chrF: {result['chrf']:.4f}")
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Output Structure
70
+
71
+ Calling `evaluate(pairs, per_sample=True)` returns a dictionary containing:
72
+
73
+ | Key | Type | Description |
74
+ |-----|------|-------------|
75
+ | `wer` | `float` | Primary Word Error Rate (0.0 = perfect match) |
76
+ | `cer` | `float` | Primary Character Error Rate |
77
+ | `word_accuracy` | `float` | `1.0 - wer` |
78
+ | `char_accuracy` | `float` | `1.0 - cer` |
79
+ | `normalized_exact_match` | `float` | Ratio of exact matches after normalization |
80
+ | `chrf` | `float` | Character n-gram F-score (sacrebleu) |
81
+ | `bleu` | `float` | Corpus BLEU score |
82
+ | `mean_char_similarity` | `float` | Mean normalized Levenshtein similarity |
83
+ | `raw_wer` / `raw_cer` | `float` | Metrics calculated on raw strings without normalization |
84
+ | `raw_exact_match` | `float` | Exact match ratio on raw strings |
85
+ | `summary` | `str` | Pre-formatted, printable evaluation report |
86
+ | `samples` | `list` | *(Optional, if `per_sample=True`)* List of dicts with per-row scores and normalized strings |
87
+
88
+ ---
89
+
90
+ ## Normalization & Customization
91
+
92
+ Urdu text often varies in orthography (e.g. Arabic vs. Urdu keyboards, presence of diacritics/اعراب, punctuation). You can control normalization behavior precisely:
93
+
94
+ ### 1. Master On/Off
95
+
96
+ ```python
97
+ # Default: Normalization ON (Fair orthographic evaluation)
98
+ result = evaluate(pairs)
99
+
100
+ # Raw evaluation (No normalization applied)
101
+ result = evaluate(pairs, normalize=False)
102
+ ```
103
+
104
+ ### 2. Convenience Overrides
105
+
106
+ You can pass boolean flags directly to `evaluate()`:
107
+
108
+ ```python
109
+ result = evaluate(
110
+ pairs,
111
+ taxonomy=True, # Convert Arabic codepoints (ك/ي/ه) to Urdu (ک/ی/ہ)
112
+ remove_diacritics=True, # Remove all اعراب (zabar, zeer, pesh, tanween, etc.)
113
+ remove_zabar=True, # Remove zabar (َ) only
114
+ remove_zeer=True, # Remove zeer (ِ) only
115
+ remove_pesh=True, # Remove pesh (ُ) only
116
+ remove_punctuation=True, # Remove Urdu (؛،؟۔٪) and ASCII punctuation
117
+ remove_digits=False, # Remove ASCII and Urdu digits
118
+ )
119
+ ```
120
+
121
+ ### 3. Using `NormalizeConfig` or Presets
122
+
123
+ For reusability across benchmarks, configure a `NormalizeConfig`:
124
+
125
+ ```python
126
+ from urdu_text_eval import evaluate, NormalizeConfig
127
+
128
+ # Presets
129
+ cfg_default = NormalizeConfig.default() # Taxonomy + diacritics + whitespace
130
+ cfg_raw = NormalizeConfig.none() # Raw string comparison
131
+ cfg_full = NormalizeConfig.full() # Everything on (including punctuation & digit stripping)
132
+ cfg_diac = NormalizeConfig.diacritics_only() # Only remove اعراب
133
+ cfg_punct = NormalizeConfig.punctuation_only() # Only remove punctuation
134
+ cfg_tax = NormalizeConfig.taxonomy_only() # Only taxonomy mapping
135
+
136
+ # Custom Configuration
137
+ cfg = NormalizeConfig(
138
+ enabled=True,
139
+ taxonomy=True, # Map Arabic/presentation characters
140
+ combine_characters=True, # Combine characters (ا + ٓ → آ)
141
+ remove_zabar=True, # Remove zabar
142
+ remove_zeer=True, # Remove zeer
143
+ remove_pesh=True, # Remove pesh
144
+ remove_tanween=True, # Remove tanween
145
+ remove_shadda=True, # Remove shadda
146
+ remove_sukun=True, # Remove sukun
147
+ remove_punctuation=False, # Keep punctuation
148
+ remove_digits=False, # Keep digits
149
+ )
150
+
151
+ result = evaluate(pairs, config=cfg)
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Standalone Normalizer Function
157
+
158
+ You can also use the Urdu normalizer directly on individual text strings:
159
+
160
+ ```python
161
+ from urdu_text_eval import normalize_urdu, NormalizeConfig
162
+
163
+ # Default normalization
164
+ clean_text = normalize_urdu("كيا يہ ہے؟")
165
+ # Output: "کیا یہ ہے؟"
166
+
167
+ # Remove diacritics (zabar/zeer/pesh) only
168
+ clean_text = normalize_urdu("شیرِ پنجاب", remove_diacritics_all=True)
169
+ # Output: "شیر پنجاب"
170
+
171
+ # Punctuation removal
172
+ clean_text = normalize_urdu("سلام، دنیا!", config=NormalizeConfig.punctuation_only())
173
+ # Output: "سلام دنیا"
174
+ ```
175
+
176
+ ---
177
+
178
+ ## API Summary
179
+
180
+ ```python
181
+ from urdu_text_eval import (
182
+ evaluate, # Core benchmark function: evaluate([{"actual": "...", "pred": "..."}])
183
+ NormalizeConfig, # Configuration dataclass & presets for text normalization
184
+ normalize_urdu, # Single-string Urdu normalization function
185
+ compute_metrics, # Lower-level function: compute_metrics(references, hypotheses)
186
+ format_metrics, # Formats metrics dict into a human-readable text summary
187
+ per_sample_errors, # Returns list of per-item error breakdown dicts
188
+ )
189
+ ```
190
+
191
+ ---
192
+
193
+ ## License
194
+
195
+ Distributed under the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,179 @@
1
+ # urdu-text-eval
2
+
3
+ [![PyPI Version](https://img.shields.io/pypi/v/urdu-text-eval.svg)](https://pypi.org/project/urdu-text-eval/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Python Version](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/downloads/)
6
+
7
+ **urdu-text-eval** is a Python benchmarking library for **Urdu text evaluation** (OCR, ASR, transliteration, machine translation, LLM text generation, and text processing).
8
+
9
+ It computes standard NLP metrics (**WER, CER, chrF, BLEU, Exact Match, Character Similarity**) with configurable **Urdu orthographic and taxonomy normalization** (handling Arabic lookalike codepoints, zabar/zeer/pesh diacritics, punctuation, numbers, and presentation forms).
10
+
11
+ ---
12
+
13
+ ## Key Features
14
+
15
+ - **Standard Metrics**: Word Error Rate (WER), Character Error Rate (CER), Word/Char Accuracy, chrF, BLEU, Exact Match, and Levenshtein Character Similarity.
16
+ - **Urdu Taxonomy Normalization**: Standardizes Arabic variants (`ك` → `ک`, `ي` → `ی`, `ه` → `ہ`), presentation forms (`ﺍ`, `ﺎ`, `ﺐ`), and combined characters (`ا`+`ٓ` → `آ`).
17
+ - **Fine-Grained Diacritic Controls**: Selective removal of اعراب (**Zabar** `َ`, **Zeer** `ِ`, **Pesh** `ُ`, **Tanween** `ً ٌ ٍ`, **Shadda** `ّ`, **Sukun** `ْ`, Maddah, and Hamza marks).
18
+ - **Flexible Input Formats**: Evaluates simple list-of-dicts `[{"actual": "...", "pred": "..."}]` with support for common key aliases (`ref`, `target`, `gold`, `Urdu`, `hyp`, `prediction`, `output`).
19
+ - **Dual Reporting**: Always reports both **taxonomy-normalized** (primary) and **raw surface** metrics side-by-side.
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ Install via pip:
26
+
27
+ ```bash
28
+ pip install urdu-text-eval
29
+ ```
30
+
31
+ *(Dependencies: `jiwer`, `sacrebleu`, `rapidfuzz`)*
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ from urdu_text_eval import evaluate
39
+
40
+ pairs = [
41
+ {"actual": "کیا یہ ہے؟", "pred": "كيا يہ ہے؟"},
42
+ {"actual": "آہ جو دل سے", "pred": "آه جو دل سے"},
43
+ ]
44
+
45
+ result = evaluate(pairs)
46
+
47
+ print(result["summary"])
48
+ print(f"WER: {result['wer']:.4f} | CER: {result['cer']:.4f} | chrF: {result['chrf']:.4f}")
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Output Structure
54
+
55
+ Calling `evaluate(pairs, per_sample=True)` returns a dictionary containing:
56
+
57
+ | Key | Type | Description |
58
+ |-----|------|-------------|
59
+ | `wer` | `float` | Primary Word Error Rate (0.0 = perfect match) |
60
+ | `cer` | `float` | Primary Character Error Rate |
61
+ | `word_accuracy` | `float` | `1.0 - wer` |
62
+ | `char_accuracy` | `float` | `1.0 - cer` |
63
+ | `normalized_exact_match` | `float` | Ratio of exact matches after normalization |
64
+ | `chrf` | `float` | Character n-gram F-score (sacrebleu) |
65
+ | `bleu` | `float` | Corpus BLEU score |
66
+ | `mean_char_similarity` | `float` | Mean normalized Levenshtein similarity |
67
+ | `raw_wer` / `raw_cer` | `float` | Metrics calculated on raw strings without normalization |
68
+ | `raw_exact_match` | `float` | Exact match ratio on raw strings |
69
+ | `summary` | `str` | Pre-formatted, printable evaluation report |
70
+ | `samples` | `list` | *(Optional, if `per_sample=True`)* List of dicts with per-row scores and normalized strings |
71
+
72
+ ---
73
+
74
+ ## Normalization & Customization
75
+
76
+ Urdu text often varies in orthography (e.g. Arabic vs. Urdu keyboards, presence of diacritics/اعراب, punctuation). You can control normalization behavior precisely:
77
+
78
+ ### 1. Master On/Off
79
+
80
+ ```python
81
+ # Default: Normalization ON (Fair orthographic evaluation)
82
+ result = evaluate(pairs)
83
+
84
+ # Raw evaluation (No normalization applied)
85
+ result = evaluate(pairs, normalize=False)
86
+ ```
87
+
88
+ ### 2. Convenience Overrides
89
+
90
+ You can pass boolean flags directly to `evaluate()`:
91
+
92
+ ```python
93
+ result = evaluate(
94
+ pairs,
95
+ taxonomy=True, # Convert Arabic codepoints (ك/ي/ه) to Urdu (ک/ی/ہ)
96
+ remove_diacritics=True, # Remove all اعراب (zabar, zeer, pesh, tanween, etc.)
97
+ remove_zabar=True, # Remove zabar (َ) only
98
+ remove_zeer=True, # Remove zeer (ِ) only
99
+ remove_pesh=True, # Remove pesh (ُ) only
100
+ remove_punctuation=True, # Remove Urdu (؛،؟۔٪) and ASCII punctuation
101
+ remove_digits=False, # Remove ASCII and Urdu digits
102
+ )
103
+ ```
104
+
105
+ ### 3. Using `NormalizeConfig` or Presets
106
+
107
+ For reusability across benchmarks, configure a `NormalizeConfig`:
108
+
109
+ ```python
110
+ from urdu_text_eval import evaluate, NormalizeConfig
111
+
112
+ # Presets
113
+ cfg_default = NormalizeConfig.default() # Taxonomy + diacritics + whitespace
114
+ cfg_raw = NormalizeConfig.none() # Raw string comparison
115
+ cfg_full = NormalizeConfig.full() # Everything on (including punctuation & digit stripping)
116
+ cfg_diac = NormalizeConfig.diacritics_only() # Only remove اعراب
117
+ cfg_punct = NormalizeConfig.punctuation_only() # Only remove punctuation
118
+ cfg_tax = NormalizeConfig.taxonomy_only() # Only taxonomy mapping
119
+
120
+ # Custom Configuration
121
+ cfg = NormalizeConfig(
122
+ enabled=True,
123
+ taxonomy=True, # Map Arabic/presentation characters
124
+ combine_characters=True, # Combine characters (ا + ٓ → آ)
125
+ remove_zabar=True, # Remove zabar
126
+ remove_zeer=True, # Remove zeer
127
+ remove_pesh=True, # Remove pesh
128
+ remove_tanween=True, # Remove tanween
129
+ remove_shadda=True, # Remove shadda
130
+ remove_sukun=True, # Remove sukun
131
+ remove_punctuation=False, # Keep punctuation
132
+ remove_digits=False, # Keep digits
133
+ )
134
+
135
+ result = evaluate(pairs, config=cfg)
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Standalone Normalizer Function
141
+
142
+ You can also use the Urdu normalizer directly on individual text strings:
143
+
144
+ ```python
145
+ from urdu_text_eval import normalize_urdu, NormalizeConfig
146
+
147
+ # Default normalization
148
+ clean_text = normalize_urdu("كيا يہ ہے؟")
149
+ # Output: "کیا یہ ہے؟"
150
+
151
+ # Remove diacritics (zabar/zeer/pesh) only
152
+ clean_text = normalize_urdu("شیرِ پنجاب", remove_diacritics_all=True)
153
+ # Output: "شیر پنجاب"
154
+
155
+ # Punctuation removal
156
+ clean_text = normalize_urdu("سلام، دنیا!", config=NormalizeConfig.punctuation_only())
157
+ # Output: "سلام دنیا"
158
+ ```
159
+
160
+ ---
161
+
162
+ ## API Summary
163
+
164
+ ```python
165
+ from urdu_text_eval import (
166
+ evaluate, # Core benchmark function: evaluate([{"actual": "...", "pred": "..."}])
167
+ NormalizeConfig, # Configuration dataclass & presets for text normalization
168
+ normalize_urdu, # Single-string Urdu normalization function
169
+ compute_metrics, # Lower-level function: compute_metrics(references, hypotheses)
170
+ format_metrics, # Formats metrics dict into a human-readable text summary
171
+ per_sample_errors, # Returns list of per-item error breakdown dicts
172
+ )
173
+ ```
174
+
175
+ ---
176
+
177
+ ## License
178
+
179
+ Distributed under the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "urdu-text-eval"
7
+ version = "1.0"
8
+ description = "Urdu text evaluation & benchmarking library with configurable orthographic normalization (WER, CER, chrF, BLEU)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "urdu-text-eval" }]
13
+ keywords = [
14
+ "urdu",
15
+ "evaluation",
16
+ "benchmarking",
17
+ "wer",
18
+ "cer",
19
+ "chrf",
20
+ "bleu",
21
+ "nlp",
22
+ "ocr",
23
+ "asr",
24
+ "transliteration",
25
+ "text-evaluation",
26
+ ]
27
+ dependencies = [
28
+ "jiwer>=3.0.0",
29
+ "sacrebleu>=2.4.0",
30
+ "rapidfuzz>=3.0.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["pytest>=7.0"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/local/urdu-text-eval"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.setuptools.package-data]
43
+ urdu_text_eval = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,16 @@
1
+ """urdu-text-eval: Urdu text evaluation and benchmarking library with orthographic normalization."""
2
+
3
+ from .evaluate import evaluate
4
+ from .metrics import compute_metrics, format_metrics, per_sample_errors
5
+ from .normalize import NormalizeConfig, normalize_urdu
6
+
7
+ __all__ = [
8
+ "evaluate",
9
+ "compute_metrics",
10
+ "format_metrics",
11
+ "per_sample_errors",
12
+ "normalize_urdu",
13
+ "NormalizeConfig",
14
+ ]
15
+
16
+ __version__ = "1.0"
@@ -0,0 +1,102 @@
1
+ """Public evaluation API for Urdu actual/pred text pairs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, Sequence
6
+
7
+ from .metrics import compute_metrics, format_metrics, per_sample_errors
8
+ from .normalize import NormalizeConfig
9
+
10
+ _ACTUAL_KEYS = ("actual", "reference", "ref", "Urdu", "urdu", "target", "gold")
11
+ _PRED_KEYS = ("pred", "prediction", "hypothesis", "hyp", "output")
12
+
13
+
14
+ def _pick(row: Mapping[str, Any], keys: tuple[str, ...], label: str) -> str:
15
+ for key in keys:
16
+ if key in row and row[key] is not None:
17
+ return str(row[key])
18
+ raise KeyError(
19
+ f"Each item must include an '{label}' field "
20
+ f"(accepted keys: {', '.join(keys)}). Got keys: {sorted(row)}"
21
+ )
22
+
23
+
24
+ def evaluate(
25
+ pairs: Sequence[Mapping[str, Any]],
26
+ *,
27
+ normalize: bool = True,
28
+ config: NormalizeConfig | None = None,
29
+ per_sample: bool = False,
30
+ # shortcuts (applied on top of config / default)
31
+ taxonomy: bool | None = None,
32
+ remove_punctuation: bool | None = None,
33
+ remove_zabar: bool | None = None,
34
+ remove_zeer: bool | None = None,
35
+ remove_pesh: bool | None = None,
36
+ remove_diacritics: bool | None = None,
37
+ remove_digits: bool | None = None,
38
+ ) -> dict[str, Any]:
39
+ """
40
+ Evaluate Urdu text prediction quality against reference ground truth.
41
+
42
+ Parameters
43
+ ----------
44
+ pairs :
45
+ ``[{"actual": "...", "pred": "..."}, ...]``
46
+ normalize :
47
+ Master switch. ``False`` → no preprocessing (raw compare).
48
+ config :
49
+ Fine-grained ``NormalizeConfig``. Ignored when ``normalize=False``.
50
+ per_sample :
51
+ Include per-row scores under ``samples``.
52
+ taxonomy / remove_punctuation / remove_zabar / remove_zeer / remove_pesh /
53
+ remove_diacritics / remove_digits :
54
+ Optional overrides on top of ``config``.
55
+ """
56
+ if not pairs:
57
+ raise ValueError("pairs must be a non-empty list of {actual, pred} dicts.")
58
+
59
+ if not normalize:
60
+ cfg = NormalizeConfig.none()
61
+ else:
62
+ cfg = config or NormalizeConfig.default()
63
+ data = cfg.to_dict()
64
+ if taxonomy is not None:
65
+ data["taxonomy"] = taxonomy
66
+ if remove_punctuation is not None:
67
+ data["remove_punctuation"] = remove_punctuation
68
+ if remove_digits is not None:
69
+ data["remove_digits"] = remove_digits
70
+ if remove_zabar is not None:
71
+ data["remove_zabar"] = remove_zabar
72
+ if remove_zeer is not None:
73
+ data["remove_zeer"] = remove_zeer
74
+ if remove_pesh is not None:
75
+ data["remove_pesh"] = remove_pesh
76
+ if remove_diacritics is not None:
77
+ for key in (
78
+ "remove_zabar",
79
+ "remove_zeer",
80
+ "remove_pesh",
81
+ "remove_tanween",
82
+ "remove_shadda",
83
+ "remove_sukun",
84
+ "remove_other_marks",
85
+ ):
86
+ data[key] = remove_diacritics
87
+ cfg = NormalizeConfig(**data)
88
+
89
+ actuals: list[str] = []
90
+ preds: list[str] = []
91
+ for i, row in enumerate(pairs):
92
+ if not isinstance(row, Mapping):
93
+ raise TypeError(f"Item {i} must be a dict, got {type(row).__name__}")
94
+ actuals.append(_pick(row, _ACTUAL_KEYS, "actual"))
95
+ preds.append(_pick(row, _PRED_KEYS, "pred"))
96
+
97
+ metrics = compute_metrics(actuals, preds, config=cfg)
98
+ result: dict[str, Any] = dict(metrics)
99
+ result["summary"] = format_metrics(metrics)
100
+ if per_sample:
101
+ result["samples"] = per_sample_errors(actuals, preds, config=cfg)
102
+ return result
@@ -0,0 +1,151 @@
1
+ """WER / CER / chrF / BLEU and related Urdu text evaluation metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Iterable, Sequence
6
+
7
+ from jiwer import cer as jiwer_cer
8
+ from jiwer import wer as jiwer_wer
9
+ from rapidfuzz.distance import Levenshtein
10
+ from sacrebleu import CHRF, corpus_bleu
11
+
12
+ from .normalize import NormalizeConfig, normalize_urdu
13
+
14
+
15
+ def _as_list(texts: Iterable[str]) -> list[str]:
16
+ return ["" if t is None else str(t).strip() for t in texts]
17
+
18
+
19
+ def _safe_pair(refs: list[str], hyps: list[str]) -> tuple[list[str], list[str]]:
20
+ return [r if r else " " for r in refs], [h if h else " " for h in hyps]
21
+
22
+
23
+ def _edit_metrics(refs: list[str], hyps: list[str]) -> tuple[float, float]:
24
+ sr, sh = _safe_pair(refs, hyps)
25
+ return float(jiwer_wer(sr, sh)), float(jiwer_cer(sr, sh))
26
+
27
+
28
+ def compute_metrics(
29
+ references: Sequence[str],
30
+ hypotheses: Sequence[str],
31
+ *,
32
+ config: NormalizeConfig | None = None,
33
+ normalize: bool | None = None,
34
+ ) -> dict[str, Any]:
35
+ """
36
+ Corpus-level metrics for Urdu text evaluation.
37
+
38
+ ``normalize=False`` disables preprocessing (same as ``NormalizeConfig.none()``).
39
+ Otherwise ``config`` (default ``NormalizeConfig()``) controls each step.
40
+ """
41
+ refs = _as_list(references)
42
+ hyps = _as_list(hypotheses)
43
+ if len(refs) != len(hyps):
44
+ raise ValueError(f"Length mismatch: refs={len(refs)} hyps={len(hyps)}")
45
+ if not refs:
46
+ raise ValueError("Empty evaluation set.")
47
+
48
+ if normalize is False:
49
+ cfg = NormalizeConfig.none()
50
+ elif config is not None:
51
+ cfg = config
52
+ else:
53
+ cfg = NormalizeConfig.default()
54
+
55
+ raw_wer, raw_cer = _edit_metrics(refs, hyps)
56
+ raw_exact = sum(1 for r, h in zip(refs, hyps) if r == h) / len(refs)
57
+
58
+ refs_n = [normalize_urdu(r, cfg) for r in refs]
59
+ hyps_n = [normalize_urdu(h, cfg) for h in hyps]
60
+ wer, cer = _edit_metrics(refs_n, hyps_n)
61
+ exact_n = sum(1 for r, h in zip(refs_n, hyps_n) if r == h) / len(refs_n)
62
+
63
+ chrf = CHRF(word_order=0).corpus_score(hyps_n, [refs_n]).score / 100.0
64
+ bleu = corpus_bleu(hyps_n, [refs_n]).score / 100.0
65
+
66
+ sims = [
67
+ 1.0 if (not r and not h) else Levenshtein.normalized_similarity(r, h)
68
+ for r, h in zip(refs_n, hyps_n)
69
+ ]
70
+
71
+ return {
72
+ "n": float(len(refs)),
73
+ "normalize_config": cfg.to_dict(),
74
+ "wer": wer,
75
+ "cer": cer,
76
+ "word_accuracy": 1.0 - wer,
77
+ "char_accuracy": 1.0 - cer,
78
+ "normalized_exact_match": exact_n,
79
+ "chrf": chrf,
80
+ "bleu": bleu,
81
+ "mean_char_similarity": sum(sims) / len(sims),
82
+ "raw_wer": raw_wer,
83
+ "raw_cer": raw_cer,
84
+ "raw_exact_match": raw_exact,
85
+ "exact_match": raw_exact,
86
+ }
87
+
88
+
89
+ def per_sample_errors(
90
+ references: Sequence[str],
91
+ hypotheses: Sequence[str],
92
+ *,
93
+ config: NormalizeConfig | None = None,
94
+ normalize: bool | None = None,
95
+ ) -> list[dict[str, float | int | str]]:
96
+ if normalize is False:
97
+ cfg = NormalizeConfig.none()
98
+ elif config is not None:
99
+ cfg = config
100
+ else:
101
+ cfg = NormalizeConfig.default()
102
+
103
+ refs = _as_list(references)
104
+ hyps = _as_list(hypotheses)
105
+ rows: list[dict[str, float | int | str]] = []
106
+ for r, h in zip(refs, hyps):
107
+ rn = normalize_urdu(r, cfg)
108
+ hn = normalize_urdu(h, cfg)
109
+ sr, sh = (rn or " "), (hn or " ")
110
+ rows.append(
111
+ {
112
+ "actual": r,
113
+ "pred": h,
114
+ "actual_normalized": rn,
115
+ "pred_normalized": hn,
116
+ "cer": float(jiwer_cer(sr, sh)),
117
+ "wer": float(jiwer_wer(sr, sh)),
118
+ "raw_cer": float(jiwer_cer(r or " ", h or " ")),
119
+ "raw_wer": float(jiwer_wer(r or " ", h or " ")),
120
+ "exact": int(r == h),
121
+ "normalized_exact": int(rn == hn),
122
+ "char_similarity": float(Levenshtein.normalized_similarity(rn, hn)),
123
+ }
124
+ )
125
+ return rows
126
+
127
+
128
+ def format_metrics(metrics: dict[str, Any]) -> str:
129
+ cfg = metrics.get("normalize_config") or {}
130
+ enabled = cfg.get("enabled", True)
131
+ return "\n".join(
132
+ [
133
+ f"n : {int(metrics['n'])}",
134
+ f"Normalization enabled : {enabled}",
135
+ "",
136
+ "— Primary scores (after configured normalization) —",
137
+ f"WER (↓ better) : {metrics['wer']:.4f}",
138
+ f"CER (↓ better) : {metrics['cer']:.4f}",
139
+ f"Word accuracy : {metrics['word_accuracy']:.4f}",
140
+ f"Char accuracy : {metrics['char_accuracy']:.4f}",
141
+ f"Norm. exact match : {metrics['normalized_exact_match']:.4f}",
142
+ f"chrF (↑ better) : {metrics['chrf']:.4f}",
143
+ f"BLEU (↑ better) : {metrics['bleu']:.4f}",
144
+ f"Mean char similarity : {metrics['mean_char_similarity']:.4f}",
145
+ "",
146
+ "— Raw surface strings —",
147
+ f"Raw WER : {metrics['raw_wer']:.4f}",
148
+ f"Raw CER : {metrics['raw_cer']:.4f}",
149
+ f"Raw exact match : {metrics['raw_exact_match']:.4f}",
150
+ ]
151
+ )
@@ -0,0 +1,376 @@
1
+ """
2
+ Urdu text normalization with fine-grained controls.
3
+
4
+ Supports turning individual steps on/off:
5
+ - character taxonomy (Arabic / presentation forms → Urdu)
6
+ - zabar / zeer / pesh and other diacritics (اعراب)
7
+ - punctuation removal
8
+ - whitespace / combine-character folding
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import string
15
+ import unicodedata
16
+ from dataclasses import asdict, dataclass, fields
17
+ from typing import Iterable
18
+
19
+ # Character taxonomy adapted from UrduHack (normalization/character.py)
20
+ # https://github.com/urduhack/urduhack
21
+ CORRECT_URDU_CHARACTERS: dict[str, list[str]] = {
22
+ "آ": ["ﺁ", "ﺂ"],
23
+ "أ": ["ﺃ"],
24
+ "ا": ["ﺍ", "ﺎ"],
25
+ "ب": ["ﺏ", "ﺐ", "ﺑ", "ﺒ"],
26
+ "پ": ["ﭖ", "ﭘ", "ﭙ"],
27
+ "ت": ["ﺕ", "ﺖ", "ﺗ", "ﺘ"],
28
+ "ٹ": ["ﭦ", "ﭧ", "ﭨ", "ﭩ"],
29
+ "ث": ["ﺛ", "ﺜ", "ﺚ"],
30
+ "ج": ["ﺝ", "ﺞ", "ﺟ", "ﺠ"],
31
+ "ح": ["ﺡ", "ﺣ", "ﺤ", "ﺢ"],
32
+ "خ": ["ﺧ", "ﺨ", "ﺦ"],
33
+ "د": ["ﺩ", "ﺪ"],
34
+ "ذ": ["ﺬ", "ﺫ"],
35
+ "ر": ["ﺭ", "ﺮ"],
36
+ "ز": ["ﺯ", "ﺰ"],
37
+ "س": ["ﺱ", "ﺲ", "ﺳ", "ﺴ"],
38
+ "ش": ["ﺵ", "ﺶ", "ﺷ", "ﺸ"],
39
+ "ص": ["ﺹ", "ﺺ", "ﺻ", "ﺼ"],
40
+ "ض": ["ﺽ", "ﺾ", "ﺿ", "ﻀ"],
41
+ "ط": ["ﻃ", "ﻄ"],
42
+ "ظ": ["ﻅ", "ﻇ", "ﻈ"],
43
+ "ع": ["ﻉ", "ﻊ", "ﻋ", "ﻌ"],
44
+ "غ": ["ﻍ", "ﻏ", "ﻐ"],
45
+ "ف": ["ﻑ", "ﻒ", "ﻓ", "ﻔ"],
46
+ "ق": ["ﻕ", "ﻖ", "ﻗ", "ﻘ"],
47
+ "ل": ["ﻝ", "ﻞ", "ﻟ", "ﻠ"],
48
+ "م": ["ﻡ", "ﻢ", "ﻣ", "ﻤ"],
49
+ "ن": ["ﻥ", "ﻦ", "ﻧ", "ﻨ"],
50
+ "چ": ["ﭺ", "ﭻ", "ﭼ", "ﭽ"],
51
+ "ڈ": ["ﮈ", "ﮉ"],
52
+ "ڑ": ["ﮍ", "ﮌ"],
53
+ "ژ": ["ﮋ"],
54
+ "ک": ["ﮎ", "ﮏ", "ﮐ", "ﮑ", "ﻛ", "ك"],
55
+ "گ": ["ﮒ", "ﮓ", "ﮔ", "ﮕ"],
56
+ "ں": ["ﮞ", "ﮟ"],
57
+ "و": ["ﻮ", "ﻭ"],
58
+ "ؤ": ["ﺅ"],
59
+ "ھ": ["ﮪ", "ﮬ", "ﮭ", "ﻬ", "ﻫ", "ﮫ"],
60
+ "ہ": ["ﻩ", "ﮦ", "ﻪ", "ﮧ", "ﮩ", "ﮨ", "ه", "ۀ"],
61
+ "ۃ": ["ة"],
62
+ "ء": ["ﺀ"],
63
+ "ی": ["ﯼ", "ى", "ﯽ", "ﻰ", "ﻱ", "ﻲ", "ﯾ", "ﯿ", "ي"],
64
+ "ئ": ["ﺋ", "ﺌ"],
65
+ "ے": ["ﮮ", "ﮯ", "ﻳ", "ﻴ"],
66
+ "۰": ["٠"],
67
+ "۱": ["١"],
68
+ "۲": ["٢"],
69
+ "۳": ["٣"],
70
+ "۴": ["٤"],
71
+ "۵": ["٥"],
72
+ "۶": ["٦"],
73
+ "۷": ["٧"],
74
+ "۸": ["٨"],
75
+ "۹": ["٩"],
76
+ "لا": ["ﻻ", "ﻼ"],
77
+ "": ["ـ"],
78
+ }
79
+
80
+ COMBINE_URDU_CHARACTERS: dict[str, str] = {
81
+ "آ": "آ",
82
+ "أ": "أ",
83
+ "ۓ": "ۓ",
84
+ }
85
+
86
+ # Named diacritic groups (اعراب)
87
+ ZABAR = "\u064E" # fatha َ
88
+ ZEER = "\u0650" # kasra ِ
89
+ PESH = "\u064F" # damma ُ
90
+ TANWEEN = "\u064B\u064C\u064D" # ً ٌ ٍ
91
+ SHADDA = "\u0651" # ّ
92
+ SUKUN = "\u0652" # ْ
93
+ OTHER_MARKS = (
94
+ "\u0653" # maddah
95
+ "\u0654" # hamza above
96
+ "\u0655" # hamza below
97
+ "\u0656" # subscript alef
98
+ "\u0657" # inverted damma
99
+ "\u0658" # noon ghunna
100
+ "\u0670" # superscript alef
101
+ )
102
+
103
+ URDU_PUNCTUATION = "؛،٫؟۔٪"
104
+ ASCII_PUNCTUATION = string.punctuation
105
+ ALL_PUNCTUATION = URDU_PUNCTUATION + ASCII_PUNCTUATION
106
+
107
+ _TRANSLATOR: dict[int, str] = {}
108
+ for _canon, _variants in CORRECT_URDU_CHARACTERS.items():
109
+ _TRANSLATOR.update(dict.fromkeys(map(ord, _variants), _canon))
110
+
111
+
112
+ @dataclass
113
+ class NormalizeConfig:
114
+ """
115
+ Fine-grained Urdu normalization switches for evaluation.
116
+
117
+ Set ``enabled=False`` to disable all preprocessing (raw string compare).
118
+ Individual flags are ignored when ``enabled`` is False.
119
+ """
120
+
121
+ enabled: bool = True
122
+ unicode_nfc: bool = True
123
+ whitespace: bool = True
124
+ taxonomy: bool = True
125
+ """Map Arabic / presentation-form lookalikes to canonical Urdu letters."""
126
+ combine_characters: bool = True
127
+ """Fold split sequences like ا+ٓ → آ."""
128
+
129
+ # Diacritics / اعراب
130
+ remove_zabar: bool = True
131
+ remove_zeer: bool = True
132
+ remove_pesh: bool = True
133
+ remove_tanween: bool = True
134
+ remove_shadda: bool = True
135
+ remove_sukun: bool = True
136
+ remove_other_marks: bool = True
137
+
138
+ # Extra cleanup
139
+ remove_punctuation: bool = False
140
+ remove_digits: bool = False
141
+ remove_tatweel: bool = True
142
+
143
+ # ---- presets ----------------------------------------------------------
144
+ @classmethod
145
+ def default(cls) -> "NormalizeConfig":
146
+ """Taxonomy + diacritics + whitespace (punctuation kept)."""
147
+ return cls()
148
+
149
+ @classmethod
150
+ def none(cls) -> "NormalizeConfig":
151
+ """No normalization at all."""
152
+ return cls(enabled=False)
153
+
154
+ @classmethod
155
+ def full(cls) -> "NormalizeConfig":
156
+ """Everything on, including punctuation and digit stripping."""
157
+ return cls(remove_punctuation=True, remove_digits=True)
158
+
159
+ @classmethod
160
+ def diacritics_only(cls) -> "NormalizeConfig":
161
+ """Only strip اعراب (zabar/zeer/pesh/…)."""
162
+ return cls(
163
+ taxonomy=False,
164
+ combine_characters=False,
165
+ whitespace=True,
166
+ unicode_nfc=True,
167
+ )
168
+
169
+ @classmethod
170
+ def punctuation_only(cls) -> "NormalizeConfig":
171
+ """Only remove punctuation (Urdu + ASCII)."""
172
+ return cls(
173
+ taxonomy=False,
174
+ combine_characters=False,
175
+ remove_zabar=False,
176
+ remove_zeer=False,
177
+ remove_pesh=False,
178
+ remove_tanween=False,
179
+ remove_shadda=False,
180
+ remove_sukun=False,
181
+ remove_other_marks=False,
182
+ remove_punctuation=True,
183
+ remove_tatweel=False,
184
+ )
185
+
186
+ @classmethod
187
+ def taxonomy_only(cls) -> "NormalizeConfig":
188
+ """Only Arabic→Urdu character taxonomy (+ whitespace)."""
189
+ return cls(
190
+ remove_zabar=False,
191
+ remove_zeer=False,
192
+ remove_pesh=False,
193
+ remove_tanween=False,
194
+ remove_shadda=False,
195
+ remove_sukun=False,
196
+ remove_other_marks=False,
197
+ combine_characters=True,
198
+ )
199
+
200
+ def with_diacritics(self, remove: bool) -> "NormalizeConfig":
201
+ """Toggle all diacritic flags together."""
202
+ return NormalizeConfig(
203
+ **{
204
+ **asdict(self),
205
+ "remove_zabar": remove,
206
+ "remove_zeer": remove,
207
+ "remove_pesh": remove,
208
+ "remove_tanween": remove,
209
+ "remove_shadda": remove,
210
+ "remove_sukun": remove,
211
+ "remove_other_marks": remove,
212
+ }
213
+ )
214
+
215
+ def to_dict(self) -> dict:
216
+ return asdict(self)
217
+
218
+
219
+ def _diacritic_chars(cfg: NormalizeConfig) -> str:
220
+ chars = ""
221
+ if cfg.remove_zabar:
222
+ chars += ZABAR
223
+ if cfg.remove_zeer:
224
+ chars += ZEER
225
+ if cfg.remove_pesh:
226
+ chars += PESH
227
+ if cfg.remove_tanween:
228
+ chars += TANWEEN
229
+ if cfg.remove_shadda:
230
+ chars += SHADDA
231
+ if cfg.remove_sukun:
232
+ chars += SUKUN
233
+ if cfg.remove_other_marks:
234
+ chars += OTHER_MARKS
235
+ return chars
236
+
237
+
238
+ def strip_characters(text: str) -> str:
239
+ return text.translate(_TRANSLATOR)
240
+
241
+
242
+ def strip_combine_characters(text: str) -> str:
243
+ for key, value in COMBINE_URDU_CHARACTERS.items():
244
+ text = text.replace(key, value)
245
+ return text
246
+
247
+
248
+ def strip_diacritics(text: str, chars: str | None = None) -> str:
249
+ """Strip selected diacritics; default = all common اعراب."""
250
+ if chars is None:
251
+ chars = ZABAR + ZEER + PESH + TANWEEN + SHADDA + SUKUN + OTHER_MARKS
252
+ if not chars:
253
+ return text
254
+ return re.sub(f"[{re.escape(chars)}]", "", text)
255
+
256
+
257
+ def strip_punctuation(text: str) -> str:
258
+ table = str.maketrans("", "", ALL_PUNCTUATION)
259
+ return text.translate(table)
260
+
261
+
262
+ def strip_digits(text: str) -> str:
263
+ return re.sub(r"[0-9۰-۹٠-٩]", "", text)
264
+
265
+
266
+ def normalize_whitespace(text: str) -> str:
267
+ text = text.replace("\u00a0", " ").replace("\u200c", "")
268
+ text = text.replace("\u200e", "").replace("\u200f", "").replace("\ufeff", "")
269
+ return " ".join(text.split())
270
+
271
+
272
+ # Aliases for helper usage
273
+ normalize_characters = strip_characters
274
+ normalize_combine_characters = strip_combine_characters
275
+ remove_diacritics = strip_diacritics
276
+ remove_punctuation = strip_punctuation
277
+ remove_digits = strip_digits
278
+
279
+
280
+ def normalize_urdu(
281
+ text: str,
282
+ config: NormalizeConfig | None = None,
283
+ *,
284
+ # convenience kwargs override config fields when provided
285
+ enabled: bool | None = None,
286
+ taxonomy: bool | None = None,
287
+ remove_punctuation: bool | None = None,
288
+ remove_zabar: bool | None = None,
289
+ remove_zeer: bool | None = None,
290
+ remove_pesh: bool | None = None,
291
+ remove_diacritics_all: bool | None = None,
292
+ **overrides: bool,
293
+ ) -> str:
294
+ """
295
+ Normalize one Urdu string according to ``NormalizeConfig``.
296
+
297
+ Pass either a ``config`` object or individual keyword flags.
298
+ ``remove_diacritics_all=True/False`` toggles every اعراب flag at once.
299
+ """
300
+ cfg = config or NormalizeConfig()
301
+ if overrides or any(
302
+ v is not None
303
+ for v in (
304
+ enabled,
305
+ taxonomy,
306
+ remove_punctuation,
307
+ remove_zabar,
308
+ remove_zeer,
309
+ remove_pesh,
310
+ remove_diacritics_all,
311
+ )
312
+ ):
313
+ data = asdict(cfg)
314
+ if enabled is not None:
315
+ data["enabled"] = enabled
316
+ if taxonomy is not None:
317
+ data["taxonomy"] = taxonomy
318
+ if remove_punctuation is not None:
319
+ data["remove_punctuation"] = remove_punctuation
320
+ if remove_zabar is not None:
321
+ data["remove_zabar"] = remove_zabar
322
+ if remove_zeer is not None:
323
+ data["remove_zeer"] = remove_zeer
324
+ if remove_pesh is not None:
325
+ data["remove_pesh"] = remove_pesh
326
+ if remove_diacritics_all is not None:
327
+ for key in (
328
+ "remove_zabar",
329
+ "remove_zeer",
330
+ "remove_pesh",
331
+ "remove_tanween",
332
+ "remove_shadda",
333
+ "remove_sukun",
334
+ "remove_other_marks",
335
+ ):
336
+ data[key] = remove_diacritics_all
337
+ valid = {f.name for f in fields(NormalizeConfig)}
338
+ for key, value in overrides.items():
339
+ if key in valid:
340
+ data[key] = value
341
+ cfg = NormalizeConfig(**data)
342
+
343
+ if text is None:
344
+ return ""
345
+ if not isinstance(text, str):
346
+ text = str(text)
347
+ if not cfg.enabled:
348
+ return text.strip()
349
+
350
+ if cfg.unicode_nfc:
351
+ text = unicodedata.normalize("NFC", text)
352
+ if cfg.whitespace:
353
+ text = normalize_whitespace(text)
354
+ if cfg.remove_tatweel:
355
+ text = text.replace("ـ", "")
356
+
357
+ marks = _diacritic_chars(cfg)
358
+ if marks:
359
+ text = strip_diacritics(text, marks)
360
+
361
+ if cfg.taxonomy:
362
+ text = strip_characters(text)
363
+ if cfg.combine_characters:
364
+ text = strip_combine_characters(text)
365
+ if cfg.remove_punctuation:
366
+ text = strip_punctuation(text)
367
+ if cfg.remove_digits:
368
+ text = strip_digits(text)
369
+ if cfg.whitespace:
370
+ text = normalize_whitespace(text)
371
+ return text
372
+
373
+
374
+ def normalize_many(texts: Iterable[str], config: NormalizeConfig | None = None) -> list[str]:
375
+ cfg = config or NormalizeConfig()
376
+ return [normalize_urdu(t, cfg) for t in texts]
File without changes
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: urdu-text-eval
3
+ Version: 1.0
4
+ Summary: Urdu text evaluation & benchmarking library with configurable orthographic normalization (WER, CER, chrF, BLEU).
5
+ Author: urdu-text-eval
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/local/urdu-text-eval
8
+ Keywords: urdu,evaluation,benchmarking,wer,cer,chrf,bleu,nlp,ocr,asr,transliteration,text-evaluation
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: jiwer>=3.0.0
12
+ Requires-Dist: sacrebleu>=2.4.0
13
+ Requires-Dist: rapidfuzz>=3.0.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == "dev"
16
+
17
+ # urdu-text-eval
18
+
19
+ [![PyPI Version](https://img.shields.io/pypi/v/urdu-text-eval.svg)](https://pypi.org/project/urdu-text-eval/)
20
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
21
+ [![Python Version](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/downloads/)
22
+
23
+ **urdu-text-eval** is a Python benchmarking library for **Urdu text evaluation** (OCR, ASR, transliteration, machine translation, LLM text generation, and text processing).
24
+
25
+ It computes standard NLP metrics (**WER, CER, chrF, BLEU, Exact Match, Character Similarity**) with configurable **Urdu orthographic and taxonomy normalization** (handling Arabic lookalike codepoints, zabar/zeer/pesh diacritics, punctuation, numbers, and presentation forms).
26
+
27
+ ---
28
+
29
+ ## Key Features
30
+
31
+ - **Standard Metrics**: Word Error Rate (WER), Character Error Rate (CER), Word/Char Accuracy, chrF, BLEU, Exact Match, and Levenshtein Character Similarity.
32
+ - **Urdu Taxonomy Normalization**: Standardizes Arabic variants (`ك` → `ک`, `ي` → `ی`, `ه` → `ہ`), presentation forms (`ﺍ`, `ﺎ`, `ﺐ`), and combined characters (`ا`+`ٓ` → `آ`).
33
+ - **Fine-Grained Diacritic Controls**: Selective removal of اعراب (**Zabar** `َ`, **Zeer** `ِ`, **Pesh** `ُ`, **Tanween** `ً ٌ ٍ`, **Shadda** `ّ`, **Sukun** `ْ`, Maddah, and Hamza marks).
34
+ - **Flexible Input Formats**: Evaluates simple list-of-dicts `[{"actual": "...", "pred": "..."}]` with support for common key aliases (`ref`, `target`, `gold`, `Urdu`, `hyp`, `prediction`, `output`).
35
+ - **Dual Reporting**: Always reports both **taxonomy-normalized** (primary) and **raw surface** metrics side-by-side.
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ Install via pip:
42
+
43
+ ```bash
44
+ pip install urdu-text-eval
45
+ ```
46
+
47
+ *(Dependencies: `jiwer`, `sacrebleu`, `rapidfuzz`)*
48
+
49
+ ---
50
+
51
+ ## Quick Start
52
+
53
+ ```python
54
+ from urdu_text_eval import evaluate
55
+
56
+ pairs = [
57
+ {"actual": "کیا یہ ہے؟", "pred": "كيا يہ ہے؟"},
58
+ {"actual": "آہ جو دل سے", "pred": "آه جو دل سے"},
59
+ ]
60
+
61
+ result = evaluate(pairs)
62
+
63
+ print(result["summary"])
64
+ print(f"WER: {result['wer']:.4f} | CER: {result['cer']:.4f} | chrF: {result['chrf']:.4f}")
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Output Structure
70
+
71
+ Calling `evaluate(pairs, per_sample=True)` returns a dictionary containing:
72
+
73
+ | Key | Type | Description |
74
+ |-----|------|-------------|
75
+ | `wer` | `float` | Primary Word Error Rate (0.0 = perfect match) |
76
+ | `cer` | `float` | Primary Character Error Rate |
77
+ | `word_accuracy` | `float` | `1.0 - wer` |
78
+ | `char_accuracy` | `float` | `1.0 - cer` |
79
+ | `normalized_exact_match` | `float` | Ratio of exact matches after normalization |
80
+ | `chrf` | `float` | Character n-gram F-score (sacrebleu) |
81
+ | `bleu` | `float` | Corpus BLEU score |
82
+ | `mean_char_similarity` | `float` | Mean normalized Levenshtein similarity |
83
+ | `raw_wer` / `raw_cer` | `float` | Metrics calculated on raw strings without normalization |
84
+ | `raw_exact_match` | `float` | Exact match ratio on raw strings |
85
+ | `summary` | `str` | Pre-formatted, printable evaluation report |
86
+ | `samples` | `list` | *(Optional, if `per_sample=True`)* List of dicts with per-row scores and normalized strings |
87
+
88
+ ---
89
+
90
+ ## Normalization & Customization
91
+
92
+ Urdu text often varies in orthography (e.g. Arabic vs. Urdu keyboards, presence of diacritics/اعراب, punctuation). You can control normalization behavior precisely:
93
+
94
+ ### 1. Master On/Off
95
+
96
+ ```python
97
+ # Default: Normalization ON (Fair orthographic evaluation)
98
+ result = evaluate(pairs)
99
+
100
+ # Raw evaluation (No normalization applied)
101
+ result = evaluate(pairs, normalize=False)
102
+ ```
103
+
104
+ ### 2. Convenience Overrides
105
+
106
+ You can pass boolean flags directly to `evaluate()`:
107
+
108
+ ```python
109
+ result = evaluate(
110
+ pairs,
111
+ taxonomy=True, # Convert Arabic codepoints (ك/ي/ه) to Urdu (ک/ی/ہ)
112
+ remove_diacritics=True, # Remove all اعراب (zabar, zeer, pesh, tanween, etc.)
113
+ remove_zabar=True, # Remove zabar (َ) only
114
+ remove_zeer=True, # Remove zeer (ِ) only
115
+ remove_pesh=True, # Remove pesh (ُ) only
116
+ remove_punctuation=True, # Remove Urdu (؛،؟۔٪) and ASCII punctuation
117
+ remove_digits=False, # Remove ASCII and Urdu digits
118
+ )
119
+ ```
120
+
121
+ ### 3. Using `NormalizeConfig` or Presets
122
+
123
+ For reusability across benchmarks, configure a `NormalizeConfig`:
124
+
125
+ ```python
126
+ from urdu_text_eval import evaluate, NormalizeConfig
127
+
128
+ # Presets
129
+ cfg_default = NormalizeConfig.default() # Taxonomy + diacritics + whitespace
130
+ cfg_raw = NormalizeConfig.none() # Raw string comparison
131
+ cfg_full = NormalizeConfig.full() # Everything on (including punctuation & digit stripping)
132
+ cfg_diac = NormalizeConfig.diacritics_only() # Only remove اعراب
133
+ cfg_punct = NormalizeConfig.punctuation_only() # Only remove punctuation
134
+ cfg_tax = NormalizeConfig.taxonomy_only() # Only taxonomy mapping
135
+
136
+ # Custom Configuration
137
+ cfg = NormalizeConfig(
138
+ enabled=True,
139
+ taxonomy=True, # Map Arabic/presentation characters
140
+ combine_characters=True, # Combine characters (ا + ٓ → آ)
141
+ remove_zabar=True, # Remove zabar
142
+ remove_zeer=True, # Remove zeer
143
+ remove_pesh=True, # Remove pesh
144
+ remove_tanween=True, # Remove tanween
145
+ remove_shadda=True, # Remove shadda
146
+ remove_sukun=True, # Remove sukun
147
+ remove_punctuation=False, # Keep punctuation
148
+ remove_digits=False, # Keep digits
149
+ )
150
+
151
+ result = evaluate(pairs, config=cfg)
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Standalone Normalizer Function
157
+
158
+ You can also use the Urdu normalizer directly on individual text strings:
159
+
160
+ ```python
161
+ from urdu_text_eval import normalize_urdu, NormalizeConfig
162
+
163
+ # Default normalization
164
+ clean_text = normalize_urdu("كيا يہ ہے؟")
165
+ # Output: "کیا یہ ہے؟"
166
+
167
+ # Remove diacritics (zabar/zeer/pesh) only
168
+ clean_text = normalize_urdu("شیرِ پنجاب", remove_diacritics_all=True)
169
+ # Output: "شیر پنجاب"
170
+
171
+ # Punctuation removal
172
+ clean_text = normalize_urdu("سلام، دنیا!", config=NormalizeConfig.punctuation_only())
173
+ # Output: "سلام دنیا"
174
+ ```
175
+
176
+ ---
177
+
178
+ ## API Summary
179
+
180
+ ```python
181
+ from urdu_text_eval import (
182
+ evaluate, # Core benchmark function: evaluate([{"actual": "...", "pred": "..."}])
183
+ NormalizeConfig, # Configuration dataclass & presets for text normalization
184
+ normalize_urdu, # Single-string Urdu normalization function
185
+ compute_metrics, # Lower-level function: compute_metrics(references, hypotheses)
186
+ format_metrics, # Formats metrics dict into a human-readable text summary
187
+ per_sample_errors, # Returns list of per-item error breakdown dicts
188
+ )
189
+ ```
190
+
191
+ ---
192
+
193
+ ## License
194
+
195
+ Distributed under the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/tranlit_eval/py.typed
4
+ src/urdu_text_eval/__init__.py
5
+ src/urdu_text_eval/evaluate.py
6
+ src/urdu_text_eval/metrics.py
7
+ src/urdu_text_eval/normalize.py
8
+ src/urdu_text_eval/py.typed
9
+ src/urdu_text_eval.egg-info/PKG-INFO
10
+ src/urdu_text_eval.egg-info/SOURCES.txt
11
+ src/urdu_text_eval.egg-info/dependency_links.txt
12
+ src/urdu_text_eval.egg-info/requires.txt
13
+ src/urdu_text_eval.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ jiwer>=3.0.0
2
+ sacrebleu>=2.4.0
3
+ rapidfuzz>=3.0.0
4
+
5
+ [dev]
6
+ pytest>=7.0
@@ -0,0 +1,3 @@
1
+ tranlit_eval
2
+ translit_eval
3
+ urdu_text_eval