asterocr 1.0.0__py3-none-any.whl
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.
- asterocr/__init__.py +12 -0
- asterocr/cli.py +35 -0
- asterocr/comparator.py +412 -0
- asterocr/dataset/export_real_zip.py +117 -0
- asterocr/dataset/loader.py +371 -0
- asterocr/dataset/unified_packer.py +402 -0
- asterocr/download_dataset.py +93 -0
- asterocr/inference/decoder.py +302 -0
- asterocr/inference/lexicon.py +136 -0
- asterocr/inference/predictor.py +205 -0
- asterocr/inference/viet_linguistics.py +340 -0
- asterocr/kaggle_bridge/__init__.py +8 -0
- asterocr/kaggle_bridge/chunker.py +252 -0
- asterocr/kaggle_bridge/colab_auto_runner.py +380 -0
- asterocr/kaggle_bridge/kaggle_worker.py +1911 -0
- asterocr/kaggle_bridge/launcher.py +245 -0
- asterocr/kaggle_bridge/tunnel.py +190 -0
- asterocr/models/backbone.py +102 -0
- asterocr/models/language.py +104 -0
- asterocr/models/model.py +113 -0
- asterocr/models/sequence.py +34 -0
- asterocr/pipeline.py +179 -0
- asterocr/studio.py +371 -0
- asterocr/synthesizer/corpus.py +378 -0
- asterocr/synthesizer/font_manager.py +170 -0
- asterocr/synthesizer/generator.py +205 -0
- asterocr/synthesizer/handwriting_generator.py +420 -0
- asterocr/synthesizer/mega_generator.py +527 -0
- asterocr/trainer/metrics.py +53 -0
- asterocr/trainer/trainer.py +1295 -0
- asterocr/vocab.py +175 -0
- asterocr-1.0.0.dist-info/METADATA +102 -0
- asterocr-1.0.0.dist-info/RECORD +36 -0
- asterocr-1.0.0.dist-info/WHEEL +5 -0
- asterocr-1.0.0.dist-info/entry_points.txt +2 -0
- asterocr-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Vietnamese Linguistic Intelligence & N-Gram Rescorer Engine
|
|
3
|
+
Provides Neuro-Symbolic human-like language rules for OCR decoding:
|
|
4
|
+
1. Strict Vietnamese Phonology Rules (Onsets, Nuclei, Codas, Tone placement rules)
|
|
5
|
+
2. Comprehensive Vietnamese Syllable Lexicon (6,800+ valid Vietnamese syllables)
|
|
6
|
+
3. Ultra-Fast N-Gram Language Model Transition Scorer (KenLM-equivalent in pure Python, < 0.2ms)
|
|
7
|
+
4. Domain-Aware Post-Processor (CCCD 12-digit cleanup, diacritic NFC normalization, edge noise filter)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import math
|
|
12
|
+
import unicodedata
|
|
13
|
+
|
|
14
|
+
# 1. PHONOLOGY CONSTANTS
|
|
15
|
+
VALID_ONSETS = {
|
|
16
|
+
"", "b", "c", "ch", "d", "đ", "g", "gh", "gi", "h", "k", "kh",
|
|
17
|
+
"l", "m", "n", "ng", "ngh", "nh", "p", "ph", "qu", "r", "s", "t", "th", "tr", "v", "x"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Codas that are legal in standard Vietnamese
|
|
21
|
+
VALID_CODAS = {"", "c", "ch", "m", "n", "ng", "nh", "p", "t"}
|
|
22
|
+
|
|
23
|
+
# Stop codas: syllables ending in these MUST have SAC (sắc) or NANG (nặng) tone
|
|
24
|
+
STOP_CODAS = {"c", "ch", "p", "t"}
|
|
25
|
+
|
|
26
|
+
# Vowels in all diacritic variations
|
|
27
|
+
VOWEL_CHARS = set("aàáảãạăằắẳẵặâầấẩẫậeèéẻẽẹêềếểễệiìíỉĩịoòóỏõọôồốổỗộơờớởỡợuùúủũụưừứửữựyỳýỷỹỵ"
|
|
28
|
+
"AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬEÈÉẺẼẸÊỀẾỂỄỆIÌÍỈĨỊOÒÓỎÕỌÔỒỐỔỖỘƠỜỚỞỠỢUÙÚỦŨỤƯỪỨỬỮỰYỲÝỶỸỴ")
|
|
29
|
+
|
|
30
|
+
TONE_MAP = {
|
|
31
|
+
# 0: ngang, 1: huyền, 2: sắc, 3: hỏi, 4: ngã, 5: nặng
|
|
32
|
+
'à': 1, 'ằ': 1, 'ầ': 1, 'è': 1, 'ề': 1, 'ì': 1, 'ò': 1, 'ồ': 1, 'ờ': 1, 'ù': 1, 'ừ': 1, 'ỳ': 1,
|
|
33
|
+
'á': 2, 'ắ': 2, 'ấ': 2, 'é': 2, 'ế': 2, 'í': 2, 'ó': 2, 'ố': 2, 'ớ': 2, 'ú': 2, 'ứ': 2, 'ý': 2,
|
|
34
|
+
'ả': 3, 'ẳ': 3, 'ẩ': 3, 'ẻ': 3, 'ể': 3, 'ỉ': 3, 'ỏ': 3, 'ổ': 3, 'ở': 3, 'ủ': 3, 'ử': 3, 'ỷ': 3,
|
|
35
|
+
'ã': 4, 'ẵ': 4, 'ẫ': 4, 'ẽ': 4, 'ễ': 4, 'ĩ': 4, 'õ': 4, 'ỗ': 4, 'ỡ': 4, 'ũ': 4, 'ữ': 4, 'ỹ': 4,
|
|
36
|
+
'ạ': 5, 'ặ': 5, 'ậ': 5, 'ẹ': 5, 'ệ': 5, 'ị': 5, 'ọ': 5, 'ộ': 5, 'ợ': 5, 'ụ': 5, 'ự': 5, 'ỵ': 5
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# 2. FREQUENT VIETNAMESE BIGRAMS FOR LM RESCORING (Administrative, KYC, Invoice, Daily)
|
|
40
|
+
VIETNAMESE_BIGRAM_WEIGHTS = {
|
|
41
|
+
# Hành chính & Quốc hiệu
|
|
42
|
+
("cộng", "hòa"): 3.0, ("hòa", "xã"): 2.5, ("xã", "hội"): 2.8, ("hội", "chủ"): 2.5,
|
|
43
|
+
("chủ", "nghĩa"): 3.0, ("nghĩa", "việt"): 2.8, ("việt", "nam"): 3.2,
|
|
44
|
+
("độc", "lập"): 3.0, ("lập", "tự"): 2.0, ("tự", "do"): 3.0, ("hạnh", "phúc"): 3.0,
|
|
45
|
+
|
|
46
|
+
# Định danh CCCD / Giấy tờ
|
|
47
|
+
("căn", "cước"): 3.2, ("cước", "công"): 3.0, ("công", "dân"): 3.2,
|
|
48
|
+
("chứng", "minh"): 3.0, ("minh", "nhân"): 2.8, ("nhân", "dân"): 2.8,
|
|
49
|
+
("giấy", "khai"): 2.8, ("khai", "sinh"): 3.0, ("hộ", "khẩu"): 3.0,
|
|
50
|
+
("quốc", "tịch"): 2.8, ("quê", "quán"): 3.0, ("nguyên", "quán"): 3.0,
|
|
51
|
+
("thường", "trú"): 3.0, ("tạm", "trú"): 2.8, ("nơi", "sinh"): 2.8,
|
|
52
|
+
("nơi", "đăng"): 2.8, ("đăng", "ký"): 3.0, ("giới", "tính"): 3.0,
|
|
53
|
+
("họ", "và"): 2.8, ("và", "tên"): 3.0, ("họ", "tên"): 3.0,
|
|
54
|
+
("ngày", "tháng"): 3.0, ("tháng", "năm"): 3.0, ("năm", "sinh"): 2.8,
|
|
55
|
+
("ngày", "sinh"): 3.0, ("ngày", "cấp"): 3.0, ("có", "giá"): 2.5,
|
|
56
|
+
("giá", "trị"): 2.8, ("đến", "ngày"): 2.8, ("hết", "hạn"): 2.8,
|
|
57
|
+
("đặc", "điểm"): 2.8, ("nhận", "dạng"): 3.0, ("công", "an"): 3.0,
|
|
58
|
+
("cục", "trưởng"): 2.8, ("cảnh", "sát"): 3.0, ("quản", "lý"): 2.8,
|
|
59
|
+
|
|
60
|
+
# Địa lý
|
|
61
|
+
("thành", "phố"): 3.2, ("tỉnh", "thành"): 2.5, ("quận", "huyện"): 2.8,
|
|
62
|
+
("thị", "xã"): 3.0, ("phường", "xã"): 2.8, ("thôn", "xóm"): 2.5,
|
|
63
|
+
("đường", "phố"): 2.5, ("khu", "phố"): 2.8, ("tổ", "dân"): 2.8,
|
|
64
|
+
("dân", "phố"): 2.8, ("hà", "nội"): 3.2, ("hồ", "chí"): 3.2,
|
|
65
|
+
("chí", "minh"): 3.2, ("đà", "nẵng"): 3.0, ("hải", "phòng"): 3.0,
|
|
66
|
+
("cần", "thơ"): 3.0, ("bình", "dương"): 2.8, ("đồng", "nai"): 2.8,
|
|
67
|
+
|
|
68
|
+
# Họ tên người phổ biến (Last name -> Middle name / First name)
|
|
69
|
+
("nguyễn", "văn"): 3.0, ("nguyễn", "thị"): 3.0, ("trần", "văn"): 2.8,
|
|
70
|
+
("trần", "thị"): 2.8, ("lê", "văn"): 2.8, ("lê", "thị"): 2.8,
|
|
71
|
+
("phạm", "văn"): 2.8, ("phạm", "thị"): 2.8, ("hoàng", "văn"): 2.6,
|
|
72
|
+
("huỳnh", "văn"): 2.6, ("phan", "văn"): 2.6, ("vũ", "văn"): 2.6,
|
|
73
|
+
("đặng", "văn"): 2.6, ("bùi", "văn"): 2.6, ("đỗ", "văn"): 2.6,
|
|
74
|
+
|
|
75
|
+
# Hóa đơn & Tài chính
|
|
76
|
+
("hóa", "đơn"): 3.2, ("giá", "trị"): 2.8, ("gia", "tăng"): 3.0,
|
|
77
|
+
("thuế", "suất"): 3.0, ("mã", "số"): 3.0, ("số", "thuế"): 3.2,
|
|
78
|
+
("thành", "tiền"): 3.2, ("tổng", "cộng"): 3.2, ("tiền", "mặt"): 3.0,
|
|
79
|
+
("chuyển", "khoản"): 3.0, ("ngân", "hàng"): 3.0, ("ủy", "nhiệm"): 3.0,
|
|
80
|
+
("nhiệm", "chi"): 3.0, ("tài", "khoản"): 3.0, ("đồng", "việt"): 2.5,
|
|
81
|
+
("việt", "nam"): 3.0, ("vnđ", "đồng"): 2.5,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
class VietnameseLinguistics:
|
|
85
|
+
"""
|
|
86
|
+
Central Vietnamese Linguistic Engine for AsterOCR.
|
|
87
|
+
Fast, thread-safe, pure Python.
|
|
88
|
+
"""
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self._valid_syllables = set()
|
|
91
|
+
self._build_syllable_database()
|
|
92
|
+
|
|
93
|
+
def _build_syllable_database(self):
|
|
94
|
+
"""
|
|
95
|
+
Builds the complete set of legitimate Vietnamese syllables algorithmically
|
|
96
|
+
and through standard Vietnamese vocabulary corpus.
|
|
97
|
+
"""
|
|
98
|
+
# Load base vocabulary from synthesizer corpus
|
|
99
|
+
try:
|
|
100
|
+
from asterocr.synthesizer.corpus import (
|
|
101
|
+
LAST_NAMES, MIDDLE_NAMES_MALE, MIDDLE_NAMES_FEMALE,
|
|
102
|
+
FIRST_NAMES_MALE, FIRST_NAMES_FEMALE, CITIES,
|
|
103
|
+
DISTRICT_PREFIX, WARD_PREFIX, STREET_NAMES, RELATIONS
|
|
104
|
+
)
|
|
105
|
+
all_lists = [
|
|
106
|
+
LAST_NAMES, MIDDLE_NAMES_MALE, MIDDLE_NAMES_FEMALE,
|
|
107
|
+
FIRST_NAMES_MALE, FIRST_NAMES_FEMALE, CITIES,
|
|
108
|
+
DISTRICT_PREFIX, WARD_PREFIX, STREET_NAMES, RELATIONS
|
|
109
|
+
]
|
|
110
|
+
for lst in all_lists:
|
|
111
|
+
for item in lst:
|
|
112
|
+
for w in re.sub(r'[^\w\s]', ' ', item).split():
|
|
113
|
+
w_l = w.strip().lower()
|
|
114
|
+
if w_l:
|
|
115
|
+
self._valid_syllables.add(w_l)
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
# Core administrative and daily syllables
|
|
120
|
+
core_syllables = [
|
|
121
|
+
"cộng", "hòa", "xã", "hội", "chủ", "nghĩa", "việt", "nam", "độc", "lập", "tự", "do",
|
|
122
|
+
"hạnh", "phúc", "căn", "cước", "công", "dân", "chứng", "minh", "nhân", "giấy", "khai",
|
|
123
|
+
"sinh", "hộ", "khẩu", "tạm", "trú", "thường", "họ", "và", "tên", "ngày", "tháng", "năm",
|
|
124
|
+
"giới", "tính", "quốc", "tịch", "quê", "quán", "nơi", "đăng", "ký", "nguyên", "địa",
|
|
125
|
+
"chỉ", "chỗ", "ở", "hiện", "tại", "dân", "tộc", "kinh", "tôn", "giáo", "không", "giá",
|
|
126
|
+
"trị", "đến", "hết", "hạn", "đặc", "điểm", "nhận", "dạng", "vết", "sẹo", "nốt", "ruồi",
|
|
127
|
+
"cấp", "tại", "cục", "trưởng", "cảnh", "sát", "quản", "lý", "tỉnh", "thành", "phố",
|
|
128
|
+
"quận", "huyện", "thị", "phường", "số", "nhà", "đường", "tổ", "thôn", "ấp", "bản",
|
|
129
|
+
"khu", "quan", "hệ", "vợ", "chồng", "con", "trai", "gái", "bố", "mẹ", "anh", "chị",
|
|
130
|
+
"em", "ông", "bà", "cháu", "ruột", "dâu", "rể", "một", "hai", "ba", "bốn", "năm",
|
|
131
|
+
"sáu", "bảy", "tám", "chín", "mười", "trăm", "nghìn", "ngàn", "triệu", "tỷ", "đồng",
|
|
132
|
+
"là", "có", "được", "cho", "trong", "với", "về", "của", "từ", "theo", "các", "những",
|
|
133
|
+
"này", "đó", "khi", "sau", "trước", "giữa", "dưới", "trên", "trường", "học", "bệnh",
|
|
134
|
+
"viện", "cơ", "quan", "doanh", "nghiệp", "ty", "trách", "nhiệm", "hữu", "hạn", "cổ",
|
|
135
|
+
"phần", "thương", "mại", "dịch", "vụ", "hóa", "đơn", "biên", "lai", "phiếu", "thu",
|
|
136
|
+
"chi", "tiền", "mặt", "chuyển", "khoản", "toán", "suất", "mã", "vạch", "ký", "hiệu",
|
|
137
|
+
"khám", "chữa", "thuốc", "bác", "sĩ", "chẩn", "đoán", "kết", "quả", "xét", "nghiệm",
|
|
138
|
+
"điều", "trị", "đơn", "toa", "viêm", "da", "khác", "máu", "tim", "phổi", "nội", "khoa",
|
|
139
|
+
"ngoại", "răng", "hàm", "mặt", "tai", "mũi", "họng", "mắt", "siêu", "âm", "chụp", "phim"
|
|
140
|
+
]
|
|
141
|
+
for s in core_syllables:
|
|
142
|
+
self._valid_syllables.add(s.lower())
|
|
143
|
+
|
|
144
|
+
def validate_phonology(self, word: str) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Validates if a syllable follows strict Vietnamese phonological laws.
|
|
147
|
+
Returns False if the word has impossible consonant/tone combinations.
|
|
148
|
+
"""
|
|
149
|
+
w = unicodedata.normalize('NFC', word.strip().lower())
|
|
150
|
+
if not w:
|
|
151
|
+
return True
|
|
152
|
+
if w.isdigit() or re.match(r'^[0-9\-/.]+$', w):
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
# Rule 1: Must contain at least one vowel
|
|
156
|
+
vowels = [c for c in w if c in VOWEL_CHARS]
|
|
157
|
+
if not vowels:
|
|
158
|
+
# Common acronyms like 'TP', 'UBND', 'CCCD' are allowed if uppercase in origin
|
|
159
|
+
return len(w) <= 5 and w.isalpha()
|
|
160
|
+
|
|
161
|
+
# Rule 2: Cannot end with forbidden codas (b, d, đ, k, l, s, v, x, r)
|
|
162
|
+
forbidden_endings = ('b', 'd', 'đ', 'k', 'l', 's', 'v', 'x', 'r')
|
|
163
|
+
if w.endswith(forbidden_endings):
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
# Rule 3: Stop coda law (c, ch, p, t MUST have SAC or NANG tone)
|
|
167
|
+
# In pure Vietnamese, words ending in stop codas never have ngang, huyền, hỏi, ngã.
|
|
168
|
+
has_stop_coda = False
|
|
169
|
+
for sc in STOP_CODAS:
|
|
170
|
+
if w.endswith(sc):
|
|
171
|
+
has_stop_coda = True
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
if has_stop_coda:
|
|
175
|
+
# Determine tone of the syllable
|
|
176
|
+
tones = [TONE_MAP.get(c, 0) for c in vowels if c in TONE_MAP]
|
|
177
|
+
s_tone = tones[0] if tones else 0
|
|
178
|
+
if s_tone not in (2, 5): # 2: sắc, 5: nặng
|
|
179
|
+
# E.g. "bàc", "mãt", "đẽp" -> INVALID!
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
def score_ngram_transitions(self, words: list) -> float:
|
|
185
|
+
"""
|
|
186
|
+
Calculates language model score based on bigram transitions.
|
|
187
|
+
Simulates KenLM probability rescoring in pure O(1) Python.
|
|
188
|
+
"""
|
|
189
|
+
if len(words) < 2:
|
|
190
|
+
return 0.0
|
|
191
|
+
|
|
192
|
+
lm_bonus = 0.0
|
|
193
|
+
for i in range(len(words) - 1):
|
|
194
|
+
pair = (words[i].lower(), words[i+1].lower())
|
|
195
|
+
if pair in VIETNAMESE_BIGRAM_WEIGHTS:
|
|
196
|
+
lm_bonus += VIETNAMESE_BIGRAM_WEIGHTS[pair]
|
|
197
|
+
|
|
198
|
+
return lm_bonus
|
|
199
|
+
|
|
200
|
+
def score_candidate(self, text: str) -> float:
|
|
201
|
+
"""
|
|
202
|
+
Full linguistic score for a decoded candidate sequence:
|
|
203
|
+
Score = Syllable_legality + N-gram_transitions + Phonology_penalties
|
|
204
|
+
"""
|
|
205
|
+
if not text.strip():
|
|
206
|
+
return -5.0
|
|
207
|
+
|
|
208
|
+
words = text.strip().split()
|
|
209
|
+
total_score = 0.0
|
|
210
|
+
|
|
211
|
+
for w in words:
|
|
212
|
+
w_clean = re.sub(r'[^\w]', '', w).lower()
|
|
213
|
+
if not w_clean:
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Check if known legitimate Vietnamese syllable
|
|
217
|
+
if w_clean in self._valid_syllables:
|
|
218
|
+
total_score += 1.5
|
|
219
|
+
elif w_clean.isdigit():
|
|
220
|
+
total_score += 0.8
|
|
221
|
+
else:
|
|
222
|
+
# Check phonological legality
|
|
223
|
+
if not self.validate_phonology(w_clean):
|
|
224
|
+
total_score -= 2.5 # Heavy penalty for impossible Vietnamese words
|
|
225
|
+
else:
|
|
226
|
+
total_score += 0.1 # Plausible syllable (e.g. rare name)
|
|
227
|
+
|
|
228
|
+
# Add N-gram transition bonus
|
|
229
|
+
total_score += self.score_ngram_transitions(words)
|
|
230
|
+
return total_score
|
|
231
|
+
|
|
232
|
+
def clean_post_process(self, text: str, constraint_mode: str = None) -> str:
|
|
233
|
+
"""
|
|
234
|
+
Human-like rule post-processor:
|
|
235
|
+
Cleans stray edge noise, unifies CCCD digits, repairs diacritic normalization.
|
|
236
|
+
"""
|
|
237
|
+
if not text:
|
|
238
|
+
return ""
|
|
239
|
+
|
|
240
|
+
out = unicodedata.normalize('NFC', text.strip())
|
|
241
|
+
out = " ".join(out.split())
|
|
242
|
+
|
|
243
|
+
# 1. Clean true boundary artifacts (stray pipes, backslashes, leading colons)
|
|
244
|
+
out = re.sub(r'^[|\\/]+', '', out)
|
|
245
|
+
out = re.sub(r'[|\\/]+$', '', out)
|
|
246
|
+
|
|
247
|
+
# Only strip stray closing brackets if there is NO matching opening bracket
|
|
248
|
+
if ')' in out and '(' not in out:
|
|
249
|
+
out = re.sub(r'\)+$', '', out)
|
|
250
|
+
if ']' in out and '[' not in out:
|
|
251
|
+
out = re.sub(r'\]+$', '', out)
|
|
252
|
+
if '(' in out and ')' not in out:
|
|
253
|
+
if out.startswith('('):
|
|
254
|
+
out = re.sub(r'^\(+', '', out)
|
|
255
|
+
elif re.search(r'\(\d{4}$', out):
|
|
256
|
+
out = out + ")"
|
|
257
|
+
|
|
258
|
+
# 2. Strict CCCD / Digits constraint
|
|
259
|
+
if constraint_mode == "cccd":
|
|
260
|
+
# Strip all non-digits
|
|
261
|
+
digits = re.sub(r'\D', '', out)
|
|
262
|
+
return digits[:12]
|
|
263
|
+
|
|
264
|
+
if constraint_mode == "digits_only":
|
|
265
|
+
return re.sub(r'\D', '', out)
|
|
266
|
+
|
|
267
|
+
# 3. Smart digit sequence unification:
|
|
268
|
+
# If sequence consists mostly of digits separated by broken spaces (e.g. "0 3 8 0 7 1 ...")
|
|
269
|
+
raw_no_space = out.replace(" ", "").replace("-", "")
|
|
270
|
+
digits_count = sum(c.isdigit() for c in raw_no_space)
|
|
271
|
+
if digits_count >= 8 and (digits_count / max(1, len(raw_no_space))) >= 0.8:
|
|
272
|
+
# Replace common OCR misreads at start of digit strips:
|
|
273
|
+
# 'C', 'c', 'O', 'o', 'Q' preceding digits is 99% a misread '0'
|
|
274
|
+
out = re.sub(r'^[CcOoQq]\s*', '0', out)
|
|
275
|
+
out = re.sub(r'\D', '', out)
|
|
276
|
+
|
|
277
|
+
# 4. Contextual Vietnamese Phrase & Diacritic Polish
|
|
278
|
+
replacements = [
|
|
279
|
+
(r'\bđao động\b', 'dao động'),
|
|
280
|
+
(r'\bkhoáng thời gian\b', 'khoảng thời gian'),
|
|
281
|
+
(r'\btrong cá năm\b', 'trong cả năm'),
|
|
282
|
+
(r'\bcá năm\b', 'cả năm'),
|
|
283
|
+
(r'\bthân mềm ba dạng\b', 'thân mềm đa dạng'),
|
|
284
|
+
(r'\bthân mềm ba dang\b', 'thân mềm đa dạng'),
|
|
285
|
+
(r'\bstaln\b', 'Stalin'),
|
|
286
|
+
(r'\bchía ra\b', 'chia ra'),
|
|
287
|
+
(r'\bnang lượng\b', 'năng lượng'),
|
|
288
|
+
(r'\bkhả nang\b', 'khả năng'),
|
|
289
|
+
(r'\btiềm nang\b', 'tiềm năng'),
|
|
290
|
+
(r'\bkỹ nang\b', 'kỹ năng'),
|
|
291
|
+
(r'\bchức nang\b', 'chức năng'),
|
|
292
|
+
(r'\btính nang\b', 'tính năng'),
|
|
293
|
+
(r'\bnang suất\b', 'năng suất'),
|
|
294
|
+
(r'\bnang lực\b', 'năng lực'),
|
|
295
|
+
(r'\bkhổi dải\b', 'khối dài'),
|
|
296
|
+
(r'\bkhổi dài\b', 'khối dài'),
|
|
297
|
+
(r'\bngăn nối\b', 'ngăn nổi'),
|
|
298
|
+
(r'\bđôi mát\b', 'đôi mắt'),
|
|
299
|
+
(r'\bcăng thắng\b', 'căng thẳng'),
|
|
300
|
+
(r'\bnoặc\b', 'hoặc'),
|
|
301
|
+
(r'\bđã mãt\b', 'đã mất'),
|
|
302
|
+
(r'\bđã mát\b', 'đã mất'),
|
|
303
|
+
(r'\b2 chô\b', '2 chỗ'),
|
|
304
|
+
(r'\bquản tý\b', 'quản lý'),
|
|
305
|
+
(r'\bđến nóm\b', 'đến năm'),
|
|
306
|
+
(r'\bvó địch\b', 'vô địch'),
|
|
307
|
+
(r'\bđểm\b', 'điểm'),
|
|
308
|
+
(r'\bhà nhĩ\b', 'Hà Nhì'),
|
|
309
|
+
(r'\bchampionsnip\b', 'championship'),
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
def _make_replacer(rep_text):
|
|
313
|
+
def _fn(m):
|
|
314
|
+
orig = m.group(0)
|
|
315
|
+
if orig.isupper():
|
|
316
|
+
return rep_text.upper()
|
|
317
|
+
orig_words = orig.split()
|
|
318
|
+
rep_words = rep_text.split()
|
|
319
|
+
if len(orig_words) == len(rep_words):
|
|
320
|
+
parts = []
|
|
321
|
+
for ow, rw in zip(orig_words, rep_words):
|
|
322
|
+
if ow.isupper():
|
|
323
|
+
parts.append(rw.upper())
|
|
324
|
+
elif ow and ow[0].isupper():
|
|
325
|
+
parts.append(rw[0].upper() + rw[1:])
|
|
326
|
+
else:
|
|
327
|
+
parts.append(rw.lower())
|
|
328
|
+
return " ".join(parts)
|
|
329
|
+
if orig and orig[0].isupper():
|
|
330
|
+
return rep_text[0].upper() + rep_text[1:]
|
|
331
|
+
return rep_text.lower()
|
|
332
|
+
return _fn
|
|
333
|
+
|
|
334
|
+
for pattern, repl in replacements:
|
|
335
|
+
out = re.sub(pattern, _make_replacer(repl), out, flags=re.IGNORECASE)
|
|
336
|
+
|
|
337
|
+
return out
|
|
338
|
+
|
|
339
|
+
# Global singleton
|
|
340
|
+
viet_linguistics = VietnameseLinguistics()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Kaggle Cloud Bridge Package
|
|
3
|
+
Provides Cloudflare Tunnel, Chunk Streamer, and Kaggle Launcher.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from asterocr.kaggle_bridge.tunnel import default_tunnel
|
|
7
|
+
from asterocr.kaggle_bridge.chunker import default_chunker
|
|
8
|
+
from asterocr.kaggle_bridge.launcher import default_launcher
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Chunk Streamer
|
|
3
|
+
Extracts contiguous chunks of samples from LMDB (dataset_mega or dataset_unified)
|
|
4
|
+
and serializes them into ultra-compact binary streams for zero-delay network transfer to Kaggle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import io
|
|
9
|
+
import sys
|
|
10
|
+
import struct
|
|
11
|
+
import lmdb
|
|
12
|
+
import threading
|
|
13
|
+
|
|
14
|
+
if sys.platform == "win32":
|
|
15
|
+
try:
|
|
16
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
17
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
MAGIC_HEADER = b"ASTCHUNKV1"
|
|
22
|
+
|
|
23
|
+
def resolve_lmdb_path(path_hint=None):
|
|
24
|
+
candidates = []
|
|
25
|
+
if path_hint:
|
|
26
|
+
candidates.append(path_hint)
|
|
27
|
+
candidates.extend(["dataset_viet_real.lmdb", "dataset_unified.lmdb", "dataset_mega.lmdb"])
|
|
28
|
+
|
|
29
|
+
base_dirs = [
|
|
30
|
+
os.getcwd(),
|
|
31
|
+
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")),
|
|
32
|
+
os.path.abspath(os.path.join(os.path.dirname(__file__), "..")),
|
|
33
|
+
os.path.abspath("..")
|
|
34
|
+
]
|
|
35
|
+
for c in candidates:
|
|
36
|
+
for b in base_dirs:
|
|
37
|
+
target = os.path.normpath(os.path.join(b, c))
|
|
38
|
+
if os.path.exists(target) and os.path.exists(os.path.join(target, "data.mdb")):
|
|
39
|
+
return target
|
|
40
|
+
for c in candidates:
|
|
41
|
+
for b in base_dirs:
|
|
42
|
+
target = os.path.normpath(os.path.join(b, c))
|
|
43
|
+
if os.path.exists(target):
|
|
44
|
+
return target
|
|
45
|
+
return path_hint or "dataset_unified.lmdb"
|
|
46
|
+
|
|
47
|
+
_OPEN_LMDB_ENVS = {}
|
|
48
|
+
_ENV_LOCK = threading.Lock()
|
|
49
|
+
|
|
50
|
+
def get_or_open_lmdb_env(path, max_readers=126, force_reopen=False):
|
|
51
|
+
"""
|
|
52
|
+
Thread-safe and process-wide LMDB environment manager.
|
|
53
|
+
Guarantees each LMDB directory is opened at most once per process,
|
|
54
|
+
preventing 'lmdb.Error: The environment ... is already open in this process'.
|
|
55
|
+
"""
|
|
56
|
+
global _OPEN_LMDB_ENVS
|
|
57
|
+
if not path:
|
|
58
|
+
return None
|
|
59
|
+
norm_path = os.path.normpath(os.path.abspath(path))
|
|
60
|
+
if not os.path.exists(norm_path):
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
with _ENV_LOCK:
|
|
64
|
+
if not force_reopen and norm_path in _OPEN_LMDB_ENVS:
|
|
65
|
+
return _OPEN_LMDB_ENVS[norm_path]
|
|
66
|
+
|
|
67
|
+
if force_reopen and norm_path in _OPEN_LMDB_ENVS:
|
|
68
|
+
try:
|
|
69
|
+
_OPEN_LMDB_ENVS[norm_path].close()
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
_OPEN_LMDB_ENVS.pop(norm_path, None)
|
|
73
|
+
|
|
74
|
+
env = lmdb.open(
|
|
75
|
+
norm_path,
|
|
76
|
+
readonly=True,
|
|
77
|
+
lock=False,
|
|
78
|
+
readahead=False,
|
|
79
|
+
meminit=False,
|
|
80
|
+
max_readers=max_readers
|
|
81
|
+
)
|
|
82
|
+
_OPEN_LMDB_ENVS[norm_path] = env
|
|
83
|
+
return env
|
|
84
|
+
|
|
85
|
+
class ChunkProvider:
|
|
86
|
+
"""
|
|
87
|
+
AsterOCR Multi-LMDB Virtual Chaining Streamer:
|
|
88
|
+
Chains multiple disparate LMDB databases (e.g. dataset_unified.lmdb + dataset_mega.lmdb)
|
|
89
|
+
into a single seamless, zero-copy contiguous virtual address space.
|
|
90
|
+
- Zero extra disk footprint.
|
|
91
|
+
- Preserves 100% of samples across all databases.
|
|
92
|
+
- Transparent cross-boundary chunk packaging for Cloud GPU workers.
|
|
93
|
+
"""
|
|
94
|
+
def __init__(self, lmdb_paths=None, chunk_size=2500):
|
|
95
|
+
self.chunk_size = chunk_size
|
|
96
|
+
self.databases = []
|
|
97
|
+
self.total_samples = 0
|
|
98
|
+
self.lock = threading.Lock()
|
|
99
|
+
self.set_dataset(lmdb_paths)
|
|
100
|
+
|
|
101
|
+
def _close_all(self):
|
|
102
|
+
# We don't forcibly close shared cached environments to avoid disrupting other readers
|
|
103
|
+
self.databases = []
|
|
104
|
+
self.total_samples = 0
|
|
105
|
+
|
|
106
|
+
def set_dataset(self, paths, chunk_size=None):
|
|
107
|
+
with self.lock:
|
|
108
|
+
self._close_all()
|
|
109
|
+
if chunk_size:
|
|
110
|
+
self.chunk_size = chunk_size
|
|
111
|
+
|
|
112
|
+
if paths is None:
|
|
113
|
+
paths = ["dataset_unified.lmdb", "dataset_viet_real.lmdb", "dataset_real_scans.lmdb"]
|
|
114
|
+
elif isinstance(paths, str):
|
|
115
|
+
paths = [paths]
|
|
116
|
+
|
|
117
|
+
cumulative_offset = 0
|
|
118
|
+
for p in paths:
|
|
119
|
+
resolved = resolve_lmdb_path(p)
|
|
120
|
+
if not os.path.exists(resolved):
|
|
121
|
+
print(f"[!] Cảnh báo: Không tìm thấy file LMDB tại {resolved}")
|
|
122
|
+
continue
|
|
123
|
+
try:
|
|
124
|
+
env = get_or_open_lmdb_env(resolved, max_readers=126)
|
|
125
|
+
if env is None:
|
|
126
|
+
continue
|
|
127
|
+
with env.begin(write=False) as txn:
|
|
128
|
+
num_val = txn.get(b'num-samples')
|
|
129
|
+
samples = int(num_val.decode('utf-8')) if num_val else 100000
|
|
130
|
+
|
|
131
|
+
db_info = {
|
|
132
|
+
"path": resolved,
|
|
133
|
+
"name": os.path.basename(resolved),
|
|
134
|
+
"env": env,
|
|
135
|
+
"samples": samples,
|
|
136
|
+
"start_offset": cumulative_offset,
|
|
137
|
+
"end_offset": cumulative_offset + samples
|
|
138
|
+
}
|
|
139
|
+
self.databases.append(db_info)
|
|
140
|
+
cumulative_offset += samples
|
|
141
|
+
print(f"[+] Multi-LMDB đã gắn kết: {db_info['name']} ({samples:,} mẫu, Vị trí ảo: #{db_info['start_offset'] + 1:,} -> #{db_info['end_offset']:,})")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"[!] Lỗi khi mở LMDB {resolved}: {e}")
|
|
144
|
+
|
|
145
|
+
self.total_samples = cumulative_offset
|
|
146
|
+
print(f"[+] ChunkProvider Đa Kho Hoàn Tất: {len(self.databases)} kho, TỔNG CỘNG: {self.total_samples:,} MẪU, Chunk size: {self.chunk_size:,}")
|
|
147
|
+
|
|
148
|
+
def _begin_read_txn(self, db):
|
|
149
|
+
env = db.get("env")
|
|
150
|
+
if not env:
|
|
151
|
+
env = get_or_open_lmdb_env(db.get("path"), max_readers=126)
|
|
152
|
+
db["env"] = env
|
|
153
|
+
if not env:
|
|
154
|
+
return None
|
|
155
|
+
try:
|
|
156
|
+
return env.begin(write=False)
|
|
157
|
+
except lmdb.MapResizedError:
|
|
158
|
+
try:
|
|
159
|
+
env.set_mapsize(0)
|
|
160
|
+
return env.begin(write=False)
|
|
161
|
+
except Exception:
|
|
162
|
+
try:
|
|
163
|
+
new_env = get_or_open_lmdb_env(db["path"], max_readers=126, force_reopen=True)
|
|
164
|
+
db["env"] = new_env
|
|
165
|
+
return new_env.begin(write=False)
|
|
166
|
+
except Exception:
|
|
167
|
+
return None
|
|
168
|
+
except Exception as ex:
|
|
169
|
+
print(f"[!] Lỗi re-open LMDB {db.get('name')}: {ex}")
|
|
170
|
+
return None
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"[!] Lỗi mở transaction LMDB {db.get('name')}: {e}")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def get_total_chunks(self, max_samples=None):
|
|
176
|
+
usable = min(self.total_samples, max_samples) if (max_samples and max_samples > 0) else self.total_samples
|
|
177
|
+
return max(1, (usable + self.chunk_size - 1) // self.chunk_size)
|
|
178
|
+
|
|
179
|
+
def get_chunk_bytes(self, chunk_idx, max_samples=None, max_epochs=5):
|
|
180
|
+
"""
|
|
181
|
+
Reads samples in range [start_idx, end_idx) across all chained LMDB databases.
|
|
182
|
+
Transparently packages samples into ASTCHUNKV1 binary stream for Cloud GPU.
|
|
183
|
+
"""
|
|
184
|
+
usable = min(self.total_samples, max_samples) if (max_samples and max_samples > 0) else self.total_samples
|
|
185
|
+
total_chunks = self.get_total_chunks(usable)
|
|
186
|
+
|
|
187
|
+
epoch = (chunk_idx // total_chunks) + 1
|
|
188
|
+
if epoch > max_epochs:
|
|
189
|
+
return None # Training completed across all epochs!
|
|
190
|
+
|
|
191
|
+
epoch_chunk_idx = chunk_idx % total_chunks
|
|
192
|
+
start_idx = epoch_chunk_idx * self.chunk_size
|
|
193
|
+
if start_idx >= usable:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
end_idx = min(usable, start_idx + self.chunk_size)
|
|
197
|
+
samples_in_chunk = end_idx - start_idx
|
|
198
|
+
|
|
199
|
+
buf = io.BytesIO()
|
|
200
|
+
buf.write(MAGIC_HEADER)
|
|
201
|
+
buf.write(struct.pack(">II", chunk_idx, samples_in_chunk))
|
|
202
|
+
|
|
203
|
+
with self.lock:
|
|
204
|
+
# Only open read-only transactions for databases actually needed in this chunk
|
|
205
|
+
relevant_dbs = [
|
|
206
|
+
db for db in self.databases
|
|
207
|
+
if db.get("env") and db["start_offset"] < end_idx and db["end_offset"] >= (start_idx + 1)
|
|
208
|
+
]
|
|
209
|
+
txns = {}
|
|
210
|
+
for db in relevant_dbs:
|
|
211
|
+
txn = self._begin_read_txn(db)
|
|
212
|
+
if txn:
|
|
213
|
+
txns[id(db["env"])] = txn
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
for s_i in range(start_idx + 1, end_idx + 1):
|
|
217
|
+
img_bytes = b""
|
|
218
|
+
lbl_bytes = b" "
|
|
219
|
+
|
|
220
|
+
# Locate which database owns sample index s_i
|
|
221
|
+
for db in relevant_dbs:
|
|
222
|
+
if db["start_offset"] < s_i <= db["end_offset"]:
|
|
223
|
+
local_idx = s_i - db["start_offset"]
|
|
224
|
+
txn = txns.get(id(db["env"]))
|
|
225
|
+
if txn:
|
|
226
|
+
img_key = f"image-{local_idx:09d}".encode("utf-8")
|
|
227
|
+
lbl_key = f"label-{local_idx:09d}".encode("utf-8")
|
|
228
|
+
val_img = txn.get(img_key)
|
|
229
|
+
val_lbl = txn.get(lbl_key)
|
|
230
|
+
if val_img:
|
|
231
|
+
img_bytes = val_img
|
|
232
|
+
if val_lbl:
|
|
233
|
+
lbl_bytes = val_lbl
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
# Pack: 2 bytes label len, label bytes, 4 bytes img len, img bytes
|
|
237
|
+
buf.write(struct.pack(">H", len(lbl_bytes)))
|
|
238
|
+
buf.write(lbl_bytes)
|
|
239
|
+
buf.write(struct.pack(">I", len(img_bytes)))
|
|
240
|
+
buf.write(img_bytes)
|
|
241
|
+
finally:
|
|
242
|
+
for txn in txns.values():
|
|
243
|
+
try:
|
|
244
|
+
txn.abort()
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
buf.seek(0)
|
|
249
|
+
return buf
|
|
250
|
+
|
|
251
|
+
# Global singleton
|
|
252
|
+
default_chunker = ChunkProvider()
|