15laddoo commited on
Commit
1b3d38f
·
verified ·
1 Parent(s): 01cbdc0

Upload folder using huggingface_hub

Browse files
.pytest_cache/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Created by pytest automatically.
2
+ *
.pytest_cache/CACHEDIR.TAG ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag created by pytest.
3
+ # For information about cache directory tags, see:
4
+ # https://bford.info/cachedir/spec.html
.pytest_cache/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # pytest cache directory #
2
+
3
+ This directory contains data from the pytest's cache plugin,
4
+ which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
5
+
6
+ **Do not** commit this to version control.
7
+
8
+ See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
.pytest_cache/v/cache/lastfailed ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
.pytest_cache/v/cache/nodeids ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "tests/test_awwer.py::TestAlignWordsDP::test_deletion",
3
+ "tests/test_awwer.py::TestAlignWordsDP::test_empty_hyp",
4
+ "tests/test_awwer.py::TestAlignWordsDP::test_empty_ref",
5
+ "tests/test_awwer.py::TestAlignWordsDP::test_identical",
6
+ "tests/test_awwer.py::TestAlignWordsDP::test_insertion",
7
+ "tests/test_awwer.py::TestAlignWordsDP::test_substitution",
8
+ "tests/test_awwer.py::TestCalculateAWWER::test_all_deletions",
9
+ "tests/test_awwer.py::TestCalculateAWWER::test_high_weight_error",
10
+ "tests/test_awwer.py::TestCalculateAWWER::test_none_on_empty_ref",
11
+ "tests/test_awwer.py::TestCalculateAWWER::test_perfect_match",
12
+ "tests/test_awwer.py::TestCalculateAWWERComponents::test_breakdown",
13
+ "tests/test_awwer.py::TestCalculateAWWERFromString::test_json_weights",
14
+ "tests/test_awwer.py::TestCleanText::test_basic_normalization",
15
+ "tests/test_awwer.py::TestCleanText::test_empty_input",
16
+ "tests/test_awwer.py::TestCleanText::test_nan_handling",
17
+ "tests/test_awwer.py::TestCleanText::test_punctuation_removal",
18
+ "tests/test_awwer.py::TestCleanText::test_whitespace_collapse",
19
+ "tests/test_awwer.py::TestGetWordWeight::test_case_insensitive",
20
+ "tests/test_awwer.py::TestGetWordWeight::test_default",
21
+ "tests/test_awwer.py::TestGetWordWeight::test_empty",
22
+ "tests/test_awwer.py::TestGetWordWeight::test_exact_match",
23
+ "tests/test_awwer.py::TestParseWordWeights::test_empty",
24
+ "tests/test_awwer.py::TestParseWordWeights::test_invalid_json",
25
+ "tests/test_awwer.py::TestParseWordWeights::test_json_string",
26
+ "tests/test_awwer.py::TestParseWordWeights::test_list_input",
27
+ "tests/test_awwer.py::TestStandardMetrics::test_cer_nonzero",
28
+ "tests/test_awwer.py::TestStandardMetrics::test_cer_perfect",
29
+ "tests/test_awwer.py::TestStandardMetrics::test_mer_bounds",
30
+ "tests/test_awwer.py::TestStandardMetrics::test_mer_perfect",
31
+ "tests/test_awwer.py::TestStandardMetrics::test_nan_handling",
32
+ "tests/test_awwer.py::TestStandardMetrics::test_wer_all_wrong",
33
+ "tests/test_awwer.py::TestStandardMetrics::test_wer_empty_hyp",
34
+ "tests/test_awwer.py::TestStandardMetrics::test_wer_none_on_empty_ref",
35
+ "tests/test_awwer.py::TestStandardMetrics::test_wer_perfect"
36
+ ]
LICENSE ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to the Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by the Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding any notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ Copyright 2025 Digital Green
179
+
180
+ Licensed under the Apache License, Version 2.0 (the "License");
181
+ you may not use this file except in compliance with the License.
182
+ You may obtain a copy of the License at
183
+
184
+ http://www.apache.org/licenses/LICENSE-2.0
185
+
186
+ Unless required by applicable law or agreed to in writing, software
187
+ distributed under the License is distributed on an "AS IS" BASIS,
188
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189
+ See the License for the specific language governing permissions and
190
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agri AWWER — Agriculture-Weighted Word Error Rate Evaluation Toolkit
2
+
3
+ A lightweight Python toolkit for evaluating Automatic Speech Recognition (ASR) systems in agricultural domains. Provides the **Agriculture-Weighted Word Error Rate (AWWER)** metric alongside standard metrics (WER, CER, MER).
4
+
5
+ AWWER penalises errors on domain-critical agricultural terms more heavily than errors on general vocabulary, giving a more realistic picture of how well an ASR system serves agricultural applications.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # From HuggingFace (recommended)
11
+ pip install git+https://huggingface.co/DigiGreen/Agri_AWWER_Toolkit
12
+
13
+ # For improved WER/CER/MER via jiwer
14
+ pip install "agri-awwer[jiwer]"
15
+ ```
16
+
17
+ **Zero required dependencies** — the toolkit works out of the box with only the Python standard library. `jiwer` is optional and used automatically when available for standard metrics.
18
+
19
+ ## Quick Start
20
+
21
+ ### AWWER — Domain-Weighted Error Rate
22
+
23
+ ```python
24
+ from agri_awwer import calculate_awwer
25
+
26
+ # Define domain word weights (1-4 scale)
27
+ weights = {
28
+ "gehun": 4, # wheat — core agriculture term
29
+ "keet": 4, # pest
30
+ "mitti": 3, # soil
31
+ "barish": 3, # rain
32
+ "gaon": 1, # village — general vocabulary
33
+ }
34
+
35
+ reference = "gehun mein keet laga hai"
36
+ hypothesis = "gaon mein keet laga hai"
37
+
38
+ awwer = calculate_awwer(reference, hypothesis, weights)
39
+ print(f"AWWER: {awwer:.3f}")
40
+ # gehun→gaon is a weight-4 error, so AWWER > standard WER
41
+ ```
42
+
43
+ ### Standard Metrics
44
+
45
+ ```python
46
+ from agri_awwer import calculate_wer, calculate_cer, calculate_mer
47
+
48
+ ref = "gehun mein keet laga hai"
49
+ hyp = "gaon mein keet laga hai"
50
+
51
+ print(f"WER: {calculate_wer(ref, hyp):.3f}")
52
+ print(f"CER: {calculate_cer(ref, hyp):.3f}")
53
+ print(f"MER: {calculate_mer(ref, hyp):.3f}")
54
+ ```
55
+
56
+ ### Detailed AWWER Breakdown
57
+
58
+ ```python
59
+ from agri_awwer import calculate_awwer_components
60
+
61
+ result = calculate_awwer_components(reference, hypothesis, weights)
62
+ print(f"AWWER: {result['awwer']:.3f}")
63
+ print(f"Substitutions: {result['n_substitutions']}")
64
+ print(f"Deletions: {result['n_deletions']}")
65
+ print(f"Insertions: {result['n_insertions']}")
66
+ print(f"High-weight errors: {result['high_weight_errors']}")
67
+ ```
68
+
69
+ ### Parse Weights from JSON
70
+
71
+ ```python
72
+ import json
73
+ from agri_awwer import calculate_awwer_from_string
74
+
75
+ weights_json = json.dumps([["gehun", 4], ["keet", 4], ["mitti", 3]])
76
+ awwer = calculate_awwer_from_string(ref, hyp, weights_json)
77
+ ```
78
+
79
+ ## Weight Scale
80
+
81
+ | Weight | Category | Examples |
82
+ |--------|----------|----------|
83
+ | **4** | Core agriculture terms | Crop names, pests, farming practices |
84
+ | **3** | Strongly agriculture-related | Soil types, weather, planting seasons |
85
+ | **2** | Indirectly related | Quantities, measurement units, locations |
86
+ | **1** | General vocabulary | Default for words not in the lexicon |
87
+
88
+ ## Language Support
89
+
90
+ Built-in text normalization for:
91
+ - **Hindi** (default) — chandrabindu, visarga, nukta removal
92
+ - **Telugu** — candrabindu, visarga removal
93
+ - **Odia** — candrabindu, visarga, nukta, isshar removal
94
+
95
+ Pass the `language` parameter to any metric function:
96
+
97
+ ```python
98
+ calculate_awwer(ref, hyp, weights, language="telugu")
99
+ calculate_wer(ref, hyp, language="odia")
100
+ ```
101
+
102
+ ## Related Resources
103
+
104
+ - **Paper**: *Benchmarking Automatic Speech Recognition for Indian Languages in Agricultural Contexts*
105
+ - **Dataset**: [Agri STT Benchmarking Dataset](https://huggingface.co/datasets/DigiGreen/Agri_STT_Benchmarking_Dataset) — 10,864 audio-transcript pairs across Hindi, Telugu, and Odia
106
+
107
+ ## Citation
108
+
109
+ ```bibtex
110
+ @misc{digigreen2025awwer,
111
+ title = {Agri {AWWER}: Agriculture-Weighted Word Error Rate Evaluation Toolkit},
112
+ author = {{Digital Green}},
113
+ year = {2025},
114
+ url = {https://huggingface.co/DigiGreen/Agri_AWWER_Toolkit},
115
+ }
116
+ ```
117
+
118
+ ## License
119
+
120
+ Apache 2.0
agri_awwer.egg-info/PKG-INFO ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: agri-awwer
3
+ Version: 0.1.0
4
+ Summary: Agriculture-Weighted Word Error Rate (AWWER) evaluation toolkit for domain-specific ASR assessment
5
+ Author-email: Digital Green <tech@digitalgreen.org>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://huggingface.co/DigiGreen/Agri_AWWER_Toolkit
8
+ Project-URL: Paper, https://huggingface.co/datasets/DigiGreen/Agri_STT_Benchmarking_Dataset
9
+ Keywords: asr,speech-recognition,agriculture,evaluation,wer,metrics
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Provides-Extra: jiwer
23
+ Requires-Dist: jiwer>=3.0; extra == "jiwer"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # Agri AWWER — Agriculture-Weighted Word Error Rate Evaluation Toolkit
29
+
30
+ A lightweight Python toolkit for evaluating Automatic Speech Recognition (ASR) systems in agricultural domains. Provides the **Agriculture-Weighted Word Error Rate (AWWER)** metric alongside standard metrics (WER, CER, MER).
31
+
32
+ AWWER penalises errors on domain-critical agricultural terms more heavily than errors on general vocabulary, giving a more realistic picture of how well an ASR system serves agricultural applications.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ # From HuggingFace (recommended)
38
+ pip install git+https://huggingface.co/DigiGreen/Agri_AWWER_Toolkit
39
+
40
+ # For improved WER/CER/MER via jiwer
41
+ pip install "agri-awwer[jiwer]"
42
+ ```
43
+
44
+ **Zero required dependencies** — the toolkit works out of the box with only the Python standard library. `jiwer` is optional and used automatically when available for standard metrics.
45
+
46
+ ## Quick Start
47
+
48
+ ### AWWER — Domain-Weighted Error Rate
49
+
50
+ ```python
51
+ from agri_awwer import calculate_awwer
52
+
53
+ # Define domain word weights (1-4 scale)
54
+ weights = {
55
+ "gehun": 4, # wheat — core agriculture term
56
+ "keet": 4, # pest
57
+ "mitti": 3, # soil
58
+ "barish": 3, # rain
59
+ "gaon": 1, # village — general vocabulary
60
+ }
61
+
62
+ reference = "gehun mein keet laga hai"
63
+ hypothesis = "gaon mein keet laga hai"
64
+
65
+ awwer = calculate_awwer(reference, hypothesis, weights)
66
+ print(f"AWWER: {awwer:.3f}")
67
+ # gehun→gaon is a weight-4 error, so AWWER > standard WER
68
+ ```
69
+
70
+ ### Standard Metrics
71
+
72
+ ```python
73
+ from agri_awwer import calculate_wer, calculate_cer, calculate_mer
74
+
75
+ ref = "gehun mein keet laga hai"
76
+ hyp = "gaon mein keet laga hai"
77
+
78
+ print(f"WER: {calculate_wer(ref, hyp):.3f}")
79
+ print(f"CER: {calculate_cer(ref, hyp):.3f}")
80
+ print(f"MER: {calculate_mer(ref, hyp):.3f}")
81
+ ```
82
+
83
+ ### Detailed AWWER Breakdown
84
+
85
+ ```python
86
+ from agri_awwer import calculate_awwer_components
87
+
88
+ result = calculate_awwer_components(reference, hypothesis, weights)
89
+ print(f"AWWER: {result['awwer']:.3f}")
90
+ print(f"Substitutions: {result['n_substitutions']}")
91
+ print(f"Deletions: {result['n_deletions']}")
92
+ print(f"Insertions: {result['n_insertions']}")
93
+ print(f"High-weight errors: {result['high_weight_errors']}")
94
+ ```
95
+
96
+ ### Parse Weights from JSON
97
+
98
+ ```python
99
+ import json
100
+ from agri_awwer import calculate_awwer_from_string
101
+
102
+ weights_json = json.dumps([["gehun", 4], ["keet", 4], ["mitti", 3]])
103
+ awwer = calculate_awwer_from_string(ref, hyp, weights_json)
104
+ ```
105
+
106
+ ## Weight Scale
107
+
108
+ | Weight | Category | Examples |
109
+ |--------|----------|----------|
110
+ | **4** | Core agriculture terms | Crop names, pests, farming practices |
111
+ | **3** | Strongly agriculture-related | Soil types, weather, planting seasons |
112
+ | **2** | Indirectly related | Quantities, measurement units, locations |
113
+ | **1** | General vocabulary | Default for words not in the lexicon |
114
+
115
+ ## Language Support
116
+
117
+ Built-in text normalization for:
118
+ - **Hindi** (default) — chandrabindu, visarga, nukta removal
119
+ - **Telugu** — candrabindu, visarga removal
120
+ - **Odia** — candrabindu, visarga, nukta, isshar removal
121
+
122
+ Pass the `language` parameter to any metric function:
123
+
124
+ ```python
125
+ calculate_awwer(ref, hyp, weights, language="telugu")
126
+ calculate_wer(ref, hyp, language="odia")
127
+ ```
128
+
129
+ ## Related Resources
130
+
131
+ - **Paper**: *Benchmarking Automatic Speech Recognition for Indian Languages in Agricultural Contexts*
132
+ - **Dataset**: [Agri STT Benchmarking Dataset](https://huggingface.co/datasets/DigiGreen/Agri_STT_Benchmarking_Dataset) — 10,864 audio-transcript pairs across Hindi, Telugu, and Odia
133
+
134
+ ## Citation
135
+
136
+ ```bibtex
137
+ @misc{digigreen2025awwer,
138
+ title = {Agri {AWWER}: Agriculture-Weighted Word Error Rate Evaluation Toolkit},
139
+ author = {{Digital Green}},
140
+ year = {2025},
141
+ url = {https://huggingface.co/DigiGreen/Agri_AWWER_Toolkit},
142
+ }
143
+ ```
144
+
145
+ ## License
146
+
147
+ Apache 2.0
agri_awwer.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ agri_awwer/__init__.py
5
+ agri_awwer/awwer.py
6
+ agri_awwer/wer.py
7
+ agri_awwer.egg-info/PKG-INFO
8
+ agri_awwer.egg-info/SOURCES.txt
9
+ agri_awwer.egg-info/dependency_links.txt
10
+ agri_awwer.egg-info/requires.txt
11
+ agri_awwer.egg-info/top_level.txt
12
+ tests/test_awwer.py
agri_awwer.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
agri_awwer.egg-info/requires.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+
2
+ [dev]
3
+ pytest>=7.0
4
+
5
+ [jiwer]
6
+ jiwer>=3.0
agri_awwer.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ agri_awwer
agri_awwer/__init__.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agri AWWER — Agriculture-Weighted Word Error Rate Evaluation Toolkit.
3
+
4
+ Provides domain-weighted ASR evaluation metrics for agricultural contexts.
5
+ """
6
+
7
+ from .awwer import (
8
+ calculate_awwer,
9
+ calculate_awwer_from_string,
10
+ calculate_awwer_components,
11
+ parse_word_weights,
12
+ get_word_weight,
13
+ align_words_dp,
14
+ )
15
+ from .wer import (
16
+ calculate_wer,
17
+ calculate_cer,
18
+ calculate_mer,
19
+ calculate_metrics_for_sample,
20
+ clean_text,
21
+ )
22
+
23
+ __all__ = [
24
+ "calculate_awwer",
25
+ "calculate_awwer_from_string",
26
+ "calculate_awwer_components",
27
+ "parse_word_weights",
28
+ "get_word_weight",
29
+ "align_words_dp",
30
+ "calculate_wer",
31
+ "calculate_cer",
32
+ "calculate_mer",
33
+ "calculate_metrics_for_sample",
34
+ "clean_text",
35
+ ]
agri_awwer/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (809 Bytes). View file
 
agri_awwer/__pycache__/awwer.cpython-313.pyc ADDED
Binary file (12.3 kB). View file
 
agri_awwer/__pycache__/wer.cpython-313.pyc ADDED
Binary file (12 kB). View file
 
agri_awwer/awwer.py ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agriculture-Weighted Word Error Rate (AWWER) Calculator.
3
+
4
+ AWWER gives more weight to agriculture-specific terms in error calculation.
5
+ This provides a domain-relevant metric for agricultural ASR evaluation.
6
+
7
+ Weight scale:
8
+ - Weight 4: Core agriculture terms (crops, pests, practices)
9
+ - Weight 3: Strongly agriculture-related (soil, weather, timing)
10
+ - Weight 2: Indirectly related (quantities, locations)
11
+ - Weight 1: General vocabulary
12
+
13
+ CRITICAL: Uses Dynamic Programming alignment (not position-based matching)
14
+ to correctly identify substitutions, deletions, and insertions.
15
+ """
16
+
17
+ import json
18
+ import math
19
+ import re
20
+ from typing import Optional, Dict, List, Tuple
21
+
22
+ from .wer import clean_text
23
+
24
+
25
+ def _isna(value) -> bool:
26
+ """Check if a value is NA/NaN/None without requiring pandas."""
27
+ if value is None:
28
+ return True
29
+ if isinstance(value, float) and math.isnan(value):
30
+ return True
31
+ if isinstance(value, (str, bytes, int)):
32
+ return False
33
+ try:
34
+ import pandas as pd
35
+ result = pd.isna(value)
36
+ if isinstance(result, bool):
37
+ return result
38
+ return False
39
+ except (ImportError, TypeError, ValueError):
40
+ return False
41
+
42
+
43
+ def parse_word_weights(word_weights_str: str) -> Dict[str, float]:
44
+ """
45
+ Parse word_weights column from JSON string.
46
+
47
+ Expected format: [["word1", weight1], ["word2", weight2], ...]
48
+
49
+ Args:
50
+ word_weights_str: JSON string of word weights
51
+
52
+ Returns:
53
+ Dictionary mapping words to weights
54
+ """
55
+ if not word_weights_str or _isna(word_weights_str):
56
+ return {}
57
+
58
+ try:
59
+ if isinstance(word_weights_str, str):
60
+ weights_list = json.loads(word_weights_str)
61
+ else:
62
+ weights_list = word_weights_str
63
+
64
+ result = {}
65
+ for item in weights_list:
66
+ if isinstance(item, (list, tuple)) and len(item) >= 2:
67
+ word = str(item[0]).strip()
68
+ weight = float(item[1])
69
+ result[word] = weight
70
+
71
+ return result
72
+ except (json.JSONDecodeError, ValueError, TypeError):
73
+ return {}
74
+
75
+
76
+ def get_word_weight(word: str, weights: Dict[str, float], default_weight: float = 1.0) -> float:
77
+ """
78
+ Get weight for a single word.
79
+
80
+ Tries exact match first, then normalized match.
81
+
82
+ Args:
83
+ word: Word to look up
84
+ weights: Dictionary of word weights
85
+ default_weight: Default weight if word not found
86
+
87
+ Returns:
88
+ Weight value (1-4)
89
+ """
90
+ if not word or not weights:
91
+ return default_weight
92
+
93
+ # Try exact match
94
+ if word in weights:
95
+ return weights[word]
96
+
97
+ # Try lowercase match
98
+ word_lower = word.lower()
99
+ for w, weight in weights.items():
100
+ if w.lower() == word_lower:
101
+ return weight
102
+
103
+ # Try with punctuation removed
104
+ word_clean = re.sub(r'[^\w]', '', word)
105
+ for w, weight in weights.items():
106
+ w_clean = re.sub(r'[^\w]', '', w)
107
+ if w_clean.lower() == word_clean.lower():
108
+ return weight
109
+
110
+ return default_weight
111
+
112
+
113
+ def align_words_dp(ref_words: List[str], hyp_words: List[str]) -> List[Tuple]:
114
+ """
115
+ Align reference and hypothesis words using dynamic programming.
116
+
117
+ This is the CORRECT alignment algorithm that uses Levenshtein-style DP
118
+ to find optimal alignment. DO NOT replace with position-based matching.
119
+
120
+ Returns list of operations:
121
+ ('match', ref_idx, hyp_idx),
122
+ ('sub', ref_idx, hyp_idx),
123
+ ('ins', None, hyp_idx),
124
+ ('del', ref_idx, None)
125
+
126
+ Args:
127
+ ref_words: Reference word list
128
+ hyp_words: Hypothesis word list
129
+
130
+ Returns:
131
+ List of alignment operations
132
+ """
133
+ n, m = len(ref_words), len(hyp_words)
134
+
135
+ # DP table
136
+ dp = [[float('inf')] * (m + 1) for _ in range(n + 1)]
137
+ dp[0][0] = 0
138
+
139
+ # Initialize first row and column
140
+ for j in range(1, m + 1):
141
+ dp[0][j] = j # All insertions
142
+ for i in range(1, n + 1):
143
+ dp[i][0] = i # All deletions
144
+
145
+ # Fill DP table
146
+ for i in range(1, n + 1):
147
+ for j in range(1, m + 1):
148
+ ref_w, hyp_w = ref_words[i-1], hyp_words[j-1]
149
+
150
+ if ref_w == hyp_w:
151
+ dp[i][j] = dp[i-1][j-1] # Match
152
+ else:
153
+ # Substitution, deletion, or insertion
154
+ dp[i][j] = min(
155
+ dp[i-1][j-1] + 1, # Substitution
156
+ dp[i-1][j] + 1, # Deletion
157
+ dp[i][j-1] + 1 # Insertion
158
+ )
159
+
160
+ # Backtrack to get alignment
161
+ alignment = []
162
+ i, j = n, m
163
+
164
+ while i > 0 or j > 0:
165
+ if i > 0 and j > 0:
166
+ ref_w, hyp_w = ref_words[i-1], hyp_words[j-1]
167
+
168
+ if ref_w == hyp_w and dp[i][j] == dp[i-1][j-1]:
169
+ alignment.append(('match', i-1, j-1))
170
+ i -= 1
171
+ j -= 1
172
+ continue
173
+ elif dp[i][j] == dp[i-1][j-1] + 1:
174
+ alignment.append(('sub', i-1, j-1))
175
+ i -= 1
176
+ j -= 1
177
+ continue
178
+
179
+ if i > 0 and dp[i][j] == dp[i-1][j] + 1:
180
+ alignment.append(('del', i-1, None))
181
+ i -= 1
182
+ elif j > 0 and dp[i][j] == dp[i][j-1] + 1:
183
+ alignment.append(('ins', None, j-1))
184
+ j -= 1
185
+ else:
186
+ break
187
+
188
+ alignment.reverse()
189
+ return alignment
190
+
191
+
192
+ def calculate_awwer(reference: str, hypothesis: str,
193
+ word_weights: Dict[str, float],
194
+ language: str = 'hindi',
195
+ default_weight: float = 1.0) -> Optional[float]:
196
+ """
197
+ Calculate Agriculture-Weighted Word Error Rate.
198
+
199
+ AWWER = sum(weights of error words) / sum(weights of all reference words)
200
+
201
+ Uses DP alignment to correctly identify errors.
202
+
203
+ Args:
204
+ reference: Reference text
205
+ hypothesis: Hypothesis text
206
+ word_weights: Dictionary mapping words to weights (1-4)
207
+ language: Language for normalization
208
+ default_weight: Default weight for words not in dictionary
209
+
210
+ Returns:
211
+ AWWER value (0 = perfect, higher = worse) or None
212
+ """
213
+ if not reference or _isna(reference):
214
+ return None
215
+
216
+ # Clean texts
217
+ ref_clean = clean_text(str(reference), language)
218
+ hyp_clean = clean_text(str(hypothesis) if hypothesis and not _isna(hypothesis) else '', language)
219
+
220
+ ref_words = ref_clean.split() if ref_clean else []
221
+ hyp_words = hyp_clean.split() if hyp_clean else []
222
+
223
+ if not ref_words:
224
+ return None
225
+
226
+ # Get alignment using DP
227
+ alignment = align_words_dp(ref_words, hyp_words)
228
+
229
+ # Calculate total reference weight
230
+ total_weight = sum(get_word_weight(w, word_weights, default_weight) for w in ref_words)
231
+
232
+ # Calculate error weight
233
+ error_weight = 0.0
234
+
235
+ for op in alignment:
236
+ op_type = op[0]
237
+
238
+ if op_type == 'match':
239
+ continue # No error
240
+ elif op_type == 'sub':
241
+ # Substitution - use reference word weight
242
+ ref_idx = op[1]
243
+ ref_word = ref_words[ref_idx]
244
+ error_weight += get_word_weight(ref_word, word_weights, default_weight)
245
+ elif op_type == 'del':
246
+ # Deletion - use reference word weight
247
+ ref_idx = op[1]
248
+ ref_word = ref_words[ref_idx]
249
+ error_weight += get_word_weight(ref_word, word_weights, default_weight)
250
+ elif op_type == 'ins':
251
+ # Insertion - count as error but with lower weight
252
+ error_weight += default_weight * 0.5 # Half weight for insertions
253
+
254
+ if total_weight == 0:
255
+ return None
256
+
257
+ return error_weight / total_weight
258
+
259
+
260
+ def calculate_awwer_components(reference: str, hypothesis: str,
261
+ word_weights: Dict[str, float],
262
+ language: str = 'hindi') -> Dict:
263
+ """
264
+ Calculate AWWER with detailed breakdown.
265
+
266
+ Args:
267
+ reference: Reference text
268
+ hypothesis: Hypothesis text
269
+ word_weights: Dictionary mapping words to weights
270
+ language: Language for normalization
271
+
272
+ Returns:
273
+ Dictionary with AWWER and breakdown details including:
274
+ - awwer: The AWWER score
275
+ - total_ref_weight: Sum of all reference word weights
276
+ - error_weight: Sum of error weights
277
+ - n_substitutions, n_deletions, n_insertions: Error counts
278
+ - high_weight_errors: List of weight 3-4 errors
279
+ """
280
+ result = {
281
+ 'awwer': None,
282
+ 'total_ref_weight': 0,
283
+ 'error_weight': 0,
284
+ 'n_substitutions': 0,
285
+ 'n_deletions': 0,
286
+ 'n_insertions': 0,
287
+ 'high_weight_errors': [],
288
+ }
289
+
290
+ if not reference or _isna(reference):
291
+ return result
292
+
293
+ # Clean texts
294
+ ref_clean = clean_text(str(reference), language)
295
+ hyp_clean = clean_text(str(hypothesis) if hypothesis and not _isna(hypothesis) else '', language)
296
+
297
+ ref_words = ref_clean.split() if ref_clean else []
298
+ hyp_words = hyp_clean.split() if hyp_clean else []
299
+
300
+ if not ref_words:
301
+ return result
302
+
303
+ # Get alignment using DP
304
+ alignment = align_words_dp(ref_words, hyp_words)
305
+
306
+ # Calculate metrics
307
+ total_weight = sum(get_word_weight(w, word_weights, 1.0) for w in ref_words)
308
+ result['total_ref_weight'] = total_weight
309
+
310
+ error_weight = 0.0
311
+
312
+ for op in alignment:
313
+ op_type = op[0]
314
+
315
+ if op_type == 'match':
316
+ continue
317
+ elif op_type == 'sub':
318
+ result['n_substitutions'] += 1
319
+ ref_idx = op[1]
320
+ hyp_idx = op[2]
321
+ ref_word = ref_words[ref_idx]
322
+ hyp_word = hyp_words[hyp_idx]
323
+ weight = get_word_weight(ref_word, word_weights, 1.0)
324
+ error_weight += weight
325
+ if weight >= 3:
326
+ result['high_weight_errors'].append({
327
+ 'type': 'substitution',
328
+ 'ref_word': ref_word,
329
+ 'hyp_word': hyp_word,
330
+ 'weight': weight
331
+ })
332
+ elif op_type == 'del':
333
+ result['n_deletions'] += 1
334
+ ref_idx = op[1]
335
+ ref_word = ref_words[ref_idx]
336
+ weight = get_word_weight(ref_word, word_weights, 1.0)
337
+ error_weight += weight
338
+ if weight >= 3:
339
+ result['high_weight_errors'].append({
340
+ 'type': 'deletion',
341
+ 'ref_word': ref_word,
342
+ 'weight': weight
343
+ })
344
+ elif op_type == 'ins':
345
+ result['n_insertions'] += 1
346
+ error_weight += 0.5 # Half weight for insertions
347
+
348
+ result['error_weight'] = error_weight
349
+ result['awwer'] = error_weight / total_weight if total_weight > 0 else None
350
+
351
+ return result
352
+
353
+
354
+ def calculate_awwer_from_string(reference: str, hypothesis: str,
355
+ word_weights_str: str,
356
+ language: str = 'hindi') -> Optional[float]:
357
+ """
358
+ Calculate AWWER from word_weights JSON string (convenience function).
359
+
360
+ Args:
361
+ reference: Reference text
362
+ hypothesis: Hypothesis text
363
+ word_weights_str: JSON string of word weights
364
+ language: Language for normalization
365
+
366
+ Returns:
367
+ AWWER value or None
368
+ """
369
+ weights = parse_word_weights(word_weights_str)
370
+ return calculate_awwer(reference, hypothesis, weights, language)
agri_awwer/wer.py ADDED
@@ -0,0 +1,352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Word Error Rate (WER) and Character Error Rate (CER) Calculation.
3
+
4
+ Clean implementation from scratch using only reference and hypothesis texts.
5
+ """
6
+
7
+ import math
8
+ import re
9
+ import unicodedata
10
+ from typing import Optional, List
11
+
12
+ try:
13
+ from jiwer import wer as jiwer_wer, cer as jiwer_cer
14
+ JIWER_AVAILABLE = True
15
+ except ImportError:
16
+ JIWER_AVAILABLE = False
17
+
18
+ try:
19
+ from jiwer import mer as jiwer_mer
20
+ JIWER_MER_AVAILABLE = True
21
+ except ImportError:
22
+ JIWER_MER_AVAILABLE = False
23
+
24
+
25
+ def _isna(value) -> bool:
26
+ """Check if a value is NA/NaN/None without requiring pandas."""
27
+ if value is None:
28
+ return True
29
+ if isinstance(value, float) and math.isnan(value):
30
+ return True
31
+ try:
32
+ import pandas as pd
33
+ return pd.isna(value)
34
+ except (ImportError, TypeError, ValueError):
35
+ return False
36
+
37
+
38
+ # Punctuation patterns to remove
39
+ PUNCTUATION_PATTERN = re.compile(r'[।,?!।॥,.;:"\'\-\(\)\[\]{}॰…\u0964\u0965]')
40
+ MULTI_SPACE_PATTERN = re.compile(r'\s+')
41
+
42
+ # Language-specific normalization patterns
43
+ # Hindi diacritics that may cause matching issues
44
+ HINDI_NORMALIZE = {
45
+ '\u0901': '', # chandrabindu (ँ) - remove
46
+ '\u0903': '', # visarga (ः) - remove
47
+ '\u093C': '', # nukta (़) - remove
48
+ '\u093D': '', # avagraha (ऽ) - remove
49
+ '\u0902': '\u0902', # anusvara (ं) - keep but could normalize
50
+ }
51
+
52
+ # Telugu diacritics
53
+ TELUGU_NORMALIZE = {
54
+ '\u0C01': '', # candrabindu - remove
55
+ '\u0C02': '\u0C02', # anusvara (sunna ం) - keep
56
+ '\u0C03': '', # visarga (ః) - remove
57
+ '\u0C3C': '', # nukta - remove (if present)
58
+ }
59
+
60
+ # Odia diacritics
61
+ ODIA_NORMALIZE = {
62
+ '\u0B01': '', # candrabindu (ଁ) - remove
63
+ '\u0B02': '\u0B02', # anusvara (ଂ) - keep
64
+ '\u0B03': '', # visarga (ଃ) - remove
65
+ '\u0B3C': '', # nukta - remove
66
+ '\u0B70': '', # isshar (୰) - remove
67
+ }
68
+
69
+ # Combined normalization map
70
+ LANGUAGE_NORMALIZE_MAPS = {
71
+ 'hindi': HINDI_NORMALIZE,
72
+ 'telugu': TELUGU_NORMALIZE,
73
+ 'odia': ODIA_NORMALIZE,
74
+ }
75
+
76
+ def _apply_language_normalization(text: str, language: str) -> str:
77
+ """Apply language-specific character normalization."""
78
+ norm_map = LANGUAGE_NORMALIZE_MAPS.get(language, {})
79
+ for char, replacement in norm_map.items():
80
+ text = text.replace(char, replacement)
81
+ return text
82
+
83
+
84
+ def clean_text(text: str, language: str = 'hindi') -> str:
85
+ """
86
+ Clean and normalize text for WER/CER calculation.
87
+
88
+ Applies language-specific normalization for diacritics that commonly
89
+ cause mismatches (chandrabindu, visarga, nukta, etc.).
90
+
91
+ Args:
92
+ text: Input text
93
+ language: Language for normalization ('hindi', 'telugu', 'odia')
94
+
95
+ Returns:
96
+ Cleaned text ready for comparison
97
+ """
98
+ if not text or not isinstance(text, str):
99
+ return ""
100
+
101
+ if _isna(text):
102
+ return ""
103
+
104
+ # Unicode normalization (NFC canonical form)
105
+ result = unicodedata.normalize('NFC', text)
106
+
107
+ # Apply language-specific diacritic normalization
108
+ result = _apply_language_normalization(result, language)
109
+
110
+ # Lowercase
111
+ result = result.lower()
112
+
113
+ # Remove punctuation
114
+ result = PUNCTUATION_PATTERN.sub(' ', result)
115
+
116
+ # Remove remaining special characters (but keep Indic scripts)
117
+ result = re.sub(r'[^\w\s]', '', result)
118
+
119
+ # Collapse multiple spaces
120
+ result = MULTI_SPACE_PATTERN.sub(' ', result).strip()
121
+
122
+ return result
123
+
124
+
125
+ def _levenshtein_distance(s1: str, s2: str) -> int:
126
+ """Calculate Levenshtein edit distance between two strings."""
127
+ if len(s1) < len(s2):
128
+ return _levenshtein_distance(s2, s1)
129
+
130
+ if len(s2) == 0:
131
+ return len(s1)
132
+
133
+ prev_row = list(range(len(s2) + 1))
134
+
135
+ for i, c1 in enumerate(s1):
136
+ curr_row = [i + 1]
137
+ for j, c2 in enumerate(s2):
138
+ insertions = prev_row[j + 1] + 1
139
+ deletions = curr_row[j] + 1
140
+ substitutions = prev_row[j] + (c1 != c2)
141
+ curr_row.append(min(insertions, deletions, substitutions))
142
+ prev_row = curr_row
143
+
144
+ return prev_row[-1]
145
+
146
+
147
+ def _simple_wer(ref_words: List[str], hyp_words: List[str]) -> float:
148
+ """Simple WER calculation using dynamic programming."""
149
+ n = len(ref_words)
150
+ m = len(hyp_words)
151
+
152
+ if n == 0:
153
+ return 0.0 if m == 0 else float(m)
154
+ if m == 0:
155
+ return 1.0
156
+
157
+ # DP table
158
+ dp = [[0] * (m + 1) for _ in range(n + 1)]
159
+
160
+ # Initialize
161
+ for i in range(n + 1):
162
+ dp[i][0] = i
163
+ for j in range(m + 1):
164
+ dp[0][j] = j
165
+
166
+ # Fill table
167
+ for i in range(1, n + 1):
168
+ for j in range(1, m + 1):
169
+ if ref_words[i-1] == hyp_words[j-1]:
170
+ dp[i][j] = dp[i-1][j-1]
171
+ else:
172
+ dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
173
+
174
+ return dp[n][m] / n
175
+
176
+
177
+ def calculate_wer(reference: str, hypothesis: str, language: str = 'hindi') -> Optional[float]:
178
+ """
179
+ Calculate Word Error Rate (WER).
180
+
181
+ WER = (Substitutions + Deletions + Insertions) / Total_Words_in_Reference
182
+
183
+ Args:
184
+ reference: Ground truth transcription
185
+ hypothesis: Model prediction
186
+ language: Language for normalization
187
+
188
+ Returns:
189
+ WER score (0.0 = perfect, higher = worse) or None if invalid
190
+ """
191
+ if not reference or _isna(reference):
192
+ return None
193
+
194
+ if not hypothesis or _isna(hypothesis):
195
+ return 1.0 # Empty hypothesis = all deletions
196
+
197
+ ref_clean = clean_text(str(reference), language)
198
+ hyp_clean = clean_text(str(hypothesis), language)
199
+
200
+ if not ref_clean:
201
+ return None
202
+
203
+ if not hyp_clean:
204
+ return 1.0
205
+
206
+ if JIWER_AVAILABLE:
207
+ try:
208
+ return float(jiwer_wer(ref_clean, hyp_clean))
209
+ except Exception:
210
+ return _simple_wer(ref_clean.split(), hyp_clean.split())
211
+ else:
212
+ return _simple_wer(ref_clean.split(), hyp_clean.split())
213
+
214
+
215
+ def calculate_cer(reference: str, hypothesis: str, language: str = 'hindi') -> Optional[float]:
216
+ """
217
+ Calculate Character Error Rate (CER).
218
+
219
+ CER = (Substitutions + Deletions + Insertions) / Total_Chars_in_Reference
220
+
221
+ Args:
222
+ reference: Ground truth transcription
223
+ hypothesis: Model prediction
224
+ language: Language for normalization
225
+
226
+ Returns:
227
+ CER score (0.0 = perfect, higher = worse) or None if invalid
228
+ """
229
+ if not reference or _isna(reference):
230
+ return None
231
+
232
+ if not hypothesis or _isna(hypothesis):
233
+ return 1.0
234
+
235
+ ref_clean = clean_text(str(reference), language)
236
+ hyp_clean = clean_text(str(hypothesis), language)
237
+
238
+ # Remove spaces for character comparison
239
+ ref_chars = ref_clean.replace(' ', '')
240
+ hyp_chars = hyp_clean.replace(' ', '')
241
+
242
+ if not ref_chars:
243
+ return None
244
+
245
+ if not hyp_chars:
246
+ return 1.0
247
+
248
+ if JIWER_AVAILABLE:
249
+ try:
250
+ return float(jiwer_cer(ref_chars, hyp_chars))
251
+ except Exception:
252
+ return _levenshtein_distance(ref_chars, hyp_chars) / len(ref_chars)
253
+ else:
254
+ return _levenshtein_distance(ref_chars, hyp_chars) / len(ref_chars)
255
+
256
+
257
+ def _simple_mer(ref_words: List[str], hyp_words: List[str]) -> float:
258
+ """Simple MER calculation using dynamic programming.
259
+
260
+ MER = (S + D + I) / (S + D + C) where C = correct matches.
261
+ The denominator is the total alignment length (substitutions + deletions + correct).
262
+ """
263
+ n = len(ref_words)
264
+ m = len(hyp_words)
265
+
266
+ if n == 0:
267
+ return 0.0 if m == 0 else 1.0
268
+ if m == 0:
269
+ return 1.0
270
+
271
+ # DP table
272
+ dp = [[0] * (m + 1) for _ in range(n + 1)]
273
+ for i in range(n + 1):
274
+ dp[i][0] = i
275
+ for j in range(m + 1):
276
+ dp[0][j] = j
277
+ for i in range(1, n + 1):
278
+ for j in range(1, m + 1):
279
+ if ref_words[i-1] == hyp_words[j-1]:
280
+ dp[i][j] = dp[i-1][j-1]
281
+ else:
282
+ dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
283
+
284
+ edit_distance = dp[n][m]
285
+ alignment_length = max(n, m)
286
+ if alignment_length == 0:
287
+ return 0.0
288
+ return edit_distance / alignment_length
289
+
290
+
291
+ def calculate_mer(reference: str, hypothesis: str, language: str = 'hindi') -> Optional[float]:
292
+ """
293
+ Calculate Match Error Rate (MER).
294
+
295
+ MER = (S + D + I) / (S + D + C) where C = correct matches.
296
+ Unlike WER (which divides by reference length N), MER divides by the
297
+ total alignment length, giving a match-aware error rate.
298
+
299
+ Args:
300
+ reference: Ground truth transcription
301
+ hypothesis: Model prediction
302
+ language: Language for normalization
303
+
304
+ Returns:
305
+ MER score (0.0 = perfect, 1.0 = worst) or None if invalid
306
+ """
307
+ if not reference or _isna(reference):
308
+ return None
309
+
310
+ if not hypothesis or _isna(hypothesis):
311
+ return 1.0
312
+
313
+ ref_clean = clean_text(str(reference), language)
314
+ hyp_clean = clean_text(str(hypothesis), language)
315
+
316
+ if not ref_clean:
317
+ return None
318
+
319
+ if not hyp_clean:
320
+ return 1.0
321
+
322
+ if JIWER_MER_AVAILABLE:
323
+ try:
324
+ return float(jiwer_mer(ref_clean, hyp_clean))
325
+ except Exception:
326
+ return _simple_mer(ref_clean.split(), hyp_clean.split())
327
+ else:
328
+ return _simple_mer(ref_clean.split(), hyp_clean.split())
329
+
330
+
331
+ def calculate_metrics_for_sample(reference: str, hypothesis: str, language: str = 'hindi') -> dict:
332
+ """
333
+ Calculate all metrics for a single sample.
334
+
335
+ Args:
336
+ reference: Ground truth transcription
337
+ hypothesis: Model prediction
338
+ language: Language for normalization
339
+
340
+ Returns:
341
+ Dictionary with wer, cer, and word counts
342
+ """
343
+ ref_clean = clean_text(str(reference) if reference else '', language)
344
+ hyp_clean = clean_text(str(hypothesis) if hypothesis else '', language)
345
+
346
+ return {
347
+ 'wer': calculate_wer(reference, hypothesis, language),
348
+ 'cer': calculate_cer(reference, hypothesis, language),
349
+ 'mer': calculate_mer(reference, hypothesis, language),
350
+ 'ref_word_count': len(ref_clean.split()) if ref_clean else 0,
351
+ 'hyp_word_count': len(hyp_clean.split()) if hyp_clean else 0,
352
+ }
pyproject.toml ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agri-awwer"
7
+ version = "0.1.0"
8
+ description = "Agriculture-Weighted Word Error Rate (AWWER) evaluation toolkit for domain-specific ASR assessment"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ { name = "Digital Green", email = "tech@digitalgreen.org" },
14
+ ]
15
+ keywords = ["asr", "speech-recognition", "agriculture", "evaluation", "wer", "metrics"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Science/Research",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.8",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ jiwer = ["jiwer>=3.0"]
30
+ dev = ["pytest>=7.0"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://huggingface.co/DigiGreen/Agri_AWWER_Toolkit"
34
+ Paper = "https://huggingface.co/datasets/DigiGreen/Agri_STT_Benchmarking_Dataset"
35
+
36
+ [tool.setuptools.packages.find]
37
+ include = ["agri_awwer*"]
tests/__pycache__/test_awwer.cpython-313-pytest-9.0.2.pyc ADDED
Binary file (44.5 kB). View file
 
tests/test_awwer.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the agri_awwer package."""
2
+
3
+ import json
4
+ import math
5
+
6
+ from agri_awwer import (
7
+ clean_text,
8
+ align_words_dp,
9
+ parse_word_weights,
10
+ calculate_awwer,
11
+ calculate_awwer_components,
12
+ calculate_awwer_from_string,
13
+ calculate_wer,
14
+ calculate_cer,
15
+ calculate_mer,
16
+ get_word_weight,
17
+ )
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # clean_text
22
+ # ---------------------------------------------------------------------------
23
+
24
+ class TestCleanText:
25
+ def test_basic_normalization(self):
26
+ assert clean_text("Hello, World!") == "hello world"
27
+
28
+ def test_empty_input(self):
29
+ assert clean_text("") == ""
30
+ assert clean_text(None) == ""
31
+
32
+ def test_punctuation_removal(self):
33
+ assert clean_text("gehun, aur makka.") == "gehun aur makka"
34
+
35
+ def test_whitespace_collapse(self):
36
+ assert clean_text(" gehun mein keet ") == "gehun mein keet"
37
+
38
+ def test_nan_handling(self):
39
+ assert clean_text(float("nan")) == ""
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # align_words_dp
44
+ # ---------------------------------------------------------------------------
45
+
46
+ class TestAlignWordsDP:
47
+ def test_identical(self):
48
+ ops = align_words_dp(["a", "b", "c"], ["a", "b", "c"])
49
+ assert all(op[0] == "match" for op in ops)
50
+
51
+ def test_substitution(self):
52
+ ops = align_words_dp(["a", "b"], ["a", "x"])
53
+ types = [op[0] for op in ops]
54
+ assert types == ["match", "sub"]
55
+
56
+ def test_deletion(self):
57
+ ops = align_words_dp(["a", "b", "c"], ["a", "c"])
58
+ types = [op[0] for op in ops]
59
+ assert "del" in types
60
+ assert sum(1 for t in types if t == "match") == 2
61
+
62
+ def test_insertion(self):
63
+ ops = align_words_dp(["a", "c"], ["a", "b", "c"])
64
+ types = [op[0] for op in ops]
65
+ assert "ins" in types
66
+ assert sum(1 for t in types if t == "match") == 2
67
+
68
+ def test_empty_ref(self):
69
+ ops = align_words_dp([], ["a", "b"])
70
+ assert all(op[0] == "ins" for op in ops)
71
+
72
+ def test_empty_hyp(self):
73
+ ops = align_words_dp(["a", "b"], [])
74
+ assert all(op[0] == "del" for op in ops)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # parse_word_weights
79
+ # ---------------------------------------------------------------------------
80
+
81
+ class TestParseWordWeights:
82
+ def test_json_string(self):
83
+ s = json.dumps([["gehun", 4], ["keet", 3]])
84
+ w = parse_word_weights(s)
85
+ assert w == {"gehun": 4.0, "keet": 3.0}
86
+
87
+ def test_empty(self):
88
+ assert parse_word_weights("") == {}
89
+ assert parse_word_weights(None) == {}
90
+
91
+ def test_invalid_json(self):
92
+ assert parse_word_weights("not json") == {}
93
+
94
+ def test_list_input(self):
95
+ w = parse_word_weights([["a", 2], ["b", 3]])
96
+ assert w == {"a": 2.0, "b": 3.0}
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # get_word_weight
101
+ # ---------------------------------------------------------------------------
102
+
103
+ class TestGetWordWeight:
104
+ def test_exact_match(self):
105
+ assert get_word_weight("gehun", {"gehun": 4.0}) == 4.0
106
+
107
+ def test_case_insensitive(self):
108
+ assert get_word_weight("Gehun", {"gehun": 4.0}) == 4.0
109
+
110
+ def test_default(self):
111
+ assert get_word_weight("unknown", {"gehun": 4.0}, default_weight=1.0) == 1.0
112
+
113
+ def test_empty(self):
114
+ assert get_word_weight("", {}) == 1.0
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # calculate_awwer
119
+ # ---------------------------------------------------------------------------
120
+
121
+ class TestCalculateAWWER:
122
+ def setup_method(self):
123
+ self.weights = {
124
+ "gehun": 4.0,
125
+ "keet": 4.0,
126
+ "mitti": 3.0,
127
+ "gaon": 1.0,
128
+ }
129
+
130
+ def test_perfect_match(self):
131
+ ref = "gehun mein keet laga hai"
132
+ assert calculate_awwer(ref, ref, self.weights) == 0.0
133
+
134
+ def test_high_weight_error(self):
135
+ ref = "gehun mein keet laga hai"
136
+ hyp = "gaon mein keet laga hai"
137
+ awwer = calculate_awwer(ref, hyp, self.weights)
138
+ wer = calculate_wer(ref, hyp)
139
+ # AWWER should be > WER because gehun (weight 4) was substituted
140
+ assert awwer is not None
141
+ assert wer is not None
142
+ assert awwer > wer
143
+
144
+ def test_none_on_empty_ref(self):
145
+ assert calculate_awwer("", "something", self.weights) is None
146
+ assert calculate_awwer(None, "something", self.weights) is None
147
+
148
+ def test_all_deletions(self):
149
+ ref = "gehun keet"
150
+ hyp = ""
151
+ awwer = calculate_awwer(ref, hyp, self.weights)
152
+ # All reference words deleted → error_weight == total_weight → AWWER = 1.0
153
+ assert awwer == 1.0
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # calculate_awwer_components
158
+ # ---------------------------------------------------------------------------
159
+
160
+ class TestCalculateAWWERComponents:
161
+ def test_breakdown(self):
162
+ weights = {"gehun": 4.0, "keet": 4.0}
163
+ ref = "gehun mein keet"
164
+ hyp = "gaon mein keet"
165
+ result = calculate_awwer_components(ref, hyp, weights)
166
+ assert result["n_substitutions"] == 1
167
+ assert result["n_deletions"] == 0
168
+ assert result["n_insertions"] == 0
169
+ assert len(result["high_weight_errors"]) == 1
170
+ assert result["high_weight_errors"][0]["ref_word"] == "gehun"
171
+
172
+
173
+ # ---------------------------------------------------------------------------
174
+ # calculate_awwer_from_string
175
+ # ---------------------------------------------------------------------------
176
+
177
+ class TestCalculateAWWERFromString:
178
+ def test_json_weights(self):
179
+ weights_json = json.dumps([["gehun", 4], ["keet", 4]])
180
+ ref = "gehun mein keet"
181
+ awwer = calculate_awwer_from_string(ref, ref, weights_json)
182
+ assert awwer == 0.0
183
+
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # calculate_wer / calculate_cer / calculate_mer
187
+ # ---------------------------------------------------------------------------
188
+
189
+ class TestStandardMetrics:
190
+ def test_wer_perfect(self):
191
+ assert calculate_wer("hello world", "hello world") == 0.0
192
+
193
+ def test_wer_all_wrong(self):
194
+ wer = calculate_wer("a b c", "x y z")
195
+ assert wer == 1.0
196
+
197
+ def test_wer_none_on_empty_ref(self):
198
+ assert calculate_wer("", "hello") is None
199
+
200
+ def test_wer_empty_hyp(self):
201
+ assert calculate_wer("hello world", "") == 1.0
202
+
203
+ def test_cer_perfect(self):
204
+ assert calculate_cer("hello", "hello") == 0.0
205
+
206
+ def test_cer_nonzero(self):
207
+ cer = calculate_cer("abc", "axc")
208
+ assert cer is not None
209
+ assert cer > 0
210
+
211
+ def test_mer_perfect(self):
212
+ assert calculate_mer("hello world", "hello world") == 0.0
213
+
214
+ def test_mer_bounds(self):
215
+ mer = calculate_mer("a b c", "x y z")
216
+ assert mer is not None
217
+ assert 0.0 <= mer <= 1.0
218
+
219
+ def test_nan_handling(self):
220
+ assert calculate_wer(float("nan"), "hello") is None
221
+ assert calculate_cer(float("nan"), "hello") is None
222
+ assert calculate_mer(float("nan"), "hello") is None