bambara-text-normalizer 1.0.0__py3-none-any.whl
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.
- bambara_normalizer/__init__.py +187 -0
- bambara_normalizer/cli.py +298 -0
- bambara_normalizer/config.py +197 -0
- bambara_normalizer/dates.py +414 -0
- bambara_normalizer/evaluation.py +610 -0
- bambara_normalizer/normalizer.py +646 -0
- bambara_normalizer/numbers.py +475 -0
- bambara_normalizer/py.typed +1 -0
- bambara_normalizer/times.py +441 -0
- bambara_normalizer/utils.py +568 -0
- bambara_text_normalizer-1.0.0.dist-info/METADATA +894 -0
- bambara_text_normalizer-1.0.0.dist-info/RECORD +16 -0
- bambara_text_normalizer-1.0.0.dist-info/WHEEL +5 -0
- bambara_text_normalizer-1.0.0.dist-info/entry_points.txt +2 -0
- bambara_text_normalizer-1.0.0.dist-info/licenses/LICENSE +21 -0
- bambara_text_normalizer-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Copyright 2026 sudoping01.
|
|
2
|
+
|
|
3
|
+
# Licensed under the MIT License; you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at:
|
|
5
|
+
|
|
6
|
+
# https://opensource.org/licenses/MIT
|
|
7
|
+
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
|
|
14
|
+
__version__ = "1.0.0"
|
|
15
|
+
__author__ = "sudoping01"
|
|
16
|
+
__license__ = "MIT"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from .dates import (
|
|
20
|
+
DAYS_OF_WEEK,
|
|
21
|
+
MONTHS,
|
|
22
|
+
bambara_to_date,
|
|
23
|
+
bambara_to_day_of_week,
|
|
24
|
+
bambara_to_month,
|
|
25
|
+
date_to_bambara,
|
|
26
|
+
day_of_week_to_bambara,
|
|
27
|
+
denormalize_dates_in_text,
|
|
28
|
+
format_date_bambara,
|
|
29
|
+
is_bambara_day,
|
|
30
|
+
is_bambara_month,
|
|
31
|
+
month_to_bambara,
|
|
32
|
+
normalize_dates_in_text,
|
|
33
|
+
)
|
|
34
|
+
from .evaluation import (
|
|
35
|
+
BambaraEvaluator,
|
|
36
|
+
BambaraTransform,
|
|
37
|
+
EvaluationResult,
|
|
38
|
+
compute_cer,
|
|
39
|
+
compute_der,
|
|
40
|
+
compute_mer,
|
|
41
|
+
compute_wer,
|
|
42
|
+
compute_wil,
|
|
43
|
+
compute_wip,
|
|
44
|
+
create_bambara_transform,
|
|
45
|
+
evaluate,
|
|
46
|
+
evaluate_batch,
|
|
47
|
+
visualize_alignment,
|
|
48
|
+
)
|
|
49
|
+
from .normalizer import (
|
|
50
|
+
BambaraNormalizer,
|
|
51
|
+
BambaraNormalizerConfig,
|
|
52
|
+
create_normalizer,
|
|
53
|
+
)
|
|
54
|
+
from .numbers import (
|
|
55
|
+
bambara_to_number,
|
|
56
|
+
denormalize_numbers_in_text,
|
|
57
|
+
is_number_word,
|
|
58
|
+
normalize_numbers_in_text,
|
|
59
|
+
number_to_bambara,
|
|
60
|
+
number_to_ordinal,
|
|
61
|
+
)
|
|
62
|
+
from .times import (
|
|
63
|
+
LERE,
|
|
64
|
+
MINITI,
|
|
65
|
+
NEGE_KANYE,
|
|
66
|
+
SEGONI,
|
|
67
|
+
bambara_to_duration,
|
|
68
|
+
bambara_to_time,
|
|
69
|
+
denormalize_times_in_text,
|
|
70
|
+
duration_to_bambara,
|
|
71
|
+
format_duration_bambara,
|
|
72
|
+
format_time_bambara,
|
|
73
|
+
is_time_word,
|
|
74
|
+
normalize_times_in_text,
|
|
75
|
+
time_to_bambara,
|
|
76
|
+
)
|
|
77
|
+
from .utils import (
|
|
78
|
+
BAMBARA_ALPHABET,
|
|
79
|
+
BAMBARA_CONSONANTS,
|
|
80
|
+
BAMBARA_SPECIAL_CHARS,
|
|
81
|
+
BAMBARA_VOWELS,
|
|
82
|
+
add_tone,
|
|
83
|
+
analyze_text,
|
|
84
|
+
count_tone_marks,
|
|
85
|
+
get_base_char,
|
|
86
|
+
get_tone,
|
|
87
|
+
get_unicode_info,
|
|
88
|
+
has_tone_marks,
|
|
89
|
+
is_bambara_char,
|
|
90
|
+
is_bambara_special_char,
|
|
91
|
+
normalize_unicode_variants,
|
|
92
|
+
remove_tones,
|
|
93
|
+
validate_bambara_text,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"BambaraNormalizer",
|
|
98
|
+
"BambaraNormalizerConfig",
|
|
99
|
+
"NormalizationLevel",
|
|
100
|
+
"BambaraEvaluator",
|
|
101
|
+
"EvaluationResult",
|
|
102
|
+
"create_normalizer",
|
|
103
|
+
"compute_wer",
|
|
104
|
+
"compute_cer",
|
|
105
|
+
"compute_der",
|
|
106
|
+
"compute_mer",
|
|
107
|
+
"compute_wil",
|
|
108
|
+
"compute_wip",
|
|
109
|
+
"evaluate",
|
|
110
|
+
"evaluate_batch",
|
|
111
|
+
"visualize_alignment",
|
|
112
|
+
"create_bambara_transform",
|
|
113
|
+
"BambaraTransform",
|
|
114
|
+
"is_bambara_char",
|
|
115
|
+
"is_bambara_special_char",
|
|
116
|
+
"get_base_char",
|
|
117
|
+
"get_tone",
|
|
118
|
+
"add_tone",
|
|
119
|
+
"remove_tones",
|
|
120
|
+
"has_tone_marks",
|
|
121
|
+
"count_tone_marks",
|
|
122
|
+
"validate_bambara_text",
|
|
123
|
+
"analyze_text",
|
|
124
|
+
"normalize_unicode_variants",
|
|
125
|
+
"get_unicode_info",
|
|
126
|
+
"BAMBARA_VOWELS",
|
|
127
|
+
"BAMBARA_CONSONANTS",
|
|
128
|
+
"BAMBARA_SPECIAL_CHARS",
|
|
129
|
+
"BAMBARA_ALPHABET",
|
|
130
|
+
"number_to_bambara",
|
|
131
|
+
"bambara_to_number",
|
|
132
|
+
"normalize_numbers_in_text",
|
|
133
|
+
"denormalize_numbers_in_text",
|
|
134
|
+
"is_number_word",
|
|
135
|
+
"number_to_ordinal",
|
|
136
|
+
"date_to_bambara",
|
|
137
|
+
"bambara_to_date",
|
|
138
|
+
"format_date_bambara",
|
|
139
|
+
"day_of_week_to_bambara",
|
|
140
|
+
"bambara_to_day_of_week",
|
|
141
|
+
"month_to_bambara",
|
|
142
|
+
"bambara_to_month",
|
|
143
|
+
"normalize_dates_in_text",
|
|
144
|
+
"denormalize_dates_in_text",
|
|
145
|
+
"is_bambara_month",
|
|
146
|
+
"is_bambara_day",
|
|
147
|
+
"DAYS_OF_WEEK",
|
|
148
|
+
"MONTHS",
|
|
149
|
+
"time_to_bambara",
|
|
150
|
+
"bambara_to_time",
|
|
151
|
+
"format_time_bambara",
|
|
152
|
+
"duration_to_bambara",
|
|
153
|
+
"bambara_to_duration",
|
|
154
|
+
"format_duration_bambara",
|
|
155
|
+
"normalize_times_in_text",
|
|
156
|
+
"denormalize_times_in_text",
|
|
157
|
+
"is_time_word",
|
|
158
|
+
"NEGE_KANYE",
|
|
159
|
+
"LERE",
|
|
160
|
+
"MINITI",
|
|
161
|
+
"SEGONI",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def normalize(text: str, preset: str = "standard", **kwargs) -> str:
|
|
166
|
+
"""Convenience function for quick normalization.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
text: Text to normalize
|
|
170
|
+
preset: Preset configuration ("standard", "wer", "cer", "minimal")
|
|
171
|
+
**kwargs: Override specific configuration options
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Normalized text
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
>>> from bambara_normalizer import normalize
|
|
178
|
+
>>> normalize("B'a fɔ́!")
|
|
179
|
+
'bɛ a fɔ'
|
|
180
|
+
>>> normalize("B'a fɔ́!", preset="wer")
|
|
181
|
+
'bɛ a fɔ'
|
|
182
|
+
"""
|
|
183
|
+
normalizer = create_normalizer(preset, **kwargs)
|
|
184
|
+
return normalizer(text)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
__all__.append("normalize")
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Copyright 2026 sudoping01.
|
|
2
|
+
|
|
3
|
+
# Licensed under the MIT License; you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at:
|
|
5
|
+
|
|
6
|
+
# https://opensource.org/licenses/MIT
|
|
7
|
+
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Command-line interface for Bambara text normalizer.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
bambara-normalize "B'a fɔ́"
|
|
20
|
+
bambara-normalize --mode expand "B'a fɔ́"
|
|
21
|
+
bambara-normalize --mode contract "bɛ a fɔ"
|
|
22
|
+
bambara-normalize --preset wer --mode contract "text"
|
|
23
|
+
echo "text" | bambara-normalize
|
|
24
|
+
bambara-normalize --file input.txt --output output.txt
|
|
25
|
+
bambara-normalize --evaluate ref.txt hyp.txt
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from .evaluation import BambaraEvaluator
|
|
35
|
+
from .normalizer import BambaraNormalizer, BambaraNormalizerConfig, create_normalizer
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_args(args: list[str] | None = None) -> argparse.Namespace:
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
prog="bambara-normalize",
|
|
41
|
+
description="Bambara text normalizer for ASR evaluation",
|
|
42
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
43
|
+
epilog="""
|
|
44
|
+
Examples:
|
|
45
|
+
Normalize text (expand contractions, default):
|
|
46
|
+
bambara-normalize "B'a fɔ́"
|
|
47
|
+
bambara-normalize --mode expand "k'a ta"
|
|
48
|
+
|
|
49
|
+
Contract expanded forms:
|
|
50
|
+
bambara-normalize --mode contract "bɛ a fɔ"
|
|
51
|
+
bambara-normalize --mode contract "ka a ta"
|
|
52
|
+
|
|
53
|
+
Preserve contractions (don't touch):
|
|
54
|
+
bambara-normalize --mode preserve "B'a fɔ"
|
|
55
|
+
|
|
56
|
+
Use WER preset with contraction:
|
|
57
|
+
bambara-normalize --preset wer --mode contract "ka a ta"
|
|
58
|
+
|
|
59
|
+
Process file:
|
|
60
|
+
bambara-normalize --file input.txt --output normalized.txt
|
|
61
|
+
bambara-normalize --mode contract --file input.txt
|
|
62
|
+
|
|
63
|
+
Evaluate ASR output:
|
|
64
|
+
bambara-normalize --evaluate reference.txt hypothesis.txt
|
|
65
|
+
bambara-normalize --evaluate --mode contract ref.txt hyp.txt
|
|
66
|
+
|
|
67
|
+
Pipe from stdin:
|
|
68
|
+
echo "B'a fɔ́" | bambara-normalize
|
|
69
|
+
echo "bɛ a fɔ" | bambara-normalize --mode contract
|
|
70
|
+
""",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"text",
|
|
75
|
+
nargs="?",
|
|
76
|
+
help="Text to normalize (reads from stdin if not provided)",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--mode",
|
|
81
|
+
"-m",
|
|
82
|
+
choices=["expand", "contract", "preserve"],
|
|
83
|
+
default="expand",
|
|
84
|
+
help="Contraction mode: expand (default), contract, or preserve",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--preset",
|
|
89
|
+
"-p",
|
|
90
|
+
choices=["standard", "wer", "cer", "minimal", "preserving_tones"],
|
|
91
|
+
default="standard",
|
|
92
|
+
help="Normalization preset (default: standard)",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--file",
|
|
97
|
+
"-f",
|
|
98
|
+
type=Path,
|
|
99
|
+
help="Input file to normalize",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--output",
|
|
104
|
+
"-o",
|
|
105
|
+
type=Path,
|
|
106
|
+
help="Output file (default: stdout)",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--evaluate",
|
|
111
|
+
"-e",
|
|
112
|
+
nargs=2,
|
|
113
|
+
metavar=("REF", "HYP"),
|
|
114
|
+
type=Path,
|
|
115
|
+
help="Evaluate hypothesis against reference (provide two files)",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--preserve-tones",
|
|
120
|
+
action="store_true",
|
|
121
|
+
help="Preserve tone marks during normalization",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--expand-numbers",
|
|
126
|
+
action="store_true",
|
|
127
|
+
help="Expand numbers to Bambara words",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--debug",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help="Show normalization steps",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--version",
|
|
138
|
+
"-v",
|
|
139
|
+
action="version",
|
|
140
|
+
version="%(prog)s 2.0.0",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return parser.parse_args(args)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def normalize_text(
|
|
147
|
+
text: str,
|
|
148
|
+
preset: str,
|
|
149
|
+
mode: str = "expand",
|
|
150
|
+
preserve_tones: bool = False,
|
|
151
|
+
expand_numbers: bool = False,
|
|
152
|
+
debug: bool = False,
|
|
153
|
+
) -> str:
|
|
154
|
+
kwargs = {}
|
|
155
|
+
if preserve_tones:
|
|
156
|
+
kwargs["preserve_tones"] = True
|
|
157
|
+
if expand_numbers:
|
|
158
|
+
kwargs["expand_numbers"] = True
|
|
159
|
+
|
|
160
|
+
normalizer = create_normalizer(preset, mode=mode, **kwargs)
|
|
161
|
+
|
|
162
|
+
if debug:
|
|
163
|
+
diff = normalizer.get_normalization_diff(text)
|
|
164
|
+
print("Normalization steps:", file=sys.stderr)
|
|
165
|
+
for step, value in diff.items():
|
|
166
|
+
print(f" {step}: {value}", file=sys.stderr)
|
|
167
|
+
print(file=sys.stderr)
|
|
168
|
+
|
|
169
|
+
return normalizer(text)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def process_file(
|
|
173
|
+
input_path: Path,
|
|
174
|
+
output_path: Path | None,
|
|
175
|
+
normalizer: BambaraNormalizer,
|
|
176
|
+
) -> None:
|
|
177
|
+
with open(input_path, encoding="utf-8") as f:
|
|
178
|
+
lines = f.readlines()
|
|
179
|
+
|
|
180
|
+
normalized = [normalizer(line.rstrip("\n")) for line in lines]
|
|
181
|
+
|
|
182
|
+
if output_path:
|
|
183
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
184
|
+
f.write("\n".join(normalized) + "\n")
|
|
185
|
+
print(f"Normalized output written to {output_path}", file=sys.stderr)
|
|
186
|
+
else:
|
|
187
|
+
for line in normalized:
|
|
188
|
+
print(line)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def run_evaluation(
|
|
192
|
+
ref_path: Path,
|
|
193
|
+
hyp_path: Path,
|
|
194
|
+
preset: str,
|
|
195
|
+
mode: str = "expand",
|
|
196
|
+
) -> None:
|
|
197
|
+
with open(ref_path, encoding="utf-8") as f:
|
|
198
|
+
references = [line.strip() for line in f if line.strip()]
|
|
199
|
+
|
|
200
|
+
with open(hyp_path, encoding="utf-8") as f:
|
|
201
|
+
hypotheses = [line.strip() for line in f if line.strip()]
|
|
202
|
+
|
|
203
|
+
if len(references) != len(hypotheses):
|
|
204
|
+
print(
|
|
205
|
+
f"Error: Reference ({len(references)} lines) and hypothesis "
|
|
206
|
+
f"({len(hypotheses)} lines) have different lengths",
|
|
207
|
+
file=sys.stderr,
|
|
208
|
+
)
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
config = BambaraNormalizerConfig.for_wer_evaluation(mode=mode)
|
|
212
|
+
evaluator = BambaraEvaluator(config=config)
|
|
213
|
+
aggregate, individual = evaluator.evaluate_batch(references, hypotheses)
|
|
214
|
+
|
|
215
|
+
print("=" * 60)
|
|
216
|
+
print("Bambara ASR Evaluation Results")
|
|
217
|
+
print("=" * 60)
|
|
218
|
+
print(f"Reference file: {ref_path}")
|
|
219
|
+
print(f"Hypothesis file: {hyp_path}")
|
|
220
|
+
print(f"Normalization preset: {preset}")
|
|
221
|
+
print(f"Contraction mode: {mode}")
|
|
222
|
+
print(f"Total utterances: {len(references)}")
|
|
223
|
+
print("-" * 60)
|
|
224
|
+
print(f"Word Error Rate (WER): {aggregate.wer:.2%}")
|
|
225
|
+
print(f" Substitutions: {aggregate.word_substitutions}")
|
|
226
|
+
print(f" Deletions: {aggregate.word_deletions}")
|
|
227
|
+
print(f" Insertions: {aggregate.word_insertions}")
|
|
228
|
+
print(f" Reference words: {aggregate.total_reference_words}")
|
|
229
|
+
print("-" * 60)
|
|
230
|
+
print(f"Character Error Rate (CER): {aggregate.cer:.2%}")
|
|
231
|
+
print(f" Substitutions: {aggregate.char_substitutions}")
|
|
232
|
+
print(f" Deletions: {aggregate.char_deletions}")
|
|
233
|
+
print(f" Insertions: {aggregate.char_insertions}")
|
|
234
|
+
print(f" Reference chars: {aggregate.total_reference_chars}")
|
|
235
|
+
print("=" * 60)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main(args: list[str] | None = None) -> int:
|
|
239
|
+
parsed = parse_args(args)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
if parsed.evaluate:
|
|
243
|
+
ref_path, hyp_path = parsed.evaluate
|
|
244
|
+
run_evaluation(ref_path, hyp_path, parsed.preset, parsed.mode)
|
|
245
|
+
return 0
|
|
246
|
+
|
|
247
|
+
kwargs = {}
|
|
248
|
+
if parsed.preserve_tones:
|
|
249
|
+
kwargs["preserve_tones"] = True
|
|
250
|
+
if parsed.expand_numbers:
|
|
251
|
+
kwargs["expand_numbers"] = True
|
|
252
|
+
|
|
253
|
+
normalizer = create_normalizer(parsed.preset, mode=parsed.mode, **kwargs)
|
|
254
|
+
|
|
255
|
+
if parsed.file:
|
|
256
|
+
process_file(parsed.file, parsed.output, normalizer)
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
if parsed.text:
|
|
260
|
+
text = parsed.text
|
|
261
|
+
elif not sys.stdin.isatty():
|
|
262
|
+
text = sys.stdin.read().strip()
|
|
263
|
+
else:
|
|
264
|
+
print("Error: No input provided", file=sys.stderr)
|
|
265
|
+
print(
|
|
266
|
+
"Usage: bambara-normalize <text> or echo <text> | bambara-normalize",
|
|
267
|
+
file=sys.stderr,
|
|
268
|
+
)
|
|
269
|
+
return 1
|
|
270
|
+
|
|
271
|
+
result = normalize_text(
|
|
272
|
+
text,
|
|
273
|
+
parsed.preset,
|
|
274
|
+
parsed.mode,
|
|
275
|
+
parsed.preserve_tones,
|
|
276
|
+
parsed.expand_numbers,
|
|
277
|
+
parsed.debug,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if parsed.output:
|
|
281
|
+
with open(parsed.output, "w", encoding="utf-8") as f:
|
|
282
|
+
f.write(result + "\n")
|
|
283
|
+
print(f"Output written to {parsed.output}", file=sys.stderr)
|
|
284
|
+
else:
|
|
285
|
+
print(result)
|
|
286
|
+
|
|
287
|
+
return 0
|
|
288
|
+
|
|
289
|
+
except FileNotFoundError as e:
|
|
290
|
+
print(f"Error: File not found: {e}", file=sys.stderr)
|
|
291
|
+
return 1
|
|
292
|
+
except Exception as e:
|
|
293
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
294
|
+
return 1
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__":
|
|
298
|
+
sys.exit(main())
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Copyright 2026 sudoping01.
|
|
2
|
+
|
|
3
|
+
# Licensed under the MIT License; you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at:
|
|
5
|
+
|
|
6
|
+
# https://opensource.org/licenses/MIT
|
|
7
|
+
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ContractionMode(Enum):
|
|
21
|
+
EXPAND = "expand"
|
|
22
|
+
CONTRACT = "contract"
|
|
23
|
+
PRESERVE = "preserve"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NormalizationLevel(Enum):
|
|
27
|
+
MINIMAL = "minimal"
|
|
28
|
+
STANDARD = "standard"
|
|
29
|
+
AGGRESSIVE = "aggressive"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class BambaraNormalizerConfig:
|
|
34
|
+
"""
|
|
35
|
+
Configuration for Bambara text normalization.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
contraction_mode: How to handle contractions ("expand", "contract", "preserve")
|
|
39
|
+
preserve_tones: Keep tone diacritics (à, á, etc.)
|
|
40
|
+
normalize_legacy_orthography: Convert old spellings (è→ɛ, ny→ɲ)
|
|
41
|
+
lowercase: Convert to lowercase
|
|
42
|
+
remove_punctuation: Remove punctuation marks
|
|
43
|
+
normalize_whitespace: Collapse multiple spaces
|
|
44
|
+
normalize_apostrophes: Standardize apostrophe variants
|
|
45
|
+
normalize_special_chars: Handle variant forms of ɛ, ɔ, ɲ, ŋ
|
|
46
|
+
expand_numbers: Convert digits to Bambara words
|
|
47
|
+
expand_dates: Convert date patterns (DD-MM-YYYY) to Bambara
|
|
48
|
+
remove_diacritics_except_tones: Remove non-tonal diacritics
|
|
49
|
+
handle_french_loanwords: Apply French word normalization
|
|
50
|
+
strip_repetitions: Normalize repeated characters
|
|
51
|
+
normalize_compounds: Standardize compound word spacing
|
|
52
|
+
expand_times: Convert time patterns to Bambara
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
contraction_mode: str = "expand"
|
|
56
|
+
preserve_tones: bool = True
|
|
57
|
+
normalize_legacy_orthography: bool = True
|
|
58
|
+
lowercase: bool = True
|
|
59
|
+
remove_punctuation: bool = True
|
|
60
|
+
normalize_whitespace: bool = True
|
|
61
|
+
normalize_apostrophes: bool = True
|
|
62
|
+
normalize_special_chars: bool = True
|
|
63
|
+
expand_numbers: bool = False
|
|
64
|
+
expand_dates: bool = False
|
|
65
|
+
expand_times: bool = False
|
|
66
|
+
remove_diacritics_except_tones: bool = False
|
|
67
|
+
handle_french_loanwords: bool = True
|
|
68
|
+
strip_repetitions: bool = False
|
|
69
|
+
normalize_compounds: bool = False
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def expand_contractions(self) -> bool:
|
|
73
|
+
"""Backward compatibility: True if mode is 'expand'."""
|
|
74
|
+
return self.contraction_mode == "expand"
|
|
75
|
+
|
|
76
|
+
@expand_contractions.setter
|
|
77
|
+
def expand_contractions(self, value: bool) -> None:
|
|
78
|
+
"""Backward compatibility setter."""
|
|
79
|
+
if value:
|
|
80
|
+
self.contraction_mode = "expand"
|
|
81
|
+
else:
|
|
82
|
+
self.contraction_mode = "preserve"
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def for_wer_evaluation(cls, mode: str = "expand") -> BambaraNormalizerConfig:
|
|
86
|
+
return cls(
|
|
87
|
+
contraction_mode=mode,
|
|
88
|
+
preserve_tones=False,
|
|
89
|
+
normalize_legacy_orthography=True,
|
|
90
|
+
lowercase=True,
|
|
91
|
+
remove_punctuation=True,
|
|
92
|
+
normalize_whitespace=True,
|
|
93
|
+
normalize_apostrophes=True,
|
|
94
|
+
normalize_special_chars=True,
|
|
95
|
+
expand_numbers=True,
|
|
96
|
+
expand_dates=True,
|
|
97
|
+
expand_times=True,
|
|
98
|
+
remove_diacritics_except_tones=True,
|
|
99
|
+
handle_french_loanwords=True,
|
|
100
|
+
strip_repetitions=True,
|
|
101
|
+
normalize_compounds=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def for_cer_evaluation(cls, mode: str = "expand") -> BambaraNormalizerConfig:
|
|
106
|
+
return cls(
|
|
107
|
+
contraction_mode=mode,
|
|
108
|
+
preserve_tones=False,
|
|
109
|
+
normalize_legacy_orthography=True,
|
|
110
|
+
lowercase=True,
|
|
111
|
+
remove_punctuation=True,
|
|
112
|
+
normalize_whitespace=True,
|
|
113
|
+
normalize_apostrophes=True,
|
|
114
|
+
normalize_special_chars=True,
|
|
115
|
+
expand_numbers=True,
|
|
116
|
+
expand_dates=True,
|
|
117
|
+
expand_times=True,
|
|
118
|
+
remove_diacritics_except_tones=True,
|
|
119
|
+
handle_french_loanwords=True,
|
|
120
|
+
strip_repetitions=False,
|
|
121
|
+
normalize_compounds=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def preserving_tones(cls, mode: str = "expand") -> BambaraNormalizerConfig:
|
|
126
|
+
return cls(
|
|
127
|
+
contraction_mode=mode,
|
|
128
|
+
preserve_tones=True,
|
|
129
|
+
normalize_legacy_orthography=True,
|
|
130
|
+
lowercase=True,
|
|
131
|
+
remove_punctuation=True,
|
|
132
|
+
normalize_whitespace=True,
|
|
133
|
+
normalize_apostrophes=True,
|
|
134
|
+
normalize_special_chars=True,
|
|
135
|
+
expand_numbers=False,
|
|
136
|
+
expand_dates=False,
|
|
137
|
+
expand_times=False,
|
|
138
|
+
remove_diacritics_except_tones=False,
|
|
139
|
+
handle_french_loanwords=False,
|
|
140
|
+
strip_repetitions=False,
|
|
141
|
+
normalize_compounds=False,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def minimal(cls) -> BambaraNormalizerConfig:
|
|
146
|
+
return cls(
|
|
147
|
+
contraction_mode="preserve",
|
|
148
|
+
preserve_tones=True,
|
|
149
|
+
normalize_legacy_orthography=False,
|
|
150
|
+
lowercase=True,
|
|
151
|
+
remove_punctuation=False,
|
|
152
|
+
normalize_whitespace=True,
|
|
153
|
+
normalize_apostrophes=True,
|
|
154
|
+
normalize_special_chars=False,
|
|
155
|
+
expand_numbers=False,
|
|
156
|
+
expand_dates=False,
|
|
157
|
+
expand_times=False,
|
|
158
|
+
remove_diacritics_except_tones=False,
|
|
159
|
+
handle_french_loanwords=False,
|
|
160
|
+
strip_repetitions=False,
|
|
161
|
+
normalize_compounds=False,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class EvaluationResult:
|
|
167
|
+
wer: float
|
|
168
|
+
cer: float
|
|
169
|
+
mer: float = 0.0
|
|
170
|
+
wil: float = 0.0
|
|
171
|
+
wip: float = 0.0
|
|
172
|
+
der: float | None = None
|
|
173
|
+
|
|
174
|
+
word_substitutions: int = 0
|
|
175
|
+
word_deletions: int = 0
|
|
176
|
+
word_insertions: int = 0
|
|
177
|
+
word_hits: int = 0
|
|
178
|
+
total_reference_words: int = 0
|
|
179
|
+
|
|
180
|
+
char_substitutions: int = 0
|
|
181
|
+
char_deletions: int = 0
|
|
182
|
+
char_insertions: int = 0
|
|
183
|
+
char_hits: int = 0
|
|
184
|
+
total_reference_chars: int = 0
|
|
185
|
+
|
|
186
|
+
reference_normalized: str = ""
|
|
187
|
+
hypothesis_normalized: str = ""
|
|
188
|
+
|
|
189
|
+
def __str__(self) -> str:
|
|
190
|
+
return (
|
|
191
|
+
f"WER: {self.wer:.2%} "
|
|
192
|
+
f"(S={self.word_substitutions}, D={self.word_deletions}, I={self.word_insertions}, H={self.word_hits}, N={self.total_reference_words})\n"
|
|
193
|
+
f"CER: {self.cer:.2%} "
|
|
194
|
+
f"(S={self.char_substitutions}, D={self.char_deletions}, I={self.char_insertions}, H={self.char_hits}, N={self.total_reference_chars})\n"
|
|
195
|
+
f"MER: {self.mer:.2%} | WIL: {self.wil:.2%} | WIP: {self.wip:.2%}"
|
|
196
|
+
+ (f"\nDER: {self.der:.2%}" if self.der is not None else "")
|
|
197
|
+
)
|