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.
@@ -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")