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,380 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
from playwright.sync_api import sync_playwright
|
|
6
|
+
|
|
7
|
+
PROFILE_DIR = os.path.join(os.environ.get("LOCALAPPDATA", ""), "Google", "Chrome", "User Data")
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
def check_quota_exceeded(page):
|
|
12
|
+
try:
|
|
13
|
+
import re
|
|
14
|
+
# Broadly search for quota limits anywhere in the DOM (including shadow DOMs)
|
|
15
|
+
loc = page.locator("text=/Cannot connect to (TPU|GPU) backend|usage limits in Colab|Usage limit exceeded/i")
|
|
16
|
+
if loc.count() > 0 and loc.first.is_visible():
|
|
17
|
+
print("[!] TAI KHOAN NAY DA HET HAN MUC GPU/TPU (Quota Exceeded)!")
|
|
18
|
+
print("[*] Chuan bi dong trinh duyet va chuyen sang Account tiep theo...")
|
|
19
|
+
return True
|
|
20
|
+
except Exception as e:
|
|
21
|
+
pass
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
def check_and_clear_sessions(page):
|
|
25
|
+
try:
|
|
26
|
+
# Bơm Script JS trực tiếp vào trình duyệt để tìm và click nút xuyên qua mọi tầng mây Shadow DOM!
|
|
27
|
+
js_clicker = """
|
|
28
|
+
() => {
|
|
29
|
+
function findAndClick(text) {
|
|
30
|
+
let els = document.querySelectorAll('*');
|
|
31
|
+
for (let el of els) {
|
|
32
|
+
if (el.shadowRoot) {
|
|
33
|
+
let btn = el.shadowRoot.querySelector('mwc-button, paper-button, button');
|
|
34
|
+
if (btn && btn.textContent && btn.textContent.toLowerCase().includes(text.toLowerCase())) {
|
|
35
|
+
btn.click();
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (el.textContent && el.textContent.toLowerCase() === text.toLowerCase()) {
|
|
40
|
+
let btn = el.closest('mwc-button, paper-button, button') || el;
|
|
41
|
+
btn.click();
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return findAndClick('Manage sessions');
|
|
48
|
+
}
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Thử click bằng JS trước
|
|
52
|
+
clicked_by_js = page.evaluate(js_clicker)
|
|
53
|
+
|
|
54
|
+
# Nếu JS thất bại thì dùng Playwright
|
|
55
|
+
manage_btn = page.locator("text=Manage sessions")
|
|
56
|
+
if clicked_by_js or (manage_btn.count() > 0 and manage_btn.first.is_visible()):
|
|
57
|
+
if not clicked_by_js:
|
|
58
|
+
manage_btn.first.click(force=True)
|
|
59
|
+
|
|
60
|
+
print("[*] Phat hien loi 'Too many sessions'! Tu dong dọn dẹp...")
|
|
61
|
+
time.sleep(4)
|
|
62
|
+
|
|
63
|
+
# Nhấn nút "Terminate other sessions"
|
|
64
|
+
term_other = page.locator("text=Terminate other sessions")
|
|
65
|
+
if term_other.count() > 0 and term_other.first.is_visible():
|
|
66
|
+
term_other.first.click(force=True)
|
|
67
|
+
time.sleep(2)
|
|
68
|
+
|
|
69
|
+
# Click thêm vào tất cả các biểu tượng thùng rác (nếu có)
|
|
70
|
+
trash_icons = page.locator("[title*='Terminate'], [aria-label*='Terminate'], [icon='delete']")
|
|
71
|
+
for i in range(trash_icons.count()):
|
|
72
|
+
try:
|
|
73
|
+
if trash_icons.nth(i).is_visible():
|
|
74
|
+
trash_icons.nth(i).click(force=True)
|
|
75
|
+
time.sleep(1)
|
|
76
|
+
except:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# Đóng hộp thoại
|
|
80
|
+
close_btn = page.locator("text=Close")
|
|
81
|
+
if close_btn.count() > 0 and close_btn.first.is_visible():
|
|
82
|
+
close_btn.first.click(force=True)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
page.keyboard.press("Escape")
|
|
86
|
+
except:
|
|
87
|
+
pass
|
|
88
|
+
time.sleep(2)
|
|
89
|
+
print("[+] Da don dep xong session cu! Tiep tuc chay code tren account hien tai...")
|
|
90
|
+
return True
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"[DEBUG] Loi khi xoa session: {e}")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def main():
|
|
96
|
+
print("=" * 65)
|
|
97
|
+
print(" AsterOCR Google Colab Zero-Touch Engine")
|
|
98
|
+
print("=" * 65)
|
|
99
|
+
|
|
100
|
+
def run_with_profile(profile_dir, is_tpu, COLAB_NOTEBOOK_CODE):
|
|
101
|
+
print(f"[*] Su dung Bot Profile tai: {profile_dir}")
|
|
102
|
+
|
|
103
|
+
with sync_playwright() as p:
|
|
104
|
+
browser = p.chromium.launch_persistent_context(
|
|
105
|
+
user_data_dir=profile_dir,
|
|
106
|
+
channel="chrome",
|
|
107
|
+
headless=False,
|
|
108
|
+
ignore_default_args=["--enable-automation"],
|
|
109
|
+
args=[
|
|
110
|
+
'--disable-blink-features=AutomationControlled',
|
|
111
|
+
'--disable-infobars',
|
|
112
|
+
'--no-sandbox',
|
|
113
|
+
'--disable-dev-shm-usage',
|
|
114
|
+
'--window-size=1920,1080'
|
|
115
|
+
],
|
|
116
|
+
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
|
117
|
+
no_viewport=True
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
page = browser.pages[0] if browser.pages else browser.new_page()
|
|
121
|
+
|
|
122
|
+
# Bơm Script tàng hình (Stealth Evasion) vào mỗi khi load trang
|
|
123
|
+
page.add_init_script("""
|
|
124
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
125
|
+
get: () => undefined
|
|
126
|
+
});
|
|
127
|
+
window.navigator.chrome = {
|
|
128
|
+
runtime: {}
|
|
129
|
+
};
|
|
130
|
+
Object.defineProperty(navigator, 'languages', {
|
|
131
|
+
get: () => ['en-US', 'en']
|
|
132
|
+
});
|
|
133
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
134
|
+
get: () => [1, 2, 3]
|
|
135
|
+
});
|
|
136
|
+
""")
|
|
137
|
+
|
|
138
|
+
print("[*] Truc tiep truy cap vao Google Colab...")
|
|
139
|
+
page.goto("https://colab.research.google.com/#create=true")
|
|
140
|
+
|
|
141
|
+
# Check login
|
|
142
|
+
try:
|
|
143
|
+
page.wait_for_selector('.monaco-editor', timeout=15000)
|
|
144
|
+
print("[+] Da dang nhap Google Colab (Co Monaco Editor).")
|
|
145
|
+
except:
|
|
146
|
+
print("[!] Colab yeu cau dang nhap hoac load qua cham! Vui long tu dang nhap tren cua so trinh duyet.")
|
|
147
|
+
try:
|
|
148
|
+
page.wait_for_selector('.monaco-editor', timeout=60000)
|
|
149
|
+
print("[+] Da dang nhap thanh cong!")
|
|
150
|
+
except:
|
|
151
|
+
print("[!] Qua gio dang nhap hoac loi mang. Chuyen sang Profile tiep theo...")
|
|
152
|
+
return "ERROR"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
print("[*] Cau hinh phan cung (TPU/GPU)...")
|
|
156
|
+
time.sleep(2)
|
|
157
|
+
try:
|
|
158
|
+
# Click Runtime -> Change runtime type (force=True de xuyen qua cac bang popup huong dan cua Google)
|
|
159
|
+
try:
|
|
160
|
+
page.locator('#runtime-menu-button').click(force=True, timeout=5000)
|
|
161
|
+
except:
|
|
162
|
+
page.get_by_text("Runtime", exact=True).click(force=True, timeout=5000)
|
|
163
|
+
|
|
164
|
+
time.sleep(1)
|
|
165
|
+
try:
|
|
166
|
+
page.locator('#ok').click(force=True, timeout=1000) # clear any dialogs
|
|
167
|
+
except:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
page.get_by_text("Change runtime type").click(force=True, timeout=5000)
|
|
171
|
+
time.sleep(2)
|
|
172
|
+
|
|
173
|
+
# Select hardware based on new Colab UI
|
|
174
|
+
if is_tpu:
|
|
175
|
+
# Try new TPU names first, fallback to old generic TPU
|
|
176
|
+
try:
|
|
177
|
+
page.get_by_text("v5e-1 TPU").first.click(force=True, timeout=3000)
|
|
178
|
+
except:
|
|
179
|
+
page.get_by_text("TPU").first.click(force=True, timeout=3000)
|
|
180
|
+
else:
|
|
181
|
+
try:
|
|
182
|
+
page.get_by_text("T4 GPU").first.click(force=True, timeout=3000)
|
|
183
|
+
except:
|
|
184
|
+
page.get_by_text("GPU").first.click(force=True, timeout=3000)
|
|
185
|
+
time.sleep(1)
|
|
186
|
+
|
|
187
|
+
# Click Save button
|
|
188
|
+
try:
|
|
189
|
+
page.get_by_role("button", name="Save").click(timeout=3000)
|
|
190
|
+
except:
|
|
191
|
+
try:
|
|
192
|
+
page.locator('button:has-text("Save")').last.click(force=True, timeout=3000)
|
|
193
|
+
except:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
# Ultimate fallback: Press Enter to submit the dialog
|
|
197
|
+
time.sleep(1)
|
|
198
|
+
page.keyboard.press("Enter")
|
|
199
|
+
time.sleep(3)
|
|
200
|
+
check_and_clear_sessions(page)
|
|
201
|
+
if check_quota_exceeded(page):
|
|
202
|
+
return "QUOTA_EXCEEDED"
|
|
203
|
+
print("[+] Cau hinh phan cung thanh cong!")
|
|
204
|
+
except Exception as e:
|
|
205
|
+
print(f"[!] Khong the cau hinh tu dong phan cung: {e}")
|
|
206
|
+
|
|
207
|
+
print("[*] Tiem code va bat dau chay...")
|
|
208
|
+
try:
|
|
209
|
+
# Click into the first cell
|
|
210
|
+
page.click('.monaco-editor')
|
|
211
|
+
time.sleep(1)
|
|
212
|
+
page.keyboard.press("Control+A")
|
|
213
|
+
page.keyboard.press("Backspace")
|
|
214
|
+
time.sleep(1)
|
|
215
|
+
# Type the code using a synthetic paste event to bypass auto-indent and clipboard locks!
|
|
216
|
+
page.evaluate('''
|
|
217
|
+
(code) => {
|
|
218
|
+
const dataTransfer = new DataTransfer();
|
|
219
|
+
dataTransfer.setData('text/plain', code);
|
|
220
|
+
const event = new ClipboardEvent('paste', {
|
|
221
|
+
clipboardData: dataTransfer,
|
|
222
|
+
bubbles: true,
|
|
223
|
+
cancelable: true
|
|
224
|
+
});
|
|
225
|
+
document.activeElement.dispatchEvent(event);
|
|
226
|
+
}
|
|
227
|
+
''', COLAB_NOTEBOOK_CODE)
|
|
228
|
+
print("[*] Dang cho Colab cap phat GPU (Co the mat 10-15s)...")
|
|
229
|
+
time.sleep(2)
|
|
230
|
+
page.keyboard.press("Control+Enter")
|
|
231
|
+
|
|
232
|
+
# Vòng lặp chờ 15s để bắt lỗi "Too many sessions" hoặc "Quota Exceeded"
|
|
233
|
+
# Vì Colab mất một lúc để Allocating, nếu check quá nhanh sẽ bị hụt!
|
|
234
|
+
is_quota_exceeded = False
|
|
235
|
+
for _ in range(8):
|
|
236
|
+
time.sleep(2)
|
|
237
|
+
if check_and_clear_sessions(page):
|
|
238
|
+
time.sleep(2)
|
|
239
|
+
page.keyboard.press("Control+Enter")
|
|
240
|
+
if check_quota_exceeded(page):
|
|
241
|
+
is_quota_exceeded = True
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
if is_quota_exceeded:
|
|
245
|
+
return "QUOTA_EXCEEDED"
|
|
246
|
+
|
|
247
|
+
print("[+] Da tiem code va vuot qua buoc kiem tra GPU!")
|
|
248
|
+
except Exception as e:
|
|
249
|
+
print(f"[!] Loi khi paste code: {e}")
|
|
250
|
+
|
|
251
|
+
print("[*] Bat che do Auto-Watcher vinh vien...")
|
|
252
|
+
while True:
|
|
253
|
+
try:
|
|
254
|
+
time.sleep(10)
|
|
255
|
+
# Kiem tra nut Reconnect
|
|
256
|
+
reconnect_btn = page.query_selector('colab-connect-button[connected="false"], #connect, colab-connect-button:has-text("Reconnect"), colab-connect-button:has-text("Connect")')
|
|
257
|
+
if reconnect_btn:
|
|
258
|
+
print("[*] Mat ket noi! Tu dong Reconnect...")
|
|
259
|
+
reconnect_btn.click()
|
|
260
|
+
time.sleep(5)
|
|
261
|
+
page.keyboard.press("Control+F9")
|
|
262
|
+
|
|
263
|
+
# Dialog "Are you still there" hoac error
|
|
264
|
+
dialog_ok = page.query_selector('md-dialog mwc-button[dialogaction="ok"], paper-button:has-text("OK")')
|
|
265
|
+
if dialog_ok:
|
|
266
|
+
print("[*] Dong hop thoai Canh bao...")
|
|
267
|
+
dialog_ok.click()
|
|
268
|
+
time.sleep(3)
|
|
269
|
+
page.keyboard.press("Control+F9")
|
|
270
|
+
|
|
271
|
+
# Kiem tra trang thai chay cua Notebook (Idle Detection)
|
|
272
|
+
# Kiem tra xem co cell nao dang "Running" khong (Bang cach tim nut Stop)
|
|
273
|
+
is_running = page.evaluate('''
|
|
274
|
+
() => {
|
|
275
|
+
let btns = document.querySelectorAll('colab-run-button');
|
|
276
|
+
if (btns.length === 0) return true; // Chua the xac dinh, gia su dang chay de an toan
|
|
277
|
+
for (let b of btns) {
|
|
278
|
+
if (b.title && b.title.toLowerCase().includes('stop')) return true;
|
|
279
|
+
if (b.getAttribute('aria-label') && b.getAttribute('aria-label').toLowerCase().includes('stop')) return true;
|
|
280
|
+
let icon = b.querySelector('iron-icon');
|
|
281
|
+
if (icon && icon.getAttribute('icon') === 'colab:stop') return true;
|
|
282
|
+
}
|
|
283
|
+
return false; // Khong co cell nao dang chay
|
|
284
|
+
}
|
|
285
|
+
''')
|
|
286
|
+
|
|
287
|
+
if not is_running:
|
|
288
|
+
print("[!] Phat hien Notebook bi DUNG chay (Idle/Crashed). Tu dong kich hoat chay lai...")
|
|
289
|
+
page.keyboard.press("Control+Enter")
|
|
290
|
+
time.sleep(5)
|
|
291
|
+
|
|
292
|
+
# Dialog "Too many sessions"
|
|
293
|
+
if check_and_clear_sessions(page):
|
|
294
|
+
page.keyboard.press("Control+Enter")
|
|
295
|
+
|
|
296
|
+
# Kiem tra Quota Limit Exceeded
|
|
297
|
+
if check_quota_exceeded(page):
|
|
298
|
+
return "QUOTA_EXCEEDED"
|
|
299
|
+
|
|
300
|
+
# Keep-alive
|
|
301
|
+
page.evaluate('() => { if (window.colab && window.colab.global) { window.colab.global.notebook.kernel.keepAlive(); } }')
|
|
302
|
+
except Exception:
|
|
303
|
+
time.sleep(10)
|
|
304
|
+
|
|
305
|
+
def main():
|
|
306
|
+
print("=" * 65)
|
|
307
|
+
print(" AsterOCR Google Colab Zero-Touch Engine")
|
|
308
|
+
print("=" * 65)
|
|
309
|
+
tunnel_url = sys.argv[1] if len(sys.argv) > 1 else "https://asterai.sglocalhub.com"
|
|
310
|
+
print(f"[*] Engine URL: {tunnel_url}")
|
|
311
|
+
|
|
312
|
+
# Read config to know if TPU or GPU
|
|
313
|
+
is_tpu = False
|
|
314
|
+
config_path = os.path.join(os.path.dirname(__file__), "..", "..", "scratch", "colab_run_config.json")
|
|
315
|
+
if os.path.exists(config_path):
|
|
316
|
+
import json
|
|
317
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
318
|
+
cfg = json.load(f)
|
|
319
|
+
if cfg.get("device") == "colab_tpu":
|
|
320
|
+
is_tpu = True
|
|
321
|
+
|
|
322
|
+
COLAB_NOTEBOOK_CODE = f'''# --- ASTEROCR COLAB WORKER (ZERO-TOUCH) ---
|
|
323
|
+
import requests, os, time, subprocess, sys
|
|
324
|
+
server_url = "{tunnel_url}"
|
|
325
|
+
|
|
326
|
+
while True:
|
|
327
|
+
try:
|
|
328
|
+
print(f"[+] Xin code tu {{server_url}}...")
|
|
329
|
+
res = requests.get(f"{{server_url}}/api/colab/worker.py", timeout=20)
|
|
330
|
+
if res.status_code == 200 and not res.text.startswith("<!DOCTYPE"):
|
|
331
|
+
with open("worker.py", "w", encoding="utf-8") as f:
|
|
332
|
+
f.write(res.text)
|
|
333
|
+
print("[🚀] Bat dau Huan luyen...")
|
|
334
|
+
p = subprocess.Popen([sys.executable, "-u", "worker.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
|
|
335
|
+
for line in p.stdout:
|
|
336
|
+
print(line, end='', flush=True)
|
|
337
|
+
p.wait()
|
|
338
|
+
print(f"[!] Tien trinh ket thuc voi ma loi: {{p.returncode}}")
|
|
339
|
+
else:
|
|
340
|
+
print(f"[ERROR] Loi tai code. Status: {{res.status_code}}")
|
|
341
|
+
if res.text.startswith("<!DOCTYPE"):
|
|
342
|
+
print("[ERROR] Nhan duoc HTML thay vi Code Python! Cloudflare Tunnel dang bi chan hoac chua san sang.")
|
|
343
|
+
print("[!] Phien ket thuc. Tu dong khoi dong lai sau 5s...")
|
|
344
|
+
time.sleep(5)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
print(f"[!] Ket noi gian doan: {{e}}. Thu lai sau 10s...")
|
|
347
|
+
time.sleep(10)
|
|
348
|
+
'''
|
|
349
|
+
|
|
350
|
+
profiles = [
|
|
351
|
+
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Google", "Chrome", "AsterOCR_Profile"),
|
|
352
|
+
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Google", "Chrome", "AsterOCR_Profile_2"),
|
|
353
|
+
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Google", "Chrome", "AsterOCR_Profile_3")
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
print("[*] Bat dau chu trinh Multi-Profile Fallback...")
|
|
357
|
+
|
|
358
|
+
for i, profile_dir in enumerate(profiles, start=1):
|
|
359
|
+
print(f"\\n{'='*50}")
|
|
360
|
+
print(f"[*] DANG SU DUNG TAI KHOAN {i}/3")
|
|
361
|
+
print(f"{'='*50}")
|
|
362
|
+
|
|
363
|
+
status = run_with_profile(profile_dir, is_tpu, COLAB_NOTEBOOK_CODE)
|
|
364
|
+
|
|
365
|
+
if status == "QUOTA_EXCEEDED":
|
|
366
|
+
print(f"[!] Tai khoan {i} da het Quota. Tu dong chuyen sang tai khoan tiep theo...")
|
|
367
|
+
time.sleep(5)
|
|
368
|
+
continue
|
|
369
|
+
elif status == "ERROR":
|
|
370
|
+
print(f"[!] Tai khoan {i} gap loi ket noi. Thu tai khoan tiep theo...")
|
|
371
|
+
time.sleep(5)
|
|
372
|
+
continue
|
|
373
|
+
else:
|
|
374
|
+
print("[+] Tien trinh dung dot ngot hoac bi huy.")
|
|
375
|
+
break
|
|
376
|
+
|
|
377
|
+
print("[!] TAT CA TAI KHOAN DEU DA HET QUOTA HOAC GAP LOI!")
|
|
378
|
+
|
|
379
|
+
if __name__ == "__main__":
|
|
380
|
+
main()
|