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,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Kaggle Launcher & Dispatcher
|
|
3
|
+
Orchestrates Cloudflare Tunnel, prepares staging kernel script,
|
|
4
|
+
pushes to Kaggle via official Kaggle CLI, and tracks remote execution lifecycle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import json
|
|
11
|
+
import uuid
|
|
12
|
+
import threading
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
if sys.platform == "win32":
|
|
16
|
+
try:
|
|
17
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
18
|
+
sys.stderr.reconfigure(encoding='utf-8')
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
from asterocr.kaggle_bridge.tunnel import default_tunnel
|
|
23
|
+
|
|
24
|
+
class KaggleLauncher:
|
|
25
|
+
def __init__(self, kernel_slug="creyt2012/asterocr-cloud-gpu-worker-v2"):
|
|
26
|
+
self.kernel_slug = kernel_slug
|
|
27
|
+
self.is_running = False
|
|
28
|
+
self.session_token = None
|
|
29
|
+
self.tunnel_url = None
|
|
30
|
+
self.monitor_thread = None
|
|
31
|
+
self.stop_requested = False
|
|
32
|
+
|
|
33
|
+
def _get_kaggle_env(self):
|
|
34
|
+
env = os.environ.copy()
|
|
35
|
+
tok_p = os.path.expanduser("~/.kaggle/access_token")
|
|
36
|
+
if os.path.exists(tok_p):
|
|
37
|
+
try:
|
|
38
|
+
env["KAGGLE_API_TOKEN"] = open(tok_p, "r", encoding="utf-8").read().strip()
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
return env
|
|
42
|
+
|
|
43
|
+
def launch(self, config=None, on_status_update=None):
|
|
44
|
+
cfg = config or {}
|
|
45
|
+
self.stop_requested = False
|
|
46
|
+
self.session_token = f"sess_{uuid.uuid4().hex[:8]}"
|
|
47
|
+
|
|
48
|
+
# 1. Start Cloudflare Tunnel to expose local port 7860
|
|
49
|
+
print("[*] Đang khởi động Cloudflare Tunnel bảo mật...")
|
|
50
|
+
try:
|
|
51
|
+
self.tunnel_url = default_tunnel.start(timeout=30)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return False, f"Lỗi khởi động Cloudflare Tunnel: {e}"
|
|
54
|
+
|
|
55
|
+
# 2. Prepare staging directory
|
|
56
|
+
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
57
|
+
staging_dir = os.path.join(base_dir, "scratch", "kaggle_run")
|
|
58
|
+
os.makedirs(staging_dir, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
# 3. Read template worker script and inject runtime config
|
|
61
|
+
worker_src_path = os.path.join(os.path.dirname(__file__), "kaggle_worker.py")
|
|
62
|
+
with open(worker_src_path, "r", encoding="utf-8") as f:
|
|
63
|
+
code = f.read()
|
|
64
|
+
|
|
65
|
+
batch_sz = min(96, int(cfg.get("batch_size", 96)))
|
|
66
|
+
epochs = int(cfg.get("epochs", 5))
|
|
67
|
+
|
|
68
|
+
models_dir = os.path.abspath(os.path.join(base_dir, "models"))
|
|
69
|
+
has_pretrained = os.path.exists(os.path.join(models_dir, "asternet_best.pth"))
|
|
70
|
+
default_lr = 0.00005 if has_pretrained else 0.0005
|
|
71
|
+
lr = float(cfg.get("lr", default_lr))
|
|
72
|
+
|
|
73
|
+
is_tpu = (cfg.get("device") == "kaggle_tpu")
|
|
74
|
+
if is_tpu:
|
|
75
|
+
self.kernel_slug = "creyt2012/asterocr-cloud-tpu-worker"
|
|
76
|
+
else:
|
|
77
|
+
self.kernel_slug = "creyt2012/asterocr-cloud-gpu-worker-v2"
|
|
78
|
+
|
|
79
|
+
import re
|
|
80
|
+
code = re.sub(r'IS_TPU_DEVICE\s*=\s*(True|False)', f'IS_TPU_DEVICE = {is_tpu}', code)
|
|
81
|
+
code = re.sub(r'TUNNEL_URL\s*=\s*["\'][^"\']+["\']', f'TUNNEL_URL = "{self.tunnel_url}"', code)
|
|
82
|
+
code = re.sub(r'SESSION_TOKEN\s*=\s*["\'][^"\']+["\']', f'SESSION_TOKEN = "{self.session_token}"', code)
|
|
83
|
+
code = re.sub(r'BATCH_SIZE\s*=\s*\d+', f'BATCH_SIZE = {batch_sz}', code)
|
|
84
|
+
code = re.sub(r'LEARNING_RATE\s*=\s*[\d\.]+', f'LEARNING_RATE = {lr}', code)
|
|
85
|
+
code = re.sub(r'MAX_EPOCHS\s*=\s*\d+', f'MAX_EPOCHS = {epochs}', code)
|
|
86
|
+
|
|
87
|
+
staged_script = os.path.join(staging_dir, "kaggle_worker.py")
|
|
88
|
+
with open(staged_script, "w", encoding="utf-8") as f:
|
|
89
|
+
f.write(code)
|
|
90
|
+
|
|
91
|
+
# Generate Jupyter Notebook (.ipynb)
|
|
92
|
+
nb = {
|
|
93
|
+
"cells": [
|
|
94
|
+
{
|
|
95
|
+
"cell_type": "markdown",
|
|
96
|
+
"metadata": {},
|
|
97
|
+
"source": [
|
|
98
|
+
"# 🚀 AsterOCR Cloud GPU Worker v2\n",
|
|
99
|
+
"Autonomous Deep Learning OCR Worker running on Kaggle GPU Cloud (Dual Tesla T4 / P100).\n",
|
|
100
|
+
"- Real-time streaming chunks via Cloudflare Tunnel\n",
|
|
101
|
+
"- FP16 Mixed Precision Training\n",
|
|
102
|
+
"- Instant checkpoint synchronization"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"cell_type": "code",
|
|
107
|
+
"execution_count": None,
|
|
108
|
+
"metadata": {},
|
|
109
|
+
"outputs": [],
|
|
110
|
+
"source": ["!nvidia-smi"]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"cell_type": "code",
|
|
114
|
+
"execution_count": None,
|
|
115
|
+
"metadata": {},
|
|
116
|
+
"outputs": [],
|
|
117
|
+
"source": [
|
|
118
|
+
"%%writefile kaggle_worker.py\n",
|
|
119
|
+
code
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"cell_type": "code",
|
|
124
|
+
"execution_count": None,
|
|
125
|
+
"metadata": {},
|
|
126
|
+
"outputs": [],
|
|
127
|
+
"source": ["!python kaggle_worker.py"]
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
"metadata": {
|
|
131
|
+
"accelerator": "GPU",
|
|
132
|
+
"kernelspec": {
|
|
133
|
+
"display_name": "Python 3",
|
|
134
|
+
"language": "python",
|
|
135
|
+
"name": "python3"
|
|
136
|
+
},
|
|
137
|
+
"language_info": {
|
|
138
|
+
"codemirror_mode": {"name": "ipython", "version": 3},
|
|
139
|
+
"file_extension": ".py",
|
|
140
|
+
"mimetype": "text/x-python",
|
|
141
|
+
"name": "python",
|
|
142
|
+
"nbconvert_exporter": "python",
|
|
143
|
+
"pygments_lexer": "ipython3",
|
|
144
|
+
"version": "3.10.12"
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"nbformat": 4,
|
|
148
|
+
"nbformat_minor": 5
|
|
149
|
+
}
|
|
150
|
+
nb_filename = "asterocr_cloud_gpu_worker_v2.ipynb"
|
|
151
|
+
with open(os.path.join(staging_dir, nb_filename), "w", encoding="utf-8") as f:
|
|
152
|
+
json.dump(nb, f, indent=2, ensure_ascii=False)
|
|
153
|
+
|
|
154
|
+
# 4. Generate kernel metadata
|
|
155
|
+
metadata = {
|
|
156
|
+
"id": self.kernel_slug,
|
|
157
|
+
"title": "AsterOCR Cloud TPU Worker" if is_tpu else "AsterOCR Cloud GPU Worker v2",
|
|
158
|
+
"code_file": nb_filename,
|
|
159
|
+
"language": "python",
|
|
160
|
+
"kernel_type": "notebook",
|
|
161
|
+
"is_private": True,
|
|
162
|
+
"enable_gpu": not is_tpu,
|
|
163
|
+
"enable_tpu": is_tpu,
|
|
164
|
+
"enable_internet": True
|
|
165
|
+
}
|
|
166
|
+
if not is_tpu:
|
|
167
|
+
metadata["machine_shape"] = "NvidiaTeslaT4"
|
|
168
|
+
meta_path = os.path.join(staging_dir, "kernel-metadata.json")
|
|
169
|
+
with open(meta_path, "w", encoding="utf-8") as f:
|
|
170
|
+
json.dump(metadata, f, indent=2)
|
|
171
|
+
|
|
172
|
+
# Bundle latest checkpoint into staging_dir for instant zero-latency recovery
|
|
173
|
+
import shutil
|
|
174
|
+
models_dir = os.path.abspath(os.path.join(base_dir, "models"))
|
|
175
|
+
best_p = os.path.join(models_dir, "asternet_best.pth")
|
|
176
|
+
last_p = os.path.join(models_dir, "asternet_last.pth")
|
|
177
|
+
chosen_pth = best_p if os.path.exists(best_p) else last_p
|
|
178
|
+
if os.path.exists(chosen_pth) and os.path.getsize(chosen_pth) > 1000:
|
|
179
|
+
shutil.copyfile(chosen_pth, os.path.join(staging_dir, "weights.pth"))
|
|
180
|
+
print(f"[+] Đã tự động đóng gói checkpoint '{os.path.basename(chosen_pth)}' ({os.path.getsize(chosen_pth)/(1024*1024):.1f} MB) vào Kernel!")
|
|
181
|
+
|
|
182
|
+
# 5. Push kernel to Cloud Compute
|
|
183
|
+
dev_str = "Cloud TPU (v3-8)" if is_tpu else "Cloud GPU (Dual NVIDIA Tesla T4 x2)"
|
|
184
|
+
print(f"[*] Đang kết nối và kích hoạt {dev_str} ({self.kernel_slug})...")
|
|
185
|
+
push_cmd = [sys.executable, "-m", "kaggle.cli", "kernels", "push", "-p", staging_dir]
|
|
186
|
+
k_env = self._get_kaggle_env()
|
|
187
|
+
try:
|
|
188
|
+
res = subprocess.run(push_cmd, env=k_env, capture_output=True, text=True, timeout=120)
|
|
189
|
+
print("[Cloud GPU Push]", res.stdout, res.stderr)
|
|
190
|
+
if res.returncode != 0 and "Kernel version" not in res.stdout:
|
|
191
|
+
default_tunnel.stop()
|
|
192
|
+
return False, f"Lỗi kích hoạt {dev_str}: {res.stderr or res.stdout}"
|
|
193
|
+
except Exception as e:
|
|
194
|
+
default_tunnel.stop()
|
|
195
|
+
return False, f"Lỗi gọi API {dev_str}: {e}"
|
|
196
|
+
|
|
197
|
+
self.is_running = True
|
|
198
|
+
print(f"[+] Đã kích hoạt thành công phiên làm việc trên {dev_str}!")
|
|
199
|
+
|
|
200
|
+
# 6. Start background status monitor
|
|
201
|
+
self.monitor_thread = threading.Thread(
|
|
202
|
+
target=self._monitor_loop,
|
|
203
|
+
args=(on_status_update, is_tpu),
|
|
204
|
+
daemon=True
|
|
205
|
+
)
|
|
206
|
+
self.monitor_thread.start()
|
|
207
|
+
return True, f"{dev_str} Trainer đã bắt đầu (Tunnel: {self.tunnel_url})"
|
|
208
|
+
|
|
209
|
+
def _monitor_loop(self, on_status_update, is_tpu):
|
|
210
|
+
time.sleep(25)
|
|
211
|
+
consecutive_errors = 0
|
|
212
|
+
dev_str = "Cloud TPU" if is_tpu else "Cloud GPU"
|
|
213
|
+
k_env = self._get_kaggle_env()
|
|
214
|
+
while self.is_running and not self.stop_requested:
|
|
215
|
+
try:
|
|
216
|
+
cmd = [sys.executable, "-m", "kaggle.cli", "kernels", "status", self.kernel_slug]
|
|
217
|
+
res = subprocess.run(cmd, env=k_env, capture_output=True, text=True, timeout=20)
|
|
218
|
+
out = res.stdout.strip().lower()
|
|
219
|
+
if "complete" in out:
|
|
220
|
+
print(f"[+] {dev_str} Kernel đã hoàn thành 100%!")
|
|
221
|
+
if on_status_update:
|
|
222
|
+
on_status_update("completed")
|
|
223
|
+
break
|
|
224
|
+
elif "error" in out or "failed" in out:
|
|
225
|
+
consecutive_errors += 1
|
|
226
|
+
if consecutive_errors >= 3:
|
|
227
|
+
print(f"[!] {dev_str} Kernel báo lỗi: {res.stdout}")
|
|
228
|
+
if on_status_update:
|
|
229
|
+
on_status_update("error", res.stdout)
|
|
230
|
+
break
|
|
231
|
+
elif "running" in out or "queued" in out:
|
|
232
|
+
consecutive_errors = 0
|
|
233
|
+
except Exception:
|
|
234
|
+
pass
|
|
235
|
+
time.sleep(10)
|
|
236
|
+
|
|
237
|
+
self.is_running = False
|
|
238
|
+
default_tunnel.stop()
|
|
239
|
+
|
|
240
|
+
def stop(self):
|
|
241
|
+
self.stop_requested = True
|
|
242
|
+
self.is_running = False
|
|
243
|
+
default_tunnel.stop()
|
|
244
|
+
|
|
245
|
+
default_launcher = KaggleLauncher()
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Kaggle Tunnel Manager
|
|
3
|
+
Automates zero-config HTTPS Cloudflare Quick Tunnel using engine/cloudflared.exe
|
|
4
|
+
Exposes local Flask port (7860) to secure public internet so Kaggle P100 can stream data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import subprocess
|
|
12
|
+
import threading
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
if sys.platform == "win32":
|
|
16
|
+
try:
|
|
17
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
18
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
class TunnelManager:
|
|
23
|
+
def __init__(self, local_port=7860, cloudflared_path=None):
|
|
24
|
+
self.local_port = local_port
|
|
25
|
+
if cloudflared_path is None:
|
|
26
|
+
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
27
|
+
self.cloudflared_path = os.path.join(base_dir, "engine", "cloudflared.exe")
|
|
28
|
+
else:
|
|
29
|
+
self.cloudflared_path = cloudflared_path
|
|
30
|
+
|
|
31
|
+
self.process = None
|
|
32
|
+
self.tunnel_url = None
|
|
33
|
+
self._stop_event = threading.Event()
|
|
34
|
+
self._watchdog_thread = None
|
|
35
|
+
|
|
36
|
+
def start(self, timeout=30):
|
|
37
|
+
# 0. Check if user configured a persistent custom static URL (e.g. named tunnel on user's own domain)
|
|
38
|
+
config_path = os.path.join(os.path.dirname(__file__), "..", "tunnel_config.json")
|
|
39
|
+
custom_token = None
|
|
40
|
+
custom_fixed_url = None
|
|
41
|
+
if os.path.exists(config_path):
|
|
42
|
+
try:
|
|
43
|
+
import json
|
|
44
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
45
|
+
cfg = json.load(f)
|
|
46
|
+
fixed_url = cfg.get("custom_url", "").strip()
|
|
47
|
+
custom_token = cfg.get("tunnel_token", "").strip()
|
|
48
|
+
if fixed_url.startswith("http://") or fixed_url.startswith("https://"):
|
|
49
|
+
if "trycloudflare.com" not in fixed_url:
|
|
50
|
+
custom_fixed_url = fixed_url.rstrip("/")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"[*] Đọc cấu hình tunnel: {e}")
|
|
53
|
+
|
|
54
|
+
if not os.path.exists(self.cloudflared_path):
|
|
55
|
+
raise FileNotFoundError(f"Không tìm thấy cloudflared.exe tại: {self.cloudflared_path}")
|
|
56
|
+
|
|
57
|
+
# Terminate any stale cloudflared instances cleanly
|
|
58
|
+
self.stop()
|
|
59
|
+
if sys.platform == "win32":
|
|
60
|
+
try:
|
|
61
|
+
subprocess.run(["taskkill", "/f", "/im", "cloudflared.exe"], capture_output=True, timeout=5)
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
self._stop_event.clear()
|
|
66
|
+
|
|
67
|
+
if custom_token:
|
|
68
|
+
print(f"[*] Kích hoạt Cloudflare Named Tunnel với Token cố định cho domain: {custom_fixed_url}...")
|
|
69
|
+
cmd = [
|
|
70
|
+
self.cloudflared_path,
|
|
71
|
+
"tunnel",
|
|
72
|
+
"run",
|
|
73
|
+
"--token", custom_token
|
|
74
|
+
]
|
|
75
|
+
else:
|
|
76
|
+
cmd = [
|
|
77
|
+
self.cloudflared_path,
|
|
78
|
+
"tunnel",
|
|
79
|
+
"--url", f"http://127.0.0.1:{self.local_port}",
|
|
80
|
+
"--no-autoupdate"
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# Use creationflags on Windows to avoid opening popup console
|
|
84
|
+
creation_flags = 0
|
|
85
|
+
if sys.platform == "win32":
|
|
86
|
+
creation_flags = subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0x08000000
|
|
87
|
+
|
|
88
|
+
self.process = subprocess.Popen(
|
|
89
|
+
cmd,
|
|
90
|
+
stdout=subprocess.PIPE,
|
|
91
|
+
stderr=subprocess.STDOUT,
|
|
92
|
+
text=True,
|
|
93
|
+
bufsize=1,
|
|
94
|
+
creationflags=creation_flags
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
start_time = time.time()
|
|
98
|
+
extracted_url = None
|
|
99
|
+
|
|
100
|
+
if custom_token:
|
|
101
|
+
extracted_url = custom_fixed_url
|
|
102
|
+
# Wait briefly to confirm connector established
|
|
103
|
+
while time.time() - start_time < timeout:
|
|
104
|
+
line = self.process.stdout.readline()
|
|
105
|
+
if line:
|
|
106
|
+
print(f"[Cloudflare] {line.strip()}")
|
|
107
|
+
if "Registered tunnel connection" in line or "Connection" in line and "registered" in line:
|
|
108
|
+
break
|
|
109
|
+
time.sleep(0.1)
|
|
110
|
+
else:
|
|
111
|
+
while time.time() - start_time < timeout:
|
|
112
|
+
line = self.process.stdout.readline()
|
|
113
|
+
if not line:
|
|
114
|
+
time.sleep(0.2)
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Match *.trycloudflare.com URL
|
|
118
|
+
match = re.search(r'https://[a-zA-Z0-9-]+\.trycloudflare\.com', line)
|
|
119
|
+
if match:
|
|
120
|
+
extracted_url = match.group(0)
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
if not extracted_url:
|
|
124
|
+
self.stop()
|
|
125
|
+
raise RuntimeError("Không thể tạo Cloudflare Tunnel trong thời gian quy định (Timeout).")
|
|
126
|
+
|
|
127
|
+
self.tunnel_url = extracted_url
|
|
128
|
+
self._save_last_url(self.tunnel_url)
|
|
129
|
+
print(f"[+] Cloudflare Tunnel kích hoạt thành công: {self.tunnel_url}")
|
|
130
|
+
|
|
131
|
+
# Start background watchdog to auto-restart tunnel if cloudflared unexpectedly dies
|
|
132
|
+
if self._watchdog_thread is None or not self._watchdog_thread.is_alive():
|
|
133
|
+
self._watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True)
|
|
134
|
+
self._watchdog_thread.start()
|
|
135
|
+
|
|
136
|
+
return self.tunnel_url
|
|
137
|
+
|
|
138
|
+
def _watchdog_loop(self):
|
|
139
|
+
"""Monitors cloudflared process; if it drops while active, resurrects it automatically."""
|
|
140
|
+
while not self._stop_event.is_set():
|
|
141
|
+
time.sleep(5)
|
|
142
|
+
if self._stop_event.is_set():
|
|
143
|
+
break
|
|
144
|
+
if self.tunnel_url and self.process and self.process.poll() is not None:
|
|
145
|
+
print("[!] Cảnh báo: Cloudflare Tunnel bị ngắt bất ngờ! Đang tự động hồi phục...")
|
|
146
|
+
try:
|
|
147
|
+
self.start(timeout=30)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"[!] Tự động hồi phục tunnel thất bại: {e}")
|
|
150
|
+
|
|
151
|
+
def _save_last_url(self, url):
|
|
152
|
+
try:
|
|
153
|
+
p = os.path.join(os.path.dirname(__file__), "..", "last_tunnel_url.txt")
|
|
154
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
155
|
+
f.write(url)
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
# Auto-publish to Cloud GPU auto-discovery channel so the notebook reconnects 100% automatically
|
|
160
|
+
try:
|
|
161
|
+
requests.post("https://ntfy.sh/asterocr_tunnel_creyt2012", data=url.encode("utf-8"), timeout=6)
|
|
162
|
+
print(f"[+] Đã tự động đồng bộ Tunnel URL ({url}) lên kênh Cloud GPU (Auto-Discovery)!")
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
def get_url(self):
|
|
167
|
+
return self.tunnel_url
|
|
168
|
+
|
|
169
|
+
def is_alive(self):
|
|
170
|
+
if self.process and self.process.poll() is None and self.tunnel_url:
|
|
171
|
+
return True
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
def stop(self):
|
|
175
|
+
self._stop_event.set()
|
|
176
|
+
if self.process:
|
|
177
|
+
try:
|
|
178
|
+
self.process.terminate()
|
|
179
|
+
self.process.wait(timeout=3)
|
|
180
|
+
except Exception:
|
|
181
|
+
try:
|
|
182
|
+
self.process.kill()
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
self.process = None
|
|
186
|
+
self.tunnel_url = None
|
|
187
|
+
print("[*] Cloudflare Tunnel đã đóng.")
|
|
188
|
+
|
|
189
|
+
# Global singleton
|
|
190
|
+
default_tunnel = TunnelManager()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Vision Encoder Backbone
|
|
3
|
+
High-Frequency Asymmetric ResNet-SVTR for Vietnamese Document Recognition:
|
|
4
|
+
- HighFrequencyStem: Dual-branch feature stem preserving micro-pixel diacritics
|
|
5
|
+
- Asymmetric Spatial Downsampling (ASD): Compresses height (32 -> 1) while preserving horizontal sequence length (W/4)
|
|
6
|
+
- SVTRLocalBlock: Depthwise stroke-grouping convolution with Squeeze-and-Excitation micro-channel attention
|
|
7
|
+
- Hallucination-proof: 100% strictly bound to optical pixel evidence (Non-autoregressive)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
from torch import nn
|
|
12
|
+
from torch.nn import functional as F
|
|
13
|
+
|
|
14
|
+
class SqueezeExcitation(nn.Module):
|
|
15
|
+
"""Channel attention mechanism to dynamically amplify diacritic-sensitive feature channels."""
|
|
16
|
+
def __init__(self, channels, reduction=16):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.avg_pool = nn.AdaptiveAvgPool2d(1)
|
|
19
|
+
mid = max(8, channels // reduction)
|
|
20
|
+
self.fc = nn.Sequential(
|
|
21
|
+
nn.Linear(channels, mid, bias=False),
|
|
22
|
+
nn.SiLU(inplace=False),
|
|
23
|
+
nn.Linear(mid, channels, bias=False),
|
|
24
|
+
nn.Sigmoid()
|
|
25
|
+
)
|
|
26
|
+
def forward(self, x):
|
|
27
|
+
b, c, _, _ = x.size()
|
|
28
|
+
y = self.fc(self.avg_pool(x).view(b, c)).view(b, c, 1, 1)
|
|
29
|
+
return x * y.expand_as(x)
|
|
30
|
+
|
|
31
|
+
class HighFrequencyStem(nn.Module):
|
|
32
|
+
def __init__(self, in_channels=3, out_channels=64):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.conv_main = nn.Sequential(
|
|
35
|
+
nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False),
|
|
36
|
+
nn.BatchNorm2d(out_channels),
|
|
37
|
+
nn.SiLU(inplace=False)
|
|
38
|
+
)
|
|
39
|
+
lap = torch.tensor([[0., -1., 0.], [-1., 4., -1.], [0., -1., 0.]]).view(1, 1, 3, 3)
|
|
40
|
+
self.register_buffer('hp_kernel', lap.repeat(in_channels, 1, 1, 1))
|
|
41
|
+
self.hp_proj = nn.Sequential(
|
|
42
|
+
nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False),
|
|
43
|
+
nn.BatchNorm2d(out_channels)
|
|
44
|
+
)
|
|
45
|
+
self.gamma = nn.Parameter(torch.tensor([0.15]))
|
|
46
|
+
|
|
47
|
+
def forward(self, x):
|
|
48
|
+
main_feat = self.conv_main(x)
|
|
49
|
+
with torch.no_grad():
|
|
50
|
+
k = self.hp_kernel.to(dtype=x.dtype, device=x.device)
|
|
51
|
+
hp = F.conv2d(x, k, padding=1, groups=x.shape[1])
|
|
52
|
+
return main_feat + self.gamma * self.hp_proj(hp)
|
|
53
|
+
|
|
54
|
+
class SVTRLocalBlock(nn.Module):
|
|
55
|
+
def __init__(self, in_channels, out_channels, stride=1):
|
|
56
|
+
super().__init__()
|
|
57
|
+
self.pw1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
|
|
58
|
+
self.bn1 = nn.BatchNorm2d(out_channels)
|
|
59
|
+
self.act1 = nn.SiLU(inplace=False)
|
|
60
|
+
self.dw = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, groups=out_channels, bias=False)
|
|
61
|
+
self.bn2 = nn.BatchNorm2d(out_channels)
|
|
62
|
+
self.act2 = nn.SiLU(inplace=False)
|
|
63
|
+
self.se = SqueezeExcitation(out_channels, reduction=max(4, out_channels // 16))
|
|
64
|
+
self.pw2 = nn.Conv2d(out_channels, out_channels, kernel_size=1, bias=False)
|
|
65
|
+
self.bn3 = nn.BatchNorm2d(out_channels)
|
|
66
|
+
self.shortcut = nn.Sequential()
|
|
67
|
+
if stride != 1 or in_channels != out_channels:
|
|
68
|
+
self.shortcut = nn.Sequential(
|
|
69
|
+
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
|
|
70
|
+
nn.BatchNorm2d(out_channels)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def forward(self, x):
|
|
74
|
+
res = self.shortcut(x)
|
|
75
|
+
out = self.act1(self.bn1(self.pw1(x)))
|
|
76
|
+
out = self.act2(self.bn2(self.dw(out)))
|
|
77
|
+
out = self.se(out)
|
|
78
|
+
out = self.bn3(self.pw2(out))
|
|
79
|
+
return F.silu(out + res)
|
|
80
|
+
|
|
81
|
+
class AsterResNetBackbone(nn.Module):
|
|
82
|
+
def __init__(self, in_channels=3, out_dim=512):
|
|
83
|
+
super().__init__()
|
|
84
|
+
self.stem = HighFrequencyStem(in_channels, 64)
|
|
85
|
+
self.layer1 = nn.Sequential(SVTRLocalBlock(64, 64), SVTRLocalBlock(64, 128, stride=(2, 1))) # H: 32 -> 16
|
|
86
|
+
self.layer2 = nn.Sequential(SVTRLocalBlock(128, 128), SVTRLocalBlock(128, 256, stride=(2, 2))) # H: 16 -> 8, W/2
|
|
87
|
+
self.layer3 = nn.Sequential(SVTRLocalBlock(256, 256), SVTRLocalBlock(256, 512, stride=(2, 2))) # H: 8 -> 4, W/4
|
|
88
|
+
self.layer4 = nn.Sequential(
|
|
89
|
+
SVTRLocalBlock(512, 512),
|
|
90
|
+
nn.Conv2d(512, out_dim, kernel_size=(4, 1), stride=(4, 1), bias=False), # H: 4 -> 1, W/4
|
|
91
|
+
nn.BatchNorm2d(out_dim),
|
|
92
|
+
nn.SiLU(inplace=False)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def forward(self, x):
|
|
96
|
+
x = self.stem(x)
|
|
97
|
+
x = self.layer1(x)
|
|
98
|
+
x = self.layer2(x)
|
|
99
|
+
x = self.layer3(x)
|
|
100
|
+
x = self.layer4(x)
|
|
101
|
+
x = x.squeeze(2) # (B, C, W/4)
|
|
102
|
+
return x.permute(2, 0, 1) # (T, B, C)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Implicit Language Modeling Head (ILMH) & Gated Fusion Layer
|
|
3
|
+
Inspired by ABINet (CVPR 2021) and PARSeq (ECCV 2022).
|
|
4
|
+
Provides bidirectional linguistic reasoning and autonomous error correction
|
|
5
|
+
for Vietnamese OCR without sacrificing real-time inference speed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
import torch
|
|
10
|
+
from torch import nn
|
|
11
|
+
|
|
12
|
+
class PositionalEncoding(nn.Module):
|
|
13
|
+
def __init__(self, d_model=512, max_len=256, dropout=0.1):
|
|
14
|
+
super(PositionalEncoding, self).__init__()
|
|
15
|
+
self.dropout = nn.Dropout(p=dropout)
|
|
16
|
+
|
|
17
|
+
pe = torch.zeros(max_len, d_model)
|
|
18
|
+
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
|
|
19
|
+
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
|
|
20
|
+
|
|
21
|
+
pe[:, 0::2] = torch.sin(position * div_term)
|
|
22
|
+
pe[:, 1::2] = torch.cos(position * div_term)
|
|
23
|
+
pe = pe.unsqueeze(1) # Shape: (max_len, 1, d_model)
|
|
24
|
+
self.register_buffer('pe', pe)
|
|
25
|
+
|
|
26
|
+
def forward(self, x):
|
|
27
|
+
# x shape: (T, B, d_model)
|
|
28
|
+
seq_len = x.size(0)
|
|
29
|
+
if seq_len > self.pe.size(0):
|
|
30
|
+
# Dynamic computation for extra-long sequences beyond max_len
|
|
31
|
+
position = torch.arange(0, seq_len, dtype=torch.float, device=x.device).unsqueeze(1)
|
|
32
|
+
d_model = x.size(-1)
|
|
33
|
+
div_term = torch.exp(torch.arange(0, d_model, 2, device=x.device).float() * (-math.log(10000.0) / d_model))
|
|
34
|
+
pe = torch.zeros(seq_len, d_model, device=x.device)
|
|
35
|
+
pe[:, 0::2] = torch.sin(position * div_term)
|
|
36
|
+
pe[:, 1::2] = torch.cos(position * div_term)
|
|
37
|
+
pe = pe.unsqueeze(1)
|
|
38
|
+
x = x + pe
|
|
39
|
+
else:
|
|
40
|
+
x = x + self.pe[:seq_len]
|
|
41
|
+
return self.dropout(x)
|
|
42
|
+
|
|
43
|
+
class AsterLanguageModelHead(nn.Module):
|
|
44
|
+
"""
|
|
45
|
+
Lightweight Bidirectional Implicit Language Modeling Head.
|
|
46
|
+
Operates on sequence features (T, B, hidden_dim) to model Vietnamese
|
|
47
|
+
orthographic, diacritical, and semantic transitions.
|
|
48
|
+
"""
|
|
49
|
+
def __init__(self, d_model=512, nhead=4, num_layers=2, dim_feedforward=1024, dropout=0.1):
|
|
50
|
+
super(AsterLanguageModelHead, self).__init__()
|
|
51
|
+
self.pos_encoder = PositionalEncoding(d_model=d_model, max_len=256, dropout=dropout)
|
|
52
|
+
|
|
53
|
+
encoder_layer = nn.TransformerEncoderLayer(
|
|
54
|
+
d_model=d_model,
|
|
55
|
+
nhead=nhead,
|
|
56
|
+
dim_feedforward=dim_feedforward,
|
|
57
|
+
dropout=dropout,
|
|
58
|
+
activation="gelu",
|
|
59
|
+
norm_first=True, # Pre-LayerNorm for stable gradient flow
|
|
60
|
+
batch_first=False
|
|
61
|
+
)
|
|
62
|
+
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
|
|
63
|
+
self.norm = nn.LayerNorm(d_model)
|
|
64
|
+
|
|
65
|
+
def forward(self, x):
|
|
66
|
+
# x shape: (T, B, d_model)
|
|
67
|
+
pos_x = self.pos_encoder(x)
|
|
68
|
+
lm_feat = self.transformer(pos_x)
|
|
69
|
+
return self.norm(lm_feat)
|
|
70
|
+
|
|
71
|
+
class GatedLanguageFusion(nn.Module):
|
|
72
|
+
"""
|
|
73
|
+
Adaptive Gated Vision-Language Fusion:
|
|
74
|
+
Computes a dynamic gate vector G = sigmoid(Linear([H_vis; H_lang])) in (0, 1).
|
|
75
|
+
When vision is clear: G -> 1 (prioritize visual features).
|
|
76
|
+
When vision is blurred/degraded/ambiguous: G -> 0 (rely on LM to correct errors).
|
|
77
|
+
"""
|
|
78
|
+
def __init__(self, d_model=512, init_vision_bias=1.5):
|
|
79
|
+
super(GatedLanguageFusion, self).__init__()
|
|
80
|
+
self.gate_linear = nn.Linear(d_model * 2, d_model)
|
|
81
|
+
# Initialize bias positively so initially G ~= sigmoid(1.5) ~= 0.82 (safe for existing weights)
|
|
82
|
+
nn.init.constant_(self.gate_linear.bias, init_vision_bias)
|
|
83
|
+
self.sigmoid = nn.Sigmoid()
|
|
84
|
+
|
|
85
|
+
def forward(self, h_vis, h_lang):
|
|
86
|
+
# h_vis, h_lang: (T, B, d_model)
|
|
87
|
+
concat = torch.cat([h_vis, h_lang], dim=-1)
|
|
88
|
+
gate = self.sigmoid(self.gate_linear(concat))
|
|
89
|
+
h_fused = gate * h_vis + (1.0 - gate) * h_lang
|
|
90
|
+
return h_fused, gate
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
lm = AsterLanguageModelHead(d_model=512, nhead=4, num_layers=2)
|
|
94
|
+
fusion = GatedLanguageFusion(d_model=512)
|
|
95
|
+
|
|
96
|
+
dummy_vis = torch.randn(64, 4, 512)
|
|
97
|
+
dummy_lang = lm(dummy_vis)
|
|
98
|
+
fused, gate = fusion(dummy_vis, dummy_lang)
|
|
99
|
+
|
|
100
|
+
print("LM output shape:", dummy_lang.shape)
|
|
101
|
+
print("Fused output shape:", fused.shape)
|
|
102
|
+
print("Mean gate value (initially vision-biased):", gate.mean().item())
|
|
103
|
+
total_params = sum(p.numel() for p in lm.parameters()) + sum(p.numel() for p in fusion.parameters())
|
|
104
|
+
print(f"Total Language + Fusion Parameters: {total_params / 1e6:.3f}M")
|