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.
@@ -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,8 @@
1
+ .venv/
2
+ .venv-ref/
3
+ .ref-model/
4
+ __pycache__/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ dist/
8
+ bench/results/*.partial
@@ -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.
@@ -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.
@@ -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])