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
asterocr/studio.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsterOCR Standalone Training Studio & Neural Flow Visualizer
|
|
3
|
+
Independent server running on its own dedicated port (default: 7860).
|
|
4
|
+
Completely decoupled from any other application or document extraction project.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import webbrowser
|
|
10
|
+
import threading
|
|
11
|
+
import base64
|
|
12
|
+
from io import BytesIO
|
|
13
|
+
|
|
14
|
+
# Ensure parent directory is in sys.path
|
|
15
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
16
|
+
|
|
17
|
+
if sys.platform == "win32":
|
|
18
|
+
try:
|
|
19
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
20
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
import time
|
|
25
|
+
import torch
|
|
26
|
+
from flask import Flask, render_template, request, jsonify, send_file, Response
|
|
27
|
+
from asterocr.trainer.trainer import global_trainer
|
|
28
|
+
from asterocr.synthesizer.generator import default_generator
|
|
29
|
+
|
|
30
|
+
template_dir = os.path.join(os.path.dirname(__file__), "web", "templates")
|
|
31
|
+
app = Flask(__name__, template_folder=template_dir)
|
|
32
|
+
|
|
33
|
+
@app.route("/")
|
|
34
|
+
def index():
|
|
35
|
+
return render_template("index.html")
|
|
36
|
+
|
|
37
|
+
@app.route("/api/asterocr/status")
|
|
38
|
+
def api_status():
|
|
39
|
+
return jsonify(global_trainer.get_state())
|
|
40
|
+
|
|
41
|
+
session_start_chunk = None
|
|
42
|
+
|
|
43
|
+
@app.route("/api/asterocr/start", methods=["POST"])
|
|
44
|
+
def api_start():
|
|
45
|
+
global session_start_chunk
|
|
46
|
+
session_start_chunk = None # Reset relative chunk mapping for new session
|
|
47
|
+
cfg = request.json or {}
|
|
48
|
+
success, msg = global_trainer.start(cfg)
|
|
49
|
+
return jsonify({"status": "ok" if success else "error", "message": msg})
|
|
50
|
+
|
|
51
|
+
@app.route("/api/asterocr/stop", methods=["POST"])
|
|
52
|
+
def api_stop():
|
|
53
|
+
global_trainer.stop()
|
|
54
|
+
return jsonify({"status": "ok", "message": "Stop requested."})
|
|
55
|
+
|
|
56
|
+
@app.route("/api/asterocr/reset", methods=["POST"])
|
|
57
|
+
def api_reset():
|
|
58
|
+
global_trainer.reset_history()
|
|
59
|
+
return jsonify({"status": "ok", "message": "Đã làm sạch toàn bộ biểu đồ và lịch sử về 0."})
|
|
60
|
+
|
|
61
|
+
@app.route("/api/asterocr/preview_synthetic", methods=["POST"])
|
|
62
|
+
def api_preview_synthetic():
|
|
63
|
+
req_data = request.json or {}
|
|
64
|
+
text = req_data.get("text", "CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM")
|
|
65
|
+
images_b64 = []
|
|
66
|
+
|
|
67
|
+
for style in ["clean", "scan", "aged"]:
|
|
68
|
+
try:
|
|
69
|
+
im = default_generator.render_text(text, target_height=32)
|
|
70
|
+
buf = BytesIO()
|
|
71
|
+
im.save(buf, format="PNG")
|
|
72
|
+
images_b64.append("data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("utf-8"))
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print("Preview synth error:", e)
|
|
75
|
+
|
|
76
|
+
return jsonify({"status": "ok", "images": images_b64})
|
|
77
|
+
|
|
78
|
+
# =========================================================================
|
|
79
|
+
# CLOUD GPU STREAMING PIPELINE ENDPOINTS
|
|
80
|
+
# =========================================================================
|
|
81
|
+
from asterocr.kaggle_bridge.chunker import default_chunker
|
|
82
|
+
from asterocr.kaggle_bridge.tunnel import default_tunnel
|
|
83
|
+
import json
|
|
84
|
+
|
|
85
|
+
@app.route("/api/tunnel/info", methods=["GET"])
|
|
86
|
+
def api_tunnel_info():
|
|
87
|
+
cfg_path = os.path.join(os.path.dirname(__file__), "tunnel_config.json")
|
|
88
|
+
custom_u = ""
|
|
89
|
+
if os.path.exists(cfg_path):
|
|
90
|
+
try:
|
|
91
|
+
with open(cfg_path, "r", encoding="utf-8") as f:
|
|
92
|
+
custom_u = json.load(f).get("custom_url", "")
|
|
93
|
+
if "trycloudflare.com" in custom_u:
|
|
94
|
+
custom_u = ""
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
active_u = default_tunnel.get_url() or ""
|
|
98
|
+
return jsonify({
|
|
99
|
+
"active_url": active_u,
|
|
100
|
+
"custom_url": custom_u,
|
|
101
|
+
"is_alive": default_tunnel.is_alive()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
@app.route("/api/tunnel/config", methods=["POST"])
|
|
105
|
+
def api_tunnel_config():
|
|
106
|
+
data = request.json or {}
|
|
107
|
+
custom_u = data.get("custom_url", "").strip()
|
|
108
|
+
if "trycloudflare.com" in custom_u:
|
|
109
|
+
return jsonify({
|
|
110
|
+
"status": "error",
|
|
111
|
+
"message": "URL trycloudflare.com là URL tạm thời ngẫu nhiên của Cloudflare, không thể lưu làm tên miền cố định. Vui lòng để trống ô này để hệ thống tự động sinh và kết nối tự động (Zero-Touch)!"
|
|
112
|
+
})
|
|
113
|
+
cfg_path = os.path.join(os.path.dirname(__file__), "tunnel_config.json")
|
|
114
|
+
try:
|
|
115
|
+
existing_cfg = {}
|
|
116
|
+
if os.path.exists(cfg_path):
|
|
117
|
+
try:
|
|
118
|
+
with open(cfg_path, "r", encoding="utf-8") as f:
|
|
119
|
+
existing_cfg = json.load(f)
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
existing_cfg["custom_url"] = custom_u
|
|
123
|
+
if "tunnel_token" in data and data["tunnel_token"]:
|
|
124
|
+
existing_cfg["tunnel_token"] = data["tunnel_token"]
|
|
125
|
+
with open(cfg_path, "w", encoding="utf-8") as f:
|
|
126
|
+
json.dump(existing_cfg, f, indent=2)
|
|
127
|
+
default_tunnel.tunnel_url = custom_u if custom_u else None
|
|
128
|
+
return jsonify({"status": "ok", "custom_url": custom_u, "message": "Đã lưu cấu hình Tunnel." if custom_u else "Đã xóa tên miền tùy chỉnh. Hệ thống chuyển sang chế độ Tự Động (Zero-Touch)."})
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return jsonify({"status": "error", "message": str(e)})
|
|
131
|
+
|
|
132
|
+
@app.route("/api/tunnel/start", methods=["POST"])
|
|
133
|
+
def api_tunnel_start():
|
|
134
|
+
try:
|
|
135
|
+
url = default_tunnel.start(timeout=35)
|
|
136
|
+
return jsonify({"status": "ok", "url": url, "message": "Cloudflare Tunnel đã kích hoạt thành công."})
|
|
137
|
+
except Exception as e:
|
|
138
|
+
return jsonify({"status": "error", "message": str(e)})
|
|
139
|
+
|
|
140
|
+
@app.route("/api/tunnel/stop", methods=["POST"])
|
|
141
|
+
def api_tunnel_stop():
|
|
142
|
+
try:
|
|
143
|
+
default_tunnel.stop()
|
|
144
|
+
return jsonify({"status": "ok", "message": "Đã dừng Cloudflare Tunnel."})
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return jsonify({"status": "error", "message": str(e)})
|
|
147
|
+
|
|
148
|
+
@app.route("/api/colab/worker.py")
|
|
149
|
+
def api_colab_worker():
|
|
150
|
+
worker_p = os.path.join(os.path.dirname(__file__), "kaggle_bridge", "kaggle_worker.py")
|
|
151
|
+
if os.path.exists(worker_p):
|
|
152
|
+
with open(worker_p, "r", encoding="utf-8") as f:
|
|
153
|
+
code = f.read()
|
|
154
|
+
|
|
155
|
+
import re, json
|
|
156
|
+
# Đọc cấu hình tạm do _train_colab_worker xuất ra
|
|
157
|
+
config_path = os.path.join(os.path.dirname(__file__), "..", "scratch", "colab_run_config.json")
|
|
158
|
+
if os.path.exists(config_path):
|
|
159
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
160
|
+
cfg = json.load(f)
|
|
161
|
+
|
|
162
|
+
is_tpu = (cfg.get("device") == "colab_tpu")
|
|
163
|
+
batch_sz = cfg.get("batch_size", 256)
|
|
164
|
+
lr = cfg.get("lr", 0.0005)
|
|
165
|
+
epochs = cfg.get("epochs", 5)
|
|
166
|
+
|
|
167
|
+
# Lấy tunnel url
|
|
168
|
+
tunnel_url = "https://asterai.sglocalhub.com"
|
|
169
|
+
from asterocr.kaggle_bridge.tunnel import default_tunnel
|
|
170
|
+
if default_tunnel.tunnel_url:
|
|
171
|
+
tunnel_url = default_tunnel.tunnel_url
|
|
172
|
+
|
|
173
|
+
code = re.sub(r'IS_TPU_DEVICE\s*=\s*(True|False)', f'IS_TPU_DEVICE = {is_tpu}', code)
|
|
174
|
+
code = re.sub(r'TUNNEL_URL\s*=\s*["\'][^"\']+["\']', f'TUNNEL_URL = "{tunnel_url}"', code)
|
|
175
|
+
code = re.sub(r'BATCH_SIZE\s*=\s*\d+', f'BATCH_SIZE = {batch_sz}', code)
|
|
176
|
+
code = re.sub(r'LEARNING_RATE\s*=\s*[\d\.]+', f'LEARNING_RATE = {lr}', code)
|
|
177
|
+
code = re.sub(r'MAX_EPOCHS\s*=\s*\d+', f'MAX_EPOCHS = {epochs}', code)
|
|
178
|
+
|
|
179
|
+
return Response(code, mimetype="text/plain; charset=utf-8")
|
|
180
|
+
return ("Worker not found", 404)
|
|
181
|
+
|
|
182
|
+
@app.route("/api/kaggle/stream/ping")
|
|
183
|
+
def api_kaggle_ping():
|
|
184
|
+
return jsonify({"status": "ok", "service": "AsterOCR Local Streamer", "time": time.time()})
|
|
185
|
+
|
|
186
|
+
@app.route("/api/kaggle/stream/heartbeat", methods=["POST"])
|
|
187
|
+
def api_kaggle_heartbeat():
|
|
188
|
+
data = request.json or {}
|
|
189
|
+
global_trainer.record_cloud_heartbeat(data)
|
|
190
|
+
return jsonify({"status": "ok", "time": time.time()})
|
|
191
|
+
|
|
192
|
+
@app.route("/api/kaggle/stream/control")
|
|
193
|
+
def api_kaggle_control():
|
|
194
|
+
return jsonify({"should_stop": bool(global_trainer.stop_requested)})
|
|
195
|
+
|
|
196
|
+
@app.route("/api/kaggle/stream/init_weights")
|
|
197
|
+
def api_kaggle_init_weights():
|
|
198
|
+
global_trainer.record_cloud_heartbeat({"event": "init_weights"})
|
|
199
|
+
best_p = os.path.join(global_trainer.output_dir, "asternet_best.pth")
|
|
200
|
+
last_p = os.path.join(global_trainer.output_dir, "asternet_last.pth")
|
|
201
|
+
|
|
202
|
+
# Intelligently select latest / highest step checkpoint
|
|
203
|
+
chosen = None
|
|
204
|
+
if os.path.exists(last_p) and os.path.exists(best_p):
|
|
205
|
+
try:
|
|
206
|
+
c_last = torch.load(last_p, map_location="cpu", weights_only=False)
|
|
207
|
+
c_best = torch.load(best_p, map_location="cpu", weights_only=False)
|
|
208
|
+
s_last = c_last.get("step", 0) or 0
|
|
209
|
+
s_best = c_best.get("step", 0) or 0
|
|
210
|
+
chosen = last_p if s_last >= s_best else best_p
|
|
211
|
+
except Exception:
|
|
212
|
+
chosen = last_p
|
|
213
|
+
elif os.path.exists(last_p):
|
|
214
|
+
chosen = last_p
|
|
215
|
+
elif os.path.exists(best_p):
|
|
216
|
+
chosen = best_p
|
|
217
|
+
|
|
218
|
+
if chosen and os.path.exists(chosen):
|
|
219
|
+
return send_file(chosen, mimetype="application/octet-stream")
|
|
220
|
+
return ("", 204)
|
|
221
|
+
|
|
222
|
+
@app.route("/api/kaggle/stream/get_chunk")
|
|
223
|
+
def api_kaggle_get_chunk():
|
|
224
|
+
global session_start_chunk
|
|
225
|
+
global_trainer.record_cloud_heartbeat({"event": "get_chunk"})
|
|
226
|
+
chunk_idx = int(request.args.get("chunk_idx", 0))
|
|
227
|
+
epochs = int(global_trainer.state.get("total_epochs", 5))
|
|
228
|
+
total_chunks = default_chunker.get_total_chunks()
|
|
229
|
+
|
|
230
|
+
# Dynamic session chunk calibration:
|
|
231
|
+
# When worker connects, it may restore an old checkpoint's chunk_idx (e.g. 10,078).
|
|
232
|
+
# We calibrate the relative chunk to start from 0 for the currently active dataset!
|
|
233
|
+
if session_start_chunk is None or chunk_idx < session_start_chunk:
|
|
234
|
+
session_start_chunk = chunk_idx
|
|
235
|
+
|
|
236
|
+
relative_chunk = max(0, chunk_idx - session_start_chunk)
|
|
237
|
+
|
|
238
|
+
buf = default_chunker.get_chunk_bytes(relative_chunk, max_epochs=epochs)
|
|
239
|
+
if buf is None:
|
|
240
|
+
return ("", 204)
|
|
241
|
+
|
|
242
|
+
cur_epoch = min(epochs, (relative_chunk // total_chunks) + 1)
|
|
243
|
+
|
|
244
|
+
resp = send_file(buf, mimetype="application/octet-stream")
|
|
245
|
+
resp.headers["X-Current-Epoch"] = str(cur_epoch)
|
|
246
|
+
resp.headers["X-Total-Epochs"] = str(epochs)
|
|
247
|
+
resp.headers["X-Total-Chunks"] = str(total_chunks)
|
|
248
|
+
return resp
|
|
249
|
+
|
|
250
|
+
@app.route("/api/kaggle/stream/telemetry", methods=["POST"])
|
|
251
|
+
def api_kaggle_telemetry():
|
|
252
|
+
data = request.json or {}
|
|
253
|
+
global_trainer.update_remote_telemetry(data)
|
|
254
|
+
return jsonify({"status": "ok"})
|
|
255
|
+
|
|
256
|
+
@app.route("/api/kaggle/stream/save_checkpoint", methods=["POST"])
|
|
257
|
+
def api_kaggle_save_checkpoint():
|
|
258
|
+
file = request.files.get("checkpoint")
|
|
259
|
+
if file:
|
|
260
|
+
os.makedirs(global_trainer.output_dir, exist_ok=True)
|
|
261
|
+
last_p = os.path.join(global_trainer.output_dir, "asternet_last.pth")
|
|
262
|
+
best_p = os.path.join(global_trainer.output_dir, "asternet_best.pth")
|
|
263
|
+
temp_p = os.path.join(global_trainer.output_dir, "temp_incoming.pth")
|
|
264
|
+
|
|
265
|
+
file.save(temp_p)
|
|
266
|
+
try:
|
|
267
|
+
test_ckpt = torch.load(temp_p, map_location="cpu", weights_only=False)
|
|
268
|
+
sd = test_ckpt.get("state_dict", test_ckpt.get("model", test_ckpt))
|
|
269
|
+
has_nan = any(torch.isnan(v).any() or torch.isinf(v).any() for v in sd.values() if isinstance(v, torch.Tensor))
|
|
270
|
+
if has_nan:
|
|
271
|
+
print(f"[!] CẢNH BÁO: Checkpoint từ Cloud chứa giá trị NaN/Inf! Đã từ chối lưu để bảo vệ mô hình an toàn!")
|
|
272
|
+
if os.path.exists(temp_p):
|
|
273
|
+
os.remove(temp_p)
|
|
274
|
+
return jsonify({"status": "rejected", "message": "Checkpoint contains NaN"}), 400
|
|
275
|
+
except Exception as e:
|
|
276
|
+
print(f"[!] Lỗi kiểm tra tính toàn vẹn của checkpoint: {e}")
|
|
277
|
+
if os.path.exists(temp_p):
|
|
278
|
+
os.remove(temp_p)
|
|
279
|
+
return jsonify({"status": "error"}), 400
|
|
280
|
+
|
|
281
|
+
# Trọng số sạch: chuyển vào last_p
|
|
282
|
+
import shutil
|
|
283
|
+
shutil.move(temp_p, last_p)
|
|
284
|
+
|
|
285
|
+
step = int(request.form.get("step", 0))
|
|
286
|
+
chunk_idx = int(request.form.get("chunk_idx", 0))
|
|
287
|
+
try:
|
|
288
|
+
loss = float(request.form.get("loss", 999.0))
|
|
289
|
+
best = float(request.form.get("best_loss", 999.0))
|
|
290
|
+
with global_trainer.lock:
|
|
291
|
+
cur_best = float(global_trainer.ledger.get("best_loss", 999.0))
|
|
292
|
+
# Protect against corrupted/dummy zero loss (< 0.01 is invalid in CTC)
|
|
293
|
+
if cur_best < 0.01:
|
|
294
|
+
cur_best = 999.0
|
|
295
|
+
global_trainer.ledger["best_loss"] = 999.0
|
|
296
|
+
global_trainer.state["best_loss"] = 999.0
|
|
297
|
+
|
|
298
|
+
# Only update best model file if loss strictly improved, valid (> 0.01) and reasonable (< 2.0)
|
|
299
|
+
# Chặn không cho giá trị 0.1500 cố định giả tạo ghi đè kỷ lục nếu không thật
|
|
300
|
+
if (0.01 < loss < cur_best and loss < 2.0) or not os.path.exists(best_p):
|
|
301
|
+
shutil.copyfile(last_p, best_p)
|
|
302
|
+
global_trainer.ledger["best_loss"] = round(loss, 4)
|
|
303
|
+
global_trainer.state["best_loss"] = round(loss, 4)
|
|
304
|
+
print(f"[+] Kỷ lục Loss mới đạt được: {loss:.4f}! Đã lưu vào {best_p}")
|
|
305
|
+
|
|
306
|
+
# Maintain strictly monotonic step and chunk accounting
|
|
307
|
+
if step > global_trainer.ledger.get("last_step", 0):
|
|
308
|
+
global_trainer.ledger["last_step"] = step
|
|
309
|
+
if chunk_idx > global_trainer.ledger.get("last_chunk_idx", 0):
|
|
310
|
+
global_trainer.ledger["last_chunk_idx"] = chunk_idx
|
|
311
|
+
global_trainer.ledger["last_updated"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
312
|
+
|
|
313
|
+
# Sync total_samples from cumulative state if available
|
|
314
|
+
ct_samp = global_trainer.state.get("cumulative_training", {}).get("total_samples", 0)
|
|
315
|
+
if ct_samp > global_trainer.ledger.get("total_samples", 0):
|
|
316
|
+
global_trainer.ledger["total_samples"] = ct_samp
|
|
317
|
+
|
|
318
|
+
global_trainer._save_ledger(global_trainer.ledger)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
print(f"[!] Lỗi khi đồng bộ checkpoint metadata vào ledger: {e}")
|
|
321
|
+
print(f"[+] Checkpoint từ Cloud GPU (Step {step}, Chunk {chunk_idx}) đã được đồng bộ an toàn về: {last_p}")
|
|
322
|
+
return jsonify({"status": "ok"})
|
|
323
|
+
|
|
324
|
+
@app.route("/api/kaggle/status", methods=["GET"])
|
|
325
|
+
def api_kaggle_status():
|
|
326
|
+
import subprocess
|
|
327
|
+
slug = request.args.get("slug", "creyt2012/asterocr-cloud-gpu-worker-v2")
|
|
328
|
+
env = os.environ.copy()
|
|
329
|
+
tok_p = os.path.expanduser("~/.kaggle/access_token")
|
|
330
|
+
if os.path.exists(tok_p):
|
|
331
|
+
try:
|
|
332
|
+
with open(tok_p, "r", encoding="utf-8") as f:
|
|
333
|
+
env["KAGGLE_API_TOKEN"] = f.read().strip()
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
try:
|
|
337
|
+
res = subprocess.run([sys.executable, "-m", "kaggle.cli", "kernels", "status", slug], env=env, capture_output=True, text=True, timeout=15)
|
|
338
|
+
out = (res.stdout + " " + res.stderr).strip()
|
|
339
|
+
return jsonify({"status": "ok", "output": out})
|
|
340
|
+
except Exception as e:
|
|
341
|
+
return jsonify({"status": "error", "message": str(e)})
|
|
342
|
+
|
|
343
|
+
@app.route("/api/kaggle/launch_finetune", methods=["POST"])
|
|
344
|
+
def api_kaggle_launch_finetune():
|
|
345
|
+
data = request.json or {}
|
|
346
|
+
epochs = int(data.get("epochs", 5))
|
|
347
|
+
batch_size = int(data.get("batch_size", 96))
|
|
348
|
+
lr = float(data.get("lr", 0.00005))
|
|
349
|
+
dataset_size = int(data.get("dataset_size", 1558564))
|
|
350
|
+
|
|
351
|
+
cfg = {
|
|
352
|
+
"dataset_mode": "pure_real_scans",
|
|
353
|
+
"dataset_size": dataset_size,
|
|
354
|
+
"epochs": epochs,
|
|
355
|
+
"batch_size": batch_size,
|
|
356
|
+
"lr": lr,
|
|
357
|
+
"device": "kaggle_t4"
|
|
358
|
+
}
|
|
359
|
+
success, msg = global_trainer.start(cfg)
|
|
360
|
+
return jsonify({"status": "ok" if success else "error", "message": msg})
|
|
361
|
+
|
|
362
|
+
def open_browser(port):
|
|
363
|
+
import time
|
|
364
|
+
time.sleep(1.2)
|
|
365
|
+
webbrowser.open(f"http://127.0.0.1:{port}")
|
|
366
|
+
|
|
367
|
+
if __name__ == "__main__":
|
|
368
|
+
PORT = 7860
|
|
369
|
+
print(f"[*] Khởi động AsterOCR Studio độc lập tại: http://127.0.0.1:{PORT}")
|
|
370
|
+
threading.Thread(target=open_browser, args=(PORT,), daemon=True).start()
|
|
371
|
+
app.run(host="127.0.0.1", port=PORT, debug=False, threaded=True)
|