bhashantar 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bhashantar-0.1.0/.github/workflows/smoke.yml +34 -0
- bhashantar-0.1.0/.gitignore +8 -0
- bhashantar-0.1.0/HANDOFF.md +5 -0
- bhashantar-0.1.0/LICENSE +21 -0
- bhashantar-0.1.0/NOTICE +25 -0
- bhashantar-0.1.0/PKG-INFO +157 -0
- bhashantar-0.1.0/README.md +131 -0
- bhashantar-0.1.0/bench/parity.py +84 -0
- bhashantar-0.1.0/bench/reference.py +72 -0
- bhashantar-0.1.0/bench/results/ours.eng_Latn-ben_Beng.txt +100 -0
- bhashantar-0.1.0/bench/results/ours.eng_Latn-hin_Deva.txt +100 -0
- bhashantar-0.1.0/bench/results/ours.eng_Latn-tam_Taml.txt +100 -0
- bhashantar-0.1.0/bench/results/ours.stats.json +16 -0
- bhashantar-0.1.0/bench/results/parity.json +26 -0
- bhashantar-0.1.0/bench/results/ref.eng_Latn-ben_Beng.txt +100 -0
- bhashantar-0.1.0/bench/results/ref.eng_Latn-hin_Deva.txt +100 -0
- bhashantar-0.1.0/bench/results/ref.eng_Latn-tam_Taml.txt +100 -0
- bhashantar-0.1.0/bench/results/ref.stats.tam_Taml.json +9 -0
- bhashantar-0.1.0/pyproject.toml +43 -0
- bhashantar-0.1.0/src/bhashantar/__init__.py +29 -0
- bhashantar-0.1.0/src/bhashantar/cli.py +70 -0
- bhashantar-0.1.0/src/bhashantar/convert.py +176 -0
- bhashantar-0.1.0/src/bhashantar/engine.py +187 -0
- bhashantar-0.1.0/src/bhashantar/indic_script.py +98 -0
- bhashantar-0.1.0/src/bhashantar/languages.py +80 -0
- bhashantar-0.1.0/src/bhashantar/textprep.py +183 -0
- bhashantar-0.1.0/tests/test_refusals.py +67 -0
- bhashantar-0.1.0/tests/test_textprep.py +50 -0
- bhashantar-0.1.0/tests/test_translate_model.py +55 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: smoke
|
|
2
|
+
|
|
3
|
+
# Failure mode 2 is breakage when a dependency releases. The weekly job
|
|
4
|
+
# installs whatever is newest within the declared ranges and translates.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
pull_request:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "17 4 * * 1"
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
offline:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python: ["3.10", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python }}
|
|
22
|
+
- run: pip install --upgrade ".[dev]"
|
|
23
|
+
- run: pytest -q
|
|
24
|
+
|
|
25
|
+
model:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
- run: pip install --upgrade ".[dev]"
|
|
33
|
+
- run: pytest -q -m model
|
|
34
|
+
- run: bhashantar translate --tgt hin_Deva "The train leaves at 6:45 from platform 3."
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# bhashantar handoff (2026-11-10)
|
|
2
|
+
|
|
3
|
+
State: pushed to private zaidwhy/bhashantar. 13 offline tests; 27 model tests + 1 xfail (Santali). IndicTrans2 on CTranslate2 int8: chrF vs reference hin -0.52, ben +0.10, tam +0.27; about 0.3 s per sentence; 707 MB.
|
|
4
|
+
|
|
5
|
+
Next: publishing int8 weights to HF and PyPI are Zaid's call.
|
bhashantar-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zaid Ali Syed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bhashantar-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
bhashantar includes code adapted from the following MIT-licensed projects.
|
|
2
|
+
|
|
3
|
+
src/bhashantar/textprep.py
|
|
4
|
+
Adapted from IndicTransToolkit (processor.pyx, IndicProcessor)
|
|
5
|
+
https://github.com/VarunGumma/IndicTransToolkit
|
|
6
|
+
MIT License, Copyright (c) Varun Gumma.
|
|
7
|
+
|
|
8
|
+
src/bhashantar/indic_script.py
|
|
9
|
+
Adapted from indic_nlp_library (unicode_transliterate.py, indic_detokenize.py, langinfo.py)
|
|
10
|
+
https://github.com/anoopkunchukuttan/indic_nlp_library
|
|
11
|
+
MIT License, Copyright (c) 2013-present Anoop Kunchukuttan.
|
|
12
|
+
|
|
13
|
+
src/bhashantar/convert.py
|
|
14
|
+
Weight mapping follows ctranslate2/converters/fairseq.py
|
|
15
|
+
https://github.com/OpenNMT/CTranslate2
|
|
16
|
+
MIT License, Copyright (c) 2019 SYSTRAN.
|
|
17
|
+
|
|
18
|
+
Model weights (downloaded at runtime, not shipped in this package)
|
|
19
|
+
IndicTrans2 en-indic-dist-200M by AI4Bharat
|
|
20
|
+
https://huggingface.co/ai4bharat/indictrans2-en-indic-dist-200M
|
|
21
|
+
MIT License. The converted CTranslate2 model directory carries a copy of
|
|
22
|
+
that LICENSE file.
|
|
23
|
+
|
|
24
|
+
Both MIT notices require that the copyright and permission notice be included
|
|
25
|
+
in copies or substantial portions; the full MIT text is in LICENSE.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bhashantar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: IndicTrans2 English-to-Indic translation on CPU via CTranslate2 int8, no transformers or fairseq at runtime
|
|
5
|
+
Author-email: Zaid Ali Syed <sidzaid72@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
License-File: NOTICE
|
|
9
|
+
Keywords: ctranslate2,hindi,indic,indictrans2,machine-translation
|
|
10
|
+
Classifier: Natural Language :: Hindi
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: ctranslate2<5,>=4.4
|
|
15
|
+
Requires-Dist: huggingface-hub>=0.24
|
|
16
|
+
Requires-Dist: regex>=2023.0
|
|
17
|
+
Requires-Dist: sacremoses>=0.1.1
|
|
18
|
+
Requires-Dist: sentencepiece<0.3,>=0.2
|
|
19
|
+
Provides-Extra: convert
|
|
20
|
+
Requires-Dist: safetensors>=0.4; extra == 'convert'
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: psutil>=5.9; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
24
|
+
Requires-Dist: sacrebleu>=2.4; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# bhashantar
|
|
28
|
+
|
|
29
|
+
English to 25 Indic languages with AI4Bharat's IndicTrans2 (`en-indic-dist-200M`), on CPU, through CTranslate2 int8. No transformers, no torch, no fairseq and no IndicTransToolkit at runtime.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from bhashantar import translate
|
|
33
|
+
|
|
34
|
+
translate(["The weather in Mumbai is pleasant today."], tgt="hin_Deva")
|
|
35
|
+
# ['मुंबई में आज मौसम सुखद है।']
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
$ bhashantar translate --tgt hin_Deva "Send the report to ops@example.com by 15-03-2025."
|
|
40
|
+
रिपोर्ट को 15-03-2025 द्वारा ops@example.com पर भेजें।
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Why this exists
|
|
44
|
+
|
|
45
|
+
The official checkpoint loads through `trust_remote_code` files written against transformers 4.3x. On a fresh Python 3.12 venv with transformers 5.22.2 and IndicTransToolkit 1.1.1 (reproduced 2026-11-09):
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
from IndicTransToolkit.processor import IndicProcessor
|
|
49
|
+
ImportError: cannot import name 'PreTrainedTokenizerBase' from 'transformers.tokenization_utils'
|
|
50
|
+
|
|
51
|
+
AutoTokenizer.from_pretrained(..., trust_remote_code=True)
|
|
52
|
+
ModuleNotFoundError: No module named 'transformers.onnx'
|
|
53
|
+
|
|
54
|
+
AutoModelForSeq2SeqLM.from_pretrained(..., trust_remote_code=True)
|
|
55
|
+
ModuleNotFoundError: No module named 'transformers.onnx'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This is [IndicTransToolkit#29](https://github.com/VarunGumma/IndicTransToolkit/issues/29). A fix for the remote code is open as discussion #15 on the model repo, unmerged at the time of writing. Pinning transformers 4.50.3 makes the stack load again; that pinned stack is the reference this package is measured against.
|
|
59
|
+
|
|
60
|
+
## Data path
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
raw text
|
|
64
|
+
-> punctuation and digit normalisation, URLs/emails/numbers swapped for <IDn> (textprep.prepare)
|
|
65
|
+
-> Moses tokenisation, "eng_Latn hin_Deva" tags, SentencePiece pieces (engine)
|
|
66
|
+
-> CTranslate2 int8 beam search (beam 5), Devanagari-unified pieces out
|
|
67
|
+
-> detokenise pieces, restore <IDn>, Devanagari -> target script, reattach punctuation (textprep.finish)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The model writes every Brahmic target in Devanagari; Tamil, Bengali and the rest come from a fixed Unicode offset afterwards. That step and the placeholder restore are where a port silently goes wrong, so both are vendored from IndicTransToolkit and indic_nlp_library (MIT, see `NOTICE`) with their regexes and quirks intact. Two deliberate changes: placeholder maps travel with each sentence instead of a shared queue, and placeholder numbering is sorted instead of following set order, which varied between processes.
|
|
71
|
+
|
|
72
|
+
## Two ways this fails, and the guard for each
|
|
73
|
+
|
|
74
|
+
**Silent quality loss from conversion or quantisation.** CTranslate2 has no converter for this checkpoint's custom class, so `bhashantar.convert` maps the Hugging Face tensors onto the same `TransformerSpec` the fairseq converter fills. A wrong position offset or a missed layer norm still produces fluent-looking text. The guard is `bench/parity.py`: chrF against human references for both this package and the upstream model, with the pass bar fixed before the first run.
|
|
75
|
+
|
|
76
|
+
**Breakage on library upgrades.** Runtime dependencies are only what inference touches: `ctranslate2` (capped below 5, since `model.bin` is tied to the major), `sentencepiece`, `sacremoses`, `regex`, `huggingface_hub`. `.github/workflows/smoke.yml` installs the newest versions in range every week and translates.
|
|
77
|
+
|
|
78
|
+
**The opinion:** parity is published as a number, not claimed. If the delta ever exceeds the bar, the table says so.
|
|
79
|
+
|
|
80
|
+
## Parity
|
|
81
|
+
|
|
82
|
+
FLORES-200 devtest, first 100 sentences, English source. The FLORES files come from the public `dl.fbaipublicfiles.com/nllb/flores200_dataset.tar.gz`; AI4Bharat's IN22 and the Hugging Face FLORES+ copy are both gated. Reference = upstream model, transformers 4.50.3 + IndicTransToolkit 1.1.1, fp32, beam 5. Pass bar, set before running: |delta| <= 1.0 chrF.
|
|
83
|
+
|
|
84
|
+
| pair | chrF reference | chrF bhashantar | delta | identical outputs | pass |
|
|
85
|
+
|---|---|---|---|---|---|
|
|
86
|
+
| eng_Latn-hin_Deva | 65.69 | 65.17 | -0.52 | 61/100 | yes |
|
|
87
|
+
| eng_Latn-ben_Beng | 54.52 | 54.62 | +0.10 | 54/100 | yes |
|
|
88
|
+
| eng_Latn-tam_Taml | 61.33 | 61.59 | +0.27 | 56/100 | yes |
|
|
89
|
+
|
|
90
|
+
Reproduce: `python bench/parity.py score <flores devtest dir> bench/results` (outputs and `parity.json` are committed in `bench/results`).
|
|
91
|
+
|
|
92
|
+
"Identical outputs" counts sentences where both systems produce the same string; the rest differ in word choice (for example कनाडाई मधुमेह संघ against कैनेडियन डायबिटीज एसोसिएशन), the expected effect of int8 weights on beam search. Conversion error and quantisation error are not separated: an fp32 CTranslate2 conversion did not fit on the build machine's disk.
|
|
93
|
+
|
|
94
|
+
Reference weights: the official repo is gated, so the reference used `Raghavan/indictrans2-en-indic-dist-200M`, whose `pytorch_model.bin` has the same Git LFS pointer as the official file (sha256 `52671a26...0fef0`, verified locally); tokenizer files match the official Git blob ids.
|
|
95
|
+
|
|
96
|
+
## Speed and memory
|
|
97
|
+
|
|
98
|
+
| | bhashantar (CT2 int8) | upstream (transformers fp32) |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| model load | 0.47 s | not timed |
|
|
101
|
+
| ms per sentence, eng to tam_Taml, beam 5 | 323 (all 100 in one call) | 2,786 (batch 2, 80 sentences timed) |
|
|
102
|
+
| peak RSS | 707 MB | 1,472 MB |
|
|
103
|
+
|
|
104
|
+
The two latency figures are not like for like: the reference had to run at batch 2 because the build machine ran out of commit memory at batch 8, and it was resumed once mid-run, so only the last 80 sentences were timed. bhashantar on Hindi and Bengali: 328 and 308 ms per sentence.
|
|
105
|
+
|
|
106
|
+
Measured on a laptop (Windows 11, 16 GB RAM, 4 threads, other jobs running), so treat them as relative, not absolute.
|
|
107
|
+
|
|
108
|
+
## Limits
|
|
109
|
+
|
|
110
|
+
- One direction: English to Indic. The `indic-en` and `indic-indic` checkpoints are not converted.
|
|
111
|
+
- Caps, refused with the cap named in the error: 1,000 characters per sentence, 512 sentences and 50,000 characters per call, 256 subword tokens per sentence after preprocessing. Long input is refused, not truncated.
|
|
112
|
+
- Santali (`sat_Olck`) output mixes in Oriya and Arabic words for some sentences. The upstream model does the same; the test marks it as a known model limit.
|
|
113
|
+
- Sentence-level: split paragraphs yourself.
|
|
114
|
+
|
|
115
|
+
## Model files
|
|
116
|
+
|
|
117
|
+
`translate()` looks for a model in this order: the `model_dir` argument, `$BHASHANTAR_MODEL`, a local conversion at `$HF_HOME/bhashantar/en-indic-dist-200M-ct2-int8`, then the Hugging Face repo `zaidwhy/indictrans2-en-indic-dist-200M-ct2-int8` (cache first, so it works offline after the first download).
|
|
118
|
+
|
|
119
|
+
That repo is not published yet. Until it is, convert once yourself (needs a Hugging Face login with the model's terms accepted):
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
pip install "bhashantar[convert]"
|
|
123
|
+
hf auth login
|
|
124
|
+
bhashantar convert
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
pip install -e ".[dev]"
|
|
131
|
+
pytest # offline tests, no model
|
|
132
|
+
pytest -m model # end to end, needs the converted model
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Maintainer: publishing (not done yet)
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
# 1. weights, from a machine with the converted model
|
|
139
|
+
hf auth login
|
|
140
|
+
hf repo create zaidwhy/indictrans2-en-indic-dist-200M-ct2-int8 --repo-type model
|
|
141
|
+
hf upload zaidwhy/indictrans2-en-indic-dist-200M-ct2-int8 ~/.cache/huggingface/bhashantar/en-indic-dist-200M-ct2-int8 .
|
|
142
|
+
|
|
143
|
+
# 2. check that a clean install fetches it
|
|
144
|
+
python -m venv /tmp/bt && /tmp/bt/bin/pip install . && /tmp/bt/bin/bhashantar translate --tgt tam_Taml "Good morning."
|
|
145
|
+
|
|
146
|
+
# 3. package
|
|
147
|
+
python -m pip install build twine
|
|
148
|
+
python -m build
|
|
149
|
+
twine check dist/*
|
|
150
|
+
twine upload dist/*
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The model directory carries AI4Bharat's MIT `LICENSE` and a `provenance.json` with the source weights' sha256; add a model card stating the parity numbers above before uploading.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT. Vendored code and model weights keep their own MIT notices; see `NOTICE`.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# bhashantar
|
|
2
|
+
|
|
3
|
+
English to 25 Indic languages with AI4Bharat's IndicTrans2 (`en-indic-dist-200M`), on CPU, through CTranslate2 int8. No transformers, no torch, no fairseq and no IndicTransToolkit at runtime.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from bhashantar import translate
|
|
7
|
+
|
|
8
|
+
translate(["The weather in Mumbai is pleasant today."], tgt="hin_Deva")
|
|
9
|
+
# ['मुंबई में आज मौसम सुखद है।']
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
$ bhashantar translate --tgt hin_Deva "Send the report to ops@example.com by 15-03-2025."
|
|
14
|
+
रिपोर्ट को 15-03-2025 द्वारा ops@example.com पर भेजें।
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
The official checkpoint loads through `trust_remote_code` files written against transformers 4.3x. On a fresh Python 3.12 venv with transformers 5.22.2 and IndicTransToolkit 1.1.1 (reproduced 2026-11-09):
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
from IndicTransToolkit.processor import IndicProcessor
|
|
23
|
+
ImportError: cannot import name 'PreTrainedTokenizerBase' from 'transformers.tokenization_utils'
|
|
24
|
+
|
|
25
|
+
AutoTokenizer.from_pretrained(..., trust_remote_code=True)
|
|
26
|
+
ModuleNotFoundError: No module named 'transformers.onnx'
|
|
27
|
+
|
|
28
|
+
AutoModelForSeq2SeqLM.from_pretrained(..., trust_remote_code=True)
|
|
29
|
+
ModuleNotFoundError: No module named 'transformers.onnx'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This is [IndicTransToolkit#29](https://github.com/VarunGumma/IndicTransToolkit/issues/29). A fix for the remote code is open as discussion #15 on the model repo, unmerged at the time of writing. Pinning transformers 4.50.3 makes the stack load again; that pinned stack is the reference this package is measured against.
|
|
33
|
+
|
|
34
|
+
## Data path
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
raw text
|
|
38
|
+
-> punctuation and digit normalisation, URLs/emails/numbers swapped for <IDn> (textprep.prepare)
|
|
39
|
+
-> Moses tokenisation, "eng_Latn hin_Deva" tags, SentencePiece pieces (engine)
|
|
40
|
+
-> CTranslate2 int8 beam search (beam 5), Devanagari-unified pieces out
|
|
41
|
+
-> detokenise pieces, restore <IDn>, Devanagari -> target script, reattach punctuation (textprep.finish)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The model writes every Brahmic target in Devanagari; Tamil, Bengali and the rest come from a fixed Unicode offset afterwards. That step and the placeholder restore are where a port silently goes wrong, so both are vendored from IndicTransToolkit and indic_nlp_library (MIT, see `NOTICE`) with their regexes and quirks intact. Two deliberate changes: placeholder maps travel with each sentence instead of a shared queue, and placeholder numbering is sorted instead of following set order, which varied between processes.
|
|
45
|
+
|
|
46
|
+
## Two ways this fails, and the guard for each
|
|
47
|
+
|
|
48
|
+
**Silent quality loss from conversion or quantisation.** CTranslate2 has no converter for this checkpoint's custom class, so `bhashantar.convert` maps the Hugging Face tensors onto the same `TransformerSpec` the fairseq converter fills. A wrong position offset or a missed layer norm still produces fluent-looking text. The guard is `bench/parity.py`: chrF against human references for both this package and the upstream model, with the pass bar fixed before the first run.
|
|
49
|
+
|
|
50
|
+
**Breakage on library upgrades.** Runtime dependencies are only what inference touches: `ctranslate2` (capped below 5, since `model.bin` is tied to the major), `sentencepiece`, `sacremoses`, `regex`, `huggingface_hub`. `.github/workflows/smoke.yml` installs the newest versions in range every week and translates.
|
|
51
|
+
|
|
52
|
+
**The opinion:** parity is published as a number, not claimed. If the delta ever exceeds the bar, the table says so.
|
|
53
|
+
|
|
54
|
+
## Parity
|
|
55
|
+
|
|
56
|
+
FLORES-200 devtest, first 100 sentences, English source. The FLORES files come from the public `dl.fbaipublicfiles.com/nllb/flores200_dataset.tar.gz`; AI4Bharat's IN22 and the Hugging Face FLORES+ copy are both gated. Reference = upstream model, transformers 4.50.3 + IndicTransToolkit 1.1.1, fp32, beam 5. Pass bar, set before running: |delta| <= 1.0 chrF.
|
|
57
|
+
|
|
58
|
+
| pair | chrF reference | chrF bhashantar | delta | identical outputs | pass |
|
|
59
|
+
|---|---|---|---|---|---|
|
|
60
|
+
| eng_Latn-hin_Deva | 65.69 | 65.17 | -0.52 | 61/100 | yes |
|
|
61
|
+
| eng_Latn-ben_Beng | 54.52 | 54.62 | +0.10 | 54/100 | yes |
|
|
62
|
+
| eng_Latn-tam_Taml | 61.33 | 61.59 | +0.27 | 56/100 | yes |
|
|
63
|
+
|
|
64
|
+
Reproduce: `python bench/parity.py score <flores devtest dir> bench/results` (outputs and `parity.json` are committed in `bench/results`).
|
|
65
|
+
|
|
66
|
+
"Identical outputs" counts sentences where both systems produce the same string; the rest differ in word choice (for example कनाडाई मधुमेह संघ against कैनेडियन डायबिटीज एसोसिएशन), the expected effect of int8 weights on beam search. Conversion error and quantisation error are not separated: an fp32 CTranslate2 conversion did not fit on the build machine's disk.
|
|
67
|
+
|
|
68
|
+
Reference weights: the official repo is gated, so the reference used `Raghavan/indictrans2-en-indic-dist-200M`, whose `pytorch_model.bin` has the same Git LFS pointer as the official file (sha256 `52671a26...0fef0`, verified locally); tokenizer files match the official Git blob ids.
|
|
69
|
+
|
|
70
|
+
## Speed and memory
|
|
71
|
+
|
|
72
|
+
| | bhashantar (CT2 int8) | upstream (transformers fp32) |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| model load | 0.47 s | not timed |
|
|
75
|
+
| ms per sentence, eng to tam_Taml, beam 5 | 323 (all 100 in one call) | 2,786 (batch 2, 80 sentences timed) |
|
|
76
|
+
| peak RSS | 707 MB | 1,472 MB |
|
|
77
|
+
|
|
78
|
+
The two latency figures are not like for like: the reference had to run at batch 2 because the build machine ran out of commit memory at batch 8, and it was resumed once mid-run, so only the last 80 sentences were timed. bhashantar on Hindi and Bengali: 328 and 308 ms per sentence.
|
|
79
|
+
|
|
80
|
+
Measured on a laptop (Windows 11, 16 GB RAM, 4 threads, other jobs running), so treat them as relative, not absolute.
|
|
81
|
+
|
|
82
|
+
## Limits
|
|
83
|
+
|
|
84
|
+
- One direction: English to Indic. The `indic-en` and `indic-indic` checkpoints are not converted.
|
|
85
|
+
- Caps, refused with the cap named in the error: 1,000 characters per sentence, 512 sentences and 50,000 characters per call, 256 subword tokens per sentence after preprocessing. Long input is refused, not truncated.
|
|
86
|
+
- Santali (`sat_Olck`) output mixes in Oriya and Arabic words for some sentences. The upstream model does the same; the test marks it as a known model limit.
|
|
87
|
+
- Sentence-level: split paragraphs yourself.
|
|
88
|
+
|
|
89
|
+
## Model files
|
|
90
|
+
|
|
91
|
+
`translate()` looks for a model in this order: the `model_dir` argument, `$BHASHANTAR_MODEL`, a local conversion at `$HF_HOME/bhashantar/en-indic-dist-200M-ct2-int8`, then the Hugging Face repo `zaidwhy/indictrans2-en-indic-dist-200M-ct2-int8` (cache first, so it works offline after the first download).
|
|
92
|
+
|
|
93
|
+
That repo is not published yet. Until it is, convert once yourself (needs a Hugging Face login with the model's terms accepted):
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
pip install "bhashantar[convert]"
|
|
97
|
+
hf auth login
|
|
98
|
+
bhashantar convert
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
pip install -e ".[dev]"
|
|
105
|
+
pytest # offline tests, no model
|
|
106
|
+
pytest -m model # end to end, needs the converted model
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Maintainer: publishing (not done yet)
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
# 1. weights, from a machine with the converted model
|
|
113
|
+
hf auth login
|
|
114
|
+
hf repo create zaidwhy/indictrans2-en-indic-dist-200M-ct2-int8 --repo-type model
|
|
115
|
+
hf upload zaidwhy/indictrans2-en-indic-dist-200M-ct2-int8 ~/.cache/huggingface/bhashantar/en-indic-dist-200M-ct2-int8 .
|
|
116
|
+
|
|
117
|
+
# 2. check that a clean install fetches it
|
|
118
|
+
python -m venv /tmp/bt && /tmp/bt/bin/pip install . && /tmp/bt/bin/bhashantar translate --tgt tam_Taml "Good morning."
|
|
119
|
+
|
|
120
|
+
# 3. package
|
|
121
|
+
python -m pip install build twine
|
|
122
|
+
python -m build
|
|
123
|
+
twine check dist/*
|
|
124
|
+
twine upload dist/*
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The model directory carries AI4Bharat's MIT `LICENSE` and a `provenance.json` with the source weights' sha256; add a model card stating the parity numbers above before uploading.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT. Vendored code and model weights keep their own MIT notices; see `NOTICE`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Parity of bhashantar (CTranslate2 int8) against the upstream transformers model.
|
|
2
|
+
|
|
3
|
+
Step 1, `run`: translate the FLORES-200 devtest slice with bhashantar and record
|
|
4
|
+
latency and peak RSS. Step 2, `score`: chrF of both systems against the human
|
|
5
|
+
references, the delta, and how often the two outputs are identical.
|
|
6
|
+
|
|
7
|
+
.venv/Scripts/python bench/parity.py run FLORES_DEVTEST_DIR bench/results
|
|
8
|
+
.venv/Scripts/python bench/parity.py score FLORES_DEVTEST_DIR bench/results
|
|
9
|
+
|
|
10
|
+
Pass bar, fixed before the first run: |chrF(ours) - chrF(reference)| <= 1.0
|
|
11
|
+
for every pair.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import psutil
|
|
20
|
+
import sacrebleu
|
|
21
|
+
|
|
22
|
+
from bhashantar import Translator
|
|
23
|
+
|
|
24
|
+
PAIRS = ["hin_Deva", "ben_Beng", "tam_Taml"]
|
|
25
|
+
SENTENCES = 100
|
|
26
|
+
PASS_BAR = 1.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run(flores_dir: str, out_dir: str) -> None:
|
|
30
|
+
out = Path(out_dir)
|
|
31
|
+
source = Path(flores_dir, "eng_Latn.devtest").read_text(encoding="utf-8").splitlines()[:SENTENCES]
|
|
32
|
+
started = time.perf_counter()
|
|
33
|
+
translator = Translator(threads=4)
|
|
34
|
+
translator.translate(["warm up"], tgt="hin_Deva")
|
|
35
|
+
stats = {"load_seconds": round(time.perf_counter() - started, 2)}
|
|
36
|
+
for tgt in PAIRS:
|
|
37
|
+
started = time.perf_counter()
|
|
38
|
+
hyps = translator.translate(source, tgt=tgt)
|
|
39
|
+
elapsed = time.perf_counter() - started
|
|
40
|
+
Path(out, f"ours.eng_Latn-{tgt}.txt").write_text("\n".join(hyps) + "\n", encoding="utf-8")
|
|
41
|
+
stats[tgt] = {"seconds": round(elapsed, 2), "ms_per_sentence": round(1000 * elapsed / len(source), 1)}
|
|
42
|
+
print(tgt, stats[tgt], flush=True)
|
|
43
|
+
stats["peak_rss_mb"] = round(psutil.Process().memory_info().peak_wset / 2**20)
|
|
44
|
+
Path(out, "ours.stats.json").write_text(json.dumps(stats, indent=2), encoding="utf-8")
|
|
45
|
+
print(stats)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _lines(path: Path) -> list[str]:
|
|
49
|
+
return path.read_text(encoding="utf-8").splitlines()[:SENTENCES]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def score(flores_dir: str, out_dir: str) -> None:
|
|
53
|
+
out = Path(out_dir)
|
|
54
|
+
rows = []
|
|
55
|
+
for tgt in PAIRS:
|
|
56
|
+
refs = _lines(Path(flores_dir, f"{tgt}.devtest"))
|
|
57
|
+
upstream = _lines(out / f"ref.eng_Latn-{tgt}.txt")
|
|
58
|
+
ours = _lines(out / f"ours.eng_Latn-{tgt}.txt")
|
|
59
|
+
assert len(refs) == len(upstream) == len(ours) == SENTENCES, tgt
|
|
60
|
+
chrf_upstream = sacrebleu.corpus_chrf(upstream, [refs]).score
|
|
61
|
+
chrf_ours = sacrebleu.corpus_chrf(ours, [refs]).score
|
|
62
|
+
identical = sum(a == b for a, b in zip(upstream, ours))
|
|
63
|
+
rows.append(
|
|
64
|
+
{
|
|
65
|
+
"pair": f"eng_Latn-{tgt}",
|
|
66
|
+
"chrf_reference": round(chrf_upstream, 2),
|
|
67
|
+
"chrf_bhashantar": round(chrf_ours, 2),
|
|
68
|
+
"delta": round(chrf_ours - chrf_upstream, 2),
|
|
69
|
+
"identical_outputs": f"{identical}/{SENTENCES}",
|
|
70
|
+
"pass": abs(chrf_ours - chrf_upstream) <= PASS_BAR,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
Path(out, "parity.json").write_text(json.dumps(rows, indent=2), encoding="utf-8")
|
|
74
|
+
print("| pair | chrF reference | chrF bhashantar | delta | identical outputs | pass |")
|
|
75
|
+
print("|---|---|---|---|---|---|")
|
|
76
|
+
for r in rows:
|
|
77
|
+
print(
|
|
78
|
+
f"| {r['pair']} | {r['chrf_reference']} | {r['chrf_bhashantar']} | {r['delta']:+} "
|
|
79
|
+
f"| {r['identical_outputs']} | {'yes' if r['pass'] else 'NO'} |"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
{"run": run, "score": score}[sys.argv[1]](*sys.argv[2:4])
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Reference translations from the upstream stack, for the parity table.
|
|
2
|
+
|
|
3
|
+
Runs only in the pinned reference venv (torch, transformers==4.50.3,
|
|
4
|
+
IndicTransToolkit 1.1.1), because that stack does not load on current
|
|
5
|
+
transformers. Writes one hypothesis file per pair plus timing and peak RSS.
|
|
6
|
+
|
|
7
|
+
.venv-ref/Scripts/python bench/reference.py MODEL_DIR FLORES_DEVTEST_DIR OUT_DIR [tgt,tgt] [batch]
|
|
8
|
+
|
|
9
|
+
One process per pair keeps peak memory to a single pair and gives each pair
|
|
10
|
+
its own peak RSS figure.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import psutil
|
|
19
|
+
import torch
|
|
20
|
+
from IndicTransToolkit.processor import IndicProcessor
|
|
21
|
+
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
|
|
22
|
+
|
|
23
|
+
PAIRS = ["hin_Deva", "ben_Beng", "tam_Taml"]
|
|
24
|
+
SENTENCES = 100
|
|
25
|
+
BATCH = 8
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(model_dir: str, flores_dir: str, out_dir: str, pairs: str = ",".join(PAIRS), batch: str = str(BATCH)) -> None:
|
|
29
|
+
step = int(batch)
|
|
30
|
+
torch.set_num_threads(4)
|
|
31
|
+
out = Path(out_dir)
|
|
32
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
|
|
34
|
+
model = AutoModelForSeq2SeqLM.from_pretrained(model_dir, trust_remote_code=True, low_cpu_mem_usage=True).eval()
|
|
35
|
+
processor = IndicProcessor(inference=True)
|
|
36
|
+
source = Path(flores_dir, "eng_Latn.devtest").read_text(encoding="utf-8").splitlines()[:SENTENCES]
|
|
37
|
+
stats = {}
|
|
38
|
+
for tgt in pairs.split(","):
|
|
39
|
+
# Hypotheses are appended per batch so a run killed by memory pressure
|
|
40
|
+
# resumes where it stopped; timing then covers the last run only.
|
|
41
|
+
partial = Path(out, f"ref.eng_Latn-{tgt}.partial")
|
|
42
|
+
hyps = partial.read_text(encoding="utf-8").splitlines() if partial.exists() else []
|
|
43
|
+
started, done_before = time.perf_counter(), len(hyps)
|
|
44
|
+
for i in range(len(hyps), len(source), step):
|
|
45
|
+
chunk = processor.preprocess_batch(source[i : i + step], src_lang="eng_Latn", tgt_lang=tgt)
|
|
46
|
+
enc = tokenizer(chunk, truncation=True, padding="longest", return_tensors="pt")
|
|
47
|
+
with torch.inference_mode():
|
|
48
|
+
ids = model.generate(**enc, num_beams=5, max_length=256, min_length=0, use_cache=True)
|
|
49
|
+
decoded = tokenizer.batch_decode(ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)
|
|
50
|
+
batch_hyps = processor.postprocess_batch(decoded, lang=tgt)
|
|
51
|
+
with partial.open("a", encoding="utf-8") as fh:
|
|
52
|
+
fh.write("".join(h + "\n" for h in batch_hyps))
|
|
53
|
+
hyps += batch_hyps
|
|
54
|
+
elapsed = time.perf_counter() - started
|
|
55
|
+
Path(out, f"ref.eng_Latn-{tgt}.txt").write_text("\n".join(hyps) + "\n", encoding="utf-8")
|
|
56
|
+
partial.unlink()
|
|
57
|
+
timed = len(source) - done_before
|
|
58
|
+
stats[tgt] = {
|
|
59
|
+
"batch": step,
|
|
60
|
+
"timed_sentences": timed,
|
|
61
|
+
"seconds": round(elapsed, 2),
|
|
62
|
+
"ms_per_sentence": round(1000 * elapsed / max(timed, 1), 1),
|
|
63
|
+
}
|
|
64
|
+
print(tgt, stats[tgt], flush=True)
|
|
65
|
+
# peak_wset is the Windows peak working set; this harness ran on Windows.
|
|
66
|
+
stats["peak_rss_mb"] = round(psutil.Process().memory_info().peak_wset / 2**20)
|
|
67
|
+
Path(out, f"ref.stats.{pairs.replace(',', '+')}.json").write_text(json.dumps(stats, indent=2), encoding="utf-8")
|
|
68
|
+
print(stats)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main(*sys.argv[1:6])
|