asterocr 1.0.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.
Files changed (41) hide show
  1. asterocr-1.0.0/PKG-INFO +102 -0
  2. asterocr-1.0.0/README.md +70 -0
  3. asterocr-1.0.0/asterocr/__init__.py +12 -0
  4. asterocr-1.0.0/asterocr/cli.py +35 -0
  5. asterocr-1.0.0/asterocr/comparator.py +412 -0
  6. asterocr-1.0.0/asterocr/dataset/export_real_zip.py +117 -0
  7. asterocr-1.0.0/asterocr/dataset/loader.py +371 -0
  8. asterocr-1.0.0/asterocr/dataset/unified_packer.py +402 -0
  9. asterocr-1.0.0/asterocr/download_dataset.py +93 -0
  10. asterocr-1.0.0/asterocr/inference/decoder.py +302 -0
  11. asterocr-1.0.0/asterocr/inference/lexicon.py +136 -0
  12. asterocr-1.0.0/asterocr/inference/predictor.py +205 -0
  13. asterocr-1.0.0/asterocr/inference/viet_linguistics.py +340 -0
  14. asterocr-1.0.0/asterocr/kaggle_bridge/__init__.py +8 -0
  15. asterocr-1.0.0/asterocr/kaggle_bridge/chunker.py +252 -0
  16. asterocr-1.0.0/asterocr/kaggle_bridge/colab_auto_runner.py +380 -0
  17. asterocr-1.0.0/asterocr/kaggle_bridge/kaggle_worker.py +1911 -0
  18. asterocr-1.0.0/asterocr/kaggle_bridge/launcher.py +245 -0
  19. asterocr-1.0.0/asterocr/kaggle_bridge/tunnel.py +190 -0
  20. asterocr-1.0.0/asterocr/models/backbone.py +102 -0
  21. asterocr-1.0.0/asterocr/models/language.py +104 -0
  22. asterocr-1.0.0/asterocr/models/model.py +113 -0
  23. asterocr-1.0.0/asterocr/models/sequence.py +34 -0
  24. asterocr-1.0.0/asterocr/pipeline.py +179 -0
  25. asterocr-1.0.0/asterocr/studio.py +371 -0
  26. asterocr-1.0.0/asterocr/synthesizer/corpus.py +378 -0
  27. asterocr-1.0.0/asterocr/synthesizer/font_manager.py +170 -0
  28. asterocr-1.0.0/asterocr/synthesizer/generator.py +205 -0
  29. asterocr-1.0.0/asterocr/synthesizer/handwriting_generator.py +420 -0
  30. asterocr-1.0.0/asterocr/synthesizer/mega_generator.py +527 -0
  31. asterocr-1.0.0/asterocr/trainer/metrics.py +53 -0
  32. asterocr-1.0.0/asterocr/trainer/trainer.py +1295 -0
  33. asterocr-1.0.0/asterocr/vocab.py +175 -0
  34. asterocr-1.0.0/asterocr.egg-info/PKG-INFO +102 -0
  35. asterocr-1.0.0/asterocr.egg-info/SOURCES.txt +39 -0
  36. asterocr-1.0.0/asterocr.egg-info/dependency_links.txt +1 -0
  37. asterocr-1.0.0/asterocr.egg-info/entry_points.txt +2 -0
  38. asterocr-1.0.0/asterocr.egg-info/requires.txt +11 -0
  39. asterocr-1.0.0/asterocr.egg-info/top_level.txt +2 -0
  40. asterocr-1.0.0/pyproject.toml +54 -0
  41. asterocr-1.0.0/setup.cfg +4 -0
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: asterocr
3
+ Version: 1.0.0
4
+ Summary: AsterOCR: High-Performance Non-Autoregressive Vietnamese Optical Character Recognition Engine
5
+ Author-email: asterxlabs <contact@asterxlabs.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://huggingface.co/asterxlabs/asternet-vietnamese-ocr
8
+ Project-URL: Bug Tracker, https://github.com/asterxlabs/asterOCR/issues
9
+ Project-URL: Demo Space, https://huggingface.co/spaces/asterxlabs/asternet-vietnamese-ocr-demo
10
+ Keywords: ocr,vietnamese,document-ai,text-recognition,onnx,deep-learning
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: numpy>=1.20.0
24
+ Requires-Dist: Pillow>=8.0.0
25
+ Requires-Dist: onnxruntime>=1.14.0
26
+ Requires-Dist: huggingface_hub>=0.19.0
27
+ Provides-Extra: torch
28
+ Requires-Dist: torch>=2.0.0; extra == "torch"
29
+ Provides-Extra: all
30
+ Requires-Dist: torch>=2.0.0; extra == "all"
31
+ Requires-Dist: gradio>=4.0.0; extra == "all"
32
+
33
+ # AsterOCR 🇻🇳
34
+
35
+ **AsterOCR** is a state-of-the-art, ultra-fast neural Vietnamese Optical Character Recognition (OCR) engine. Designed as a modern, non-autoregressive replacement for legacy OCR systems like VietOCR, AsterOCR provides sub-60ms recognition speeds and exceptional accuracy across administrative records (CCCD, Passports, Driver's Licenses), invoices, financial receipts, and degraded smartphone captures.
36
+
37
+ [![PyPI Version](https://img.shields.io/badge/pypi-v1.0.0-blue.svg)](https://pypi.org/project/asterocr/)
38
+ [![Hugging Face Model](https://img.shields.io/badge/Hugging%20Face-Model%20Hub-yellow.svg)](https://huggingface.co/asterxlabs/asternet-vietnamese-ocr)
39
+ [![Live Demo](https://img.shields.io/badge/Gradio-Live%20Demo-orange.svg)](https://huggingface.co/spaces/asterxlabs/asternet-vietnamese-ocr-demo)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
41
+
42
+ ---
43
+
44
+ ## ⚡ Quickstart (Python)
45
+
46
+ ### 1. Installation
47
+
48
+ ```bash
49
+ pip install asterocr
50
+ ```
51
+
52
+ *(No heavy PyTorch installation required! Uses high-performance ONNX Runtime by default.)*
53
+
54
+ ### 2. Python API
55
+
56
+ ```python
57
+ from asterocr import AsterOCR
58
+
59
+ # Initialize engine (automatically downloads weights on first run)
60
+ ocr = AsterOCR()
61
+
62
+ # Recognize text from file path, PIL Image, or bytes
63
+ text = ocr.predict("path/to/invoice.jpg")
64
+ print(text)
65
+ # Output: CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM
66
+ ```
67
+
68
+ ### 3. Command Line Interface (CLI)
69
+
70
+ ```bash
71
+ asterocr path/to/image.jpg
72
+ ```
73
+
74
+ ---
75
+
76
+ ## 🌐 Cross-Platform Distribution Ecosystem
77
+
78
+ AsterOCR models are exported in universal **ONNX format** (~40MB) hosted on Hugging Face Hub:
79
+
80
+ | Platform | Package | Engine | Status |
81
+ | :--- | :--- | :--- | :--- |
82
+ | **Python** | `asterocr` | `onnxruntime` / `torch` | ✅ Ready (PyPI) |
83
+ | **Node.js** | `@asterxlabs/asterocr` | `onnxruntime-node` | ✅ Ready (npm) |
84
+ | **Flutter** | `asterocr_flutter` | `onnxruntime_flutter` | ✅ Ready (pub.dev) |
85
+ | **Hugging Face** | `asterxlabs/asternet-vietnamese-ocr` | Model Hub & Space Demo | ✅ Active |
86
+
87
+ ---
88
+
89
+ ## 📊 Benchmarks vs Industry Standard
90
+
91
+ | Metric | VietOCR (Transformer) | AsterOCR v1.0 | Improvement |
92
+ | :--- | :---: | :---: | :---: |
93
+ | **Exact Match (EM)** | 71.4% | **86.0%** | **+14.6%** |
94
+ | **Character Error Rate (CER)** | 7.82% | **3.45%** | **-55.8%** |
95
+ | **CPU Inference Latency** | 340ms | **60.2ms** | **5.6x faster** |
96
+ | **Model Size** | 145 MB | **40.76 MB** | **72% smaller** |
97
+
98
+ ---
99
+
100
+ ## 📜 License
101
+
102
+ MIT License © 2026 asterxlabs.
@@ -0,0 +1,70 @@
1
+ # AsterOCR 🇻🇳
2
+
3
+ **AsterOCR** is a state-of-the-art, ultra-fast neural Vietnamese Optical Character Recognition (OCR) engine. Designed as a modern, non-autoregressive replacement for legacy OCR systems like VietOCR, AsterOCR provides sub-60ms recognition speeds and exceptional accuracy across administrative records (CCCD, Passports, Driver's Licenses), invoices, financial receipts, and degraded smartphone captures.
4
+
5
+ [![PyPI Version](https://img.shields.io/badge/pypi-v1.0.0-blue.svg)](https://pypi.org/project/asterocr/)
6
+ [![Hugging Face Model](https://img.shields.io/badge/Hugging%20Face-Model%20Hub-yellow.svg)](https://huggingface.co/asterxlabs/asternet-vietnamese-ocr)
7
+ [![Live Demo](https://img.shields.io/badge/Gradio-Live%20Demo-orange.svg)](https://huggingface.co/spaces/asterxlabs/asternet-vietnamese-ocr-demo)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ---
11
+
12
+ ## ⚡ Quickstart (Python)
13
+
14
+ ### 1. Installation
15
+
16
+ ```bash
17
+ pip install asterocr
18
+ ```
19
+
20
+ *(No heavy PyTorch installation required! Uses high-performance ONNX Runtime by default.)*
21
+
22
+ ### 2. Python API
23
+
24
+ ```python
25
+ from asterocr import AsterOCR
26
+
27
+ # Initialize engine (automatically downloads weights on first run)
28
+ ocr = AsterOCR()
29
+
30
+ # Recognize text from file path, PIL Image, or bytes
31
+ text = ocr.predict("path/to/invoice.jpg")
32
+ print(text)
33
+ # Output: CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM
34
+ ```
35
+
36
+ ### 3. Command Line Interface (CLI)
37
+
38
+ ```bash
39
+ asterocr path/to/image.jpg
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 🌐 Cross-Platform Distribution Ecosystem
45
+
46
+ AsterOCR models are exported in universal **ONNX format** (~40MB) hosted on Hugging Face Hub:
47
+
48
+ | Platform | Package | Engine | Status |
49
+ | :--- | :--- | :--- | :--- |
50
+ | **Python** | `asterocr` | `onnxruntime` / `torch` | ✅ Ready (PyPI) |
51
+ | **Node.js** | `@asterxlabs/asterocr` | `onnxruntime-node` | ✅ Ready (npm) |
52
+ | **Flutter** | `asterocr_flutter` | `onnxruntime_flutter` | ✅ Ready (pub.dev) |
53
+ | **Hugging Face** | `asterxlabs/asternet-vietnamese-ocr` | Model Hub & Space Demo | ✅ Active |
54
+
55
+ ---
56
+
57
+ ## 📊 Benchmarks vs Industry Standard
58
+
59
+ | Metric | VietOCR (Transformer) | AsterOCR v1.0 | Improvement |
60
+ | :--- | :---: | :---: | :---: |
61
+ | **Exact Match (EM)** | 71.4% | **86.0%** | **+14.6%** |
62
+ | **Character Error Rate (CER)** | 7.82% | **3.45%** | **-55.8%** |
63
+ | **CPU Inference Latency** | 340ms | **60.2ms** | **5.6x faster** |
64
+ | **Model Size** | 145 MB | **40.76 MB** | **72% smaller** |
65
+
66
+ ---
67
+
68
+ ## 📜 License
69
+
70
+ MIT License © 2026 asterxlabs.
@@ -0,0 +1,12 @@
1
+ """
2
+ AsterOCR - Modern High-Performance Vietnamese OCR Engine
3
+ Designed to replace VietOCR with non-autoregressive CTC sequence modeling,
4
+ dynamic synthetic data generation, and sub-second recognition.
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+
9
+ from asterocr.pipeline import AsterOCR
10
+ from asterocr.vocab import default_vocab
11
+
12
+ __all__ = ["AsterOCR", "default_vocab", "__version__"]
@@ -0,0 +1,35 @@
1
+ """
2
+ Command Line Interface for AsterOCR
3
+ Usage:
4
+ asterocr path/to/image.jpg
5
+ asterocr path/to/image.jpg --backend onnx
6
+ """
7
+
8
+ import sys
9
+ import argparse
10
+ import os
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(
14
+ prog="asterocr",
15
+ description="AsterOCR: Ultra-fast Vietnamese Text Recognition CLI"
16
+ )
17
+ parser.add_argument("image", help="Path to input image file (jpg, png, webp)")
18
+ parser.add_argument("--backend", default="onnx", choices=["onnx", "torch"], help="Inference backend (default: onnx)")
19
+ parser.add_argument("--device", default="cpu", help="Computation device (cpu or cuda)")
20
+ parser.add_argument("--model-path", default=None, help="Explicit path to asternet.onnx or asternet.pth")
21
+ parser.add_argument("--version", action="version", version="AsterOCR v1.0.0")
22
+
23
+ args = parser.parse_args()
24
+
25
+ if not os.path.exists(args.image):
26
+ print(f"[-] Lỗi: File ảnh không tồn tại: {args.image}", file=sys.stderr)
27
+ sys.exit(1)
28
+
29
+ from asterocr.pipeline import AsterOCR
30
+ ocr = AsterOCR(model_path=args.model_path, backend=args.backend, device=args.device)
31
+ result = ocr.predict(args.image)
32
+ print(result)
33
+
34
+ if __name__ == "__main__":
35
+ main()
@@ -0,0 +1,412 @@
1
+ """
2
+ AsterOCR vs VietOCR Comparison Engine
3
+ Provides simultaneous side-by-side inference, precision comparison,
4
+ latency benchmark, and full page document parsing.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import time
10
+ import io
11
+ import json
12
+ import base64
13
+ import torch
14
+ import cv2
15
+ import numpy as np
16
+ from PIL import Image
17
+
18
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
19
+
20
+ from asterocr.inference.predictor import AsterPredictor
21
+ from asterocr.trainer.metrics import calculate_cer, calculate_wer, levenshtein_distance
22
+ from vietocr.tool.config import Cfg
23
+ from vietocr.tool.predictor import Predictor
24
+ from rapidocr_onnxruntime import RapidOCR
25
+
26
+ class OCRComparator:
27
+ _instance = None
28
+
29
+ @classmethod
30
+ def get_instance(cls):
31
+ if cls._instance is None:
32
+ cls._instance = OCRComparator()
33
+ return cls._instance
34
+
35
+ def __init__(self):
36
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
37
+ print(f"[*] OCRComparator khởi tạo trên thiết bị: {self.device}")
38
+
39
+ # 1. AsterNet SOTA Predictor
40
+ model_path = os.path.join(os.path.dirname(__file__), "..", "models", "asternet_best.pth")
41
+ if not os.path.exists(model_path):
42
+ model_path = os.path.join("models", "asternet_best.pth")
43
+ self.model_path = os.path.abspath(model_path)
44
+ self.model_mtime = os.path.getmtime(self.model_path) if os.path.exists(self.model_path) else 0
45
+ self.aster_pred = AsterPredictor(model_path=self.model_path, device=self.device)
46
+ print(f"[+] AsterNet nạp thành công từ: {self.model_path}")
47
+
48
+ # 2. VietOCR Predictor
49
+ config = Cfg.load_config_from_name('vgg_transformer')
50
+ config['device'] = self.device
51
+ config['predictor']['beamsearch'] = False
52
+ self.viet_pred = Predictor(config)
53
+ print("[+] VietOCR (vgg_transformer) nạp thành công!")
54
+
55
+ # 3. Detector cho Full-page document
56
+ try:
57
+ self.detector = RapidOCR(use_cls=False, use_rec=False)
58
+ print("[+] RapidOCR text line detector sẵn sàng!")
59
+ except Exception as e:
60
+ print(f"[!] Cảnh báo khởi tạo RapidOCR: {e}")
61
+ self.detector = None
62
+
63
+ # Benchmark running state
64
+ self.benchmark_state = {
65
+ "status": "idle",
66
+ "progress": 0,
67
+ "total": 0,
68
+ "current": 0,
69
+ "dataset_name": "",
70
+ "metrics": {},
71
+ "results": [],
72
+ "error": None
73
+ }
74
+
75
+ def check_and_reload(self):
76
+ """Hot-reloads AsterNet if weights file has been updated by training."""
77
+ if os.path.exists(self.model_path):
78
+ cur_mtime = os.path.getmtime(self.model_path)
79
+ if cur_mtime > self.model_mtime:
80
+ try:
81
+ print(f"[*] Phát hiện weights mới ({time.ctime(cur_mtime)}), đang tự động nạp lại AsterNet...")
82
+ from asterocr.inference.predictor import AsterPredictor
83
+ self.aster_pred = AsterPredictor(model_path=self.model_path, device=self.device)
84
+ self.model_mtime = cur_mtime
85
+ print("[+] Tự động nạp lại AsterNet thành công!")
86
+ except Exception as e:
87
+ print(f"[!] Lỗi khi nạp lại AsterNet: {e}")
88
+
89
+ def compare_crop(self, pil_img, gt_text=None):
90
+ """
91
+ Runs both models on a single line crop and returns side-by-side metrics.
92
+ """
93
+ self.check_and_reload()
94
+ # Warmup/ensure RGB
95
+ if pil_img.mode != "RGB":
96
+ pil_img = pil_img.convert("RGB")
97
+
98
+ # AsterNet inference
99
+ t0 = time.perf_counter()
100
+ aster_text = self.aster_pred.predict(pil_img)
101
+ aster_lat = (time.perf_counter() - t0) * 1000
102
+
103
+ # VietOCR inference
104
+ t0 = time.perf_counter()
105
+ viet_text = self.viet_pred.predict(pil_img)
106
+ viet_lat = (time.perf_counter() - t0) * 1000
107
+
108
+ speedup = viet_lat / max(0.001, aster_lat)
109
+
110
+ res = {
111
+ "aster": {
112
+ "text": aster_text,
113
+ "latency_ms": round(aster_lat, 2),
114
+ "fps": round(1000.0 / max(1.0, aster_lat), 1)
115
+ },
116
+ "vietocr": {
117
+ "text": viet_text,
118
+ "latency_ms": round(viet_lat, 2),
119
+ "fps": round(1000.0 / max(1.0, viet_lat), 1)
120
+ },
121
+ "speedup": round(speedup, 2)
122
+ }
123
+
124
+ if gt_text is not None and len(gt_text.strip()) > 0:
125
+ gt = gt_text.strip()
126
+ aster_cer = calculate_cer([gt], [aster_text]) * 100
127
+ aster_wer = calculate_wer([gt], [aster_text]) * 100
128
+ viet_cer = calculate_cer([gt], [viet_text]) * 100
129
+ viet_wer = calculate_wer([gt], [viet_text]) * 100
130
+
131
+ res["gt"] = gt
132
+ res["aster"]["cer"] = round(aster_cer, 2)
133
+ res["aster"]["wer"] = round(aster_wer, 2)
134
+ res["aster"]["match"] = (aster_text.strip() == gt)
135
+
136
+ res["vietocr"]["cer"] = round(viet_cer, 2)
137
+ res["vietocr"]["wer"] = round(viet_wer, 2)
138
+ res["vietocr"]["match"] = (viet_text.strip() == gt)
139
+
140
+ if aster_cer < viet_cer:
141
+ winner = "asternet"
142
+ elif viet_cer < aster_cer:
143
+ winner = "vietocr"
144
+ else:
145
+ winner = "draw"
146
+ res["accuracy_winner"] = winner
147
+
148
+ return res
149
+
150
+ def compare_document_page(self, np_img_bgr):
151
+ """
152
+ Detects all text lines on a document page and recognizes with both models.
153
+ Returns visual annotated image base64 and line-by-line comparison data.
154
+ """
155
+ self.check_and_reload()
156
+ if self.detector is None:
157
+ return {"error": "RapidOCR detector không khả dụng."}
158
+
159
+ boxes, _ = self.detector(np_img_bgr)
160
+ if not boxes:
161
+ return {"lines": [], "annotated_image": None, "total_lines": 0}
162
+
163
+ h_img, w_img = np_img_bgr.shape[:2]
164
+ lines_info = []
165
+ pil_crops = []
166
+ box_coords = []
167
+
168
+ # Sort boxes top to bottom
169
+ sorted_boxes = sorted(boxes, key=lambda b: np.min(np.array(b[0] if isinstance(b[0], list) and len(b) == 2 else b)[:, 1]))
170
+
171
+ for idx, b in enumerate(sorted_boxes):
172
+ pts = np.array(b[0] if isinstance(b[0], list) and len(b) == 2 else b)
173
+ x_min, y_min = np.min(pts, axis=0)
174
+ x_max, y_max = np.max(pts, axis=0)
175
+
176
+ pad = 2
177
+ x1 = max(0, int(x_min) - pad)
178
+ y1 = max(0, int(y_min) - pad)
179
+ x2 = min(w_img, int(x_max) + pad)
180
+ y2 = min(h_img, int(y_max) + pad)
181
+
182
+ if y2 - y1 < 6 or x2 - x1 < 6:
183
+ continue
184
+
185
+ crop = np_img_bgr[y1:y2, x1:x2]
186
+ if crop.size == 0:
187
+ continue
188
+
189
+ crop_pil = Image.fromarray(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
190
+ pil_crops.append(crop_pil)
191
+ box_coords.append((x1, y1, x2, y2))
192
+
193
+ # Annotate image
194
+ annotated = np_img_bgr.copy()
195
+
196
+ # Batch inference
197
+ t0 = time.perf_counter()
198
+ aster_preds = [self.aster_pred.predict(c) for c in pil_crops]
199
+ aster_total_time = (time.perf_counter() - t0) * 1000
200
+
201
+ t0 = time.perf_counter()
202
+ viet_preds = [self.viet_pred.predict(c) for c in pil_crops]
203
+ viet_total_time = (time.perf_counter() - t0) * 1000
204
+
205
+ for i, (coords, ap, vp) in enumerate(zip(box_coords, aster_preds, viet_preds)):
206
+ x1, y1, x2, y2 = coords
207
+ # Draw bbox: Cyan for line
208
+ cv2.rectangle(annotated, (x1, y1), (x2, y2), (0, 255, 255), 1)
209
+ cv2.putText(annotated, str(i + 1), (x1, max(12, y1 - 2)), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1)
210
+
211
+ # Crop thumbnail base64
212
+ crop_rgb = pil_crops[i]
213
+ buf = io.BytesIO()
214
+ crop_rgb.save(buf, format="JPEG", quality=85)
215
+ crop_b64 = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
216
+
217
+ lines_info.append({
218
+ "line_id": i + 1,
219
+ "bbox": [x1, y1, x2, y2],
220
+ "crop_thumbnail": crop_b64,
221
+ "aster_text": ap,
222
+ "viet_text": vp,
223
+ "is_identical": (ap.strip() == vp.strip())
224
+ })
225
+
226
+ # Encode annotated image
227
+ _, enc_jpg = cv2.imencode(".jpg", annotated, [cv2.IMWRITE_JPEG_QUALITY, 80])
228
+ annotated_b64 = "data:image/jpeg;base64," + base64.b64encode(enc_jpg).decode()
229
+
230
+ return {
231
+ "total_lines": len(lines_info),
232
+ "aster_total_ms": round(aster_total_time, 2),
233
+ "viet_total_ms": round(viet_total_time, 2),
234
+ "speedup": round(viet_total_time / max(0.001, aster_total_time), 2),
235
+ "lines": lines_info,
236
+ "annotated_image": annotated_b64
237
+ }
238
+
239
+ def start_batch_benchmark(self, dataset_name, max_samples=100):
240
+ """
241
+ Runs benchmark in a background thread and updates self.benchmark_state.
242
+ """
243
+ import threading
244
+ if self.benchmark_state["status"] == "running":
245
+ return False, "Benchmark đang chạy. Hãy đợi hoặc dừng phiên hiện tại."
246
+
247
+ self.benchmark_state = {
248
+ "status": "running",
249
+ "progress": 0,
250
+ "total": 0,
251
+ "current": 0,
252
+ "dataset_name": dataset_name,
253
+ "metrics": {},
254
+ "results": [],
255
+ "error": None
256
+ }
257
+
258
+ t = threading.Thread(target=self._run_benchmark_worker, args=(dataset_name, max_samples), daemon=True)
259
+ t.start()
260
+ return True, "Benchmark đã bắt đầu."
261
+
262
+ def _run_benchmark_worker(self, dataset_name, max_samples):
263
+ try:
264
+ # Hot-reload linguistics and weights before running benchmark
265
+ try:
266
+ import importlib
267
+ import asterocr.inference.viet_linguistics as vl
268
+ importlib.reload(vl)
269
+ self.check_and_reload()
270
+ except Exception as e:
271
+ print(f"[!] Warning reloading modules: {e}")
272
+
273
+ samples = []
274
+ if dataset_name == "mega_test":
275
+ labels_file = os.path.join("dataset_mega_test", "labels.json")
276
+ if os.path.exists(labels_file):
277
+ with open(labels_file, "r", encoding="utf-8") as f:
278
+ meta = json.load(f)
279
+ for item in meta:
280
+ img_p = os.path.join("dataset_mega_test", item["image_path"])
281
+ if os.path.exists(img_p):
282
+ img = Image.open(img_p).convert("RGB")
283
+ samples.append((img, item["text"], os.path.basename(img_p)))
284
+ elif dataset_name in ["real_scans", "unified", "viet_real"]:
285
+ if dataset_name == "viet_real":
286
+ lmdb_path = "dataset_viet_real.lmdb"
287
+ elif dataset_name == "unified":
288
+ lmdb_path = "dataset_unified.lmdb"
289
+ else:
290
+ lmdb_path = "dataset_real_scans.lmdb"
291
+ if os.path.exists(lmdb_path):
292
+ import lmdb
293
+ env = lmdb.open(lmdb_path, readonly=True, max_readers=1)
294
+ with env.begin() as txn:
295
+ num_val = txn.get(b'num-samples')
296
+ total_db = int(num_val.decode('utf-8')) if num_val else 0
297
+ step = max(1, total_db // max(1, max_samples))
298
+ for i in range(1, total_db, step):
299
+ if len(samples) >= max_samples:
300
+ break
301
+ img_key = f"image-{i:09d}".encode()
302
+ lbl_key = f"label-{i:09d}".encode()
303
+ img_b = txn.get(img_key)
304
+ lbl_b = txn.get(lbl_key)
305
+ if img_b and lbl_b:
306
+ try:
307
+ img = Image.open(io.BytesIO(img_b)).convert("RGB")
308
+ txt = lbl_b.decode('utf-8').strip()
309
+ if txt:
310
+ samples.append((img, txt, f"sample_{i:07d}.jpg"))
311
+ except Exception:
312
+ pass
313
+ env.close()
314
+
315
+ if not samples:
316
+ self.benchmark_state["status"] = "error"
317
+ self.benchmark_state["error"] = f"Không tìm thấy dữ liệu cho '{dataset_name}'."
318
+ return
319
+
320
+ self.benchmark_state["total"] = len(samples)
321
+ results = []
322
+ gt_all = []
323
+ aster_all = []
324
+ viet_all = []
325
+
326
+ t_aster_total = 0.0
327
+ t_viet_total = 0.0
328
+
329
+ for idx, (img, gt, name) in enumerate(samples):
330
+ # AsterNet
331
+ t0 = time.perf_counter()
332
+ ap = self.aster_pred.predict(img)
333
+ t_a = (time.perf_counter() - t0) * 1000
334
+ t_aster_total += t_a
335
+
336
+ # VietOCR
337
+ t0 = time.perf_counter()
338
+ vp = self.viet_pred.predict(img)
339
+ t_v = (time.perf_counter() - t0) * 1000
340
+ t_viet_total += t_v
341
+
342
+ gt_all.append(gt)
343
+ aster_all.append(ap)
344
+ viet_all.append(vp)
345
+
346
+ # Thumbnail
347
+ buf = io.BytesIO()
348
+ img.save(buf, format="JPEG", quality=80)
349
+ img_b64 = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
350
+
351
+ # Per-sample metrics
352
+ a_match = (ap.strip() == gt.strip())
353
+ v_match = (vp.strip() == gt.strip())
354
+
355
+ results.append({
356
+ "id": idx + 1,
357
+ "name": name,
358
+ "thumbnail": img_b64,
359
+ "gt": gt,
360
+ "aster_pred": ap,
361
+ "viet_pred": vp,
362
+ "aster_match": a_match,
363
+ "viet_match": v_match,
364
+ "aster_ms": round(t_a, 1),
365
+ "viet_ms": round(t_v, 1),
366
+ "aster_cer": round(calculate_cer([gt], [ap]) * 100, 1),
367
+ "viet_cer": round(calculate_cer([gt], [vp]) * 100, 1)
368
+ })
369
+
370
+ self.benchmark_state["current"] = idx + 1
371
+ self.benchmark_state["progress"] = round((idx + 1) / len(samples) * 100, 1)
372
+
373
+ # Final metrics
374
+ N = len(samples)
375
+ aster_cer = round(calculate_cer(gt_all, aster_all) * 100, 2)
376
+ aster_wer = round(calculate_wer(gt_all, aster_all) * 100, 2)
377
+ aster_exact = round(sum(1 for g, p in zip(gt_all, aster_all) if g.strip() == p.strip()) / N * 100, 2)
378
+ aster_fps = round(N / (t_aster_total / 1000.0), 1)
379
+ aster_avg_ms = round(t_aster_total / N, 2)
380
+
381
+ viet_cer = round(calculate_cer(gt_all, viet_all) * 100, 2)
382
+ viet_wer = round(calculate_wer(gt_all, viet_all) * 100, 2)
383
+ viet_exact = round(sum(1 for g, p in zip(gt_all, viet_all) if g.strip() == p.strip()) / N * 100, 2)
384
+ viet_fps = round(N / (t_viet_total / 1000.0), 1)
385
+ viet_avg_ms = round(t_viet_total / N, 2)
386
+
387
+ speedup = round(aster_fps / max(0.01, viet_fps), 2)
388
+
389
+ self.benchmark_state["metrics"] = {
390
+ "aster": {
391
+ "cer": aster_cer,
392
+ "wer": aster_wer,
393
+ "exact": aster_exact,
394
+ "fps": aster_fps,
395
+ "avg_ms": aster_avg_ms
396
+ },
397
+ "vietocr": {
398
+ "cer": viet_cer,
399
+ "wer": viet_wer,
400
+ "exact": viet_exact,
401
+ "fps": viet_fps,
402
+ "avg_ms": viet_avg_ms
403
+ },
404
+ "speedup": speedup,
405
+ "winner": "AsterNet" if aster_cer <= viet_cer else "VietOCR"
406
+ }
407
+ self.benchmark_state["results"] = results
408
+ self.benchmark_state["status"] = "finished"
409
+
410
+ except Exception as e:
411
+ self.benchmark_state["status"] = "error"
412
+ self.benchmark_state["error"] = str(e)