Upload folder using huggingface_hub
Browse files- .pytest_cache/.gitignore +2 -0
- .pytest_cache/CACHEDIR.TAG +4 -0
- .pytest_cache/README.md +8 -0
- .pytest_cache/v/cache/lastfailed +1 -0
- .pytest_cache/v/cache/nodeids +36 -0
- LICENSE +190 -0
- README.md +120 -0
- agri_awwer.egg-info/PKG-INFO +147 -0
- agri_awwer.egg-info/SOURCES.txt +12 -0
- agri_awwer.egg-info/dependency_links.txt +1 -0
- agri_awwer.egg-info/requires.txt +6 -0
- agri_awwer.egg-info/top_level.txt +1 -0
- agri_awwer/__init__.py +35 -0
- agri_awwer/__pycache__/__init__.cpython-313.pyc +0 -0
- agri_awwer/__pycache__/awwer.cpython-313.pyc +0 -0
- agri_awwer/__pycache__/wer.cpython-313.pyc +0 -0
- agri_awwer/awwer.py +370 -0
- agri_awwer/wer.py +352 -0
- pyproject.toml +37 -0
- tests/__pycache__/test_awwer.cpython-313-pytest-9.0.2.pyc +0 -0
- tests/test_awwer.py +222 -0
.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
|