Reza2kn commited on
Commit
618450b
·
verified ·
1 Parent(s): 1ddc08b

Final Rizeh reader INT4 conversions; source 51b8c314b055; full-page qualification limits documented

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. BENCHMARK.md +26 -0
  2. MANIFEST.json +221 -0
  3. README.md +55 -0
  4. RECIPE.md +31 -0
  5. VALIDATION.json +0 -0
  6. __pycache__/inference.cpython-311.pyc +0 -0
  7. checks/browser-decoder-check.json +6 -0
  8. checks/browser-loader-check.json +12 -0
  9. checks/package-litert-smoke.json +7 -0
  10. checks/package-smoke.json +16 -0
  11. components.json +67 -0
  12. components/body/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  13. components/body/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  14. components/body/models/recognizer-w4-block16.mlpackage/Manifest.json +18 -0
  15. components/degraded/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  16. components/degraded/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  17. components/degraded/models/recognizer-w4-block16.mlpackage/Manifest.json +18 -0
  18. components/numeric_math/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  19. components/numeric_math/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  20. components/numeric_math/models/recognizer-w4-block16.mlpackage/Manifest.json +18 -0
  21. components/printed/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  22. components/printed/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  23. components/printed/models/recognizer-w4-block16.mlpackage/Manifest.json +18 -0
  24. components/short_labels/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  25. components/short_labels/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  26. components/short_labels/models/recognizer-w4-block16.mlpackage/Manifest.json +18 -0
  27. experimental-detectors/line_guard/config.json +54 -0
  28. experimental-detectors/line_guard/detector-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  29. experimental-detectors/line_guard/detector-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  30. experimental-detectors/line_guard/detector-w4-block16.mlpackage/Manifest.json +18 -0
  31. experimental-detectors/line_guard/preprocessor_config.json +23 -0
  32. experimental-detectors/main_detector/config.json +54 -0
  33. experimental-detectors/main_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  34. experimental-detectors/main_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  35. experimental-detectors/main_detector/detector-w4-block16.mlpackage/Manifest.json +18 -0
  36. experimental-detectors/main_detector/preprocessor_config.json +23 -0
  37. experimental-detectors/printed_detector/config.json +54 -0
  38. experimental-detectors/printed_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  39. experimental-detectors/printed_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  40. experimental-detectors/printed_detector/detector-w4-block16.mlpackage/Manifest.json +18 -0
  41. experimental-detectors/printed_detector/preprocessor_config.json +23 -0
  42. experimental/portable_pipeline.py +176 -0
  43. geometry.py +27 -0
  44. inference.py +60 -0
  45. polarity.py +37 -0
  46. quantization_config.json +79 -0
  47. recipe/build_recognizer.py +87 -0
  48. recipe/export_litert.py +45 -0
  49. recipe/portable_model.py +53 -0
  50. recipe/requirements-litert.txt +5 -0
BENCHMARK.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Reader throughput measurements
2
+
3
+ Crops/second on the same 300 frozen development crops per reader. Exact-result caches were disabled. This measures preprocessed input to compact CTC outputs, excluding loading, page segmentation, cropping, text assembly and table extraction. The source and ONNX/LiteRT measurements are batch one; tract reports warmed native model execution. Implementation timing boundaries differ slightly; these are indicative measurements, not a controlled cross-runtime ranking.
4
+
5
+ **Mac conversions and browser validation ran concurrently, so Mac timing numbers are contended.** WASM body/short-label tests used two threads and the other three used one. Browser background scheduling may affect these numbers. Linux CPU and GPU measurements must not be treated as the same device. Native Windows, iOS and Android hardware remain untested.
6
+
7
+ | Runtime/device | Body | Short labels | Printed | Numeric/math | Degraded |
8
+ |---|---:|---:|---:|---:|---:|
9
+ | Source FP32 / PyTorch2.10 CUDA / RTX5080 Laptop | 210.08 | 210.76 | 211.76 | 211.45 | 210.52 |
10
+ | Linux / RTX5080 Laptop / ORT CUDA1.23.2 | 347.08 | 342.76 | 315.42 | 330.48 | 323.72 |
11
+ | Linux / RTX5080 Laptop / tract CUDA | 230.45 | 242.53 | 225.46 | 222.40 | 216.53 |
12
+ | Linux / CPU / ORT1.31,2 threads | 14.42 | 14.50 | 10.16 | 13.45 | 13.47 |
13
+ | Linux / CPU / LiteRT2.3,2 threads | 43.50 | 45.07 | 42.84 | 42.01 | 43.30 |
14
+ | M2 Mac / Core ML CPU_AND_GPU | 51.39 | 37.93 | 37.90 | 47.45 | 78.94 |
15
+ | M2 Mac / tract CPU | 21.78 | 21.85 | 17.85 | 17.48 | 27.58 |
16
+ | M2 Mac / tract Metal | 27.30 | 27.76 | 14.09 | 20.46 | 43.57 |
17
+ | M2 Mac / ORT1.31 CPU | 8.34 | 8.34 | 5.11 | 5.23 | 7.32 |
18
+ | M2 Mac / ORT1.31 CPU HQQ | 7.69 | 7.67 | 5.34 | 5.27 | 9.59 |
19
+ | M2 Mac / Chrome WebGPU | 8.43 | 8.82 | 8.62 | 7.43 | 5.81 |
20
+ | M2 Mac / Chrome WASM | 0.91 | 0.99 | 0.55 | 0.45 | 0.47 |
21
+
22
+ All 55 reader/runtime pairs passed the >=98% character-parity gate in every development bucket. The lowest bucket across all pairs was 98.2586%. This is character agreement with the uncompressed model, not exact-line agreement or OCR accuracy.
23
+
24
+ The pure-W4 full-page handwritten-table diagnostic failed (~86.51% parity on the CPU block16 route). An optional image-only route retained FP32 readers for color-separated handwriting and reached 100% on that example. The three public page diagnostics with that mixed-precision policy reached 100%,99.8895%,100%. No table-grid extraction was exercised (table_grids=0); full document qualification remains open.
25
+
26
+ See VALIDATION.json for the complete per-reader bucket metrics and diagnostic scope.
MANIFEST.json ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base_revision": "51b8c314b0552b1884c1f0e28016cb12a143f637",
3
+ "files": {
4
+ "requirements.txt": {
5
+ "bytes": 69,
6
+ "sha256": "9cd44c4193ee2ecb75fa477e10dd43690492120201bf43820151f38030e45df3"
7
+ },
8
+ "polarity.py": {
9
+ "bytes": 1442,
10
+ "sha256": "f006a8a94626496af91f604489efba22d29cca75621c3766a912e271a5139e9c"
11
+ },
12
+ "RECIPE.md": {
13
+ "bytes": 4075,
14
+ "sha256": "34dee9f78a3e9639def9c64926b088eb31f5cb6dca22ee54c92b811f10feb80f"
15
+ },
16
+ "BENCHMARK.md": {
17
+ "bytes": 2584,
18
+ "sha256": "adaab95a33f2e857a2f295a52e1c6fc61c7926c60b28e1e011dcc7865b0c618f"
19
+ },
20
+ "README.md": {
21
+ "bytes": 5015,
22
+ "sha256": "5f1b76bc9a908613348ea5a004374c659b5d27bf96037a8c2b49a61267929c42"
23
+ },
24
+ "quantization_config.json": {
25
+ "bytes": 2590,
26
+ "sha256": "da732a8e71b618ca018705b6e0aec17590fc067f7a8f5a801377d07944c5f18a"
27
+ },
28
+ "geometry.py": {
29
+ "bytes": 1922,
30
+ "sha256": "e8b6e0d5cabeeb5a6eeff6aa2ef9b317f8f34c83e88f0a5f82e17a99f7d0a6cc"
31
+ },
32
+ "VALIDATION.json": {
33
+ "bytes": 140369,
34
+ "sha256": "245ed6184048bfecefa67b188c0eeb04ea45943713cc2fbb5ddda0832f01c749"
35
+ },
36
+ "inference.py": {
37
+ "bytes": 4595,
38
+ "sha256": "2fab88114deb001eba8e0cb4cd4f9af7d190867fea5c84245d9ae78981ff712b"
39
+ },
40
+ "components.json": {
41
+ "bytes": 2016,
42
+ "sha256": "5d49aa0d30d5a7e0eebde35cdf4f149b07e6903d3e1461cf30b22e565ee15be5"
43
+ },
44
+ "vocab.json": {
45
+ "bytes": 20007,
46
+ "sha256": "d4330d46b65dca6f328fa6191bf02240c819ad83d9216bd9c78c13de895048dc"
47
+ },
48
+ "rtl.py": {
49
+ "bytes": 6097,
50
+ "sha256": "d5e90e33913d66d1204a2966cc54f42b7b8fd3f37ab476c493c564fe4d020b8b"
51
+ },
52
+ "experimental/portable_pipeline.py": {
53
+ "bytes": 11120,
54
+ "sha256": "c2d0b4a620b7e04b0ad46766950ead312a59f602a41293e99f7bc85bb174ed97"
55
+ },
56
+ "checks/package-smoke.json": {
57
+ "bytes": 311,
58
+ "sha256": "f4acb4534946a3ac7f8e6bff6cc25e92c62beb6dce3de7337734c3e82729138d"
59
+ },
60
+ "checks/browser-loader-check.json": {
61
+ "bytes": 338,
62
+ "sha256": "6681534f6712a591fb71c09e6a32fe49d4ab61d669036400106475ef94696d5e"
63
+ },
64
+ "checks/package-litert-smoke.json": {
65
+ "bytes": 140,
66
+ "sha256": "f1be9492777c493cf1beeacf99efa53815e0f96718830ebf872787c4f40eeae7"
67
+ },
68
+ "checks/browser-decoder-check.json": {
69
+ "bytes": 84,
70
+ "sha256": "7aa2b6c50334737c5f22721a7d689f7c45fc032e7dc9d94cbc52baabae4105a0"
71
+ },
72
+ "recipe/requirements-macos.txt": {
73
+ "bytes": 93,
74
+ "sha256": "8eb76eb39c4a004abd208f8ebe994a72a2669d07ff04fc3214b68ebf57b0d9cd"
75
+ },
76
+ "recipe/portable_model.py": {
77
+ "bytes": 2129,
78
+ "sha256": "f68eae9b098cd23e38560797f33e986ea922e56c886282b08631fe0d422ee283"
79
+ },
80
+ "recipe/export_litert.py": {
81
+ "bytes": 2807,
82
+ "sha256": "be7054e54d4d491074b49b294e31a9eab8a854756a96334e5eed8c1b30f4fe13"
83
+ },
84
+ "recipe/build_recognizer.py": {
85
+ "bytes": 5638,
86
+ "sha256": "1f0229995217d7265bc9cfba67948c2c9fce84922e86b4ceccc41d53e3da2431"
87
+ },
88
+ "recipe/requirements-litert.txt": {
89
+ "bytes": 97,
90
+ "sha256": "3def04424ae7d7f8023bf6164a16361744a36a6cae79b6ad385a5b0ddfbb017e"
91
+ },
92
+ "recipe/source/recognizer.py": {
93
+ "bytes": 3455,
94
+ "sha256": "03ffcc439ba4ada082c71b1e08e9bdcc8589332ae1e882557d44f004e3515dd2"
95
+ },
96
+ "__pycache__/inference.cpython-311.pyc": {
97
+ "bytes": 11159,
98
+ "sha256": "4daf2b018aa95dc40d25ce5db54ce592d1e96de8d95714fd358af32b2053c0e2"
99
+ },
100
+ "components/degraded/models/recognizer-w4-block16.mlpackage/Manifest.json": {
101
+ "bytes": 617,
102
+ "sha256": "675633649969487f825f5e61559d8fdeec20b22639b384f46f8713d55a3c5fa3"
103
+ },
104
+ "components/degraded/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel": {
105
+ "bytes": 136655,
106
+ "sha256": "fbb59f5a96148ffd4cbcda007e715e76bc7b9beed799b9700a4b6dd6f087280a"
107
+ },
108
+ "components/degraded/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin": {
109
+ "bytes": 26263424,
110
+ "sha256": "b300ceb473e7c9e3d97220cb34be29f7b49b030fa8693e58ae1b45710da8f55d"
111
+ },
112
+ "components/body/models/recognizer-w4-block16.mlpackage/Manifest.json": {
113
+ "bytes": 617,
114
+ "sha256": "2c7c591fa00b408a154d4623215337654358c5b2191ef35934c614f3889ffc4a"
115
+ },
116
+ "components/body/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel": {
117
+ "bytes": 136655,
118
+ "sha256": "068b9dd28a4464a544732ae3972b7da23629eb2fdd0434d855cdcec17be71fa9"
119
+ },
120
+ "components/body/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin": {
121
+ "bytes": 26263424,
122
+ "sha256": "45e4f1c7d1d8e0689dbe458d99014bef4ecf1e3f3e308a1399a94dfda1056e17"
123
+ },
124
+ "components/short_labels/models/recognizer-w4-block16.mlpackage/Manifest.json": {
125
+ "bytes": 617,
126
+ "sha256": "ce7c7bbce2d2f841baf7af009dd827cf0be193bbac12fab75ebbc62eb146f670"
127
+ },
128
+ "components/short_labels/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel": {
129
+ "bytes": 136655,
130
+ "sha256": "236f3f7de9ce5e2b2356333e653d996ff335d0bb6acde2c612c7f3190f57631a"
131
+ },
132
+ "components/short_labels/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin": {
133
+ "bytes": 26263424,
134
+ "sha256": "3d5c895f49c743122580765a36e37ab4a5ed6ae88526e940296e949cdd27cdd8"
135
+ },
136
+ "components/numeric_math/models/recognizer-w4-block16.mlpackage/Manifest.json": {
137
+ "bytes": 617,
138
+ "sha256": "6befae03e35e7c8b0214292bb2c9cfd56946157d4950795c637461cefefaf867"
139
+ },
140
+ "components/numeric_math/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel": {
141
+ "bytes": 136655,
142
+ "sha256": "c29f36bd45b9469a594b67e420efde84dfd8d663c138e0f1bee325c7d059debe"
143
+ },
144
+ "components/numeric_math/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin": {
145
+ "bytes": 26263424,
146
+ "sha256": "d15e99ea723d66fedbe3ab66319266a156273f84a352081b69f9650af4802ba1"
147
+ },
148
+ "components/printed/models/recognizer-w4-block16.mlpackage/Manifest.json": {
149
+ "bytes": 617,
150
+ "sha256": "81016c05409bafc0b082d3b4d3b4a60827adec441c383b41761648f2834b429b"
151
+ },
152
+ "components/printed/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel": {
153
+ "bytes": 136655,
154
+ "sha256": "5bfd191c319ff8ede42f41f35ac4ed609d1373c02f97f14b012649d705d08374"
155
+ },
156
+ "components/printed/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin": {
157
+ "bytes": 26263424,
158
+ "sha256": "b7c7da642b5bb0f429cdde70b0ea8815cce86175a1eb8b2a98a1d04c88c40f79"
159
+ },
160
+ "experimental-detectors/line_guard/config.json": {
161
+ "bytes": 858,
162
+ "sha256": "0f99d8176e3fc194966659cf6f1c52ab9ca1d573f47ba57aca55036ff5267d69"
163
+ },
164
+ "experimental-detectors/line_guard/preprocessor_config.json": {
165
+ "bytes": 373,
166
+ "sha256": "594344843fcd54db03240194c7677b5c459e78d349e68d4a89d7d91e4fa90587"
167
+ },
168
+ "experimental-detectors/line_guard/detector-w4-block16.mlpackage/Manifest.json": {
169
+ "bytes": 617,
170
+ "sha256": "893c116ab8685122991388c4ecf30ee0f9c7b05eb290c94f9e29da429585d26f"
171
+ },
172
+ "experimental-detectors/line_guard/detector-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel": {
173
+ "bytes": 183292,
174
+ "sha256": "3bbc26f64bd5168c8df476ce207d70977a29391ac85fc8745d967f2b573f6064"
175
+ },
176
+ "experimental-detectors/line_guard/detector-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin": {
177
+ "bytes": 30454400,
178
+ "sha256": "e9614d48774aae3a1e28ff645664d9b8f654f71c80bad4585432be258875b58c"
179
+ },
180
+ "experimental-detectors/printed_detector/config.json": {
181
+ "bytes": 858,
182
+ "sha256": "0f99d8176e3fc194966659cf6f1c52ab9ca1d573f47ba57aca55036ff5267d69"
183
+ },
184
+ "experimental-detectors/printed_detector/preprocessor_config.json": {
185
+ "bytes": 373,
186
+ "sha256": "594344843fcd54db03240194c7677b5c459e78d349e68d4a89d7d91e4fa90587"
187
+ },
188
+ "experimental-detectors/printed_detector/detector-w4-block16.mlpackage/Manifest.json": {
189
+ "bytes": 617,
190
+ "sha256": "55e73dae6485700c908feedaaafd055315a5b2fa40e2e2bf1ea741984518f173"
191
+ },
192
+ "experimental-detectors/printed_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel": {
193
+ "bytes": 183292,
194
+ "sha256": "b65e5fc8008ef59de2d91e4c7ec3e44a3d7e037367678564bfd5fb5810b69021"
195
+ },
196
+ "experimental-detectors/printed_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin": {
197
+ "bytes": 30454400,
198
+ "sha256": "02ba967fc37e29fe11eca73bdc77334272af1d2533cb9c7004f5eb0eb6d7591d"
199
+ },
200
+ "experimental-detectors/main_detector/config.json": {
201
+ "bytes": 858,
202
+ "sha256": "0f99d8176e3fc194966659cf6f1c52ab9ca1d573f47ba57aca55036ff5267d69"
203
+ },
204
+ "experimental-detectors/main_detector/preprocessor_config.json": {
205
+ "bytes": 373,
206
+ "sha256": "594344843fcd54db03240194c7677b5c459e78d349e68d4a89d7d91e4fa90587"
207
+ },
208
+ "experimental-detectors/main_detector/detector-w4-block16.mlpackage/Manifest.json": {
209
+ "bytes": 617,
210
+ "sha256": "ba24569c2754a72a8d28dd3eeccd666812aca11b6e98bce29b4afcefa820241e"
211
+ },
212
+ "experimental-detectors/main_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel": {
213
+ "bytes": 183292,
214
+ "sha256": "a7df9637d121d036e2bb2e251b9004240f441d83433cde53226e56a36f6c27a8"
215
+ },
216
+ "experimental-detectors/main_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin": {
217
+ "bytes": 30454400,
218
+ "sha256": "0640641e7f2df4053feb5f831e4974739f7839deee0b7c146229db7f10000267"
219
+ }
220
+ }
221
+ }
README.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - fa
4
+ license: other
5
+ license_name: bina-0.4-rizeh
6
+ license_link: https://huggingface.co/Reza2kn/Bina-0.4-Rizeh/blob/51b8c314b0552b1884c1f0e28016cb12a143f637/README.md
7
+ base_model: Reza2kn/Bina-0.4-Rizeh
8
+ base_model_relation: quantized
9
+ pipeline_tag: image-to-text
10
+ tags:
11
+ - bina
12
+ - ocr
13
+ - int4
14
+ - coreml
15
+ ---
16
+
17
+ # Bina0.4 Rizeh — CoreML-INT4
18
+
19
+ Quantized CTC reader components and experimental line-detector conversions of [the final release](https://huggingface.co/Reza2kn/Bina-0.4-Rizeh/tree/51b8c314b0552b1884c1f0e28016cb12a143f637), pinned at `51b8c314b0552b1884c1f0e28016cb12a143f637`. All five readers are included: body95k, short labels, printed, numeric/math and degraded scans. Original pipeline code and nonconverted dependencies remain in the source repository.
20
+
21
+ These are mixed-weight4-bit artifacts. They are not an all-INT4 document pipeline. The reference Python pipeline also uses Surya layout/order, native detector paths, table machinery and small admission/ordering models. No Sherpa-ONNX OCR support is claimed.
22
+
23
+ ## Measured reader parity
24
+
25
+ Fresh uncompressed PyTorch2.10 CUDA baselines were generated for each published checkpoint. Each target uses the same300 frozen internal development crops (50 each: English, handwriting, mixed, newspaper, printed, synthetic). No text normalization is applied to parity. At least98% character parity is required in every bucket; exact-line agreement is reported separately in VALIDATION.json. These are development canaries, not held-out accuracy or full-page certification.
26
+
27
+ | Runtime | Reader components | Lowest bucket character parity |
28
+ |---|---:|---:|
29
+ | coreml-w4-block16 | 5/5 | 98.466% |
30
+
31
+ Mac timings were collected during concurrent conversion/browser work and are not controlled speed rankings. Linux CPU/CUDA results are host-specific; native Windows is untested. LiteRT was tested on Linux CPU, without an Android test device. Apple validation used an M2 Mac; iOS hardware was not tested.
32
+
33
+ ## Page-pipeline limitation
34
+
35
+ The public release demo matched the original text in a small ONNX page test, and exact reader-cache reuse avoided recognition calls on its repeat. A broader public handwritten-table sample exposed a failure: the pure-W4 pipeline changed the output substantially even though line canaries passed. Therefore this bundle does **not** pass the full-page98% transcription gate and is not a drop-in fully qualified4-bit replacement for the production pipeline. Detector mask IoU is not transcription parity.
36
+
37
+ The experimental Python adapter preserves upstream crop geometry, routing and decoding. It also partitions an upstream geometry-reread cache by actual backend/weight hash, preventing cross-model cache reuse. Its optional image-only safety route uses original FP32 readers on color-separated handwritten pages, before OCR, avoiding a trial quantized pass. That route is mixed-precision and requires separate full-pipeline qualification. It does not make the pure-W4 model pass.
38
+
39
+ ## Line-level use
40
+
41
+ Install requirements.txt, then run `python inference.py line.png --role body`. This expects one text line or an explicit line box through `Recognizer.read_line`; it does not detect or assemble a whole document. Persistent sessions and bounded exact-input caching are included. Widths are never squashed or truncated.
42
+
43
+ Inputs are64px-high float32 ink maps with aspect preserved, black=1/paper=0,20% vertical and30% horizontal margins measured in original line height. True widths retain masking semantics. CTC blank is0 and nonblank indices map to vocab.json. Persian visual-to-logical conversion follows the original rtl.py.
44
+
45
+ Core ML ML Program packages require iOS18/macOS15 or later. Inputs: pixels float32 `[B,1,64,W]`, widths int32 `[B]`; batch1..8 and width8..8192. Use CPU_AND_GPU. W4/block16 applies to compatible large weights, retaining floating operations where required. GPU-only/ANE placement is not claimed.
46
+
47
+ ## Experimental detectors
48
+
49
+ Three detector roles are included under experimental-detectors/: combined main detector1000, rule-C line guard500 and the printed detector/head. Their procedural-dev heatmaps were compared against their original FP32 models. Their full geometry/table/transcription impact is unqualified. Do not replace the production detectors based on mask IoU alone.
50
+
51
+ ## Provenance and licensing
52
+
53
+ Quantization did not alter the source repository. The aggregate model.safetensors was verified tensor-for-tensor against the published native core checkpoints. Each component records its exact source checkpoint SHA256 in quantization_config.json. MANIFEST.json hashes the shipped artifacts. No private calibration images, benchmark fixtures, or private prediction texts are shipped.
54
+
55
+ Original recognizer weights/code remain under the source release CC BY-NC4.0 terms; Surya-derived detector/layout components retain their upstream Surya model terms. These conversions grant no additional rights. Retain the original Bina/Indra/Surya attribution. See the source release model card for details.
RECIPE.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Conversion recipe
2
+
3
+ Source: Reza2kn/Bina-0.4-Rizeh, revision51b8c314b0552b1884c1f0e28016cb12a143f637. components.json records the five exact native checkpoint paths and SHA256 values. Verify the checkpoint hash before converting. All five readers have the same architecture and unchanged vocabulary IDs. Body weights matched the previously exported body graph and were reused only after identity verification; every runtime was retested with fresh source baselines.
4
+
5
+ The recipe/source/recognizer.py file comes from that release. portable_model.py algebraically replaces pointwise Conv1d with Linear, preserving masked squeeze-excitation, true widths and CTC lengths. build_recognizer.py checks FP32 logit/argmax equivalence on varied synthetic shapes before exporting. Compact outputs contain tokens, frame confidence and lengths; callers do not need to transfer full logits.
6
+
7
+ Use separate environments for Mac and Linux LiteRT. The requirements files list the measured key converter versions, not complete dependency locks. Python3.11 was used on Mac; LiteRT was built in its existing Linux Python3.12 environment. No upstream training environment was changed.
8
+
9
+ ```sh
10
+ # Mac: repeat for each source checkpoint listed in components.json.
11
+ python -m pip install -r recipe/requirements-macos.txt
12
+ python recipe/build_recognizer.py --checkpoint /absolute/path/to/checkpoint.pt --onnx --hqq --coreml --blocks 16
13
+
14
+ # Separate Linux environment; same source checkpoint.
15
+ python -m pip install -r recipe/requirements-litert.txt
16
+ python recipe/export_litert.py --checkpoint /absolute/path/to/checkpoint.pt
17
+ ```
18
+
19
+ The LiteRT script's reader branch is portable; its legacy --detector branch requires additional detector helpers and the original Stallion checkout, and is not a standalone detector recipe.
20
+
21
+ ONNX: opset18, dynamic batch/width, ORT1.31 MatMulNBits RTN symmetric4-bit group16, accuracy_level4; HQQ4 group32 is an additional CPU alternative. Eighty-one pointwise linear layers are packed. The stem, depthwise convolutions, biases and other unsupported values stay floating. Graph inputs are float32; accuracy_level4 permits lower internal precision in runtime kernels. HQQ does not require image calibration. The tract importer tested here supports RTN group16 but rejects HQQ floating zero points.
22
+
23
+ Core ML: coremltools9 ML Program, FLOAT32 compute request, iOS18/macOS15 minimum, batch1..8, width8..8192. Apply linear_symmetric int4 per_block16 with weight_threshold512. Validate CPU_AND_GPU on Mac. Quantization storage does not guarantee a particular GPU/ANE placement.
24
+
25
+ LiteRT: litert-torch0.10 converts five static batch-one signatures, widths512/1024/2048/4096/8192. True-width masks remain dynamic inputs. ai-edge-quantizer0.10 recipe.dynamic_wi4b32_afp32 is the accepted reader candidate. recipe.weight_only_wi4_afp32 is produced as an experiment, not the qualified reader artifact shipped here. Keep the dynamic4-block32 file and audit real INT4 buffers; about95.5% of stored constant elements are INT4. This denominator is not a trainable-parameter fraction.
26
+
27
+ Qualification uses the same frozen300 development crops per reader,50 in each of six buckets. Compare every target to a freshly run uncompressed checkpoint without text normalization, requiring character parity>=0.98 in every bucket; report exact-line agreement separately. The frozen private manifest SHA256 is7e7049a3b56323588ae1f6bd38b1753afc2b1149d275c6f0f07c6993be991978. Private images and predictions are deliberately not included. Reproducing this exact canary requires authorized access to those frozen inputs; new public tests do not replace it.
28
+
29
+ The successful recipes above do not use image calibration. A separate GPTQ experiment used144 frozen training-only calibration crops, did not recover the full-page target, and was not promoted or shipped. No public page was used to fit weights. Full-page pure-W4 qualification failed on a handwritten-table diagnostic; see the model card and VALIDATION.json.
30
+
31
+ All original recognizer/code and Surya-derived model licensing and attribution still apply.
VALIDATION.json ADDED
The diff for this file is too large to render. See raw diff
 
__pycache__/inference.cpython-311.pyc ADDED
Binary file (11.2 kB). View file
 
checks/browser-decoder-check.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "cases": 1500,
3
+ "failed": 0,
4
+ "passed": true,
5
+ "unicode_version": "13.0.0"
6
+ }
checks/browser-loader-check.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "webgpu",
3
+ "text_matches_measured_candidate": true,
4
+ "concurrent_duplicate_identical": true,
5
+ "repeat_identical": true,
6
+ "repeat_cached": true,
7
+ "model_calls": 1,
8
+ "session_creates": 1,
9
+ "session_reused": true,
10
+ "model_sha256": "2cb6c60a23a2e9948b9df9679de0f8b2c849dd81bc54abbf4b23f64148edf574",
11
+ "passed": true
12
+ }
checks/package-litert-smoke.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "format": "LiteRT-INT4",
3
+ "text_matches_measured_candidate": true,
4
+ "repeat_identical": true,
5
+ "model_calls": 1,
6
+ "cache_hits": 1
7
+ }
checks/package-smoke.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "format": "ONNX-INT4",
4
+ "text_matches_measured_candidate": true,
5
+ "repeat_identical": true,
6
+ "model_calls": 1,
7
+ "cache_hits": 1
8
+ },
9
+ {
10
+ "format": "CoreML-INT4",
11
+ "text_matches_measured_candidate": true,
12
+ "repeat_identical": true,
13
+ "model_calls": 1,
14
+ "cache_hits": 1
15
+ }
16
+ ]
components.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "role": "short_labels",
4
+ "path": "stallion/bina04-order-form-batch-20261101/recognizer/weights-001000.pt",
5
+ "sha256": "367084525dc644ef6d69ad981819af07e3c6a41421359594a54ccc989c2fb90a",
6
+ "tensor_sha256": "c6c4db8e963a4a7d0c2ee4108972acf6980b688a464b5a184f2f4fa838b0145b",
7
+ "head": [
8
+ 1625,
9
+ 512,
10
+ 1
11
+ ],
12
+ "parameters": 32795225,
13
+ "state_elements": 32796250
14
+ },
15
+ {
16
+ "role": "body",
17
+ "path": "stallion/bina04-pagepath-v3-round3-clean-20261031/run/weights-095000.pt",
18
+ "sha256": "036be44e87c05759c18297426d2d531b0c3ac04af172886071de2ecdd6703981",
19
+ "tensor_sha256": "f9d6e2479d18deee48d563e46121188bb5b7353ab9725948930ad12261fba51e",
20
+ "head": [
21
+ 1625,
22
+ 512,
23
+ 1
24
+ ],
25
+ "parameters": 32795225,
26
+ "state_elements": 32796250
27
+ },
28
+ {
29
+ "role": "numeric_math",
30
+ "path": "stallion/bina04-numeric-math-recognizer-95k-20261102/recognizer/selected.pt",
31
+ "sha256": "6e3869a5cdebff89ce6cfbbcde8ce2d5a1aa9cfe342679d469d5da08bb82b443",
32
+ "tensor_sha256": "b77ac34fa6ba5478002c01b0ad702a9a7ad0f8ed91ce11d65b66fd7fb4471e03",
33
+ "head": [
34
+ 1625,
35
+ 512,
36
+ 1
37
+ ],
38
+ "parameters": 32795225,
39
+ "state_elements": 32796250
40
+ },
41
+ {
42
+ "role": "printed",
43
+ "path": "stallion/bina04-print-textgate-20261102/run/selected-printed-recognizer.pt",
44
+ "sha256": "28c4ca8f9fbbc8c578bb130d997598d2e9c2c4a2dad0b22cb5cc06818ecb988a",
45
+ "tensor_sha256": "8302658e43d391299491dd0d4921dde0cefb35d15c707bc88ae04acf6c608d09",
46
+ "head": [
47
+ 1625,
48
+ 512,
49
+ 1
50
+ ],
51
+ "parameters": 32795225,
52
+ "state_elements": 32796250
53
+ },
54
+ {
55
+ "role": "degraded",
56
+ "path": "stallion/bina04-real-degraded-95k-20261103/recognizer/selected.pt",
57
+ "sha256": "8867b309e881b698685e5b6e06a37c9f495e04dcf7027a044ad44c13cf8abd64",
58
+ "tensor_sha256": "6cb20a3e2d84233758efaabb90994ac3074d9cc4557deb531c859f0bbb7fe5dc",
59
+ "head": [
60
+ 1625,
61
+ 512,
62
+ 1
63
+ ],
64
+ "parameters": 32795225,
65
+ "state_elements": 32796250
66
+ }
67
+ ]
components/body/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:068b9dd28a4464a544732ae3972b7da23629eb2fdd0434d855cdcec17be71fa9
3
+ size 136655
components/body/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:45e4f1c7d1d8e0689dbe458d99014bef4ecf1e3f3e308a1399a94dfda1056e17
3
+ size 26263424
components/body/models/recognizer-w4-block16.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "7F9E401C-5F1D-40F2-BAB4-296CF4EE443D": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Weights",
7
+ "name": "weights",
8
+ "path": "com.apple.CoreML/weights"
9
+ },
10
+ "C8427A65-5938-46B4-A568-974F293FFEB6": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Specification",
13
+ "name": "model.mlmodel",
14
+ "path": "com.apple.CoreML/model.mlmodel"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "C8427A65-5938-46B4-A568-974F293FFEB6"
18
+ }
components/degraded/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fbb59f5a96148ffd4cbcda007e715e76bc7b9beed799b9700a4b6dd6f087280a
3
+ size 136655
components/degraded/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b300ceb473e7c9e3d97220cb34be29f7b49b030fa8693e58ae1b45710da8f55d
3
+ size 26263424
components/degraded/models/recognizer-w4-block16.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "B81BD9C2-F516-4993-913D-48720F04B6EB": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Specification",
7
+ "name": "model.mlmodel",
8
+ "path": "com.apple.CoreML/model.mlmodel"
9
+ },
10
+ "E39C1CFD-C8EA-42D0-A2BE-458AAA9087ED": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Weights",
13
+ "name": "weights",
14
+ "path": "com.apple.CoreML/weights"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "B81BD9C2-F516-4993-913D-48720F04B6EB"
18
+ }
components/numeric_math/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c29f36bd45b9469a594b67e420efde84dfd8d663c138e0f1bee325c7d059debe
3
+ size 136655
components/numeric_math/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d15e99ea723d66fedbe3ab66319266a156273f84a352081b69f9650af4802ba1
3
+ size 26263424
components/numeric_math/models/recognizer-w4-block16.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "D43BAB18-2A03-4550-8D6B-2C5382EB5516": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Weights",
7
+ "name": "weights",
8
+ "path": "com.apple.CoreML/weights"
9
+ },
10
+ "F80FD108-F2CF-476D-A813-0F126C5BA6F7": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Specification",
13
+ "name": "model.mlmodel",
14
+ "path": "com.apple.CoreML/model.mlmodel"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "F80FD108-F2CF-476D-A813-0F126C5BA6F7"
18
+ }
components/printed/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5bfd191c319ff8ede42f41f35ac4ed609d1373c02f97f14b012649d705d08374
3
+ size 136655
components/printed/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b7c7da642b5bb0f429cdde70b0ea8815cce86175a1eb8b2a98a1d04c88c40f79
3
+ size 26263424
components/printed/models/recognizer-w4-block16.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "129E53A2-6ECD-453B-A355-413C35B490C2": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Specification",
7
+ "name": "model.mlmodel",
8
+ "path": "com.apple.CoreML/model.mlmodel"
9
+ },
10
+ "9A7DDB3E-8AEE-49FD-8DB8-5C5D7D37CE2B": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Weights",
13
+ "name": "weights",
14
+ "path": "com.apple.CoreML/weights"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "129E53A2-6ECD-453B-A355-413C35B490C2"
18
+ }
components/short_labels/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:236f3f7de9ce5e2b2356333e653d996ff335d0bb6acde2c612c7f3190f57631a
3
+ size 136655
components/short_labels/models/recognizer-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3d5c895f49c743122580765a36e37ab4a5ed6ae88526e940296e949cdd27cdd8
3
+ size 26263424
components/short_labels/models/recognizer-w4-block16.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "8B16FAA1-CC03-401E-A352-4E583220EE42": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Specification",
7
+ "name": "model.mlmodel",
8
+ "path": "com.apple.CoreML/model.mlmodel"
9
+ },
10
+ "92B0EED9-262B-44CF-8D73-69D65C318DD5": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Weights",
13
+ "name": "weights",
14
+ "path": "com.apple.CoreML/weights"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "8B16FAA1-CC03-401E-A352-4E583220EE42"
18
+ }
experimental-detectors/line_guard/config.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "datalab-to/line_det_2.20",
3
+ "architectures": [
4
+ "EfficientViTForSemanticSegmentation"
5
+ ],
6
+ "attention_probs_dropout_prob": 0.0,
7
+ "classifier_dropout_prob": 0.0,
8
+ "decoder_hidden_size": 512,
9
+ "decoder_layer_hidden_size": 128,
10
+ "depths": [
11
+ 1,
12
+ 1,
13
+ 1,
14
+ 6,
15
+ 6
16
+ ],
17
+ "head_dim": 32,
18
+ "hidden_dropout_prob": 0.0,
19
+ "hidden_sizes": [
20
+ 32,
21
+ 64,
22
+ 160,
23
+ 256
24
+ ],
25
+ "initializer_range": 0.02,
26
+ "layer_norm_eps": 1e-06,
27
+ "model_type": "efficientvit",
28
+ "num_channels": 3,
29
+ "num_classes": 2,
30
+ "num_stages": 4,
31
+ "patch_size": [
32
+ 7,
33
+ 7
34
+ ],
35
+ "pos_weight": 1.0,
36
+ "semantic_loss_ignore_index": -1,
37
+ "strides": [
38
+ 2,
39
+ 2,
40
+ 2,
41
+ 2,
42
+ 2
43
+ ],
44
+ "torch_dtype": "float16",
45
+ "transformers_version": "4.49.0",
46
+ "use_focal": false,
47
+ "widths": [
48
+ 32,
49
+ 64,
50
+ 128,
51
+ 256,
52
+ 512
53
+ ]
54
+ }
experimental-detectors/line_guard/detector-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3bbc26f64bd5168c8df476ce207d70977a29391ac85fc8745d967f2b573f6064
3
+ size 183292
experimental-detectors/line_guard/detector-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e9614d48774aae3a1e28ff645664d9b8f654f71c80bad4585432be258875b58c
3
+ size 30454400
experimental-detectors/line_guard/detector-w4-block16.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "B9835E68-04AC-48C8-B053-470F990C6089": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Weights",
7
+ "name": "weights",
8
+ "path": "com.apple.CoreML/weights"
9
+ },
10
+ "CAB21B1A-9480-4150-8DBC-1AB7DF83912B": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Specification",
13
+ "name": "model.mlmodel",
14
+ "path": "com.apple.CoreML/model.mlmodel"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "CAB21B1A-9480-4150-8DBC-1AB7DF83912B"
18
+ }
experimental-detectors/line_guard/preprocessor_config.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_normalize": true,
3
+ "do_reduce_labels": true,
4
+ "do_rescale": true,
5
+ "do_resize": true,
6
+ "image_mean": [
7
+ 0.485,
8
+ 0.456,
9
+ 0.406
10
+ ],
11
+ "image_processor_type": "SegformerImageProcessor",
12
+ "image_std": [
13
+ 0.229,
14
+ 0.224,
15
+ 0.225
16
+ ],
17
+ "resample": 2,
18
+ "rescale_factor": 0.00392156862745098,
19
+ "size": {
20
+ "height": 1200,
21
+ "width": 1200
22
+ }
23
+ }
experimental-detectors/main_detector/config.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "datalab-to/line_det_2.20",
3
+ "architectures": [
4
+ "EfficientViTForSemanticSegmentation"
5
+ ],
6
+ "attention_probs_dropout_prob": 0.0,
7
+ "classifier_dropout_prob": 0.0,
8
+ "decoder_hidden_size": 512,
9
+ "decoder_layer_hidden_size": 128,
10
+ "depths": [
11
+ 1,
12
+ 1,
13
+ 1,
14
+ 6,
15
+ 6
16
+ ],
17
+ "head_dim": 32,
18
+ "hidden_dropout_prob": 0.0,
19
+ "hidden_sizes": [
20
+ 32,
21
+ 64,
22
+ 160,
23
+ 256
24
+ ],
25
+ "initializer_range": 0.02,
26
+ "layer_norm_eps": 1e-06,
27
+ "model_type": "efficientvit",
28
+ "num_channels": 3,
29
+ "num_classes": 2,
30
+ "num_stages": 4,
31
+ "patch_size": [
32
+ 7,
33
+ 7
34
+ ],
35
+ "pos_weight": 1.0,
36
+ "semantic_loss_ignore_index": -1,
37
+ "strides": [
38
+ 2,
39
+ 2,
40
+ 2,
41
+ 2,
42
+ 2
43
+ ],
44
+ "torch_dtype": "float16",
45
+ "transformers_version": "4.49.0",
46
+ "use_focal": false,
47
+ "widths": [
48
+ 32,
49
+ 64,
50
+ 128,
51
+ 256,
52
+ 512
53
+ ]
54
+ }
experimental-detectors/main_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a7df9637d121d036e2bb2e251b9004240f441d83433cde53226e56a36f6c27a8
3
+ size 183292
experimental-detectors/main_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0640641e7f2df4053feb5f831e4974739f7839deee0b7c146229db7f10000267
3
+ size 30454400
experimental-detectors/main_detector/detector-w4-block16.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "6AD650CC-ED3F-40BF-8974-8446E47C0705": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Weights",
7
+ "name": "weights",
8
+ "path": "com.apple.CoreML/weights"
9
+ },
10
+ "78907290-76FC-41EB-B44C-49D23AB10120": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Specification",
13
+ "name": "model.mlmodel",
14
+ "path": "com.apple.CoreML/model.mlmodel"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "78907290-76FC-41EB-B44C-49D23AB10120"
18
+ }
experimental-detectors/main_detector/preprocessor_config.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_normalize": true,
3
+ "do_reduce_labels": true,
4
+ "do_rescale": true,
5
+ "do_resize": true,
6
+ "image_mean": [
7
+ 0.485,
8
+ 0.456,
9
+ 0.406
10
+ ],
11
+ "image_processor_type": "SegformerImageProcessor",
12
+ "image_std": [
13
+ 0.229,
14
+ 0.224,
15
+ 0.225
16
+ ],
17
+ "resample": 2,
18
+ "rescale_factor": 0.00392156862745098,
19
+ "size": {
20
+ "height": 1200,
21
+ "width": 1200
22
+ }
23
+ }
experimental-detectors/printed_detector/config.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "datalab-to/line_det_2.20",
3
+ "architectures": [
4
+ "EfficientViTForSemanticSegmentation"
5
+ ],
6
+ "attention_probs_dropout_prob": 0.0,
7
+ "classifier_dropout_prob": 0.0,
8
+ "decoder_hidden_size": 512,
9
+ "decoder_layer_hidden_size": 128,
10
+ "depths": [
11
+ 1,
12
+ 1,
13
+ 1,
14
+ 6,
15
+ 6
16
+ ],
17
+ "head_dim": 32,
18
+ "hidden_dropout_prob": 0.0,
19
+ "hidden_sizes": [
20
+ 32,
21
+ 64,
22
+ 160,
23
+ 256
24
+ ],
25
+ "initializer_range": 0.02,
26
+ "layer_norm_eps": 1e-06,
27
+ "model_type": "efficientvit",
28
+ "num_channels": 3,
29
+ "num_classes": 2,
30
+ "num_stages": 4,
31
+ "patch_size": [
32
+ 7,
33
+ 7
34
+ ],
35
+ "pos_weight": 1.0,
36
+ "semantic_loss_ignore_index": -1,
37
+ "strides": [
38
+ 2,
39
+ 2,
40
+ 2,
41
+ 2,
42
+ 2
43
+ ],
44
+ "torch_dtype": "float16",
45
+ "transformers_version": "4.49.0",
46
+ "use_focal": false,
47
+ "widths": [
48
+ 32,
49
+ 64,
50
+ 128,
51
+ 256,
52
+ 512
53
+ ]
54
+ }
experimental-detectors/printed_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/model.mlmodel ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b65e5fc8008ef59de2d91e4c7ec3e44a3d7e037367678564bfd5fb5810b69021
3
+ size 183292
experimental-detectors/printed_detector/detector-w4-block16.mlpackage/Data/com.apple.CoreML/weights/weight.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:02ba967fc37e29fe11eca73bdc77334272af1d2533cb9c7004f5eb0eb6d7591d
3
+ size 30454400
experimental-detectors/printed_detector/detector-w4-block16.mlpackage/Manifest.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "fileFormatVersion": "1.0.0",
3
+ "itemInfoEntries": {
4
+ "73141E41-7FD9-48F3-9AB5-9EB5BB522A59": {
5
+ "author": "com.apple.CoreML",
6
+ "description": "CoreML Model Weights",
7
+ "name": "weights",
8
+ "path": "com.apple.CoreML/weights"
9
+ },
10
+ "CBF54C80-86F8-4AA1-90FB-5CE3AE0B0F28": {
11
+ "author": "com.apple.CoreML",
12
+ "description": "CoreML Model Specification",
13
+ "name": "model.mlmodel",
14
+ "path": "com.apple.CoreML/model.mlmodel"
15
+ }
16
+ },
17
+ "rootModelIdentifier": "CBF54C80-86F8-4AA1-90FB-5CE3AE0B0F28"
18
+ }
experimental-detectors/printed_detector/preprocessor_config.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_normalize": true,
3
+ "do_reduce_labels": true,
4
+ "do_rescale": true,
5
+ "do_resize": true,
6
+ "image_mean": [
7
+ 0.485,
8
+ 0.456,
9
+ 0.406
10
+ ],
11
+ "image_processor_type": "SegformerImageProcessor",
12
+ "image_std": [
13
+ 0.229,
14
+ 0.224,
15
+ 0.225
16
+ ],
17
+ "resample": 2,
18
+ "rescale_factor": 0.00392156862745098,
19
+ "size": {
20
+ "height": 1200,
21
+ "width": 1200
22
+ }
23
+ }
experimental/portable_pipeline.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Use quantized CTC readers inside the pinned, otherwise unchanged Rizeh pipeline.
2
+
3
+ Install before constructing Rizeh. Existing crop geometry, routing, decoding,
4
+ confidence thresholds and table assembly remain owned by the upstream code.
5
+ """
6
+ import collections,hashlib,json,math,sys
7
+ from pathlib import Path
8
+ import numpy as np
9
+
10
+ class CompactCTCOutput:
11
+ """Expose only CTC consumers used upstream, from already computed outputs.
12
+
13
+ This object does not invent logits. Full probability distributions and
14
+ arbitrary tensor operations are deliberately unavailable.
15
+ """
16
+ def __init__(self,tokens,confidence):
17
+ import torch
18
+ self.tokens=torch.from_numpy(tokens.astype(np.int64));self.confidence=torch.from_numpy(confidence.astype(np.float32))
19
+ def float(self):return self
20
+ def softmax(self,dim):
21
+ if dim!=-1:raise ValueError('Only original CTC class softmax is available')
22
+ return self
23
+ def max(self,dim):
24
+ if dim!=-1:raise ValueError('Only original CTC class maximum is available')
25
+ return self.confidence,self.tokens
26
+ def argmax(self,dim):
27
+ if dim!=-1:raise ValueError('Only original CTC class argmax is available')
28
+ return self.tokens
29
+
30
+ class ReaderModel:
31
+ def __init__(self,root,role,backend,provider,cache_bytes,variant='block16'):
32
+ self.root,self.role,self.backend,self.provider=root,role,backend,provider
33
+ self.cache=collections.OrderedDict();self.used=0;self.capacity=cache_bytes;self.calls=0;self.hits=0
34
+ self.session=None;self.variant=variant
35
+ self.high_precision=False;self.precise_session=None;self.source_checkpoint=None
36
+ def to(self,*args,**kwargs):return self
37
+ def eval(self):return self
38
+ def parameters(self):return iter(())
39
+ def _load(self):
40
+ if self.session is not None:return
41
+ d=self.root/'components'/self.role
42
+ if self.backend=='onnx':
43
+ import onnxruntime as ort
44
+ if self.provider not in ort.get_available_providers():raise RuntimeError('Unavailable provider: '+self.provider)
45
+ name={'hqq32':'recognizer-hqq4-block32.onnx','stable-head':'recognizer-w4-stable-head.onnx','stable-se':'recognizer-w4-stable-se.onnx','accurate':'recognizer-w4-accurate.onnx','fp32':'recognizer-fp32.onnx'}.get(self.variant,'recognizer-w4-block16.onnx')
46
+ options=ort.SessionOptions();options.intra_op_num_threads=2;options.inter_op_num_threads=1
47
+ self.session=ort.InferenceSession(str(d/'models'/name),sess_options=options,providers=[self.provider])
48
+ self.invoke=lambda x,w:self.session.run(None,dict(pixels=x,widths=w))
49
+ elif self.backend=='coreml':
50
+ import coremltools as ct
51
+ self.session=ct.models.MLModel(str(d/'models/recognizer-w4-block16.mlpackage'),compute_units=ct.ComputeUnit.CPU_AND_GPU)
52
+ def invoke(x,w):
53
+ o=self.session.predict(dict(pixels=x,widths=w.astype(np.int32)));return o['tokens'],o['confidence'],o['lengths']
54
+ self.invoke=invoke
55
+ elif self.backend=='litert':
56
+ from ai_edge_litert.interpreter import Interpreter
57
+ self.session=Interpreter(model_path=str(d/'litert/recognizer-dynamic4-block32.tflite'),num_threads=2)
58
+ runners={}
59
+ def invoke(x,w):
60
+ width=x.shape[-1]
61
+ if width not in runners:runners[width]=self.session.get_signature_runner('width'+str(width))
62
+ o=runners[width](args_0=x,args_1=w.astype(np.int32));return o['output_0'],o['output_1'],o['output_2']
63
+ self.invoke=invoke
64
+ elif self.backend=='source':
65
+ import torch
66
+ from recognizer import Recognizer
67
+ state=torch.load(self.source_checkpoint,map_location='cpu',weights_only=True)['model']
68
+ device='cuda'if self.provider=='CUDAExecutionProvider'else'cpu'
69
+ self.session=Recognizer(classes=state['head.weight'].shape[0],gradient_checkpointing=False).eval()
70
+ self.session.load_state_dict(state);self.session.to(device)
71
+ def invoke(x,w):
72
+ with torch.inference_mode():
73
+ logits,lengths=self.session(torch.from_numpy(x).to(device),torch.from_numpy(w).to(device));confidence,tokens=logits.float().softmax(-1).max(-1)
74
+ return tokens.cpu().numpy(),confidence.cpu().numpy(),lengths.cpu().numpy()
75
+ self.invoke=invoke
76
+ else:raise ValueError(self.backend)
77
+ def __call__(self,pixels,widths):
78
+ import torch
79
+ x=pixels.detach().cpu().numpy();w=widths.detach().cpu().numpy().astype(np.int64)
80
+ if x.ndim!=4 or x.shape[1:3]!=(1,64)or len(w)!=len(x):raise ValueError('Original Bx1x64xW input required')
81
+ if self.high_precision:
82
+ if self.precise_session is None:
83
+ self.precise_session=ReaderModel(self.root,self.role,'source',self.provider,self.capacity,'fp32')
84
+ self.precise_session.source_checkpoint=self.source_checkpoint
85
+ return self.precise_session(pixels,widths)
86
+ keys=[];values={};pending={}
87
+ for i,n in enumerate(w):
88
+ if n<1 or n>x.shape[-1]:raise ValueError('Invalid original width')
89
+ crop=np.ascontiguousarray(x[i,0,:,:n]);key=hashlib.sha256(str(crop.shape).encode()+crop.tobytes()).hexdigest();keys.append(key)
90
+ if key in self.cache:self.cache.move_to_end(key);values[key]=self.cache[key][0];self.hits+=1
91
+ elif key not in pending:pending[key]=i
92
+ self._load()
93
+ order=sorted(pending,key=lambda k:w[pending[k]])
94
+ # Core ML is bounded to eight; LiteRT has batch-one signatures.
95
+ batch=1 if self.backend=='litert'else 8
96
+ for start in range(0,len(order),batch):
97
+ group=order[start:start+batch];ids=[pending[k]for k in group];true=w[ids]
98
+ bucket=max(512,2**int(np.ceil(np.log2(int(max(true))))))
99
+ if self.backend in ['coreml','litert']and bucket>8192:raise ValueError('Input exceeds validated8192 width; never truncated')
100
+ canvas=np.zeros((len(ids),1,64,bucket),np.float32)
101
+ for j,i in enumerate(ids):canvas[j,0,:,:w[i]]=x[i,0,:,:w[i]]
102
+ tokens,confidence,lengths=self.invoke(canvas,true);self.calls+=1
103
+ for j,key in enumerate(group):
104
+ n=int(lengths[j]);value=(np.array(tokens[j,:n],copy=True),np.array(confidence[j,:n],copy=True),n);values[key]=value
105
+ size=value[0].nbytes+value[1].nbytes+128
106
+ if size<=self.capacity:
107
+ while self.used+size>self.capacity:
108
+ _,(_,old)=self.cache.popitem(last=False);self.used-=old
109
+ self.cache[key]=(value,size);self.used+=size
110
+ frames=math.ceil(x.shape[-1]/4);tokens=np.zeros((len(x),frames),np.int32);confidence=np.zeros((len(x),frames),np.float32);lengths=[]
111
+ for i,key in enumerate(keys):
112
+ a,b,n=values[key];tokens[i,:n]=a;confidence[i,:n]=b;lengths.append(n)
113
+ return CompactCTCOutput(tokens,confidence),torch.tensor(lengths,dtype=torch.int64)
114
+
115
+ def install(root,runtime,backend='onnx',provider='CPUExecutionProvider',cache_bytes=16*1024*1024,variant='block16'):
116
+ """Return shared model sessions, keyed by exact published checkpoint hash."""
117
+ root=Path(root);runtime=Path(runtime)
118
+ sys.path.insert(0,str(runtime/'stallion/indra-scale'))
119
+ import rtl_reader,torch
120
+ entries=json.loads((root/'components.json').read_text());lookup={r['sha256']:r['role']for r in entries};sessions={}
121
+ def initialize(reader,checkpoint,vocabulary,device='cpu',frame_budget=8192):
122
+ digest=hashlib.sha256(Path(checkpoint).read_bytes()).hexdigest()
123
+ if digest not in lookup:raise ValueError('Unrecognized published checkpoint: '+digest)
124
+ role=lookup[digest]
125
+ chosen='block16'if variant=='stable-se'and role not in ['body','printed']else variant
126
+ if role not in sessions:sessions[role]=ReaderModel(root,role,backend,provider,cache_bytes,chosen)
127
+ sessions[role].source_checkpoint=Path(checkpoint)
128
+ reader.alphabet=json.loads(Path(vocabulary).read_text())['chars'];reader.model=sessions[role];reader.frame_budget=frame_budget;reader.device=torch.device('cpu')
129
+ rtl_reader.RTLReader.__init__=initialize
130
+ # Upstream .to('cuda') calls are harmless; inputs stay on the CPU until ORT
131
+ # transfers them directly, avoiding a redundant Torch CPU->GPU->CPU copy.
132
+ rtl_reader.RTLReader.device=property(lambda self:torch.device('cpu'),lambda self,value:None)
133
+ return sessions
134
+
135
+ def namespace_intermediate_cache(root,sessions=None,policy='quantized-only'):
136
+ """Partition upstream geometry reread caches by actual backend/weight identity.
137
+
138
+ The upstream key includes pixels and geometry but omits reader identity.
139
+ Sharing it between original/quantized models can silently reuse old OCR.
140
+ Call after Rizeh construction, before its first image.
141
+ """
142
+ root=Path(root);identities=[]
143
+ for role,model in sorted((sessions or {}).items()):
144
+ name={'hqq32':'recognizer-hqq4-block32.onnx','stable-head':'recognizer-w4-stable-head.onnx','stable-se':'recognizer-w4-stable-se.onnx','accurate':'recognizer-w4-accurate.onnx','fp32':'recognizer-fp32.onnx'}.get(model.variant,'recognizer-w4-block16.onnx')
145
+ if model.backend=='onnx':
146
+ digest=hashlib.sha256((root/'components'/role/'models'/name).read_bytes()).hexdigest()
147
+ else:digest=model.backend+':'+role+':'+model.variant
148
+ identities.append([role,model.backend,model.provider,digest])
149
+ if not identities:identities=['original-pytorch',json.loads((root/'components.json').read_text())]
150
+ if policy!='quantized-only':
151
+ for role in sorted(sessions or {}):
152
+ precise=(sessions or {})[role].source_checkpoint
153
+ identities.append([role,'safety-original-pytorch',hashlib.sha256(precise.read_bytes()).hexdigest()])
154
+ key=hashlib.sha256(json.dumps([policy,identities],sort_keys=True).encode()).hexdigest()[:24]
155
+ module=sys.modules.get('geometry_fix')
156
+ if module is None:return key
157
+ original=module.apply_geometry
158
+ if getattr(original,'_cache_partitioned',False):return key
159
+ def apply(old,image,reader,expert,cache):return original(old,image,reader,expert,Path(cache)/key)
160
+ apply._cache_partitioned=True
161
+ for loaded in list(sys.modules.values()):
162
+ if loaded is not None and getattr(loaded,'apply_geometry',None)is original:loaded.apply_geometry=apply
163
+ return key
164
+
165
+ def select_page_precision(sessions,image):
166
+ """Image-only safety route, chosen before OCR; no trial quantized pass.
167
+
168
+ Color-separated handwritten form pages exposed a pure-W4 parity failure.
169
+ This optional hybrid policy uses original FP32 readers for that scope.
170
+ Quantized reader artifacts remain available independently of this policy.
171
+ """
172
+ from print_scope import handwriting_guard
173
+ scope=handwriting_guard(image)
174
+ fallback=bool(scope.get('protected'))
175
+ for model in sessions.values():model.high_precision=fallback
176
+ return dict(policy='color-separated-handwriting-fp32-v1',original_precision=fallback,scope=scope)
geometry.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """One 64px crop contract for training/inference; width is never clamped or squashed.
2
+
3
+ Margins and polarity follow the Apache-2.0 Indra rec_preproc implementation.
4
+ Only right-padding batches changes the canvas; widths retain the unpadded size.
5
+ """
6
+ import math
7
+ import numpy as np
8
+ def crop_line64(gray,box,*,pad_y=.2,pad_x=.3,dark=None):
9
+ import cv2
10
+ from polarity import is_dark_line
11
+ if gray.ndim!=2 or gray.dtype!=np.uint8:raise ValueError('Original uint8 grayscale required')
12
+ x0,y0,x1,y1=map(float,box[:4])
13
+ if not all(math.isfinite(v)for v in [x0,y0,x1,y1])or x1<=x0 or y1<=y0:raise ValueError('Invalid line geometry')
14
+ height=y1-y0;ix0=math.floor(x0-pad_x*height);ix1=math.ceil(x1+pad_x*height);iy0=math.floor(y0-pad_y*height);iy1=math.ceil(y1+pad_y*height);cw,ch=ix1-ix0,iy1-iy0
15
+ H,W=gray.shape;sx0,sy0,sx1,sy1=max(0,ix0),max(0,iy0),min(W,ix1),min(H,iy1)
16
+ if dark is None:dark=is_dark_line(gray,box)
17
+ canvas=np.full((ch,cw),0 if dark else 255,dtype=np.uint8)
18
+ if sx1>sx0 and sy1>sy0:canvas[sy0-iy0:sy1-iy0,sx0-ix0:sx1-ix0]=gray[sy0:sy1,sx0:sx1]
19
+ width=max(1,round(cw*64/ch));resized=cv2.resize(canvas,(width,64),interpolation=cv2.INTER_AREA if ch>64 else cv2.INTER_LINEAR).astype(np.float32)/255.
20
+ ink=resized if dark else 1.-resized
21
+ receipt=dict(original_WH=[W,H],ink_box=[x0,y0,x1,y1],padded_source_extent=[ix0,iy0,ix1,iy1],padded_source_WH=[cw,ch],resized_WH=[width,64],scale_XY=[width/cw,64/ch],width_clamped=False,aspect_rounding_error_pixels=abs(width-cw*64/ch),dark_polarity=bool(dark))
22
+ return ink,receipt
23
+ def pad_batch(crops,multiple=8):
24
+ if not crops or any(c.ndim!=2 or c.shape[0]!=64 for c in crops):raise ValueError('Every recognizer crop must be 64px high')
25
+ widths=np.array([c.shape[1]for c in crops],dtype=np.int64);padded=math.ceil(int(widths.max())/multiple)*multiple;out=np.zeros((len(crops),1,64,padded),dtype=np.float32)
26
+ for i,c in enumerate(crops):out[i,0,:,:c.shape[1]]=c
27
+ return out,widths
inference.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone quantized line recognition; the page pipeline is a separate layer."""
2
+ import argparse,collections,hashlib,json
3
+ from pathlib import Path
4
+ import numpy as np
5
+ from rtl import flip
6
+
7
+ class Recognizer:
8
+ def __init__(self,root=None,role='body',provider='CPUExecutionProvider',cache_entries=512,variant='block16'):
9
+ self.root=Path(root or Path(__file__).resolve().parent);self.role=role
10
+ config=json.loads((self.root/'quantization_config.json').read_text());self.backend=config['format'];self.variant=config.get('default_variant','block16')
11
+ self.alphabet=json.loads((self.root/'vocab.json').read_text())['chars'];d=self.root/'components'/role
12
+ self.cache=collections.OrderedDict();self.cache_entries=cache_entries;self.calls=0;self.cache_hits=0
13
+ if self.backend=='onnx':
14
+ import onnxruntime as ort
15
+ if provider not in ort.get_available_providers():raise RuntimeError('Unavailable provider: '+provider)
16
+ options=ort.SessionOptions();options.intra_op_num_threads=2;options.inter_op_num_threads=1
17
+ if variant=='hqq32'and provider!='CPUExecutionProvider':raise ValueError('HQQ32 helper is qualified for ORT CPU only')
18
+ name='recognizer-hqq4-block32.onnx'if variant=='hqq32'else'recognizer-w4-block16.onnx'
19
+ self.session=ort.InferenceSession(str(d/'models'/name),sess_options=options,providers=[provider]);self.run=lambda x,w:self.session.run(None,dict(pixels=x,widths=w))
20
+ elif self.backend=='coreml':
21
+ import coremltools as ct
22
+ self.session=ct.models.MLModel(str(d/'models/recognizer-w4-block16.mlpackage'),compute_units=ct.ComputeUnit.CPU_AND_GPU)
23
+ def run(x,w):
24
+ o=self.session.predict(dict(pixels=x,widths=w.astype(np.int32)));return o['tokens'],o['confidence'],o['lengths']
25
+ self.run=run
26
+ elif self.backend=='litert':
27
+ from ai_edge_litert.interpreter import Interpreter
28
+ self.session=Interpreter(model_path=str(d/'litert/recognizer-dynamic4-block32.tflite'),num_threads=2);self.runners={}
29
+ def run(x,w):
30
+ width=x.shape[-1]
31
+ if width not in self.runners:self.runners[width]=self.session.get_signature_runner('width'+str(width))
32
+ o=self.runners[width](args_0=x,args_1=w.astype(np.int32));return o['output_0'],o['output_1'],o['output_2']
33
+ self.run=run
34
+ else:raise ValueError(self.backend)
35
+ def predict_ink(self,ink):
36
+ ink=np.asarray(ink,dtype=np.float32)
37
+ if ink.ndim!=2 or ink.shape[0]!=64 or ink.shape[1]<1 or not np.isfinite(ink).all():raise ValueError('Finite float32 ink map [64,true_width] required')
38
+ key=hashlib.sha256(str(ink.shape).encode()+ink.tobytes()).hexdigest()
39
+ if key in self.cache:
40
+ self.cache_hits+=1;self.cache.move_to_end(key);return dict(self.cache[key])
41
+ width=ink.shape[1];bucket=max(512,2**int(np.ceil(np.log2(width))))
42
+ if bucket>8192:raise ValueError('Width exceeds the validated8192 bound; never truncate a line')
43
+ x=np.zeros((1,1,64,bucket),np.float32);x[0,0,:,:width]=ink
44
+ tokens,confidence,lengths=self.run(x,np.array([width],np.int64));self.calls+=1
45
+ n=int(lengths[0]);seq=np.asarray(tokens[0,:n]);keep=(seq!=0)&np.r_[True,seq[1:]!=seq[:-1]];ids=seq[keep].tolist()
46
+ visual=''.join(self.alphabet[int(i)-1]for i in ids)
47
+ result=dict(text=flip(visual,base_rtl=True),visual_text=visual,confidence=float(np.asarray(confidence[0,:n])[keep].mean())if keep.any()else 0.,role=self.role)
48
+ if self.cache_entries>0:
49
+ self.cache[key]=result
50
+ while len(self.cache)>self.cache_entries:self.cache.popitem(last=False)
51
+ return dict(result)
52
+ def read_line(self,image,box=None):
53
+ from geometry import crop_line64
54
+ gray=np.asarray(image.convert('L'));box=box or [0,0,gray.shape[1],gray.shape[0]]
55
+ ink,receipt=crop_line64(gray,box);result=self.predict_ink(ink);result['geometry']=receipt;return result
56
+
57
+ if __name__=='__main__':
58
+ p=argparse.ArgumentParser(description=__doc__);p.add_argument('line_image');p.add_argument('--role',default='body',choices=['body','short_labels','printed','numeric_math','degraded']);p.add_argument('--provider',default='CPUExecutionProvider');p.add_argument('--variant',default='block16',choices=['block16','hqq32']);a=p.parse_args()
59
+ from PIL import Image
60
+ reader=Recognizer(role=a.role,provider=a.provider,variant=a.variant);print(json.dumps(reader.read_line(Image.open(a.line_image)),ensure_ascii=False,indent=2))
polarity.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import numpy as np
3
+
4
+ def _percentiles(b: np.ndarray, qs=(0.02, 0.5, 0.98)) -> list[float]:
5
+ """np.percentile(b, 100 * qs) (linear) of a uint8 array, from its histogram:
6
+ no sort of the pixels."""
7
+ n = b.size
8
+ cum = np.cumsum(np.bincount(b.ravel(), minlength=256))
9
+ out = []
10
+ for q in qs:
11
+ x = q * (n - 1)
12
+ i = math.floor(x)
13
+ g = x - i
14
+ lo = float(np.searchsorted(cum, i, side="right"))
15
+ hi = float(np.searchsorted(cum, min(i + 1, n - 1), side="right"))
16
+ d = hi - lo
17
+ # numpy's lerp: from the nearer end
18
+ out.append(hi - d * (1 - g) if g >= 0.5 else lo + d * g)
19
+ return out
20
+
21
+ def is_dark_line(gray: np.ndarray, box) -> bool:
22
+ """Light text on a dark ground (a banner, a dark table head, a slide): the
23
+ ground (the median of the line box) lies nearer the dark end than the text
24
+ does, so the bright tail is the longer one. A shadowed line keeps dark text
25
+ on a dimmer ground. The recognizers read dark text on light paper, so a
26
+ light-on-dark line is read inverted."""
27
+ H, W = gray.shape
28
+ x0, y0, x1, y1 = (float(v) for v in box[:4])
29
+ b = gray[
30
+ max(0, int(y0)) : min(H, int(y1) + 1), max(0, int(x0)) : min(W, int(x1) + 1)
31
+ ]
32
+ if not b.size:
33
+ return False
34
+ lo, med, hi = (
35
+ _percentiles(b) if b.dtype == np.uint8 else np.percentile(b, (2, 50, 98))
36
+ )
37
+ return bool(hi - med > 2 * (med - lo) + 8)
quantization_config.json ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "format": "coreml",
3
+ "base_model": "Reza2kn/Bina-0.4-Rizeh",
4
+ "base_revision": "51b8c314b0552b1884c1f0e28016cb12a143f637",
5
+ "weight_bits": 4,
6
+ "mixed_precision": true,
7
+ "reader_components": [
8
+ {
9
+ "role": "short_labels",
10
+ "path": "stallion/bina04-order-form-batch-20261101/recognizer/weights-001000.pt",
11
+ "sha256": "367084525dc644ef6d69ad981819af07e3c6a41421359594a54ccc989c2fb90a",
12
+ "tensor_sha256": "c6c4db8e963a4a7d0c2ee4108972acf6980b688a464b5a184f2f4fa838b0145b",
13
+ "head": [
14
+ 1625,
15
+ 512,
16
+ 1
17
+ ],
18
+ "parameters": 32795225,
19
+ "state_elements": 32796250
20
+ },
21
+ {
22
+ "role": "body",
23
+ "path": "stallion/bina04-pagepath-v3-round3-clean-20261031/run/weights-095000.pt",
24
+ "sha256": "036be44e87c05759c18297426d2d531b0c3ac04af172886071de2ecdd6703981",
25
+ "tensor_sha256": "f9d6e2479d18deee48d563e46121188bb5b7353ab9725948930ad12261fba51e",
26
+ "head": [
27
+ 1625,
28
+ 512,
29
+ 1
30
+ ],
31
+ "parameters": 32795225,
32
+ "state_elements": 32796250
33
+ },
34
+ {
35
+ "role": "numeric_math",
36
+ "path": "stallion/bina04-numeric-math-recognizer-95k-20261102/recognizer/selected.pt",
37
+ "sha256": "6e3869a5cdebff89ce6cfbbcde8ce2d5a1aa9cfe342679d469d5da08bb82b443",
38
+ "tensor_sha256": "b77ac34fa6ba5478002c01b0ad702a9a7ad0f8ed91ce11d65b66fd7fb4471e03",
39
+ "head": [
40
+ 1625,
41
+ 512,
42
+ 1
43
+ ],
44
+ "parameters": 32795225,
45
+ "state_elements": 32796250
46
+ },
47
+ {
48
+ "role": "printed",
49
+ "path": "stallion/bina04-print-textgate-20261102/run/selected-printed-recognizer.pt",
50
+ "sha256": "28c4ca8f9fbbc8c578bb130d997598d2e9c2c4a2dad0b22cb5cc06818ecb988a",
51
+ "tensor_sha256": "8302658e43d391299491dd0d4921dde0cefb35d15c707bc88ae04acf6c608d09",
52
+ "head": [
53
+ 1625,
54
+ 512,
55
+ 1
56
+ ],
57
+ "parameters": 32795225,
58
+ "state_elements": 32796250
59
+ },
60
+ {
61
+ "role": "degraded",
62
+ "path": "stallion/bina04-real-degraded-95k-20261103/recognizer/selected.pt",
63
+ "sha256": "8867b309e881b698685e5b6e06a37c9f495e04dcf7027a044ad44c13cf8abd64",
64
+ "tensor_sha256": "6cb20a3e2d84233758efaabb90994ac3074d9cc4557deb531c859f0bbb7fe5dc",
65
+ "head": [
66
+ 1625,
67
+ 512,
68
+ 1
69
+ ],
70
+ "parameters": 32795225,
71
+ "state_elements": 32796250
72
+ }
73
+ ],
74
+ "default_variant": "block16",
75
+ "coreml_minimum": "iOS18/macOS15",
76
+ "qualified_widths": "true-width mask; canvas powers of two512..8192; no truncation",
77
+ "full_pipeline_all_int4": false,
78
+ "full_pipeline_transcription_qualified": false
79
+ }
recipe/build_recognizer.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse, hashlib, json, sys, time
2
+ from pathlib import Path
3
+ import numpy as np
4
+ import torch
5
+ from portable_model import PortableRecognizer, Compact
6
+
7
+ R = Path(__file__).resolve().parent
8
+ sys.path.insert(0, str(R/'source'))
9
+ from recognizer import Recognizer
10
+
11
+ def load(path):
12
+ state = torch.load(path, map_location='cpu', weights_only=True)['model']
13
+ source = Recognizer(classes=state['head.weight'].shape[0], gradient_checkpointing=False).eval()
14
+ source.load_state_dict(state)
15
+ return source
16
+
17
+ def main():
18
+ p=argparse.ArgumentParser()
19
+ p.add_argument('--checkpoint',type=Path,default=R/'source/recognizer-095000.pt')
20
+ p.add_argument('--out',type=Path,default=R/'models')
21
+ p.add_argument('--coreml',action='store_true')
22
+ p.add_argument('--onnx',action='store_true')
23
+ p.add_argument('--hqq',action='store_true')
24
+ p.add_argument('--blocks',default='16,32,64')
25
+ a=p.parse_args();a.out.mkdir(parents=True,exist_ok=True)
26
+ torch.set_num_threads(2);torch.manual_seed(18404)
27
+ source=load(a.checkpoint);model=PortableRecognizer(source).eval()
28
+ controls=[]
29
+ with torch.inference_mode():
30
+ for widths in [[1,7,64],[63,257,511],[769,1025,1537]]:
31
+ w=torch.tensor(widths,dtype=torch.int64);x=torch.rand(len(w),1,64,((max(widths)+7)//8)*8)
32
+ for i,n in enumerate(widths):x[i,:,:,n:]=0
33
+ y,n=source(x,w);z,m=model(x,w)
34
+ error=float((y-z).abs().max()); agreement=float((y.argmax(-1)==z.argmax(-1)).float().mean())
35
+ # Near-empty synthetic crops amplify fp32 summation-order noise in SE.
36
+ tolerance = .02 if min(widths)<8 else 2e-4
37
+ assert error < tolerance and agreement == 1.0 and torch.equal(n,m), (widths,error,agreement)
38
+ controls.append(dict(widths=widths,max_abs_logit_error=error,frame_argmax_agreement=agreement))
39
+ identity=dict(checkpoint=str(a.checkpoint.resolve()),sha256=hashlib.sha256(a.checkpoint.read_bytes()).hexdigest(),
40
+ parameters=sum(t.numel()for t in source.parameters()),lowering_controls=controls,
41
+ torch=torch.__version__,output_contract=['tokens:int32','confidence:float32','lengths:int32'])
42
+ (a.out/'source.json').write_text(json.dumps(identity,indent=2)+'\n')
43
+ compact=Compact(model).eval();x=torch.zeros(1,1,64,512);w=torch.tensor([512])
44
+ if a.onnx:
45
+ import onnx
46
+ from onnxruntime.quantization.matmul_nbits_quantizer import MatMulNBitsQuantizer,DefaultWeightOnlyQuantConfig
47
+ from onnxruntime.quantization.quant_utils import load_model_with_shape_infer
48
+ fp=a.out/'recognizer-fp32.onnx'
49
+ torch.onnx.export(compact,(x,w),str(fp),opset_version=18,dynamo=False,
50
+ input_names=['pixels','widths'],output_names=['tokens','confidence','lengths'],
51
+ dynamic_axes={'pixels':{0:'batch',3:'width'},'widths':{0:'batch'},
52
+ 'tokens':{0:'batch',1:'frames'},'confidence':{0:'batch',1:'frames'},'lengths':{0:'batch'}})
53
+ graph=onnx.load(fp);onnx.checker.check_model(graph)
54
+ for block in map(int,a.blocks.split(',')):
55
+ q=MatMulNBitsQuantizer(load_model_with_shape_infer(fp),
56
+ algo_config=DefaultWeightOnlyQuantConfig(block_size=block,is_symmetric=True,accuracy_level=4))
57
+ q.process();target=a.out/f'recognizer-w4-block{block}.onnx'
58
+ q.model.save_model_to_file(str(target),use_external_data_format=False)
59
+ graph=onnx.load(target);onnx.checker.check_model(graph)
60
+ counts={}
61
+ for node in graph.graph.node:counts[node.op_type]=counts.get(node.op_type,0)+1
62
+ (target.with_suffix('.audit.json')).write_text(json.dumps(dict(bytes=target.stat().st_size,
63
+ ops=counts,weight_bits=4,graph_input_bits=32,accuracy_level=4,retained_float_ops=['stem','depthwise convolutions','biases'],
64
+ format='ORT MatMulNBits W4 with runtime-dependent internal precision; mixed-weight precision, not all-INT4'),indent=2)+'\n')
65
+ if a.hqq:
66
+ from onnxruntime.quantization.matmul_nbits_quantizer import HQQWeightOnlyQuantConfig
67
+ q=MatMulNBitsQuantizer(load_model_with_shape_infer(fp),algo_config=HQQWeightOnlyQuantConfig(block_size=32,bits=4))
68
+ q.process();q.model.save_model_to_file(str(a.out/'recognizer-hqq4-block32.onnx'),use_external_data_format=False)
69
+ if a.coreml:
70
+ import coremltools as ct
71
+ from coremltools.optimize.coreml import OptimizationConfig,OpLinearQuantizerConfig,linear_quantize_weights
72
+ # Dynamic width with exact mask semantics; small bounded batch for mobile.
73
+ traced=torch.jit.trace(compact,(x,w))
74
+ ml=ct.convert(traced,inputs=[ct.TensorType(name='pixels',shape=(ct.RangeDim(1,8,default=1),1,64,ct.RangeDim(8,8192,default=512))),
75
+ ct.TensorType(name='widths',shape=(ct.RangeDim(1,8,default=1),),dtype=np.int32)],
76
+ outputs=[ct.TensorType(name='tokens'),ct.TensorType(name='confidence'),ct.TensorType(name='lengths')],
77
+ minimum_deployment_target=ct.target.iOS18,compute_precision=ct.precision.FLOAT32,
78
+ compute_units=ct.ComputeUnit.CPU_AND_GPU)
79
+ ml.save(str(a.out/'recognizer-fp32.mlpackage'))
80
+ for block in map(int,a.blocks.split(',')):
81
+ config=OptimizationConfig(global_config=OpLinearQuantizerConfig(mode='linear_symmetric',dtype='int4',
82
+ granularity='per_block',block_size=block,weight_threshold=512))
83
+ quant=linear_quantize_weights(ml,config=config)
84
+ quant.save(str(a.out/f'recognizer-w4-block{block}.mlpackage'))
85
+ print(json.dumps(identity),flush=True)
86
+
87
+ if __name__=='__main__':main()
recipe/export_litert.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Linux CPU exporter; use an isolated venv, never the training environment."""
2
+ import os
3
+ os.environ.update(CUDA_VISIBLE_DEVICES='',OMP_NUM_THREADS='2',OPENBLAS_NUM_THREADS='1',TF_NUM_INTRAOP_THREADS='2',TF_NUM_INTEROP_THREADS='1')
4
+ import argparse,json,time,sys
5
+ from pathlib import Path
6
+ import torch
7
+ import litert_torch
8
+ from ai_edge_quantizer import quantizer,recipe
9
+ from build_recognizer import load,R
10
+ from portable_model import PortableRecognizer,Compact
11
+
12
+ def main():
13
+ parser=argparse.ArgumentParser();parser.add_argument('--detector',action='store_true');parser.add_argument('--checkpoint',type=Path);args=parser.parse_args()
14
+ torch.set_num_threads(2);out=R/'litert';out.mkdir(exist_ok=True)
15
+ if args.detector:
16
+ sys.path.append('/home/rezo/bina02-surya-layout-env/lib/python3.10/site-packages')
17
+ from export_detector import EfficientViTConfig,EfficientViTForSemanticSegmentation,BASE,S,Model
18
+ m=EfficientViTForSemanticSegmentation(EfficientViTConfig.from_pretrained(str(BASE))).eval()
19
+ m.load_state_dict(torch.load(args.checkpoint or S/'run/checkpoints/step-001000.pt',map_location='cpu',weights_only=True)['model'])
20
+ converter=litert_torch.signature('detector',Model(m).eval(),(torch.zeros(1,3,1200,1200),))
21
+ prefix='detector'
22
+ else:
23
+ model=Compact(PortableRecognizer(load(args.checkpoint or R/'source/recognizer-095000.pt'))).eval()
24
+ model.model.index_dtype=torch.int32
25
+ converter=None
26
+ for width in [512,1024,2048,4096,8192]:
27
+ x=torch.zeros(1,1,64,width);w=torch.tensor([width])
28
+ if converter is None:converter=litert_torch.signature('width'+str(width),model,(x,w))
29
+ else:converter.signature('width'+str(width),model,(x,w))
30
+ prefix='recognizer'
31
+ with torch.no_grad():edge=converter.convert()
32
+ fp=out/(prefix+'-fp32.tflite');edge.export(str(fp))
33
+ results=[]
34
+ for name,rp in [('weight-only4',recipe.weight_only_wi4_afp32()),('dynamic4-block32',recipe.dynamic_wi4b32_afp32())]:
35
+ t=time.monotonic()
36
+ try:
37
+ q=quantizer.Quantizer(str(fp));q.load_quantization_recipe(rp)
38
+ target=out/f'{prefix}-{name}.tflite';q.quantize().export_model(str(target))
39
+ results.append(dict(name=name,path=str(target),bytes=target.stat().st_size,recipe=rp,seconds=time.monotonic()-t))
40
+ except Exception as e:results.append(dict(name=name,error=repr(e)))
41
+ (out/(prefix+'-build.json')).write_text(json.dumps(dict(converter='litert-torch 0.10.0',quantizer='ai-edge-quantizer 0.10.0',
42
+ input='Five static width signatures512/1024/2048/4096/8192; true widths input; zero right pad, never truncate',results=results),indent=2,default=str)+'\n')
43
+ print(json.dumps(results,default=str),flush=True)
44
+
45
+ if __name__=='__main__':main()
recipe/portable_model.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Algebraic pointwise-Conv -> Linear lowering; unchanged masked SE semantics."""
2
+ import copy
3
+ import torch
4
+ from torch import nn
5
+ from torch.nn import functional as F
6
+
7
+ def linear(conv):
8
+ layer = nn.Linear(conv.in_channels, conv.out_channels)
9
+ with torch.no_grad():
10
+ layer.weight.copy_(conv.weight[..., 0])
11
+ layer.bias.copy_(conv.bias)
12
+ return layer
13
+
14
+ class PortableBlock(nn.Module):
15
+ def __init__(self, block):
16
+ super().__init__()
17
+ self.dw = copy.deepcopy(block.dw)
18
+ for name in ['expand', 'fc1', 'fc2', 'project']:
19
+ setattr(self, name, linear(getattr(block, name)))
20
+
21
+ def forward(self, x, mask):
22
+ z = F.hardswish(self.expand(self.dw(x.transpose(1,2)).transpose(1,2)))
23
+ pool = (z * mask).sum(1, keepdim=True) / mask.sum(1, keepdim=True).clamp_min(1.)
24
+ gate = torch.sigmoid(self.fc2(F.relu(self.fc1(pool))))
25
+ return (x + self.project(z * gate)) * mask
26
+
27
+ class PortableRecognizer(nn.Module):
28
+ """Input widths are true unpadded widths; canvas must be a multiple of eight."""
29
+ def __init__(self, source):
30
+ super().__init__()
31
+ self.index_dtype = torch.int64
32
+ self.stem = copy.deepcopy(source.stem)
33
+ self.bn = copy.deepcopy(source.bn)
34
+ self.blocks = nn.ModuleList(PortableBlock(b) for b in source.blocks)
35
+ self.head = linear(source.head)
36
+
37
+ def forward(self, pixels, widths):
38
+ z = self.stem(F.pad(pixels, (0,4,0,0))).squeeze(2)
39
+ lengths = (widths + 3) // 4
40
+ mask = (torch.arange(z.shape[-1], device=z.device, dtype=self.index_dtype)[None,:] < lengths[:,None])[:,:,None].to(z.dtype)
41
+ z = F.hardswish(self.bn(z)).transpose(1,2) * mask
42
+ for block in self.blocks:
43
+ z = block(z, mask)
44
+ return self.head(z), lengths
45
+
46
+ class Compact(nn.Module):
47
+ def __init__(self, source):
48
+ super().__init__()
49
+ self.model = source
50
+ def forward(self, pixels, widths):
51
+ logits, lengths = self.model(pixels, widths)
52
+ confidence, tokens = logits.softmax(-1).max(-1)
53
+ return tokens.to(torch.int32), confidence, lengths.to(torch.int32)
recipe/requirements-litert.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch==2.10.0
2
+ numpy==1.26.4
3
+ litert-torch==0.10.0
4
+ ai-edge-quantizer==0.10.0
5
+ ai-edge-litert==2.3.0