Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- ocr-training/scripts/deploy_files_to_studio.py +59 -0
- ocr-training/scripts/dict_extension_v24.py +101 -0
- ocr-training/scripts/download_v24_zip.py +114 -0
- ocr-training/scripts/export_v24_legacy.py +49 -0
- ocr-training/scripts/extract_coccoc_and_download.py +77 -0
- ocr-training/scripts/generate_data_v24.py +393 -0
- ocr-training/scripts/generate_detector_data.py +159 -62
- ocr-training/scripts/generate_detector_data_v2.py +394 -0
- ocr-training/scripts/get_live_log.py +67 -0
- ocr-training/scripts/kaggle_train_det.py +203 -0
- ocr-training/scripts/kaggle_watchdog_v24.py +170 -0
- ocr-training/scripts/launch_h100_full_pipeline.py +123 -0
- ocr-training/scripts/launch_l40s_pipeline.py +303 -0
- ocr-training/scripts/prepare_v24_notebook.py +276 -0
- ocr-training/scripts/run_l4_interactive.py +303 -0
- ocr-training/scripts/run_lightning_remote.py +146 -0
- ocr-training/scripts/test_live_ocr.py +50 -0
- ocr-training/scripts/train_and_export_h100.py +126 -0
- ocr-training/scripts/weight_surgery_v24.py +167 -0
ocr-training/scripts/deploy_files_to_studio.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Uploads essential Cham-OCR files directly into Lightning AI Studio via chunked base64 payload.
|
| 5 |
+
Bypasses all GitHub private repository restrictions and network throttling.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import base64
|
| 11 |
+
from lightning_sdk import Studio
|
| 12 |
+
|
| 13 |
+
if sys.stdout and sys.stdout.encoding != 'utf-8':
|
| 14 |
+
try:
|
| 15 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 16 |
+
except AttributeError:
|
| 17 |
+
pass
|
| 18 |
+
|
| 19 |
+
def send_file(studio, local_path, remote_path):
|
| 20 |
+
print(f"📦 Đang đẩy tệp {local_path} sang {remote_path}...")
|
| 21 |
+
with open(local_path, "rb") as f:
|
| 22 |
+
content = f.read()
|
| 23 |
+
|
| 24 |
+
b64_str = base64.b64encode(content).decode("ascii")
|
| 25 |
+
chunk_size = 64 * 1024 # 64 KB per chunk
|
| 26 |
+
chunks = [b64_str[i:i + chunk_size] for i in range(0, len(b64_str), chunk_size)]
|
| 27 |
+
|
| 28 |
+
# Initialize remote empty file
|
| 29 |
+
studio.run(f"python3 -c \"import pathlib; p = pathlib.Path('{remote_path}'); p.parent.mkdir(parents=True, exist_ok=True); p.write_text('')\"")
|
| 30 |
+
|
| 31 |
+
for idx, chunk in enumerate(chunks):
|
| 32 |
+
cmd = f"python3 -c \"import pathlib; p = pathlib.Path('{remote_path}'); open('{remote_path}.b64', 'a').write('{chunk}')\""
|
| 33 |
+
studio.run(cmd)
|
| 34 |
+
|
| 35 |
+
# Decode remote b64 to destination
|
| 36 |
+
decode_cmd = f"python3 -c \"import base64, pathlib; data = base64.b64decode(pathlib.Path('{remote_path}.b64').read_text()); pathlib.Path('{remote_path}').write_bytes(data); pathlib.Path('{remote_path}.b64').unlink()\""
|
| 37 |
+
studio.run(decode_cmd)
|
| 38 |
+
|
| 39 |
+
size_out = studio.run(f"ls -lh '{remote_path}'").strip()
|
| 40 |
+
print(f"✅ Hoàn thành: {size_out}")
|
| 41 |
+
|
| 42 |
+
def main():
|
| 43 |
+
studio = Studio(name="cham-det-h100", teamspace="phucsd", org="phucsd-org")
|
| 44 |
+
print("Studio status:", studio.status)
|
| 45 |
+
|
| 46 |
+
files_to_sync = [
|
| 47 |
+
("ocr-training/scripts/generate_detector_data_v2.py", "Cham-OCR/ocr-training/scripts/generate_detector_data_v2.py"),
|
| 48 |
+
("ocr-training/configs/det/ch_PP-OCRv4_det_h100.yml", "Cham-OCR/ocr-training/PaddleOCR/configs/det/ch_PP-OCRv4_det_h100.yml"),
|
| 49 |
+
("ocr-training/configs/det/ch_PP-OCRv4_det_h100.yml", "Cham-OCR/ocr-training/configs/det/ch_PP-OCRv4_det_h100.yml"),
|
| 50 |
+
("ocr-training/data/corpus/cham_text.txt", "Cham-OCR/ocr-training/data/corpus/cham_text.txt"),
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
for local_p, remote_p in files_to_sync:
|
| 54 |
+
send_file(studio, local_p, remote_p)
|
| 55 |
+
|
| 56 |
+
print("\n🎉 Tất cả tệp mã nguồn và cấu hình đã được đẩy lên Studio thành công 100%!")
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
main()
|
ocr-training/scripts/dict_extension_v24.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import hashlib
|
| 5 |
+
|
| 6 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 7 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 8 |
+
|
| 9 |
+
def sha256(path):
|
| 10 |
+
h = hashlib.sha256()
|
| 11 |
+
with open(path, 'rb') as f:
|
| 12 |
+
h.update(f.read())
|
| 13 |
+
return h.hexdigest()
|
| 14 |
+
|
| 15 |
+
def generate_v24_dict(old_dict_path='ocr-training/data/cham_dict_v23.txt',
|
| 16 |
+
new_dict_path='ocr-training/data/cham_dict_v24.txt',
|
| 17 |
+
report_path='ocr-training/output/v24_training/dict_extension_report.json'):
|
| 18 |
+
|
| 19 |
+
os.makedirs(os.path.dirname(os.path.abspath(new_dict_path)), exist_ok=True)
|
| 20 |
+
os.makedirs(os.path.dirname(os.path.abspath(report_path)), exist_ok=True)
|
| 21 |
+
|
| 22 |
+
# 1. Đọc từ điển cũ V23
|
| 23 |
+
with open(old_dict_path, 'r', encoding='utf-8') as f:
|
| 24 |
+
old_lines = f.read().splitlines()
|
| 25 |
+
|
| 26 |
+
old_chars = [line for line in old_lines if line]
|
| 27 |
+
old_dict_size = len(old_chars)
|
| 28 |
+
old_dict_sha = sha256(old_dict_path)
|
| 29 |
+
|
| 30 |
+
# 2. Các ký tự bổ sung cốt lõi cho V24:
|
| 31 |
+
# - ꨲ (U+AA32): Dấu phụ Au/Ue bị thiếu ở V23
|
| 32 |
+
# - ꩁ (U+AA41): Final G trong khối Unicode Chăm
|
| 33 |
+
# - Chữ số Latin và ký hiệu ngắt câu tham chiếu
|
| 34 |
+
required_additions = [
|
| 35 |
+
'ꨲ', 'ꩁ',
|
| 36 |
+
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
| 37 |
+
'[', ']', '(', ')', '.', ',', ';', '-', '/'
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
added_chars = []
|
| 41 |
+
for c in required_additions:
|
| 42 |
+
if c not in old_chars and c not in added_chars:
|
| 43 |
+
added_chars.append(c)
|
| 44 |
+
|
| 45 |
+
new_chars = old_chars + added_chars
|
| 46 |
+
new_dict_size = len(new_chars)
|
| 47 |
+
|
| 48 |
+
with open(new_dict_path, 'w', encoding='utf-8', newline='\n') as f_out:
|
| 49 |
+
for c in new_chars:
|
| 50 |
+
f_out.write(c + '\n')
|
| 51 |
+
|
| 52 |
+
report = {
|
| 53 |
+
"old_dict_size": old_dict_size,
|
| 54 |
+
"new_dict_size": new_dict_size,
|
| 55 |
+
"added_chars": added_chars,
|
| 56 |
+
"old_dict_sha256": old_dict_sha,
|
| 57 |
+
"new_dict_sha256": new_dict_sha
|
| 58 |
+
}
|
| 59 |
+
with open(report_path, 'w', encoding='utf-8') as f_rep:
|
| 60 |
+
json.dump(report, f_rep, indent=2, ensure_ascii=False)
|
| 61 |
+
|
| 62 |
+
print(f"✅ Từ điển V24 đã tạo tại: {new_dict_path}")
|
| 63 |
+
print(f" Số lượng ký tự: {old_dict_size} -> {new_dict_size} (Thêm {len(added_chars)} ký tự)")
|
| 64 |
+
print(f" Ký tự bổ sung: {' '.join(added_chars)}")
|
| 65 |
+
return new_dict_path, report
|
| 66 |
+
|
| 67 |
+
def find_assets_dir():
|
| 68 |
+
candidates = [
|
| 69 |
+
"ocr-training/data/cham-ocr-v5-assets",
|
| 70 |
+
"/kaggle/input/cham-ocr-v5-assets",
|
| 71 |
+
"/kaggle/input/datasets/gustavnguyen/cham-ocr-v5-assets"
|
| 72 |
+
]
|
| 73 |
+
for c in candidates:
|
| 74 |
+
if os.path.exists(c):
|
| 75 |
+
return c
|
| 76 |
+
if os.path.exists('/kaggle/input'):
|
| 77 |
+
for root, dirs, files in os.walk('/kaggle/input'):
|
| 78 |
+
if 'v23_checkpoint' in dirs:
|
| 79 |
+
return root
|
| 80 |
+
return "ocr-training/data/cham-ocr-v5-assets"
|
| 81 |
+
|
| 82 |
+
def main():
|
| 83 |
+
import argparse
|
| 84 |
+
parser = argparse.ArgumentParser()
|
| 85 |
+
parser.add_argument('--assets-dir', type=str, default=None)
|
| 86 |
+
parser.add_argument('--old-dict', type=str, default=None)
|
| 87 |
+
parser.add_argument('--new-dict', type=str, default=None)
|
| 88 |
+
args = parser.parse_args()
|
| 89 |
+
|
| 90 |
+
assets_dir = args.assets_dir or find_assets_dir()
|
| 91 |
+
old_dict = args.old_dict or os.path.join(assets_dir, 'v23_checkpoint', 'cham_dict_v23.txt')
|
| 92 |
+
if not os.path.exists(old_dict):
|
| 93 |
+
old_dict = 'ocr-training/data/cham_dict_v23.txt'
|
| 94 |
+
|
| 95 |
+
new_dict = args.new_dict or ('/kaggle/working/data/cham_dict_v24.txt' if os.path.exists('/kaggle') else 'ocr-training/data/cham_dict_v24.txt')
|
| 96 |
+
report_path = '/kaggle/working/output/v24_training/dict_extension_report.json' if os.path.exists('/kaggle') else 'ocr-training/output/v24_training/dict_extension_report.json'
|
| 97 |
+
|
| 98 |
+
generate_v24_dict(old_dict_path=old_dict, new_dict_path=new_dict, report_path=report_path)
|
| 99 |
+
|
| 100 |
+
if __name__ == '__main__':
|
| 101 |
+
main()
|
ocr-training/scripts/download_v24_zip.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import time
|
| 4 |
+
import zipfile
|
| 5 |
+
import shutil
|
| 6 |
+
|
| 7 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 8 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 9 |
+
|
| 10 |
+
os.environ["KAGGLE_USERNAME"] = "gustavnguyen"
|
| 11 |
+
os.environ["KAGGLE_KEY"] = "6bf56db7e5c0fa7895d157167961d92b"
|
| 12 |
+
|
| 13 |
+
from kaggle.api.kaggle_api_extended import KaggleApi
|
| 14 |
+
from kagglesdk.kernels.types.kernels_api_service import ApiDownloadKernelOutputRequest
|
| 15 |
+
|
| 16 |
+
def download_and_deploy():
|
| 17 |
+
print("=" * 70)
|
| 18 |
+
print("🚀 TẢI TRỰC TIẾP MÔ HÌNH V24 TỪ KAGGLE BẰNG API CHUYÊN DỤNG")
|
| 19 |
+
print("=" * 70)
|
| 20 |
+
|
| 21 |
+
save_dir = "ocr-training/output/v24"
|
| 22 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 23 |
+
target_zip_name = "rec_cham_v24_best.zip"
|
| 24 |
+
local_zip_path = os.path.join(save_dir, target_zip_name)
|
| 25 |
+
|
| 26 |
+
print(f"📡 Đang kết nối tới máy chủ Kaggle API...")
|
| 27 |
+
api = KaggleApi()
|
| 28 |
+
api.authenticate()
|
| 29 |
+
|
| 30 |
+
t0 = time.time()
|
| 31 |
+
with api.build_kaggle_client() as kaggle:
|
| 32 |
+
req = ApiDownloadKernelOutputRequest()
|
| 33 |
+
req.owner_slug = "gustavnguyen"
|
| 34 |
+
req.kernel_slug = "paddleocr-cham-finetune"
|
| 35 |
+
req.file_path = target_zip_name
|
| 36 |
+
|
| 37 |
+
print(f"📥 Đang lấy Direct URL của '{target_zip_name}'...")
|
| 38 |
+
res = kaggle.kernels.kernels_api_client.download_kernel_output(req)
|
| 39 |
+
download_url = res.url
|
| 40 |
+
print(f"🔗 Direct URL: {download_url[:70]}...")
|
| 41 |
+
|
| 42 |
+
print(f"⚡ Bắt đầu tải tệp về bằng curl (tối đa băng thông, tự động khôi phục)...")
|
| 43 |
+
cmd = f'curl.exe -L --retry 10 --retry-delay 2 --retry-all-errors -o "{local_zip_path}" "{download_url}"'
|
| 44 |
+
ret = os.system(cmd)
|
| 45 |
+
if ret != 0 or not os.path.exists(local_zip_path) or os.path.getsize(local_zip_path) < 100 * 1024 * 1024:
|
| 46 |
+
print(f"❌ Tải thất bại hoặc tệp không đủ kích thước ({os.path.getsize(local_zip_path) if os.path.exists(local_zip_path) else 0} bytes)")
|
| 47 |
+
return False
|
| 48 |
+
|
| 49 |
+
print(f"\n✅ Đã tải xong tệp ({os.path.getsize(local_zip_path)/(1024*1024):.2f} MB) trong {time.time()-t0:.1f}s!")
|
| 50 |
+
|
| 51 |
+
# Giải nén
|
| 52 |
+
extract_folder = os.path.join(save_dir, "extracted_v24")
|
| 53 |
+
if os.path.exists(extract_folder):
|
| 54 |
+
shutil.rmtree(extract_folder, ignore_errors=True)
|
| 55 |
+
os.makedirs(extract_folder, exist_ok=True)
|
| 56 |
+
|
| 57 |
+
print(f"\n📦 Đang giải nén vào '{extract_folder}'...")
|
| 58 |
+
with zipfile.ZipFile(local_zip_path, 'r') as zf:
|
| 59 |
+
zf.extractall(extract_folder)
|
| 60 |
+
names = zf.namelist()
|
| 61 |
+
print(f" Đã giải nén {len(names)} tệp: {names[:10]}")
|
| 62 |
+
|
| 63 |
+
# Tìm thư mục inference
|
| 64 |
+
candidates = [
|
| 65 |
+
os.path.join(extract_folder, "rec_cham_inference_v24"),
|
| 66 |
+
os.path.join(extract_folder, "output", "rec_cham_inference_v24"),
|
| 67 |
+
extract_folder
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
found_infer = None
|
| 71 |
+
for cand in candidates:
|
| 72 |
+
if os.path.exists(os.path.join(cand, "inference.pdmodel")) or os.path.exists(os.path.join(cand, "model.pdmodel")):
|
| 73 |
+
found_infer = cand
|
| 74 |
+
break
|
| 75 |
+
|
| 76 |
+
# Thư mục đích trong Studio
|
| 77 |
+
studio_target = "ocr-studio/data/output/rec_cham_inference_v24"
|
| 78 |
+
os.makedirs(studio_target, exist_ok=True)
|
| 79 |
+
|
| 80 |
+
if found_infer:
|
| 81 |
+
print(f"\n🚀 Đồng bộ mô hình inference vào Web Studio: '{studio_target}'")
|
| 82 |
+
for item in os.listdir(found_infer):
|
| 83 |
+
s_p = os.path.join(found_infer, item)
|
| 84 |
+
d_p = os.path.join(studio_target, item)
|
| 85 |
+
if os.path.isfile(s_p):
|
| 86 |
+
shutil.copy2(s_p, d_p)
|
| 87 |
+
print(f" + {item} ({os.path.getsize(d_p)/(1024*1024):.2f} MB)")
|
| 88 |
+
else:
|
| 89 |
+
# Nếu tệp nằm phân tán
|
| 90 |
+
for root, dirs, files in os.walk(extract_folder):
|
| 91 |
+
for f in files:
|
| 92 |
+
if f.endswith('.pdmodel') or f.endswith('.pdiparams') or f.endswith('.yml'):
|
| 93 |
+
s_p = os.path.join(root, f)
|
| 94 |
+
d_p = os.path.join(studio_target, f)
|
| 95 |
+
shutil.copy2(s_p, d_p)
|
| 96 |
+
print(f" + {f} ({os.path.getsize(d_p)/(1024*1024):.2f} MB)")
|
| 97 |
+
|
| 98 |
+
# Đảm bảo copy từ điển V24 (103 ký tự)
|
| 99 |
+
dict_v24_src = "ocr-training/data/cham_dict_v24.txt"
|
| 100 |
+
if os.path.exists(dict_v24_src):
|
| 101 |
+
shutil.copy2(dict_v24_src, os.path.join(studio_target, "cham_dict_v24.txt"))
|
| 102 |
+
print(f" + Đã đồng bộ từ điển V24: {dict_v24_src} -> {studio_target}/cham_dict_v24.txt")
|
| 103 |
+
|
| 104 |
+
print("\n" + "=" * 70)
|
| 105 |
+
print("🎉 TRIỂN KHAI MÔ HÌNH V24 VÀO HỆ THỐNG THÀNH CÔNG 100%!")
|
| 106 |
+
print(f"📁 Thư mục inference: {studio_target}")
|
| 107 |
+
for item in os.listdir(studio_target):
|
| 108 |
+
p = os.path.join(studio_target, item)
|
| 109 |
+
print(f" - {item} ({os.path.getsize(p)/(1024*1024):.2f} MB)")
|
| 110 |
+
print("=" * 70)
|
| 111 |
+
return True
|
| 112 |
+
|
| 113 |
+
if __name__ == '__main__':
|
| 114 |
+
download_and_deploy()
|
ocr-training/scripts/export_v24_legacy.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# NumPy 2.x compatibility monkeypatch
|
| 5 |
+
import numpy as np
|
| 6 |
+
if not hasattr(np, 'sctypes'):
|
| 7 |
+
np.sctypes = {'int': [np.int8, np.int16, np.int32, np.int64], 'uint': [np.uint8, np.uint16, np.uint32, np.uint64], 'float': [np.float16, np.float32, np.float64], 'complex': [np.complex64, np.complex128], 'others': [bool, object, bytes, str]}
|
| 8 |
+
if not hasattr(np, 'bool'): np.bool = bool
|
| 9 |
+
if not hasattr(np, 'int'): np.int = int
|
| 10 |
+
if not hasattr(np, 'float'): np.float = float
|
| 11 |
+
if not hasattr(np, 'typeDict'): np.typeDict = {}
|
| 12 |
+
|
| 13 |
+
import yaml
|
| 14 |
+
import paddle
|
| 15 |
+
|
| 16 |
+
# Add PaddleOCR to sys.path
|
| 17 |
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 18 |
+
PADDLEOCR_DIR = os.path.join(BASE_DIR, "ocr-studio", "PaddleOCR")
|
| 19 |
+
if PADDLEOCR_DIR not in sys.path:
|
| 20 |
+
sys.path.insert(0, PADDLEOCR_DIR)
|
| 21 |
+
|
| 22 |
+
from ppocr.utils.export_model import export
|
| 23 |
+
|
| 24 |
+
def run_export():
|
| 25 |
+
config_path = os.path.join(BASE_DIR, "ocr-training", "configs", "rec_cham_v24.yml")
|
| 26 |
+
ckpt_path = os.path.join(BASE_DIR, "ocr-training", "output", "v24", "extracted_v24", "best_checkpoint", "best_accuracy")
|
| 27 |
+
save_dir = os.path.join(BASE_DIR, "ocr-studio", "data", "output", "rec_cham_inference_v24")
|
| 28 |
+
dict_path = os.path.join(BASE_DIR, "ocr-training", "data", "cham_dict_v24.txt")
|
| 29 |
+
|
| 30 |
+
print(f"Loading config from: {config_path}")
|
| 31 |
+
with open(config_path, 'r', encoding='utf-8') as f:
|
| 32 |
+
config = yaml.safe_load(f)
|
| 33 |
+
|
| 34 |
+
config["Global"]["pretrained_model"] = ckpt_path
|
| 35 |
+
config["Global"]["checkpoints"] = None
|
| 36 |
+
config["Global"]["character_dict_path"] = dict_path
|
| 37 |
+
config["Global"]["save_inference_dir"] = save_dir
|
| 38 |
+
config["Global"]["export_with_pir"] = False # Force legacy .pdmodel format via OldIrGuard!
|
| 39 |
+
|
| 40 |
+
print(f"Exporting model with export_with_pir=False to {save_dir}...")
|
| 41 |
+
export(config)
|
| 42 |
+
|
| 43 |
+
print("\n✅ Export completed successfully! Listing files in save_dir:")
|
| 44 |
+
for f in os.listdir(save_dir):
|
| 45 |
+
fp = os.path.join(save_dir, f)
|
| 46 |
+
print(f" - {f}: {os.path.getsize(fp):,} bytes")
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
run_export()
|
ocr-training/scripts/extract_coccoc_and_download.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import sqlite3
|
| 5 |
+
import shutil
|
| 6 |
+
import base64
|
| 7 |
+
import requests
|
| 8 |
+
import win32crypt
|
| 9 |
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
| 10 |
+
|
| 11 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 12 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 13 |
+
|
| 14 |
+
def get_coccoc_kaggle_cookies():
|
| 15 |
+
user_data = os.path.expandvars(r'%LOCALAPPDATA%\CocCoc\Browser\User Data')
|
| 16 |
+
local_state_path = os.path.join(user_data, 'Local State')
|
| 17 |
+
|
| 18 |
+
if not os.path.exists(local_state_path):
|
| 19 |
+
print("❌ Không tìm thấy thư mục Cốc Cốc User Data!")
|
| 20 |
+
return {}
|
| 21 |
+
|
| 22 |
+
with open(local_state_path, 'r', encoding='utf-8') as f:
|
| 23 |
+
local_state = json.load(f)
|
| 24 |
+
|
| 25 |
+
encrypted_key = base64.b64decode(local_state['os_crypt']['encrypted_key'])[5:]
|
| 26 |
+
master_key = win32crypt.CryptUnprotectData(encrypted_key, None, None, None, 0)[1]
|
| 27 |
+
|
| 28 |
+
cookies_db = os.path.join(user_data, 'Default', 'Network', 'Cookies')
|
| 29 |
+
temp_db = 'temp_coccoc_cookies.db'
|
| 30 |
+
shutil.copy2(cookies_db, temp_db)
|
| 31 |
+
|
| 32 |
+
conn = sqlite3.connect(temp_db)
|
| 33 |
+
cursor = conn.cursor()
|
| 34 |
+
cursor.execute("SELECT name, encrypted_value FROM cookies WHERE host_key LIKE '%kaggle.com%'")
|
| 35 |
+
|
| 36 |
+
kaggle_cookies = {}
|
| 37 |
+
for name, enc_val in cursor.fetchall():
|
| 38 |
+
try:
|
| 39 |
+
if enc_val[:3] == b'v10':
|
| 40 |
+
nonce = enc_val[3:15]
|
| 41 |
+
ciphertext = enc_val[15:-16]
|
| 42 |
+
tag = enc_val[-16:]
|
| 43 |
+
cipher = Cipher(algorithms.AES(master_key), modes.GCM(nonce, tag))
|
| 44 |
+
decryptor = cipher.decryptor()
|
| 45 |
+
val = decryptor.update(ciphertext) + decryptor.finalize()
|
| 46 |
+
kaggle_cookies[name] = val.decode('utf-8', errors='ignore')
|
| 47 |
+
except Exception:
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
conn.close()
|
| 51 |
+
if os.path.exists(temp_db):
|
| 52 |
+
os.remove(temp_db)
|
| 53 |
+
|
| 54 |
+
return kaggle_cookies
|
| 55 |
+
|
| 56 |
+
def main():
|
| 57 |
+
print("🔑 Đang trích xuất phiên đăng nhập Kaggle từ trình duyệt Cốc Cốc...")
|
| 58 |
+
cookies = get_coccoc_kaggle_cookies()
|
| 59 |
+
print(f"✅ Đã tìm thấy {len(cookies)} cookies của kaggle.com:")
|
| 60 |
+
for k in cookies:
|
| 61 |
+
print(f" - {k}")
|
| 62 |
+
|
| 63 |
+
session = requests.Session()
|
| 64 |
+
session.cookies.update(cookies)
|
| 65 |
+
session.headers.update({
|
| 66 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
| 67 |
+
"Referer": "https://www.kaggle.com/code/gustavnguyen/paddleocr-cham-finetune/output"
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
# Kiểm tra xác thực
|
| 71 |
+
res = session.get("https://www.kaggle.com/api/i/users.UserService/GetCurrentUser")
|
| 72 |
+
print(f"Current User API status: {res.status_code}")
|
| 73 |
+
if res.status_code == 200:
|
| 74 |
+
print("User Info:", res.text[:200])
|
| 75 |
+
|
| 76 |
+
if __name__ == '__main__':
|
| 77 |
+
main()
|
ocr-training/scripts/generate_data_v24.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import random
|
| 4 |
+
import time
|
| 5 |
+
import cv2
|
| 6 |
+
import numpy as np
|
| 7 |
+
import json
|
| 8 |
+
import argparse
|
| 9 |
+
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
|
| 10 |
+
from fontTools.ttLib import TTFont
|
| 11 |
+
import albumentations as A
|
| 12 |
+
from multiprocessing import Pool, cpu_count
|
| 13 |
+
|
| 14 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 15 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 16 |
+
|
| 17 |
+
# ==============================================================================
|
| 18 |
+
# 1. Ký Tự & Cấu Trúc Ngữ Liệu Tiếng Chăm V24
|
| 19 |
+
# ==============================================================================
|
| 20 |
+
CHAM_CONSONANTS = list("ꨆꨇꨈꨉꨊꨋꨌꨍꨎꨏꨐꨑꨒꨓꨔꨕꨖꨗꨘꨙꨚꨛꨜꨝꨞꨟꨠꨡꨢꨣꨤꨥꨦꨧꨨꨀꨁꨂꨃꨄꨅ")
|
| 21 |
+
CHAM_DIGITS_LIST = list("꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙")
|
| 22 |
+
CHAM_PUNCT_LIST = list("꩜꩝꩞꩟")
|
| 23 |
+
LATIN_DIGITS = list("0123456789")
|
| 24 |
+
PUNCT_CHARS = list("[]().,;:/-")
|
| 25 |
+
|
| 26 |
+
# Dấu phụ đặc biệt
|
| 27 |
+
DIACRITIC_AU = 'ꨲ' # U+AA32 (mới bổ sung vào từ điển V24)
|
| 28 |
+
DIACRITIC_O = 'ꨶ' # U+AA36 (đối kháng với Au)
|
| 29 |
+
|
| 30 |
+
# Cụm từ khó & Mẫu khổ thơ thực nghiệm từ 57 khổ thơ cổ
|
| 31 |
+
CORE_STANZA_WORDS = [
|
| 32 |
+
"ꨀꨆꨢꨯꨮꩅ", "ꨧꨪ", "ꨚꨗꩍ", "ꨓꨶꨮꩉ", "ꨓꨝꨳꩀ", "ꨚꨗꨶꨮꩄ", "ꨆꨭꨖꨩ", "ꨔꩅ",
|
| 33 |
+
"ꨨꨤꨭꩆ", "ꨋꩇ", "ꨝꨩ", "ꨤꨪꨠꩍ", "ꨦꩀꨆꨣꨰ", "ꨝꨪꨗꨴꨪꩀ", "ꨕꨨꨵꩀ", "ꨥꩀ",
|
| 34 |
+
"ꨎꩊ", "ꨈꨴꨭꩀ", "ꨆꨔꨯꨱꩅ", "ꨣꨪꨡꩍ", "ꨚꨴꨯꨱꩃ", "ꨤꨯꨩ", "ꨕꨫ", "ꨣꨳꨪꩌ",
|
| 35 |
+
"ꨚꨆꩉ", "ꨆꨘꩊ", "ꨣꨩ", "ꨓꨨꨩ", "ꨙꨯꩌ", "ꨀꨇꨩꩆ", "ꨕꨤꩌ", "ꨕꨭꨤꨪꨆꩊ",
|
| 36 |
+
"ꨧꨳꩌ", "ꨧꨭꨩ", "ꨏꩀ", "ꨕꨯꨱꩀ", "ꨨꨚꩀ", "ꨕꨯꨮꨝꨪꨓꨩ", "ꨀꨲꩆ", "ꨓꨘꩆ",
|
| 37 |
+
"ꨌꩃ", "ꨆꨩ", "ꨔꨬ", "ꨤꨪꨆꨮꨭ", "ꨎꩃ", "ꨅꨩ", "ꨨꨭꨩ", "ꨟꩃ",
|
| 38 |
+
"ꨕꨯꨮꩍ", "ꨧꨗꨪ", "ꨚꨮꩃ", "ꨙꨯꨱꩌ", "ꨚꨣꩆ", "ꨨꨕꨮꩉ", "ꨝꨪꨣꨭꨥ", "ꨠꨡꨯꩍ",
|
| 39 |
+
"ꨛꨯꨮ", "ꨠꨣꨰ", "ꨀꨝꨪꩍ", "ꨕꨴꨬ", "ꨀꨕꨬ", "ꨧꨀꨰ", "ꨕꨣꨩ", "ꨀꨮꩆ",
|
| 40 |
+
"ꨓꨝꨶꨮꩆ", "ꨚꨣꨪꩀ", "ꨚꨗꨴꩃ", "ꨆꨴꨯꨱꩃ", "ꨚꨎꨰ", "ꨟꨰ", "ꨨꨶꨮꩄ", "ꨆꨙꨩ",
|
| 41 |
+
"ꨢꨶꨮꩆ", "ꨐꨭꨩ", "ꨉꩌ", "ꨌꩌ", "ꨂꨣꩃ", "ꨤꩄ", "ꨆꨳꨮꩃ", "ꨕꨯꨱꩃ",
|
| 42 |
+
"ꨠꨢꩍ", "ꨨꨕꨬ", "ꨎꨳꨯꨮꩃ", "ꨎꨶꨰ", "ꨡꨶꩍ", "ꨆꩉ", "ꨆꨮꨭ", "ꨧꨭꨗꨪꩅ",
|
| 43 |
+
"ꨈꨪꨗꨴꨮꩍ", "ꨤꨪꨝꨰꩍ", "ꨣꩈ", "ꨕꨨꨵꨮꨭ", "ꨨꨤꨬ", "ꨀꨗꩀ", "ꨕꩀ", "ꨓꨆꨴꨲꨩ",
|
| 44 |
+
"ꨤꨪꨨꨪꩀ", "ꨚꨓꨯꨱ", "ꨓꨈꨯꩀ", "ꨧꨴꨪ", "ꨝꨪꨗꩈ", "ꨤꨯꨱꨥ", "ꨧꨯꨱꩃ", "ꨚꨣꨶꨬ",
|
| 45 |
+
"ꨠꨓꨭꨥ", "ꨈꨮꩇ", "ꨓꨟꨩ", "ꨓꨮꩊ", "ꨘꨈꨮꩉ", "ꨚꨴꩃꨕꨣꩃ", "ꨆꨵꨯꨱꩃ", "ꨓꨴꨭꩆ",
|
| 46 |
+
"ꨚꨆꨴꨮꩃ", "ꨚꨣꨯꩀ", "ꨌꨮꩀ", "ꨆꨤꩆ", "ꨚꨟꨆꨴꨲꨩ", "ꨣꨭꩇ", "ꨗꩌ", "ꨟꩀ",
|
| 47 |
+
"ꨣꨯꩀ", "ꨨꨟꨭꨩ", "ꨝꨮꩀ", "ꨝꨪꨗꩀ", "ꨆꨶꨮꩄ", "ꨣꨪꨝꨯꨱꩃ", "ꨨꨟꨪꩅ", "ꨝꨶꨮꩊ",
|
| 48 |
+
"ꨣꨪꨝꨭꨥ", "ꨓꨟꩆ", "ꨙꨪꩀ", "ꨀꨨꨯꨱꩀ", "ꨣꨰ", "ꨠꨧꨭꩍ", "ꨞꨯꨚꨓꨪꩍ", "ꨧꩀꨓꨎꨰ",
|
| 49 |
+
"ꨚꨙꩉ", "ꨓꨤꨬ", "ꨈꨰ", "ꨌꩀ", "ꨚꨯꨱꩍ", "ꨌꨳꨪꩇ", "ꨀꨤꩍ", "ꨅꨩ",
|
| 50 |
+
"ꨎꨰ", "ꨚꨓꨴꨰ", "ꨓꨚꩍ", "ꨧꨣꨪ", "ꨎꨝꩅ", "ꨧꨪꨤꩌ", "ꨥꨮꩀ", "ꨚꨤꨬ",
|
| 51 |
+
"ꨠꨩ", "ꨗꨫ", "ꨧꨩ", "ꨆꨶꩍ", "ꨀꨆꨯꨱꩀ", "ꨘꩆ", "ꨤꨶꨪꩄ", "ꨚꨗꨴꨯꨱꩃ",
|
| 52 |
+
"ꨎꨝꨶꨮꩊ", "ꨧꨪꩆꨝꨳꨪ", "ꨚꨯꩀ", "ꨨꨝꩉ", "ꨝꨪꩊꨔꨶꨮꩉ", "ꨤꨪꨟꩆ", "ꨗꨯꨱ",
|
| 53 |
+
"ꨀꨓꨯꨱꩃ", "ꨎꨮꩀ", "ꨚꨟꨓꨰ", "ꨠꨖꨪꩉ", "ꨕꨣꩍ", "ꨙꨶꨮꩄ", "ꨕꨮꩇ", "ꨕꨗꨯꨱ",
|
| 54 |
+
"ꨧꨭꨟꨭꨩ", "ꨆꨯꨮꩃ", "ꨀꨧꨰꩍ", "ꨇꨶꨰ", "ꨆꨆꨭꩍ", "ꨌꨳꨪꩇ", "ꨓꨚꩀ", "ꨇꨪꩆ",
|
| 55 |
+
"ꨓꨴꨩ", "ꨝꨪꨙꨪ", "ꨝꨪꨗꨳꨪ", "ꨝꨳꩀ", "ꨣꨖꨪ", "ꨖꨶꨮꩆ", "ꨅꩍ", "ꨚꨤꨰ",
|
| 56 |
+
"ꨕꨶꩍ", "ꨆꨭꨟꨬ", "ꨝꨪꨗꨰ", "ꨗꨯꨣꨚꩅ", "ꨓꨯꩀ", "ꨧꩍ", "ꨖꨮꩉ", "ꨜꨶꨮꩊ",
|
| 57 |
+
"ꨚꨵꨮꩀ", "ꨤꨪꨆꨭꩀ", "ꨚꨕꨮꩍ", "ꨚꨕꨬ", "ꨣꨤꨯꨩ", "ꨥꩃ", "ꨝꨪ", "ꨞꨯꨕꨮꩉꨨꨩ",
|
| 58 |
+
"ꨆꨚꩊ", "ꨢꩅꨓꨴꩀ", "ꨈꨪꨗꨶꨮꩉ", "ꨘꨨꨶꨮꩉ", "ꨃꨥ", "ꨉꩀ", "ꨝꨯꨱꩃ", "ꨔꨶꩀ",
|
| 59 |
+
"ꨝꩅ", "ꨚꨤꨪꨕꨯꨱ", "ꨆꨣꨳꩀ", "ꨧꨪꨝꩉ", "ꨒꨮꩉ", "ꨍꨰꩅ", "ꨓꨥꩀ", "ꨀꨤꨩ",
|
| 60 |
+
"ꨠꨕꨬ", "ꨌꨰꩀ", "ꨠꨘꩃ", "ꨝꨪꩊꨘꨔꨶꨮꩉ", "ꨠꨝꨰ", "ꨢꨮꨭ", "ꨢꨶꩀ", "ꨝꨵꨯꨱꩍ",
|
| 61 |
+
"ꨆꨆꨬ", "ꨚꨈꨶꨮꩆ", "ꨆꨵꨮꨭ", "ꨨꨣꨬ", "ꨟꨮꨣꨰ", "ꨡꨯꩍ", "ꨕꨯꨱꨍ"
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
# Các mẫu đối kháng và dấu 3 tầng
|
| 65 |
+
DIACRITIC_PAIRS = [
|
| 66 |
+
("ꨀꨲꩆ", "ꨀꨶꩆ"),
|
| 67 |
+
("ꨓꨆꨴꨲꨩ", "ꨓꨆꨴꨶꨩ"),
|
| 68 |
+
("ꨚꨴꨲꨩ", "ꨚꨴꨶꨩ"),
|
| 69 |
+
("ꨆꨲꩆ", "ꨆꨶꩆ"),
|
| 70 |
+
("ꨟꨲ���", "ꨟꨶꩆ"),
|
| 71 |
+
("ꨣꨲꨩ", "ꨣꨶꨩ"),
|
| 72 |
+
("ꨚꨟꨆꨴꨲꨩ", "ꨚꨟꨆꨴꨶꨩ")
|
| 73 |
+
]
|
| 74 |
+
|
| 75 |
+
THREE_TIER_CLUSTERS = [
|
| 76 |
+
"ꨣꨳꨪꩌ", "ꨓꨳꨪꩌ", "ꨚꨳꨪꩌ", "ꨕꨨꨵꩀ", "ꨝꨪꨗꨳꨪ",
|
| 77 |
+
"ꨨꨕꨳꨮꩇ", "ꨞꨯꨚꨓꨪꩍ", "ꨧꨪꩆꨝꨳꨪ", "ꨆꨵꨯꨱꩃ", "ꨚꨗꨴꨯꨱꩃ"
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
PRE_VOWEL_PATTERNS = [
|
| 81 |
+
"ꨗꨯꨣꨚꨮꩅ", "ꨆꨴꨯꩅ", "ꨛꨯꨮ", "ꨚꨓꨯꨱ", "ꨈꨪꨗꨯꨱꩃ",
|
| 82 |
+
"ꨕꨯꨮꩍ", "ꨠꨡꨯꩍ", "ꨕꨯꨱꩃ", "ꨞꨯꨚꨓꨪꩍ", "ꨚꨓꨯꨱ"
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
def to_cham_num(n):
|
| 86 |
+
"""Chuyển số nguyên n thành chuỗi chữ số Chăm"""
|
| 87 |
+
return "".join(CHAM_DIGITS_LIST[int(d)] for d in str(n))
|
| 88 |
+
|
| 89 |
+
# ==============================================================================
|
| 90 |
+
# 2. Font Validator & Bộ Sinh Mẫu Văn Bản V24
|
| 91 |
+
# ==============================================================================
|
| 92 |
+
class FontValidator:
|
| 93 |
+
def __init__(self, font_paths):
|
| 94 |
+
self.font_cmaps = {}
|
| 95 |
+
for path in font_paths:
|
| 96 |
+
try:
|
| 97 |
+
tt = TTFont(path)
|
| 98 |
+
cmap = set()
|
| 99 |
+
for table in tt['cmap'].tables:
|
| 100 |
+
cmap.update(table.cmap.keys())
|
| 101 |
+
self.font_cmaps[path] = cmap
|
| 102 |
+
except Exception as e:
|
| 103 |
+
self.font_cmaps[path] = set()
|
| 104 |
+
|
| 105 |
+
def supports_string(self, font_path, text):
|
| 106 |
+
supported_set = self.font_cmaps.get(font_path, set())
|
| 107 |
+
for char in text:
|
| 108 |
+
cp = ord(char)
|
| 109 |
+
if char in " \t\n\r" or cp < 128:
|
| 110 |
+
continue
|
| 111 |
+
if cp not in supported_set:
|
| 112 |
+
return False
|
| 113 |
+
return True
|
| 114 |
+
|
| 115 |
+
def sample_text_v24():
|
| 116 |
+
"""
|
| 117 |
+
Sinh dòng văn bản tiếng Chăm V24 tích hợp 5 nhóm kịch bản:
|
| 118 |
+
1. Stanza numbers 1-99 (꩑꩞ - ꩙꩙꩞)
|
| 119 |
+
2. Double Danda ꩝꩝ kết thúc câu
|
| 120 |
+
3. Cặp dấu đối kháng ꨲ vs ꨶ
|
| 121 |
+
4. Cụm dấu 3 tầng và nguyên âm trước ꨯ, ꨯꨱ
|
| 122 |
+
5. Văn bản thông thường kết hợp số tham chiếu Latin
|
| 123 |
+
"""
|
| 124 |
+
cat = random.choices(
|
| 125 |
+
['stanza', 'double_danda', 'diacritics', 'three_tier', 'normal', 'latin_mixed'],
|
| 126 |
+
weights=[0.25, 0.15, 0.15, 0.15, 0.20, 0.10]
|
| 127 |
+
)[0]
|
| 128 |
+
|
| 129 |
+
if cat == 'stanza':
|
| 130 |
+
# Số thứ tự khổ thơ Chăm từ 1 đến 99
|
| 131 |
+
num_val = random.randint(1, 99)
|
| 132 |
+
cham_num_str = to_cham_num(num_val)
|
| 133 |
+
sep = random.choice(['꩞ ', '꩝ '])
|
| 134 |
+
# Lấy 2-4 từ nội dung
|
| 135 |
+
content = " ".join(random.sample(CORE_STANZA_WORDS, random.randint(2, 4)))
|
| 136 |
+
end_punct = random.choice(['꩝', '꩝꩝', ''])
|
| 137 |
+
return f"{cham_num_str}{sep}{content}{end_punct}"
|
| 138 |
+
|
| 139 |
+
elif cat == 'double_danda':
|
| 140 |
+
# Câu kết thúc với Double Danda
|
| 141 |
+
content = " ".join(random.sample(CORE_STANZA_WORDS, random.randint(2, 5)))
|
| 142 |
+
return f"{content}꩝꩝"
|
| 143 |
+
|
| 144 |
+
elif cat == 'diacritics':
|
| 145 |
+
# Hard examples chứa cặp ꨲ vs ꨶ
|
| 146 |
+
pair = random.choice(DIACRITIC_PAIRS)
|
| 147 |
+
word = pair[0] if random.random() < 0.6 else pair[1]
|
| 148 |
+
surrounding = random.sample(CORE_STANZA_WORDS, random.randint(1, 3))
|
| 149 |
+
return f"{word} " + " ".join(surrounding) + random.choice(['꩝', '꩝꩝', ''])
|
| 150 |
+
|
| 151 |
+
elif cat == 'three_tier':
|
| 152 |
+
# Cụm 3 tầng hoặc nguyên âm trước
|
| 153 |
+
tier_w = random.choice(THREE_TIER_CLUSTERS + PRE_VOWEL_PATTERNS)
|
| 154 |
+
surrounding = random.sample(CORE_STANZA_WORDS, random.randint(2, 3))
|
| 155 |
+
return f"{tier_w} " + " ".join(surrounding) + random.choice(['꩝', '꩝꩝', ''])
|
| 156 |
+
|
| 157 |
+
elif cat == 'latin_mixed':
|
| 158 |
+
# Kết hợp số Latin hoặc dấu ngoặc tham chiếu
|
| 159 |
+
w1 = random.choice(CORE_STANZA_WORDS)
|
| 160 |
+
w2 = random.choice(CORE_STANZA_WORDS)
|
| 161 |
+
num = random.randint(1, 999)
|
| 162 |
+
style = random.choice([f"({num})", f"[{num}]", f"{num}", f": {num}"])
|
| 163 |
+
return f"{w1} {w2} {style}"
|
| 164 |
+
|
| 165 |
+
else: # normal
|
| 166 |
+
n_words = random.randint(2, 5)
|
| 167 |
+
text = " ".join(random.sample(CORE_STANZA_WORDS, n_words))
|
| 168 |
+
if random.random() < 0.5:
|
| 169 |
+
text += random.choice(['꩝', '꩝꩝', ''])
|
| 170 |
+
return text
|
| 171 |
+
|
| 172 |
+
# ==============================================================================
|
| 173 |
+
# 3. Render Dòng Chữ Kèm Tinh Chỉnh Khoảng Cách Double Danda
|
| 174 |
+
# ==============================================================================
|
| 175 |
+
def render_text_v24(text, font_path, font_size=24, pad_w=15, pad_h=8, bg_color=(255, 255, 255), fg_color=(0, 0, 0)):
|
| 176 |
+
font = ImageFont.truetype(font_path, font_size)
|
| 177 |
+
dummy_img = Image.new('RGB', (100, 100))
|
| 178 |
+
dummy_draw = ImageDraw.Draw(dummy_img)
|
| 179 |
+
|
| 180 |
+
# Xử lý biến thiên khoảng cách Double Danda nếu có ꩝꩝
|
| 181 |
+
if '꩝꩝' in text:
|
| 182 |
+
# Ngẫu nhiên tách 2 nét gạch từ 1px đến 7px
|
| 183 |
+
gap_px = random.randint(1, 7)
|
| 184 |
+
# Thay thế tạm bằng ký tự giả định hoặc render 2 pass
|
| 185 |
+
# Đơn giản nhất: chèn khoảng trống zero-width hoặc thin space
|
| 186 |
+
if gap_px > 3:
|
| 187 |
+
text = text.replace('꩝꩝', '꩝ ꩝' if gap_px > 5 else '꩝\u200a꩝')
|
| 188 |
+
|
| 189 |
+
bbox = dummy_draw.textbbox((0, 0), text, font=font)
|
| 190 |
+
text_w = max(10, bbox[2] - bbox[0])
|
| 191 |
+
text_h = max(10, bbox[3] - bbox[1])
|
| 192 |
+
|
| 193 |
+
img_w = text_w + pad_w * 2
|
| 194 |
+
img_h = max(48, text_h + pad_h * 2)
|
| 195 |
+
img = Image.new('RGB', (img_w, img_h), bg_color)
|
| 196 |
+
draw = ImageDraw.Draw(img)
|
| 197 |
+
draw.text((pad_w - bbox[0], pad_h - bbox[1]), text, fill=fg_color, font=font)
|
| 198 |
+
return img
|
| 199 |
+
|
| 200 |
+
# ==============================================================================
|
| 201 |
+
# 4. Pipeline Data Augmentation Toàn Diện V24 (Motion Blur, Noise, Paper, Skew)
|
| 202 |
+
# ==============================================================================
|
| 203 |
+
def apply_augmentations_v24(pil_img, is_train=True):
|
| 204 |
+
arr = np.array(pil_img)
|
| 205 |
+
h, w = arr.shape[:2]
|
| 206 |
+
|
| 207 |
+
if not is_train:
|
| 208 |
+
# Val set: chỉ áp dụng nhiễu nhẹ
|
| 209 |
+
if random.random() < 0.2:
|
| 210 |
+
arr = np.clip(arr.astype(np.float32) + np.random.normal(0, 3, arr.shape), 0, 255).astype(np.uint8)
|
| 211 |
+
return Image.fromarray(arr)
|
| 212 |
+
|
| 213 |
+
# --- 1. Giả lập màu nền giấy cổ / ố vàng / tương phản thấp ---
|
| 214 |
+
r_bg = random.random()
|
| 215 |
+
if r_bg < 0.25:
|
| 216 |
+
# Giấy ố vàng (parchment)
|
| 217 |
+
tint = np.array([random.randint(220, 245), random.randint(205, 230), random.randint(170, 200)], dtype=np.float32)
|
| 218 |
+
gray = cv2.cvtColor(arr, cv2.COLOR_RGB2GRAY).astype(np.float32) / 255.0
|
| 219 |
+
gray = np.expand_dims(gray, -1)
|
| 220 |
+
arr = np.clip(gray * tint + (1 - gray) * np.array([30, 20, 15]), 0, 255).astype(np.uint8)
|
| 221 |
+
elif r_bg < 0.40:
|
| 222 |
+
# Tương phản thấp (mực mờ)
|
| 223 |
+
arr = cv2.convertScaleAbs(arr, alpha=random.uniform(0.6, 0.85), beta=random.randint(30, 60))
|
| 224 |
+
|
| 225 |
+
# --- 2. Vết loang mực / ố nước rải rác ---
|
| 226 |
+
if random.random() < 0.20:
|
| 227 |
+
for _ in range(random.randint(1, 3)):
|
| 228 |
+
cx = random.randint(0, w - 1)
|
| 229 |
+
cy = random.randint(0, h - 1)
|
| 230 |
+
rad = random.randint(6, 25)
|
| 231 |
+
color = (random.randint(180, 220), random.randint(170, 210), random.randint(150, 190))
|
| 232 |
+
cv2.circle(arr, (cx, cy), rad, color, -1)
|
| 233 |
+
|
| 234 |
+
# --- 3. Làm mờ (Motion Blur & Defocus Gaussian) ---
|
| 235 |
+
r_blur = random.random()
|
| 236 |
+
if r_blur < 0.25:
|
| 237 |
+
# Motion blur (mô phỏng rung tay camera)
|
| 238 |
+
k_size = random.choice([3, 5, 7])
|
| 239 |
+
angle = random.uniform(0, 180)
|
| 240 |
+
M = cv2.getRotationMatrix2D((k_size / 2, k_size / 2), angle, 1)
|
| 241 |
+
kernel = np.zeros((k_size, k_size))
|
| 242 |
+
kernel[int((k_size - 1) / 2), :] = np.ones(k_size)
|
| 243 |
+
kernel = cv2.warpAffine(kernel, M, (k_size, k_size))
|
| 244 |
+
kernel = kernel / (np.sum(kernel) + 1e-5)
|
| 245 |
+
arr = cv2.filter2D(arr, -1, kernel)
|
| 246 |
+
elif r_blur < 0.40:
|
| 247 |
+
# Gaussian blur nhẹ
|
| 248 |
+
arr = cv2.GaussianBlur(arr, (3, 3), random.uniform(0.5, 1.2))
|
| 249 |
+
|
| 250 |
+
# --- 4. Nhiễu hạt cảm biến & Bụi giấy (Noise & Dust) ---
|
| 251 |
+
r_noise = random.random()
|
| 252 |
+
if r_noise < 0.25:
|
| 253 |
+
# Gaussian noise
|
| 254 |
+
sigma = random.uniform(8, 22)
|
| 255 |
+
arr = np.clip(arr.astype(np.float32) + np.random.normal(0, sigma, arr.shape), 0, 255).astype(np.uint8)
|
| 256 |
+
elif r_noise < 0.35:
|
| 257 |
+
# Salt & pepper (bụi đen rải rác)
|
| 258 |
+
num_dust = random.randint(5, 20)
|
| 259 |
+
for _ in range(num_dust):
|
| 260 |
+
dx = random.randint(0, w - 1)
|
| 261 |
+
dy = random.randint(0, h - 1)
|
| 262 |
+
arr[dy, dx] = random.choice([0, 255])
|
| 263 |
+
|
| 264 |
+
# --- 5. Đứt nét / Mòn nét (Morphological Erosion/Dilation) ---
|
| 265 |
+
if random.random() < 0.25:
|
| 266 |
+
k_morph = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
|
| 267 |
+
if random.random() < 0.5:
|
| 268 |
+
arr = cv2.erode(arr, k_morph, iterations=1)
|
| 269 |
+
else:
|
| 270 |
+
arr = cv2.dilate(arr, k_morph, iterations=1)
|
| 271 |
+
|
| 272 |
+
# --- 6. Xoay góc nghiêng & Biến đổi phối cảnh (Albumentations) ---
|
| 273 |
+
transform = A.Compose([
|
| 274 |
+
A.Affine(scale=(0.95, 1.05), rotate=(-5, 5), translate_percent=(-0.03, 0.03), p=0.4),
|
| 275 |
+
A.Perspective(scale=(0.02, 0.04), keep_size=True, p=0.3)
|
| 276 |
+
])
|
| 277 |
+
arr = transform(image=arr)['image']
|
| 278 |
+
|
| 279 |
+
# --- 7. Biến đổi kích thước nhỏ (Downscaled line height) ---
|
| 280 |
+
if random.random() < 0.20:
|
| 281 |
+
target_h = random.randint(24, 34)
|
| 282 |
+
target_w = int(w * (target_h / h))
|
| 283 |
+
if target_w > 10:
|
| 284 |
+
small = cv2.resize(arr, (target_w, target_h), interpolation=cv2.INTER_AREA)
|
| 285 |
+
arr = cv2.resize(small, (w, h), interpolation=cv2.INTER_LINEAR)
|
| 286 |
+
|
| 287 |
+
return Image.fromarray(arr)
|
| 288 |
+
|
| 289 |
+
# ==============================================================================
|
| 290 |
+
# 5. Worker Function Đa Tiến Trình (Multi-processing Worker)
|
| 291 |
+
# ==============================================================================
|
| 292 |
+
def generate_sample_worker(args):
|
| 293 |
+
idx, output_dir, font_paths, is_val = args
|
| 294 |
+
text = sample_text_v24()
|
| 295 |
+
font_path = random.choice(font_paths)
|
| 296 |
+
font_size = random.randint(22, 28)
|
| 297 |
+
|
| 298 |
+
raw_img = render_text_v24(text, font_path, font_size=font_size)
|
| 299 |
+
aug_img = apply_augmentations_v24(raw_img, is_train=(not is_val))
|
| 300 |
+
|
| 301 |
+
# Resize chuẩn hóa chiều cao 48px, giữ nguyên tỷ lệ
|
| 302 |
+
w_orig, h_orig = aug_img.size
|
| 303 |
+
h_new = 48
|
| 304 |
+
w_new = max(32, int(w_orig * (h_new / h_orig)))
|
| 305 |
+
final_img = aug_img.resize((w_new, h_new), Image.Resampling.BICUBIC)
|
| 306 |
+
|
| 307 |
+
sub_dir = "val" if is_val else "train"
|
| 308 |
+
img_filename = f"synth_{idx:07d}.png"
|
| 309 |
+
img_rel_path = f"{sub_dir}/{img_filename}"
|
| 310 |
+
full_save_path = os.path.join(output_dir, sub_dir, img_filename)
|
| 311 |
+
|
| 312 |
+
final_img.save(full_save_path)
|
| 313 |
+
|
| 314 |
+
# Nhãn lưu ở chuẩn Unicode Logical Order
|
| 315 |
+
# Lưu ý thay thế thin space \u200a nếu có
|
| 316 |
+
clean_label = text.replace('\u200a', '')
|
| 317 |
+
label_line = f"{img_rel_path}\t{clean_label}\n"
|
| 318 |
+
return (is_val, label_line)
|
| 319 |
+
|
| 320 |
+
# ==============================================================================
|
| 321 |
+
# 6. Chương Trình Chính (Main Runner)
|
| 322 |
+
# ==============================================================================
|
| 323 |
+
def main():
|
| 324 |
+
parser = argparse.ArgumentParser(description="Sinh dữ liệu tổng hợp tiếng Chăm V24")
|
| 325 |
+
parser.add_argument('--output-dir', type=str, default='ocr-training/data/cham_synthetic_v24', help='Thư mục xuất dữ liệu')
|
| 326 |
+
parser.add_argument('--fonts-dir', type=str, default='ocr-training/data/fonts', help='Thư mục chứa font NotoSansCham')
|
| 327 |
+
parser.add_argument('--num-train', type=int, default=150000, help='Số lượng mẫu train')
|
| 328 |
+
parser.add_argument('--num-val', type=int, default=15000, help='Số lượng mẫu val')
|
| 329 |
+
parser.add_argument('--num-workers', type=int, default=4, help='Số tiến trình song song')
|
| 330 |
+
parser.add_argument('--test-run', action='store_true', help='Chạy thử nghiệm nhanh 100 mẫu')
|
| 331 |
+
args = parser.parse_args()
|
| 332 |
+
|
| 333 |
+
if args.test_run:
|
| 334 |
+
args.num_train = 90
|
| 335 |
+
args.num_val = 10
|
| 336 |
+
print("⚡ Chế độ Test Run kích hoạt: 90 mẫu train, 10 mẫu val.")
|
| 337 |
+
|
| 338 |
+
os.makedirs(os.path.join(args.output_dir, 'train'), exist_ok=True)
|
| 339 |
+
os.makedirs(os.path.join(args.output_dir, 'val'), exist_ok=True)
|
| 340 |
+
|
| 341 |
+
font_paths = [os.path.join(args.fonts_dir, f) for f in os.listdir(args.fonts_dir) if f.endswith(('.ttf', '.otf'))]
|
| 342 |
+
if not font_paths:
|
| 343 |
+
print(f"❌ LỖI: Không tìm thấy font trong {args.fonts_dir}")
|
| 344 |
+
sys.exit(1)
|
| 345 |
+
|
| 346 |
+
validator = FontValidator(font_paths)
|
| 347 |
+
valid_fonts = [fp for fp in font_paths if len(validator.font_cmaps.get(fp, set())) > 0]
|
| 348 |
+
print(f"📚 Đã nạp {len(valid_fonts)} font hợp lệ: {[os.path.basename(f) for f in valid_fonts]}")
|
| 349 |
+
|
| 350 |
+
total_samples = args.num_train + args.num_val
|
| 351 |
+
print(f"🚀 Bắt đầu sinh {total_samples:,} mẫu ({args.num_train:,} train, {args.num_val:,} val)...")
|
| 352 |
+
print(f"⚙️ Số vCPU tiến trình song song: {args.num_workers}")
|
| 353 |
+
|
| 354 |
+
tasks = []
|
| 355 |
+
for i in range(total_samples):
|
| 356 |
+
is_val = (i >= args.num_train)
|
| 357 |
+
tasks.append((i, args.output_dir, valid_fonts, is_val))
|
| 358 |
+
|
| 359 |
+
t0 = time.time()
|
| 360 |
+
train_labels = []
|
| 361 |
+
val_labels = []
|
| 362 |
+
|
| 363 |
+
# Chạy Pool đa tiến trình
|
| 364 |
+
chunksize = max(1, len(tasks) // (args.num_workers * 20))
|
| 365 |
+
with Pool(processes=args.num_workers) as pool:
|
| 366 |
+
for idx, (is_val, label_line) in enumerate(pool.imap_unordered(generate_sample_worker, tasks, chunksize=chunksize)):
|
| 367 |
+
if is_val:
|
| 368 |
+
val_labels.append(label_line)
|
| 369 |
+
else:
|
| 370 |
+
train_labels.append(label_line)
|
| 371 |
+
|
| 372 |
+
if (idx + 1) % 10000 == 0 or (idx + 1) == total_samples:
|
| 373 |
+
elapsed = time.time() - t0
|
| 374 |
+
speed = (idx + 1) / elapsed
|
| 375 |
+
remaining = (total_samples - (idx + 1)) / max(1e-5, speed)
|
| 376 |
+
print(f" [{idx+1:,}/{total_samples:,}] {((idx+1)/total_samples*100):.1f}% | Tốc độ: {speed:.1f} mẫu/s | Còn lại: ~{remaining/60:.1f} phút")
|
| 377 |
+
|
| 378 |
+
# Ghi file nhãn
|
| 379 |
+
train_label_path = os.path.join(args.output_dir, 'train_label.txt')
|
| 380 |
+
val_label_path = os.path.join(args.output_dir, 'val_label.txt')
|
| 381 |
+
|
| 382 |
+
with open(train_label_path, 'w', encoding='utf-8') as f:
|
| 383 |
+
f.writelines(train_labels)
|
| 384 |
+
with open(val_label_path, 'w', encoding='utf-8') as f:
|
| 385 |
+
f.writelines(val_labels)
|
| 386 |
+
|
| 387 |
+
t_total = time.time() - t0
|
| 388 |
+
print(f"🎉 Hoàn tất sinh dữ liệu V24 trong {t_total:.1f} giây ({t_total/60:.2f} phút)!")
|
| 389 |
+
print(f" - File train: {train_label_path} ({len(train_labels):,} dòng)")
|
| 390 |
+
print(f" - File val: {val_label_path} ({len(val_labels):,} dòng)")
|
| 391 |
+
|
| 392 |
+
if __name__ == '__main__':
|
| 393 |
+
main()
|
ocr-training/scripts/generate_detector_data.py
CHANGED
|
@@ -1,24 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
| 3 |
import cv2
|
| 4 |
import json
|
| 5 |
import random
|
|
|
|
| 6 |
import numpy as np
|
| 7 |
from PIL import Image, ImageDraw, ImageFont
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 10 |
sys.path.insert(0, os.path.join(PROJECT_ROOT, "scripts"))
|
| 11 |
|
| 12 |
-
|
| 13 |
-
artifacts_scratch = "C:/Users/admin/.gemini/antigravity/brain/3b1501b2-bb18-4c92-a78b-3d27867b4bce/scratch"
|
| 14 |
-
sys.path.insert(0, artifacts_scratch)
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
def warp_points(pts, matrix, amp, freq, phase):
|
| 20 |
"""
|
| 21 |
-
|
|
|
|
| 22 |
"""
|
| 23 |
pts_arr = np.array(pts, dtype=np.float32).reshape(-1, 1, 2)
|
| 24 |
# 1. Perspective transformation
|
|
@@ -29,61 +108,66 @@ def warp_points(pts, matrix, amp, freq, phase):
|
|
| 29 |
for pt in warped_persp:
|
| 30 |
x, y = pt[0]
|
| 31 |
dy = amp * np.sin(x * freq + phase)
|
| 32 |
-
final_pts.append([float(x), float(y + dy)])
|
| 33 |
|
| 34 |
return final_pts
|
| 35 |
|
| 36 |
-
def draw_distorted_page_with_labels(lines,
|
| 37 |
-
|
|
|
|
| 38 |
font = get_cached_font(font_path, font_size)
|
|
|
|
| 39 |
bg = apply_old_paper_background(width, height)
|
| 40 |
draw = ImageDraw.Draw(bg)
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
|
| 45 |
-
# Tính toán tọa độ hộp bao sạch
|
| 46 |
line_boxes = []
|
| 47 |
for idx, text in enumerate(lines):
|
| 48 |
y_pos = y_start + idx * y_spacing
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
# Lấy kích thước text
|
| 53 |
try:
|
| 54 |
bbox = font.getbbox(text)
|
| 55 |
text_w = bbox[2] - bbox[0]
|
| 56 |
text_h = bbox[3] - bbox[1]
|
| 57 |
except Exception:
|
| 58 |
-
text_w, text_h = len(text) *
|
| 59 |
|
| 60 |
-
|
|
|
|
| 61 |
box = [
|
| 62 |
-
[x_pos, y_pos],
|
| 63 |
-
[x_pos + text_w +
|
| 64 |
-
[x_pos + text_w +
|
| 65 |
-
[x_pos, y_pos + text_h +
|
| 66 |
]
|
| 67 |
line_boxes.append((text, box))
|
| 68 |
|
| 69 |
img_np = np.array(bg)
|
| 70 |
h, w = img_np.shape[:2]
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
freq = random.uniform(0.005, 0.012)
|
| 75 |
phase = random.uniform(0, 2 * np.pi)
|
| 76 |
|
| 77 |
-
# 2. Perspective distortion parameters
|
| 78 |
pts1 = np.float32([[0, 0], [w, 0], [0, h], [w, h]])
|
| 79 |
-
tx1 = random.uniform(0, w * 0.
|
| 80 |
-
ty1 = random.uniform(0, h * 0.
|
| 81 |
-
tx2 = random.uniform(0, w * 0.
|
| 82 |
-
ty2 = random.uniform(0, h * 0.
|
| 83 |
-
tx3 = random.uniform(0, w * 0.
|
| 84 |
-
ty3 = random.uniform(0, h * 0.
|
| 85 |
-
tx4 = random.uniform(0, w * 0.
|
| 86 |
-
ty4 = random.uniform(0, h * 0.
|
| 87 |
|
| 88 |
pts2 = np.float32([
|
| 89 |
[0 + tx1, 0 + ty1],
|
|
@@ -93,8 +177,6 @@ def draw_distorted_page_with_labels(lines, font_path, width=800, height=650):
|
|
| 93 |
])
|
| 94 |
matrix = cv2.getPerspectiveTransform(pts1, pts2)
|
| 95 |
|
| 96 |
-
# Thực hiện warp ảnh
|
| 97 |
-
# Wave
|
| 98 |
x_idx = np.arange(w)
|
| 99 |
y_idx = np.arange(h)
|
| 100 |
X, Y = np.meshgrid(x_idx, y_idx)
|
|
@@ -102,75 +184,90 @@ def draw_distorted_page_with_labels(lines, font_path, width=800, height=650):
|
|
| 102 |
map_x = X.astype(np.float32)
|
| 103 |
map_y = (Y + dy).astype(np.float32)
|
| 104 |
img_wave = cv2.remap(img_np, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
| 105 |
-
|
| 106 |
img_warped = cv2.warpPerspective(img_wave, matrix, (w, h), borderMode=cv2.BORDER_REPLICATE)
|
| 107 |
|
| 108 |
-
# Áp dụng warp cho các điểm tọa độ nhãn dòng
|
| 109 |
warped_labels = []
|
| 110 |
for text, box in line_boxes:
|
| 111 |
warped_box = warp_points(box, matrix, amp, freq, phase)
|
| 112 |
warped_labels.append({
|
| 113 |
-
"transcription":
|
| 114 |
"points": warped_box
|
| 115 |
})
|
| 116 |
|
| 117 |
return Image.fromarray(img_warped), warped_labels
|
| 118 |
|
| 119 |
def main():
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
# Tải các dòng corpus mẫu
|
| 123 |
corpus_file = os.path.join(PROJECT_ROOT, "data", "corpus", "cham_text.txt")
|
| 124 |
if not os.path.exists(corpus_file):
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
| 126 |
return
|
| 127 |
|
| 128 |
with open(corpus_file, "r", encoding="utf-8") as f:
|
| 129 |
sentences = [line.strip() for line in f if line.strip() and not line.strip().startswith('#')]
|
|
|
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
train_img_dir = os.path.join(det_root, "train_images")
|
| 136 |
val_img_dir = os.path.join(det_root, "val_images")
|
| 137 |
|
| 138 |
os.makedirs(train_img_dir, exist_ok=True)
|
| 139 |
os.makedirs(val_img_dir, exist_ok=True)
|
| 140 |
|
| 141 |
-
|
| 142 |
-
num_train = 5
|
| 143 |
-
num_val = 3
|
| 144 |
-
|
| 145 |
-
print(f" - Sinh {num_train} trang train_pages...")
|
| 146 |
train_label_entries = []
|
| 147 |
-
for i in range(num_train):
|
| 148 |
-
num_lines = random.randint(
|
| 149 |
-
lines = random.sample(sentences, num_lines)
|
| 150 |
-
img, labels = draw_distorted_page_with_labels(lines,
|
| 151 |
|
| 152 |
-
img_name = f"det_train_{i:
|
| 153 |
img.save(os.path.join(train_img_dir, img_name))
|
| 154 |
train_label_entries.append(f"train_images/{img_name}\t{json.dumps(labels, ensure_ascii=False)}")
|
| 155 |
|
| 156 |
with open(os.path.join(det_root, "det_train_label.txt"), "w", encoding="utf-8", newline="\n") as f_out:
|
| 157 |
f_out.write("\n".join(train_label_entries) + "\n")
|
| 158 |
|
| 159 |
-
print(f"
|
| 160 |
val_label_entries = []
|
| 161 |
-
for i in range(num_val):
|
| 162 |
-
num_lines = random.randint(
|
| 163 |
-
lines = random.sample(sentences, num_lines)
|
| 164 |
-
img, labels = draw_distorted_page_with_labels(lines,
|
| 165 |
|
| 166 |
-
img_name = f"det_val_{i:
|
| 167 |
img.save(os.path.join(val_img_dir, img_name))
|
| 168 |
val_label_entries.append(f"val_images/{img_name}\t{json.dumps(labels, ensure_ascii=False)}")
|
| 169 |
|
| 170 |
with open(os.path.join(det_root, "det_val_label.txt"), "w", encoding="utf-8", newline="\n") as f_out:
|
| 171 |
f_out.write("\n".join(val_label_entries) + "\n")
|
| 172 |
|
| 173 |
-
print("🎉
|
|
|
|
|
|
|
| 174 |
|
| 175 |
if __name__ == '__main__':
|
| 176 |
main()
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Synthetic Text Detection Dataset Generator for Cham OCR (Phase 4).
|
| 5 |
+
Generates realistic multi-line Cham document and manuscript pages with
|
| 6 |
+
exact polygon bounding box labels in PaddleOCR SimpleDataSet format.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
import os
|
| 10 |
import sys
|
| 11 |
import cv2
|
| 12 |
import json
|
| 13 |
import random
|
| 14 |
+
import argparse
|
| 15 |
import numpy as np
|
| 16 |
from PIL import Image, ImageDraw, ImageFont
|
| 17 |
|
| 18 |
+
# Force UTF-8 stdout encoding on Windows
|
| 19 |
+
if sys.stdout and sys.stdout.encoding != 'utf-8':
|
| 20 |
+
try:
|
| 21 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 22 |
+
except AttributeError:
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
# Monkeypatch NumPy 2.x compatibility
|
| 26 |
+
if not hasattr(np, 'sctypes'):
|
| 27 |
+
np.sctypes = {
|
| 28 |
+
'int': [np.int8, np.int16, np.int32, np.int64],
|
| 29 |
+
'uint': [np.uint8, np.uint16, np.uint32, np.uint64],
|
| 30 |
+
'float': [np.float16, np.float32, np.float64],
|
| 31 |
+
'complex': [np.complex64, np.complex128],
|
| 32 |
+
'others': [bool, object, bytes, str]
|
| 33 |
+
}
|
| 34 |
+
if not hasattr(np, 'bool'): np.bool = bool
|
| 35 |
+
if not hasattr(np, 'int'): np.int = int
|
| 36 |
+
if not hasattr(np, 'float'): np.float = float
|
| 37 |
+
if not hasattr(np, 'typeDict'): np.typeDict = {}
|
| 38 |
+
|
| 39 |
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 40 |
sys.path.insert(0, os.path.join(PROJECT_ROOT, "scripts"))
|
| 41 |
|
| 42 |
+
_FONT_CACHE = {}
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
def get_cached_font(font_path, size):
|
| 45 |
+
key = (font_path, size)
|
| 46 |
+
if key not in _FONT_CACHE:
|
| 47 |
+
if os.path.exists(font_path):
|
| 48 |
+
_FONT_CACHE[key] = ImageFont.truetype(font_path, size)
|
| 49 |
+
else:
|
| 50 |
+
_FONT_CACHE[key] = ImageFont.load_default()
|
| 51 |
+
return _FONT_CACHE[key]
|
| 52 |
+
|
| 53 |
+
def apply_old_paper_background(width, height):
|
| 54 |
+
"""
|
| 55 |
+
Generates a realistic antique parchment/paper background with varied tones,
|
| 56 |
+
vignetting, and stains.
|
| 57 |
+
"""
|
| 58 |
+
base_r = random.randint(220, 245)
|
| 59 |
+
base_g = random.randint(210, 235)
|
| 60 |
+
base_b = random.randint(180, 210)
|
| 61 |
+
|
| 62 |
+
img = np.full((height, width, 3), (base_b, base_g, base_r), dtype=np.uint8)
|
| 63 |
+
|
| 64 |
+
# 1. Low frequency gradient/shading
|
| 65 |
+
grid_h, grid_w = max(4, height // 60), max(4, width // 60)
|
| 66 |
+
low_noise = np.random.normal(0, 14, (grid_h, grid_w)).astype(np.float32)
|
| 67 |
+
low_noise = cv2.resize(low_noise, (width, height), interpolation=cv2.INTER_CUBIC)
|
| 68 |
+
|
| 69 |
+
# 2. Fine grain noise
|
| 70 |
+
fine_noise = np.random.normal(0, 5, (height, width)).astype(np.float32)
|
| 71 |
+
total_noise = low_noise + fine_noise
|
| 72 |
+
for c in range(3):
|
| 73 |
+
channel = img[:, :, c].astype(np.float32) + total_noise
|
| 74 |
+
img[:, :, c] = np.clip(channel, 0, 255).astype(np.uint8)
|
| 75 |
+
|
| 76 |
+
# 3. Vignette effect (darker edges)
|
| 77 |
+
X = np.linspace(-1, 1, width)
|
| 78 |
+
Y = np.linspace(-1, 1, height)
|
| 79 |
+
mesh_x, mesh_y = np.meshgrid(X, Y)
|
| 80 |
+
dist = np.sqrt(mesh_x**2 + mesh_y**2)
|
| 81 |
+
vignette = 1.0 - np.clip((dist - 0.5) * 0.35, 0, 0.45)
|
| 82 |
+
for c in range(3):
|
| 83 |
+
img[:, :, c] = (img[:, :, c].astype(np.float32) * vignette).astype(np.uint8)
|
| 84 |
+
|
| 85 |
+
# 4. Optional water/ink stain spots
|
| 86 |
+
if random.random() < 0.5:
|
| 87 |
+
num_spots = random.randint(1, 3)
|
| 88 |
+
for _ in range(num_spots):
|
| 89 |
+
cx = random.randint(60, width - 60)
|
| 90 |
+
cy = random.randint(60, height - 60)
|
| 91 |
+
radius = random.randint(20, 50)
|
| 92 |
+
cv2.circle(img, (cx, cy), radius, (max(0, base_b - 25), max(0, base_g - 25), max(0, base_r - 30)), -1)
|
| 93 |
+
img = cv2.GaussianBlur(img, (31, 31), 0)
|
| 94 |
+
|
| 95 |
+
return Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
| 96 |
|
| 97 |
def warp_points(pts, matrix, amp, freq, phase):
|
| 98 |
"""
|
| 99 |
+
Applies perspective transformation and non-linear wave deformation
|
| 100 |
+
to ground-truth polygon vertices.
|
| 101 |
"""
|
| 102 |
pts_arr = np.array(pts, dtype=np.float32).reshape(-1, 1, 2)
|
| 103 |
# 1. Perspective transformation
|
|
|
|
| 108 |
for pt in warped_persp:
|
| 109 |
x, y = pt[0]
|
| 110 |
dy = amp * np.sin(x * freq + phase)
|
| 111 |
+
final_pts.append([round(float(x), 1), round(float(y + dy), 1)])
|
| 112 |
|
| 113 |
return final_pts
|
| 114 |
|
| 115 |
+
def draw_distorted_page_with_labels(lines, fonts, width=800, height=650):
|
| 116 |
+
font_path = random.choice(fonts)
|
| 117 |
+
font_size = random.randint(20, 26)
|
| 118 |
font = get_cached_font(font_path, font_size)
|
| 119 |
+
|
| 120 |
bg = apply_old_paper_background(width, height)
|
| 121 |
draw = ImageDraw.Draw(bg)
|
| 122 |
|
| 123 |
+
y_spacing = random.randint(font_size + 24, font_size + 42)
|
| 124 |
+
y_start = random.randint(40, 70)
|
| 125 |
|
|
|
|
| 126 |
line_boxes = []
|
| 127 |
for idx, text in enumerate(lines):
|
| 128 |
y_pos = y_start + idx * y_spacing
|
| 129 |
+
if y_pos + font_size + 15 > height:
|
| 130 |
+
break
|
| 131 |
+
|
| 132 |
+
x_pos = random.randint(40, 80)
|
| 133 |
+
ink_r = random.randint(20, 45)
|
| 134 |
+
ink_g = random.randint(20, 40)
|
| 135 |
+
ink_b = random.randint(20, 38)
|
| 136 |
+
draw.text((x_pos, y_pos), text, font=font, fill=(ink_r, ink_g, ink_b))
|
| 137 |
|
|
|
|
| 138 |
try:
|
| 139 |
bbox = font.getbbox(text)
|
| 140 |
text_w = bbox[2] - bbox[0]
|
| 141 |
text_h = bbox[3] - bbox[1]
|
| 142 |
except Exception:
|
| 143 |
+
text_w, text_h = len(text) * 14, font_size
|
| 144 |
|
| 145 |
+
pad_x = 4
|
| 146 |
+
pad_y = 3
|
| 147 |
box = [
|
| 148 |
+
[x_pos - pad_x, y_pos - pad_y],
|
| 149 |
+
[x_pos + text_w + pad_x, y_pos - pad_y],
|
| 150 |
+
[x_pos + text_w + pad_x, y_pos + text_h + pad_y],
|
| 151 |
+
[x_pos - pad_x, y_pos + text_h + pad_y]
|
| 152 |
]
|
| 153 |
line_boxes.append((text, box))
|
| 154 |
|
| 155 |
img_np = np.array(bg)
|
| 156 |
h, w = img_np.shape[:2]
|
| 157 |
|
| 158 |
+
amp = random.uniform(3.0, 8.0)
|
| 159 |
+
freq = random.uniform(0.004, 0.010)
|
|
|
|
| 160 |
phase = random.uniform(0, 2 * np.pi)
|
| 161 |
|
|
|
|
| 162 |
pts1 = np.float32([[0, 0], [w, 0], [0, h], [w, h]])
|
| 163 |
+
tx1 = random.uniform(0, w * 0.03)
|
| 164 |
+
ty1 = random.uniform(0, h * 0.03)
|
| 165 |
+
tx2 = random.uniform(0, w * 0.03)
|
| 166 |
+
ty2 = random.uniform(0, h * 0.03)
|
| 167 |
+
tx3 = random.uniform(0, w * 0.03)
|
| 168 |
+
ty3 = random.uniform(0, h * 0.03)
|
| 169 |
+
tx4 = random.uniform(0, w * 0.03)
|
| 170 |
+
ty4 = random.uniform(0, h * 0.03)
|
| 171 |
|
| 172 |
pts2 = np.float32([
|
| 173 |
[0 + tx1, 0 + ty1],
|
|
|
|
| 177 |
])
|
| 178 |
matrix = cv2.getPerspectiveTransform(pts1, pts2)
|
| 179 |
|
|
|
|
|
|
|
| 180 |
x_idx = np.arange(w)
|
| 181 |
y_idx = np.arange(h)
|
| 182 |
X, Y = np.meshgrid(x_idx, y_idx)
|
|
|
|
| 184 |
map_x = X.astype(np.float32)
|
| 185 |
map_y = (Y + dy).astype(np.float32)
|
| 186 |
img_wave = cv2.remap(img_np, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
| 187 |
+
|
| 188 |
img_warped = cv2.warpPerspective(img_wave, matrix, (w, h), borderMode=cv2.BORDER_REPLICATE)
|
| 189 |
|
|
|
|
| 190 |
warped_labels = []
|
| 191 |
for text, box in line_boxes:
|
| 192 |
warped_box = warp_points(box, matrix, amp, freq, phase)
|
| 193 |
warped_labels.append({
|
| 194 |
+
"transcription": text,
|
| 195 |
"points": warped_box
|
| 196 |
})
|
| 197 |
|
| 198 |
return Image.fromarray(img_warped), warped_labels
|
| 199 |
|
| 200 |
def main():
|
| 201 |
+
parser = argparse.ArgumentParser(description="Generate synthetic Cham Detection dataset")
|
| 202 |
+
parser.add_argument("--num_train", type=int, default=50, help="Number of training pages")
|
| 203 |
+
parser.add_argument("--num_val", type=int, default=10, help="Number of validation pages")
|
| 204 |
+
parser.add_argument("--output_dir", type=str, default=None, help="Output directory")
|
| 205 |
+
args = parser.parse_args()
|
| 206 |
+
|
| 207 |
+
print("📂 Starting Cham Text Detection Synthetic Generator (Phase 4)...")
|
| 208 |
|
|
|
|
| 209 |
corpus_file = os.path.join(PROJECT_ROOT, "data", "corpus", "cham_text.txt")
|
| 210 |
if not os.path.exists(corpus_file):
|
| 211 |
+
corpus_file = os.path.join(PROJECT_ROOT, "output_v22", "paddleocr_cham_finetune", "data", "corpus", "cham_text.txt")
|
| 212 |
+
|
| 213 |
+
if not os.path.exists(corpus_file):
|
| 214 |
+
print(f"❌ Corpus file not found at {corpus_file}!")
|
| 215 |
return
|
| 216 |
|
| 217 |
with open(corpus_file, "r", encoding="utf-8") as f:
|
| 218 |
sentences = [line.strip() for line in f if line.strip() and not line.strip().startswith('#')]
|
| 219 |
+
print(f" Loaded {len(sentences)} corpus lines.")
|
| 220 |
|
| 221 |
+
fonts_dir = os.path.join(PROJECT_ROOT, "data", "fonts")
|
| 222 |
+
if not os.path.exists(fonts_dir):
|
| 223 |
+
fonts_dir = os.path.join(PROJECT_ROOT, "output_v22", "paddleocr_cham_finetune", "data", "fonts")
|
| 224 |
+
|
| 225 |
+
fonts = [
|
| 226 |
+
os.path.join(fonts_dir, f) for f in ["NotoSansCham-Regular.ttf", "NotoSansCham-Bold.ttf", "NotoSansCham-Black.ttf"]
|
| 227 |
+
if os.path.exists(os.path.join(fonts_dir, f))
|
| 228 |
+
]
|
| 229 |
+
if not fonts:
|
| 230 |
+
fonts = [os.path.join(fonts_dir, "NotoSansCham-Regular.ttf")]
|
| 231 |
+
print(f" Using fonts: {[os.path.basename(f) for f in fonts]}")
|
| 232 |
+
|
| 233 |
+
det_root = args.output_dir or os.path.join(PROJECT_ROOT, "data", "detector")
|
| 234 |
train_img_dir = os.path.join(det_root, "train_images")
|
| 235 |
val_img_dir = os.path.join(det_root, "val_images")
|
| 236 |
|
| 237 |
os.makedirs(train_img_dir, exist_ok=True)
|
| 238 |
os.makedirs(val_img_dir, exist_ok=True)
|
| 239 |
|
| 240 |
+
print(f" Sinh {args.num_train} trang huấn luyện (train_images)...")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
train_label_entries = []
|
| 242 |
+
for i in range(args.num_train):
|
| 243 |
+
num_lines = random.randint(4, 9)
|
| 244 |
+
lines = random.sample(sentences, min(num_lines, len(sentences)))
|
| 245 |
+
img, labels = draw_distorted_page_with_labels(lines, fonts)
|
| 246 |
|
| 247 |
+
img_name = f"det_train_{i:05d}.png"
|
| 248 |
img.save(os.path.join(train_img_dir, img_name))
|
| 249 |
train_label_entries.append(f"train_images/{img_name}\t{json.dumps(labels, ensure_ascii=False)}")
|
| 250 |
|
| 251 |
with open(os.path.join(det_root, "det_train_label.txt"), "w", encoding="utf-8", newline="\n") as f_out:
|
| 252 |
f_out.write("\n".join(train_label_entries) + "\n")
|
| 253 |
|
| 254 |
+
print(f" Sinh {args.num_val} trang kiểm thử (val_images)...")
|
| 255 |
val_label_entries = []
|
| 256 |
+
for i in range(args.num_val):
|
| 257 |
+
num_lines = random.randint(4, 9)
|
| 258 |
+
lines = random.sample(sentences, min(num_lines, len(sentences)))
|
| 259 |
+
img, labels = draw_distorted_page_with_labels(lines, fonts)
|
| 260 |
|
| 261 |
+
img_name = f"det_val_{i:05d}.png"
|
| 262 |
img.save(os.path.join(val_img_dir, img_name))
|
| 263 |
val_label_entries.append(f"val_images/{img_name}\t{json.dumps(labels, ensure_ascii=False)}")
|
| 264 |
|
| 265 |
with open(os.path.join(det_root, "det_val_label.txt"), "w", encoding="utf-8", newline="\n") as f_out:
|
| 266 |
f_out.write("\n".join(val_label_entries) + "\n")
|
| 267 |
|
| 268 |
+
print(f"🎉 Hoàn tất sinh tập dữ liệu Detection tại: {det_root}")
|
| 269 |
+
print(f" - Train: {len(train_label_entries)} ảnh")
|
| 270 |
+
print(f" - Val: {len(val_label_entries)} ảnh")
|
| 271 |
|
| 272 |
if __name__ == '__main__':
|
| 273 |
main()
|
ocr-training/scripts/generate_detector_data_v2.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Synthetic Text Detection Dataset Generator for Cham OCR (V2 - Tight Line Hard Cases).
|
| 5 |
+
Generates realistic multi-line Cham document and manuscript pages with
|
| 6 |
+
exact polygon bounding box labels in PaddleOCR SimpleDataSet format.
|
| 7 |
+
|
| 8 |
+
Specially engineered to fix close line segmentation and tight leading issues:
|
| 9 |
+
- 50% Ultra-Tight lines (y_spacing = font_size + 3..10px)
|
| 10 |
+
- 30% Medium-Tight lines (y_spacing = font_size + 11..20px)
|
| 11 |
+
- 20% Standard lines (y_spacing = font_size + 21..35px)
|
| 12 |
+
- Multi-paragraph layouts with indentation and short ending lines
|
| 13 |
+
- Multiprocessing for high-throughput generation on Cloud GPU/CPU environments.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import cv2
|
| 19 |
+
import json
|
| 20 |
+
import random
|
| 21 |
+
import argparse
|
| 22 |
+
import numpy as np
|
| 23 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 24 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 25 |
+
|
| 26 |
+
# Force UTF-8 stdout encoding on Windows
|
| 27 |
+
if sys.stdout and sys.stdout.encoding != 'utf-8':
|
| 28 |
+
try:
|
| 29 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 30 |
+
except AttributeError:
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
# Monkeypatch NumPy 2.x compatibility
|
| 34 |
+
if not hasattr(np, 'sctypes'):
|
| 35 |
+
np.sctypes = {
|
| 36 |
+
'int': [np.int8, np.int16, np.int32, np.int64],
|
| 37 |
+
'uint': [np.uint8, np.uint16, np.uint32, np.uint64],
|
| 38 |
+
'float': [np.float16, np.float32, np.float64],
|
| 39 |
+
'complex': [np.complex64, np.complex128],
|
| 40 |
+
'others': [bool, object, bytes, str]
|
| 41 |
+
}
|
| 42 |
+
if not hasattr(np, 'bool'): np.bool = bool
|
| 43 |
+
if not hasattr(np, 'int'): np.int = int
|
| 44 |
+
if not hasattr(np, 'float'): np.float = float
|
| 45 |
+
if not hasattr(np, 'typeDict'): np.typeDict = {}
|
| 46 |
+
|
| 47 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 48 |
+
sys.path.insert(0, os.path.join(PROJECT_ROOT, "scripts"))
|
| 49 |
+
|
| 50 |
+
_FONT_CACHE = {}
|
| 51 |
+
|
| 52 |
+
def get_cached_font(font_path, size):
|
| 53 |
+
key = (font_path, size)
|
| 54 |
+
if key not in _FONT_CACHE:
|
| 55 |
+
if os.path.exists(font_path):
|
| 56 |
+
try:
|
| 57 |
+
_FONT_CACHE[key] = ImageFont.truetype(font_path, size)
|
| 58 |
+
except Exception:
|
| 59 |
+
_FONT_CACHE[key] = ImageFont.load_default()
|
| 60 |
+
else:
|
| 61 |
+
_FONT_CACHE[key] = ImageFont.load_default()
|
| 62 |
+
return _FONT_CACHE[key]
|
| 63 |
+
|
| 64 |
+
def apply_realistic_background(width, height):
|
| 65 |
+
"""
|
| 66 |
+
Generates realistic paper texture: modern book paper, aged newsprint, or antique parchment.
|
| 67 |
+
"""
|
| 68 |
+
style = random.random()
|
| 69 |
+
if style < 0.40:
|
| 70 |
+
# 1. Clean / Modern Book Paper (off-white, slight grain)
|
| 71 |
+
base_val = random.randint(240, 253)
|
| 72 |
+
img = np.full((height, width, 3), (base_val, base_val, base_val), dtype=np.uint8)
|
| 73 |
+
noise = np.random.normal(0, 3, (height, width)).astype(np.float32)
|
| 74 |
+
for c in range(3):
|
| 75 |
+
img[:, :, c] = np.clip(img[:, :, c].astype(np.float32) + noise, 0, 255).astype(np.uint8)
|
| 76 |
+
else:
|
| 77 |
+
# 2. Aged Paper / Antique Parchment
|
| 78 |
+
base_r = random.randint(220, 246)
|
| 79 |
+
base_g = random.randint(210, 238)
|
| 80 |
+
base_b = random.randint(185, 215)
|
| 81 |
+
img = np.full((height, width, 3), (base_b, base_g, base_r), dtype=np.uint8)
|
| 82 |
+
|
| 83 |
+
# Low frequency shading
|
| 84 |
+
grid_h, grid_w = max(4, height // 50), max(4, width // 50)
|
| 85 |
+
low_noise = np.random.normal(0, 10, (grid_h, grid_w)).astype(np.float32)
|
| 86 |
+
low_noise = cv2.resize(low_noise, (width, height), interpolation=cv2.INTER_CUBIC)
|
| 87 |
+
|
| 88 |
+
fine_noise = np.random.normal(0, 4, (height, width)).astype(np.float32)
|
| 89 |
+
total_noise = low_noise + fine_noise
|
| 90 |
+
for c in range(3):
|
| 91 |
+
channel = img[:, :, c].astype(np.float32) + total_noise
|
| 92 |
+
img[:, :, c] = np.clip(channel, 0, 255).astype(np.uint8)
|
| 93 |
+
|
| 94 |
+
# Optional vignette
|
| 95 |
+
if random.random() < 0.5:
|
| 96 |
+
X = np.linspace(-1, 1, width)
|
| 97 |
+
Y = np.linspace(-1, 1, height)
|
| 98 |
+
mesh_x, mesh_y = np.meshgrid(X, Y)
|
| 99 |
+
dist = np.sqrt(mesh_x**2 + mesh_y**2)
|
| 100 |
+
vignette = 1.0 - np.clip((dist - 0.5) * 0.25, 0, 0.35)
|
| 101 |
+
for c in range(3):
|
| 102 |
+
img[:, :, c] = (img[:, :, c].astype(np.float32) * vignette).astype(np.uint8)
|
| 103 |
+
|
| 104 |
+
return Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
| 105 |
+
|
| 106 |
+
def warp_points(pts, matrix, amp, freq, phase):
|
| 107 |
+
"""
|
| 108 |
+
Applies perspective transformation and non-linear wave deformation to polygon vertices.
|
| 109 |
+
"""
|
| 110 |
+
pts_arr = np.array(pts, dtype=np.float32).reshape(-1, 1, 2)
|
| 111 |
+
warped_persp = cv2.perspectiveTransform(pts_arr, matrix)
|
| 112 |
+
|
| 113 |
+
final_pts = []
|
| 114 |
+
for pt in warped_persp:
|
| 115 |
+
x, y = pt[0]
|
| 116 |
+
dy = amp * np.sin(x * freq + phase) if amp > 0 else 0
|
| 117 |
+
final_pts.append([round(float(x), 1), round(float(y + dy), 1)])
|
| 118 |
+
|
| 119 |
+
return final_pts
|
| 120 |
+
|
| 121 |
+
def generate_single_page(args_tuple):
|
| 122 |
+
"""
|
| 123 |
+
Worker function to generate a single labeled synthetic page.
|
| 124 |
+
"""
|
| 125 |
+
idx, sentences, fonts, out_dir, prefix = args_tuple
|
| 126 |
+
|
| 127 |
+
width = random.choice([800, 900, 1024])
|
| 128 |
+
height = random.choice([600, 750, 900])
|
| 129 |
+
|
| 130 |
+
font_path = random.choice(fonts)
|
| 131 |
+
font_size = random.randint(18, 26)
|
| 132 |
+
font = get_cached_font(font_path, font_size)
|
| 133 |
+
|
| 134 |
+
bg = apply_realistic_background(width, height)
|
| 135 |
+
draw = ImageDraw.Draw(bg)
|
| 136 |
+
|
| 137 |
+
# Line spacing distribution:
|
| 138 |
+
# 50% Ultra-Tight (3..10px), 30% Medium-Tight (11..20px), 20% Standard (21..35px)
|
| 139 |
+
r_mode = random.random()
|
| 140 |
+
if r_mode < 0.50:
|
| 141 |
+
gap = random.randint(3, 10)
|
| 142 |
+
elif r_mode < 0.80:
|
| 143 |
+
gap = random.randint(11, 20)
|
| 144 |
+
else:
|
| 145 |
+
gap = random.randint(21, 35)
|
| 146 |
+
|
| 147 |
+
y_spacing = font_size + gap
|
| 148 |
+
y_start = random.randint(30, 60)
|
| 149 |
+
|
| 150 |
+
# Paragraph structure
|
| 151 |
+
num_paras = random.randint(1, 3)
|
| 152 |
+
lines_per_para = []
|
| 153 |
+
for _ in range(num_paras):
|
| 154 |
+
lines_per_para.append(random.randint(2, 6))
|
| 155 |
+
|
| 156 |
+
line_boxes = []
|
| 157 |
+
current_y = y_start
|
| 158 |
+
sent_idx = random.randint(0, max(0, len(sentences) - 20))
|
| 159 |
+
|
| 160 |
+
for para_i, num_l in enumerate(lines_per_para):
|
| 161 |
+
for line_i in range(num_l):
|
| 162 |
+
if current_y + font_size + 15 > height:
|
| 163 |
+
break
|
| 164 |
+
|
| 165 |
+
text = sentences[sent_idx % len(sentences)]
|
| 166 |
+
sent_idx += 1
|
| 167 |
+
|
| 168 |
+
# Paragraph formatting:
|
| 169 |
+
# - First line indented
|
| 170 |
+
# - Last line of paragraph may be shorter
|
| 171 |
+
is_first_line = (line_i == 0)
|
| 172 |
+
is_last_line = (line_i == num_l - 1)
|
| 173 |
+
|
| 174 |
+
x_pos = random.randint(60, 90) if is_first_line else random.randint(30, 45)
|
| 175 |
+
|
| 176 |
+
# Trim text if too long for width
|
| 177 |
+
while len(text) > 5:
|
| 178 |
+
try:
|
| 179 |
+
tb = font.getbbox(text)
|
| 180 |
+
tw = tb[2] - tb[0]
|
| 181 |
+
except Exception:
|
| 182 |
+
tw = len(text) * int(font_size * 0.7)
|
| 183 |
+
if x_pos + tw < width - 40:
|
| 184 |
+
break
|
| 185 |
+
parts = text.rsplit(' ', 1)
|
| 186 |
+
text = parts[0] if len(parts) > 1 else text[:-2]
|
| 187 |
+
|
| 188 |
+
if is_last_line and random.random() < 0.6:
|
| 189 |
+
words = text.split()
|
| 190 |
+
if len(words) > 3:
|
| 191 |
+
cut = random.randint(3, len(words) - 1)
|
| 192 |
+
text = ' '.join(words[:cut])
|
| 193 |
+
if not text.endswith(('꩞', '꩝')):
|
| 194 |
+
text += random.choice(['꩞', '꩝'])
|
| 195 |
+
|
| 196 |
+
if not text.strip():
|
| 197 |
+
continue
|
| 198 |
+
|
| 199 |
+
ink_r = random.randint(15, 50)
|
| 200 |
+
ink_g = random.randint(15, 45)
|
| 201 |
+
ink_b = random.randint(15, 45)
|
| 202 |
+
draw.text((x_pos, current_y), text, font=font, fill=(ink_r, ink_g, ink_b))
|
| 203 |
+
|
| 204 |
+
try:
|
| 205 |
+
bbox = font.getbbox(text)
|
| 206 |
+
text_w = bbox[2] - bbox[0]
|
| 207 |
+
text_h = bbox[3] - bbox[1]
|
| 208 |
+
except Exception:
|
| 209 |
+
text_w = len(text) * int(font_size * 0.7)
|
| 210 |
+
text_h = font_size
|
| 211 |
+
|
| 212 |
+
pad_x = random.randint(2, 4)
|
| 213 |
+
# When lines are ultra tight, pad_y must be tight to avoid overlapping labels
|
| 214 |
+
pad_y = 1 if gap <= 6 else random.randint(2, 3)
|
| 215 |
+
|
| 216 |
+
box = [
|
| 217 |
+
[x_pos - pad_x, current_y - pad_y],
|
| 218 |
+
[x_pos + text_w + pad_x, current_y - pad_y],
|
| 219 |
+
[x_pos + text_w + pad_x, current_y + text_h + pad_y],
|
| 220 |
+
[x_pos - pad_x, current_y + text_h + pad_y]
|
| 221 |
+
]
|
| 222 |
+
line_boxes.append((text, box))
|
| 223 |
+
current_y += y_spacing
|
| 224 |
+
|
| 225 |
+
# Paragraph gap
|
| 226 |
+
current_y += random.randint(12, 24)
|
| 227 |
+
if current_y + font_size + 15 > height:
|
| 228 |
+
break
|
| 229 |
+
|
| 230 |
+
if not line_boxes:
|
| 231 |
+
return None
|
| 232 |
+
|
| 233 |
+
img_np = np.array(bg)
|
| 234 |
+
h, w = img_np.shape[:2]
|
| 235 |
+
|
| 236 |
+
# Geometric transformations: mild perspective & wave
|
| 237 |
+
has_distortion = random.random() < 0.65
|
| 238 |
+
if has_distortion:
|
| 239 |
+
amp = random.uniform(1.5, 4.0)
|
| 240 |
+
freq = random.uniform(0.005, 0.010)
|
| 241 |
+
phase = random.uniform(0, 2 * np.pi)
|
| 242 |
+
|
| 243 |
+
pts1 = np.float32([[0, 0], [w, 0], [0, h], [w, h]])
|
| 244 |
+
skew_amt = 0.02
|
| 245 |
+
pts2 = np.float32([
|
| 246 |
+
[random.uniform(0, w * skew_amt), random.uniform(0, h * skew_amt)],
|
| 247 |
+
[w - random.uniform(0, w * skew_amt), random.uniform(0, h * skew_amt)],
|
| 248 |
+
[random.uniform(0, w * skew_amt), h - random.uniform(0, h * skew_amt)],
|
| 249 |
+
[w - random.uniform(0, w * skew_amt), h - random.uniform(0, h * skew_amt)]
|
| 250 |
+
])
|
| 251 |
+
matrix = cv2.getPerspectiveTransform(pts1, pts2)
|
| 252 |
+
|
| 253 |
+
x_idx = np.arange(w)
|
| 254 |
+
y_idx = np.arange(h)
|
| 255 |
+
X, Y = np.meshgrid(x_idx, y_idx)
|
| 256 |
+
dy = amp * np.sin(X * freq + phase)
|
| 257 |
+
map_x = X.astype(np.float32)
|
| 258 |
+
map_y = (Y + dy).astype(np.float32)
|
| 259 |
+
img_wave = cv2.remap(img_np, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REPLICATE)
|
| 260 |
+
img_warped = cv2.warpPerspective(img_wave, matrix, (w, h), borderMode=cv2.BORDER_REPLICATE)
|
| 261 |
+
|
| 262 |
+
warped_labels = []
|
| 263 |
+
for text, box in line_boxes:
|
| 264 |
+
w_box = warp_points(box, matrix, amp, freq, phase)
|
| 265 |
+
warped_labels.append({
|
| 266 |
+
"transcription": text,
|
| 267 |
+
"points": w_box
|
| 268 |
+
})
|
| 269 |
+
else:
|
| 270 |
+
img_warped = img_np
|
| 271 |
+
warped_labels = [{
|
| 272 |
+
"transcription": text,
|
| 273 |
+
"points": box
|
| 274 |
+
} for text, box in line_boxes]
|
| 275 |
+
|
| 276 |
+
img_name = f"{prefix}_{idx:05d}.png"
|
| 277 |
+
img_path = os.path.join(out_dir, img_name)
|
| 278 |
+
Image.fromarray(img_warped).save(img_path)
|
| 279 |
+
|
| 280 |
+
return f"{prefix}/{img_name}\t{json.dumps(warped_labels, ensure_ascii=False)}"
|
| 281 |
+
|
| 282 |
+
def main():
|
| 283 |
+
parser = argparse.ArgumentParser(description="Generate synthetic Cham Detection dataset V2")
|
| 284 |
+
parser.add_argument("--num_train", type=int, default=1000, help="Number of training pages")
|
| 285 |
+
parser.add_argument("--num_val", type=int, default=100, help="Number of validation pages")
|
| 286 |
+
parser.add_argument("--output_dir", type=str, default=None, help="Output directory")
|
| 287 |
+
parser.add_argument("--workers", type=int, default=4, help="Number of parallel worker processes")
|
| 288 |
+
args = parser.parse_args()
|
| 289 |
+
|
| 290 |
+
print(f"🚀 Cham Detection Generator V2 (Tight Line Hard-Cases)")
|
| 291 |
+
print(f" - Target Train: {args.num_train} pages")
|
| 292 |
+
print(f" - Target Val: {args.num_val} pages")
|
| 293 |
+
print(f" - Workers: {args.workers}")
|
| 294 |
+
|
| 295 |
+
corpus_candidates = [
|
| 296 |
+
os.path.join(PROJECT_ROOT, "data", "corpus", "cham_text.txt"),
|
| 297 |
+
os.path.join(PROJECT_ROOT, "ocr-training", "data", "corpus", "cham_text.txt"),
|
| 298 |
+
os.path.join(os.getcwd(), "data", "corpus", "cham_text.txt"),
|
| 299 |
+
os.path.join(os.getcwd(), "ocr-training", "data", "corpus", "cham_text.txt"),
|
| 300 |
+
os.path.join(PROJECT_ROOT, "output_v22", "paddleocr_cham_finetune", "data", "corpus", "cham_text.txt"),
|
| 301 |
+
os.path.join(PROJECT_ROOT, "data", "cham-ocr-v5-assets", "corpus", "corpus", "cham_text.txt")
|
| 302 |
+
]
|
| 303 |
+
corpus_file = None
|
| 304 |
+
for cand in corpus_candidates:
|
| 305 |
+
if os.path.exists(cand):
|
| 306 |
+
corpus_file = cand
|
| 307 |
+
break
|
| 308 |
+
|
| 309 |
+
if not corpus_file:
|
| 310 |
+
print(f"📥 Chưa tìm thấy corpus cục bộ, đang tải cham_text.txt từ GitHub...")
|
| 311 |
+
corpus_file = os.path.join(PROJECT_ROOT, "data", "corpus", "cham_text.txt")
|
| 312 |
+
os.makedirs(os.path.dirname(corpus_file), exist_ok=True)
|
| 313 |
+
try:
|
| 314 |
+
import urllib.request
|
| 315 |
+
raw_url = "https://raw.githubusercontent.com/phucsd1/Cham-OCR/main/ocr-training/data/corpus/cham_text.txt"
|
| 316 |
+
urllib.request.urlretrieve(raw_url, corpus_file)
|
| 317 |
+
print(f"✅ Đã tải corpus thành công về: {corpus_file}")
|
| 318 |
+
except Exception as e:
|
| 319 |
+
print(f"⚠️ Không thể tải từ GitHub ({e}), tự động sinh corpus dự phòng...")
|
| 320 |
+
# Fallback Cham sentence generator
|
| 321 |
+
cham_consonants = list("ꨆꨇꨈꨉꨊꨋꨌꨍꨎꨏꨐꨑꨒꨓꨔꨕꨖꨗꨘꨙꨚꨛꨜꨝꨞꨟꨠꨡꨢꨣꨤꨥꨦꨧꨨꨀ")
|
| 322 |
+
cham_vowels = list("ꨩꨪꨫꨬꨭꨮꨯꨰꨱꨲꨴꨵ")
|
| 323 |
+
cham_finals = list("ꩀꩃꩌꩍꩆꩉꩊꩂꩅ")
|
| 324 |
+
synthetic_corpus = []
|
| 325 |
+
for _ in range(5000):
|
| 326 |
+
words = []
|
| 327 |
+
for _ in range(random.randint(4, 10)):
|
| 328 |
+
syl = random.choice(cham_consonants)
|
| 329 |
+
if random.random() < 0.3: syl += "ꨳ"
|
| 330 |
+
if random.random() < 0.7: syl += random.choice(cham_vowels)
|
| 331 |
+
if random.random() < 0.5: syl += random.choice(cham_finals)
|
| 332 |
+
words.append(syl)
|
| 333 |
+
synthetic_corpus.append(" ".join(words) + random.choice([" ꩞", " ꩝"]))
|
| 334 |
+
with open(corpus_file, "w", encoding="utf-8") as f:
|
| 335 |
+
f.write("\n".join(synthetic_corpus) + "\n")
|
| 336 |
+
print(f"✅ Đã tạo {len(synthetic_corpus)} câu corpus dự phòng.")
|
| 337 |
+
|
| 338 |
+
with open(corpus_file, "r", encoding="utf-8") as f:
|
| 339 |
+
sentences = [line.strip() for line in f if line.strip() and not line.strip().startswith('#')]
|
| 340 |
+
print(f" - Đã nạp {len(sentences)} dòng ngữ liệu Chăm ({corpus_file}).")
|
| 341 |
+
|
| 342 |
+
fonts_dir = os.path.join(PROJECT_ROOT, "data", "fonts")
|
| 343 |
+
if not os.path.exists(fonts_dir):
|
| 344 |
+
fonts_dir = os.path.join(PROJECT_ROOT, "output_v22", "paddleocr_cham_finetune", "data", "fonts")
|
| 345 |
+
|
| 346 |
+
font_names = ["NotoSansCham-Regular.ttf", "NotoSansCham-Bold.ttf", "NotoSansCham-Black.ttf"]
|
| 347 |
+
fonts = [os.path.join(fonts_dir, f) for f in font_names if os.path.exists(os.path.join(fonts_dir, f))]
|
| 348 |
+
if not fonts:
|
| 349 |
+
fonts = [os.path.join(fonts_dir, "NotoSansCham-Regular.ttf")]
|
| 350 |
+
print(f" - Using fonts: {[os.path.basename(f) for f in fonts]}")
|
| 351 |
+
|
| 352 |
+
det_root = args.output_dir or os.path.join(PROJECT_ROOT, "data", "detector_v2")
|
| 353 |
+
train_dir = os.path.join(det_root, "train_images")
|
| 354 |
+
val_dir = os.path.join(det_root, "val_images")
|
| 355 |
+
os.makedirs(train_dir, exist_ok=True)
|
| 356 |
+
os.makedirs(val_dir, exist_ok=True)
|
| 357 |
+
|
| 358 |
+
# 1. Generate Training Data
|
| 359 |
+
print(f"\n⏳ Generating {args.num_train} training pages...")
|
| 360 |
+
train_tasks = [(i, sentences, fonts, train_dir, "train_images") for i in range(args.num_train)]
|
| 361 |
+
train_labels = []
|
| 362 |
+
|
| 363 |
+
with ProcessPoolExecutor(max_workers=args.workers) as executor:
|
| 364 |
+
for res in executor.map(generate_single_page, train_tasks):
|
| 365 |
+
if res:
|
| 366 |
+
train_labels.append(res)
|
| 367 |
+
if len(train_labels) % 200 == 0:
|
| 368 |
+
print(f" ... Generated {len(train_labels)}/{args.num_train} training pages")
|
| 369 |
+
|
| 370 |
+
with open(os.path.join(det_root, "det_train_label.txt"), "w", encoding="utf-8", newline="\n") as f:
|
| 371 |
+
f.write("\n".join(train_labels) + "\n")
|
| 372 |
+
print(f"✅ Finished training data: {len(train_labels)} pages saved.")
|
| 373 |
+
|
| 374 |
+
# 2. Generate Validation Data
|
| 375 |
+
print(f"\n⏳ Generating {args.num_val} validation pages...")
|
| 376 |
+
val_tasks = [(i, sentences, fonts, val_dir, "val_images") for i in range(args.num_val)]
|
| 377 |
+
val_labels = []
|
| 378 |
+
|
| 379 |
+
with ProcessPoolExecutor(max_workers=args.workers) as executor:
|
| 380 |
+
for res in executor.map(generate_single_page, val_tasks):
|
| 381 |
+
if res:
|
| 382 |
+
val_labels.append(res)
|
| 383 |
+
if len(val_labels) % 50 == 0:
|
| 384 |
+
print(f" ... Generated {len(val_labels)}/{args.num_val} validation pages")
|
| 385 |
+
|
| 386 |
+
with open(os.path.join(det_root, "det_val_label.txt"), "w", encoding="utf-8", newline="\n") as f:
|
| 387 |
+
f.write("\n".join(val_labels) + "\n")
|
| 388 |
+
print(f"✅ Finished validation data: {len(val_labels)} pages saved.")
|
| 389 |
+
|
| 390 |
+
print(f"\n🎉 Dataset successfully generated at: {det_root}")
|
| 391 |
+
print(f" - Label format: PaddleOCR SimpleDataSet (points + transcription)")
|
| 392 |
+
|
| 393 |
+
if __name__ == '__main__':
|
| 394 |
+
main()
|
ocr-training/scripts/get_live_log.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import time
|
| 4 |
+
import threading
|
| 5 |
+
import queue
|
| 6 |
+
|
| 7 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 8 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 9 |
+
|
| 10 |
+
os.environ['KAGGLE_USERNAME'] = 'gustavnguyen'
|
| 11 |
+
os.environ['KAGGLE_KEY'] = '6bf56db7e5c0fa7895d157167961d92b'
|
| 12 |
+
|
| 13 |
+
from kaggle.api.kaggle_api_extended import KaggleApi
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
api = KaggleApi()
|
| 17 |
+
api.authenticate()
|
| 18 |
+
|
| 19 |
+
q = queue.Queue()
|
| 20 |
+
stop_event = threading.Event()
|
| 21 |
+
|
| 22 |
+
def worker():
|
| 23 |
+
try:
|
| 24 |
+
for event in api.kernels_logs_stream('gustavnguyen/paddleocr-cham-finetune'):
|
| 25 |
+
if stop_event.is_set():
|
| 26 |
+
break
|
| 27 |
+
data = event.get('data', '')
|
| 28 |
+
if data:
|
| 29 |
+
q.put(data)
|
| 30 |
+
except Exception:
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
t = threading.Thread(target=worker, daemon=True)
|
| 34 |
+
t.start()
|
| 35 |
+
|
| 36 |
+
raw_text = []
|
| 37 |
+
# Thu thập tất cả log đã có (cho đến khi không còn event mới trong 3 giây)
|
| 38 |
+
t_start = time.time()
|
| 39 |
+
last_recv = time.time()
|
| 40 |
+
|
| 41 |
+
while time.time() - t_start < 20:
|
| 42 |
+
try:
|
| 43 |
+
chunk = q.get(timeout=2.0)
|
| 44 |
+
raw_text.append(chunk)
|
| 45 |
+
last_recv = time.time()
|
| 46 |
+
except queue.Empty:
|
| 47 |
+
if raw_text and (time.time() - last_recv > 3.0):
|
| 48 |
+
# Đã đọc hết log hiện có
|
| 49 |
+
break
|
| 50 |
+
|
| 51 |
+
stop_event.set()
|
| 52 |
+
|
| 53 |
+
full_str = "".join(raw_text)
|
| 54 |
+
clean_lines = []
|
| 55 |
+
for line in full_str.splitlines():
|
| 56 |
+
l = line.strip()
|
| 57 |
+
if l and 'MB/s' not in l and 'â”' not in l and not l.startswith('\x1b['):
|
| 58 |
+
clean_lines.append(l)
|
| 59 |
+
|
| 60 |
+
print(f"📊 Thu thập được {len(clean_lines)} dòng log từ Kaggle:")
|
| 61 |
+
print("="*70)
|
| 62 |
+
for l in clean_lines[-40:]:
|
| 63 |
+
print(l)
|
| 64 |
+
print("="*70)
|
| 65 |
+
|
| 66 |
+
if __name__ == '__main__':
|
| 67 |
+
main()
|
ocr-training/scripts/kaggle_train_det.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Kaggle Training Automation for Cham Text Detection (Phase 4).
|
| 5 |
+
Prepares and submits a multi-GPU (T4x2) PaddleOCR DBNet training job to Kaggle,
|
| 6 |
+
strictly following the Cham-OCR project rules:
|
| 7 |
+
- Kaggle user: "gustavnguyen"
|
| 8 |
+
- Key: "6bf56db7e5c0fa7895d157167961d92b"
|
| 9 |
+
- Accelerator: NvidiaTeslaT4
|
| 10 |
+
- Distributed multi-GPU execution: paddle.distributed.launch --gpus '0,1'
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import json
|
| 16 |
+
import subprocess
|
| 17 |
+
import shutil
|
| 18 |
+
import tempfile
|
| 19 |
+
|
| 20 |
+
# Force UTF-8 standard output for Windows console
|
| 21 |
+
if sys.stdout and sys.stdout.encoding != 'utf-8':
|
| 22 |
+
try:
|
| 23 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 24 |
+
except AttributeError:
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
# Force Kaggle credentials
|
| 28 |
+
os.environ["KAGGLE_USERNAME"] = "gustavnguyen"
|
| 29 |
+
os.environ["KAGGLE_KEY"] = "6bf56db7e5c0fa7895d157167961d92b"
|
| 30 |
+
|
| 31 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 32 |
+
sys.path.insert(0, os.path.join(PROJECT_ROOT, "scripts"))
|
| 33 |
+
|
| 34 |
+
from kaggle_ops import get_authenticated_api, monitor_kernel, download_kernel_outputs
|
| 35 |
+
|
| 36 |
+
def build_det_notebook():
|
| 37 |
+
"""Generates the Jupyter Notebook for running Cham Detection training on Kaggle."""
|
| 38 |
+
nb = {
|
| 39 |
+
"cells": [
|
| 40 |
+
{
|
| 41 |
+
"cell_type": "markdown",
|
| 42 |
+
"metadata": {},
|
| 43 |
+
"source": [
|
| 44 |
+
"# Cham OCR Text Detection Fine-Tuning (Phase 4 - DBNet PP-OCRv4)\n",
|
| 45 |
+
"Automated training pipeline for Cham scene text & manuscript detection on Kaggle T4x2."
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"cell_type": "code",
|
| 50 |
+
"execution_count": None,
|
| 51 |
+
"metadata": {},
|
| 52 |
+
"outputs": [],
|
| 53 |
+
"source": [
|
| 54 |
+
"# 1. Environment & Dependencies Setup\n",
|
| 55 |
+
"!pip install --upgrade pip\n",
|
| 56 |
+
"!pip install \"paddlepaddle-gpu>=2.6.0\" -i https://www.paddlepaddle.org.cn/packages/stable/cu118/\n",
|
| 57 |
+
"!pip install imgaug pyclipper shapely lmdb attrdict\n",
|
| 58 |
+
"\n",
|
| 59 |
+
"import paddle\n",
|
| 60 |
+
"print(f\"PaddlePaddle version: {paddle.__version__}\")\n",
|
| 61 |
+
"print(f\"CUDA available: {paddle.is_compiled_with_cuda()}\")\n",
|
| 62 |
+
"num_gpus = paddle.device.cuda.device_count()\n",
|
| 63 |
+
"print(f\"GPU count: {num_gpus}\")\n"
|
| 64 |
+
]
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"cell_type": "code",
|
| 68 |
+
"execution_count": None,
|
| 69 |
+
"metadata": {},
|
| 70 |
+
"outputs": [],
|
| 71 |
+
"source": [
|
| 72 |
+
"# 2. Clone PaddleOCR repository\n",
|
| 73 |
+
"import os\n",
|
| 74 |
+
"if not os.path.exists('PaddleOCR'):\n",
|
| 75 |
+
" !git clone -b release/2.7 https://github.com/PaddlePaddle/PaddleOCR.git\n",
|
| 76 |
+
"\n",
|
| 77 |
+
"%cd PaddleOCR\n",
|
| 78 |
+
"!pip install -r requirements.txt\n"
|
| 79 |
+
]
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"cell_type": "code",
|
| 83 |
+
"execution_count": None,
|
| 84 |
+
"metadata": {},
|
| 85 |
+
"outputs": [],
|
| 86 |
+
"source": [
|
| 87 |
+
"# 3. Prepare Cham Detection Dataset\n",
|
| 88 |
+
"%cd /kaggle/working/PaddleOCR\n",
|
| 89 |
+
"os.makedirs('data/detector', exist_ok=True)\n",
|
| 90 |
+
"os.makedirs('data/fonts', exist_ok=True)\n",
|
| 91 |
+
"os.makedirs('data/corpus', exist_ok=True)\n",
|
| 92 |
+
"\n",
|
| 93 |
+
"# Download Cham fonts\n",
|
| 94 |
+
"!wget -q -O data/fonts/NotoSansCham-Regular.ttf https://raw.githubusercontent.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansCham/NotoSansCham-Regular.ttf\n",
|
| 95 |
+
"!wget -q -O data/fonts/NotoSansCham-Bold.ttf https://raw.githubusercontent.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansCham/NotoSansCham-Bold.ttf\n",
|
| 96 |
+
"\n",
|
| 97 |
+
"print(\"Fonts downloaded successfully.\")\n"
|
| 98 |
+
]
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"cell_type": "code",
|
| 102 |
+
"execution_count": None,
|
| 103 |
+
"metadata": {},
|
| 104 |
+
"outputs": [],
|
| 105 |
+
"source": [
|
| 106 |
+
"# 4. Adaptive Multi-GPU Training\n",
|
| 107 |
+
"import paddle\n",
|
| 108 |
+
"num_gpus = paddle.device.cuda.device_count()\n",
|
| 109 |
+
"print(f\"Detected {num_gpus} GPUs for training.\")\n",
|
| 110 |
+
"\n",
|
| 111 |
+
"config_path = 'configs/det/ch_PP-OCRv4_det_cham.yml'\n",
|
| 112 |
+
"\n",
|
| 113 |
+
"# Download pre-trained DBNet weights\n",
|
| 114 |
+
"!wget -q https://paddleocr.bj.bcebos.com/pretrained/PPLCNetV3_x0_75_ocr_det.pdparams -O ./pretrained_det.pdparams\n",
|
| 115 |
+
"\n",
|
| 116 |
+
"if num_gpus > 1:\n",
|
| 117 |
+
" gpus = ','.join(str(i) for i in range(num_gpus))\n",
|
| 118 |
+
" print(f\"Launching distributed training on GPUs {gpus}...\")\n",
|
| 119 |
+
" !python3 -m paddle.distributed.launch --gpus '{gpus}' tools/train.py -c {config_path} -o Global.pretrained_model=./pretrained_det.pdparams\n",
|
| 120 |
+
"else:\n",
|
| 121 |
+
" print(\"Launching single GPU training...\")\n",
|
| 122 |
+
" !python3 tools/train.py -c {config_path} -o Global.pretrained_model=./pretrained_det.pdparams\n"
|
| 123 |
+
]
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"cell_type": "code",
|
| 127 |
+
"execution_count": None,
|
| 128 |
+
"metadata": {},
|
| 129 |
+
"outputs": [],
|
| 130 |
+
"source": [
|
| 131 |
+
"# 5. Export Inference Model\n",
|
| 132 |
+
"print(\"Exporting detection model to inference format...\")\n",
|
| 133 |
+
"!python3 tools/export_model.py -c configs/det/ch_PP-OCRv4_det_cham.yml -o Global.checkpoints=./output/ch_PP-OCRv4_det_cham/latest Global.save_inference_dir=./output/det_cham_inference\n",
|
| 134 |
+
"\n",
|
| 135 |
+
"# Archive outputs\n",
|
| 136 |
+
"!zip -r /kaggle/working/det_cham_inference.zip ./output/det_cham_inference\n",
|
| 137 |
+
"print(\"Done! Checkpoint ready for download.\")\n"
|
| 138 |
+
]
|
| 139 |
+
}
|
| 140 |
+
],
|
| 141 |
+
"metadata": {
|
| 142 |
+
"kernelspec": {
|
| 143 |
+
"display_name": "Python 3",
|
| 144 |
+
"language": "python",
|
| 145 |
+
"name": "python3"
|
| 146 |
+
},
|
| 147 |
+
"language_info": {
|
| 148 |
+
"codemirror_mode": {"name": "ipython", "version": 3},
|
| 149 |
+
"file_extension": ".py",
|
| 150 |
+
"mimetype": "text/x-python",
|
| 151 |
+
"name": "python",
|
| 152 |
+
"nbformat": 4,
|
| 153 |
+
"nbformat_minor": 4
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
"nbformat": 4,
|
| 157 |
+
"nbformat_minor": 4
|
| 158 |
+
}
|
| 159 |
+
return nb
|
| 160 |
+
|
| 161 |
+
def main():
|
| 162 |
+
print("🚀 Cham Text Detection - Kaggle Dispatcher (Phase 4)")
|
| 163 |
+
api = get_authenticated_api()
|
| 164 |
+
if not api:
|
| 165 |
+
print("❌ Cannot authenticate with Kaggle API.")
|
| 166 |
+
return
|
| 167 |
+
|
| 168 |
+
work_dir = os.path.join(PROJECT_ROOT, "output", "kaggle_det_submission")
|
| 169 |
+
os.makedirs(work_dir, exist_ok=True)
|
| 170 |
+
|
| 171 |
+
# 1. Write notebook
|
| 172 |
+
nb_path = os.path.join(work_dir, "kaggle_det_notebook.ipynb")
|
| 173 |
+
nb_data = build_det_notebook()
|
| 174 |
+
with open(nb_path, "w", encoding="utf-8") as f:
|
| 175 |
+
json.dump(nb_data, f, indent=2, ensure_ascii=False)
|
| 176 |
+
print(f"📝 Generated Kaggle notebook at: {nb_path}")
|
| 177 |
+
|
| 178 |
+
# 2. Write metadata
|
| 179 |
+
kernel_slug = "cham-ocr-v4-det-train"
|
| 180 |
+
meta = {
|
| 181 |
+
"id": f"gustavnguyen/{kernel_slug}",
|
| 182 |
+
"title": kernel_slug,
|
| 183 |
+
"code_file": "kaggle_det_notebook.ipynb",
|
| 184 |
+
"language": "python",
|
| 185 |
+
"kernel_type": "notebook",
|
| 186 |
+
"is_private": "true",
|
| 187 |
+
"enable_gpu": "true",
|
| 188 |
+
"enable_internet": "true",
|
| 189 |
+
"dataset_sources": [],
|
| 190 |
+
"competition_sources": [],
|
| 191 |
+
"kernel_sources": []
|
| 192 |
+
}
|
| 193 |
+
meta_path = os.path.join(work_dir, "kernel-metadata.json")
|
| 194 |
+
with open(meta_path, "w", encoding="utf-8") as f:
|
| 195 |
+
json.dump(meta, f, indent=2)
|
| 196 |
+
print(f"📋 Generated kernel-metadata.json at: {meta_path}")
|
| 197 |
+
|
| 198 |
+
print("\n📦 To launch training on Kaggle:")
|
| 199 |
+
print(f" kaggle kernels push -p \"{work_dir}\"")
|
| 200 |
+
print("\n⚙️ Configured GPU: NvidiaTeslaT4 (T4x2) with adaptive paddle.distributed.launch")
|
| 201 |
+
|
| 202 |
+
if __name__ == '__main__':
|
| 203 |
+
main()
|
ocr-training/scripts/kaggle_watchdog_v24.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import time
|
| 4 |
+
import json
|
| 5 |
+
import zipfile
|
| 6 |
+
import socket
|
| 7 |
+
import threading
|
| 8 |
+
import queue
|
| 9 |
+
from kaggle.api.kaggle_api_extended import KaggleApi
|
| 10 |
+
|
| 11 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 12 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 13 |
+
|
| 14 |
+
socket.setdefaulttimeout(60)
|
| 15 |
+
|
| 16 |
+
def get_api():
|
| 17 |
+
os.environ["KAGGLE_USERNAME"] = "gustavnguyen"
|
| 18 |
+
os.environ["KAGGLE_KEY"] = "6bf56db7e5c0fa7895d157167961d92b"
|
| 19 |
+
api = KaggleApi()
|
| 20 |
+
api.authenticate()
|
| 21 |
+
return api
|
| 22 |
+
|
| 23 |
+
def stream_logs_worker(api, kernel_id, log_file_path, stop_event):
|
| 24 |
+
"""Lắng nghe luồng SSE log trực tiếp từ Kaggle và lưu file đồng thời phân tích sự kiện (tự động kết nối lại)"""
|
| 25 |
+
while not stop_event.is_set():
|
| 26 |
+
try:
|
| 27 |
+
with open(log_file_path, 'a', encoding='utf-8') as f_log:
|
| 28 |
+
for event in api.kernels_logs_stream(kernel_id):
|
| 29 |
+
if stop_event.is_set():
|
| 30 |
+
break
|
| 31 |
+
data = event.get('data', '')
|
| 32 |
+
if data:
|
| 33 |
+
f_log.write(data)
|
| 34 |
+
f_log.flush()
|
| 35 |
+
# In các dòng quan trọng
|
| 36 |
+
for line in data.splitlines():
|
| 37 |
+
line_str = line.strip()
|
| 38 |
+
if any(k in line_str for k in [
|
| 39 |
+
'epoch: [', 'best metric', 'cur metric',
|
| 40 |
+
'save best model', 'save model in',
|
| 41 |
+
'Detected GPU Count', 'Place(gpu', 'train dataloader has',
|
| 42 |
+
'Hoàn thành', 'HOÀN TẤT', 'Error', 'Exception'
|
| 43 |
+
]) and not 'MB/s' in line_str and not 'â”' in line_str:
|
| 44 |
+
print(f"🔥 [KAGGLE LIVE] {line_str}")
|
| 45 |
+
except Exception:
|
| 46 |
+
pass
|
| 47 |
+
if not stop_event.is_set():
|
| 48 |
+
time.sleep(4)
|
| 49 |
+
|
| 50 |
+
def run_watchdog(user="gustavnguyen", slug="paddleocr-cham-finetune", output_dir="ocr-training/output/v24", poll_interval=30):
|
| 51 |
+
kernel_id = f"{user}/{slug}"
|
| 52 |
+
print("="*80)
|
| 53 |
+
print(f"🛡️ ACTIVE WATCHDOG V24 - LIVE STREAM LOG TỪ KAGGLE DUAL T4x2")
|
| 54 |
+
print(f"🎯 Kernel: {kernel_id}")
|
| 55 |
+
print(f"⏱️ Cơ chế: SSE Live Stream + Heartbeat {poll_interval}s")
|
| 56 |
+
print("="*80)
|
| 57 |
+
|
| 58 |
+
api = get_api()
|
| 59 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 60 |
+
live_log_path = os.path.join(output_dir, "live_training.log")
|
| 61 |
+
|
| 62 |
+
# Khởi chạy luồng SSE Live Stream
|
| 63 |
+
stop_stream_event = threading.Event()
|
| 64 |
+
stream_thread = threading.Thread(
|
| 65 |
+
target=stream_logs_worker,
|
| 66 |
+
args=(api, kernel_id, live_log_path, stop_stream_event),
|
| 67 |
+
daemon=True
|
| 68 |
+
)
|
| 69 |
+
stream_thread.start()
|
| 70 |
+
print(f"📡 Đã kết nối kênh Live Log SSE. Ghi nhật ký thời gian thực tại: {live_log_path}\n")
|
| 71 |
+
|
| 72 |
+
start_time = time.time()
|
| 73 |
+
last_status = None
|
| 74 |
+
fail_count = 0
|
| 75 |
+
|
| 76 |
+
while True:
|
| 77 |
+
try:
|
| 78 |
+
res = api.kernels_status(kernel_id)
|
| 79 |
+
if hasattr(res, 'status') and hasattr(res.status, 'name'):
|
| 80 |
+
status = res.status.name.lower()
|
| 81 |
+
elif hasattr(res, 'status'):
|
| 82 |
+
status = str(res.status).split('.')[-1].lower()
|
| 83 |
+
else:
|
| 84 |
+
status = 'unknown'
|
| 85 |
+
|
| 86 |
+
elapsed_sec = int(time.time() - start_time)
|
| 87 |
+
elapsed_min = elapsed_sec // 60
|
| 88 |
+
|
| 89 |
+
if status != last_status or elapsed_sec % 180 < poll_interval:
|
| 90 |
+
print(f"⏱️ [Heartbeat {elapsed_min:02d}m{elapsed_sec%60:02d}s] Trạng thái: {status.upper()}")
|
| 91 |
+
last_status = status
|
| 92 |
+
|
| 93 |
+
# 1. THÀNH CÔNG
|
| 94 |
+
if 'complete' in status:
|
| 95 |
+
stop_stream_event.set()
|
| 96 |
+
print("\n" + "="*80)
|
| 97 |
+
print(f"🎉 KERNEL ĐÃ HOÀN TẤT THÀNH CÔNG TRONG {elapsed_min} PHÚT!")
|
| 98 |
+
print("📥 Bắt đầu tải và đồng bộ mô hình (rec_cham_v24_best.zip)...")
|
| 99 |
+
print("="*80)
|
| 100 |
+
|
| 101 |
+
download_success = download_outputs(api, kernel_id, output_dir)
|
| 102 |
+
if download_success:
|
| 103 |
+
print("✅ Tải và giải nén mô hình V24 hoàn tất!")
|
| 104 |
+
return True
|
| 105 |
+
else:
|
| 106 |
+
print("❌ Lỗi khi tải hoặc giải nén kết quả từ Kaggle.")
|
| 107 |
+
return False
|
| 108 |
+
|
| 109 |
+
# 2. LỖI SỚM
|
| 110 |
+
elif any(k in status for k in ['error', 'cancel', 'abort', 'fail']):
|
| 111 |
+
stop_stream_event.set()
|
| 112 |
+
print("\n" + "!"*80)
|
| 113 |
+
print(f"🚨 BÁO ĐỘNG ĐỎ: KERNEL DỪNG VỚI TRẠNG THÁI LỖI: {status.upper()}!")
|
| 114 |
+
print(f"⏱️ Thời gian chạy: {elapsed_min} phút {elapsed_sec%60} giây.")
|
| 115 |
+
print("!"*80)
|
| 116 |
+
return False
|
| 117 |
+
|
| 118 |
+
fail_count = 0
|
| 119 |
+
|
| 120 |
+
except Exception as e:
|
| 121 |
+
fail_count += 1
|
| 122 |
+
if fail_count >= 5:
|
| 123 |
+
print(f"⚠️ Mất kết nối kiểm tra Kaggle API ({fail_count}/5): {e}")
|
| 124 |
+
|
| 125 |
+
time.sleep(poll_interval)
|
| 126 |
+
|
| 127 |
+
def download_outputs(api, kernel_id, output_dir):
|
| 128 |
+
try:
|
| 129 |
+
socket.setdefaulttimeout(600)
|
| 130 |
+
api.kernels_output(kernel_id, path=output_dir)
|
| 131 |
+
print("✅ Đã tải file output từ Kaggle.")
|
| 132 |
+
|
| 133 |
+
zip_files = [f for f in os.listdir(output_dir) if f.endswith('.zip')]
|
| 134 |
+
if not zip_files:
|
| 135 |
+
print("❌ Không tìm thấy file .zip trong output!")
|
| 136 |
+
return False
|
| 137 |
+
|
| 138 |
+
for zf in zip_files:
|
| 139 |
+
zp = os.path.join(output_dir, zf)
|
| 140 |
+
extract_folder = os.path.join(output_dir, zf.replace('.zip', ''))
|
| 141 |
+
os.makedirs(extract_folder, exist_ok=True)
|
| 142 |
+
print(f"📦 Đang giải nén {zf} ({os.path.getsize(zp) / (1024*1024):.2f} MB)...")
|
| 143 |
+
with zipfile.ZipFile(zp, 'r') as zref:
|
| 144 |
+
zref.extractall(extract_folder)
|
| 145 |
+
print(f"✅ Giải nén xong vào: {extract_folder}")
|
| 146 |
+
|
| 147 |
+
infer_candidates = [
|
| 148 |
+
os.path.join(extract_folder, 'output', 'rec_cham_inference_v24'),
|
| 149 |
+
os.path.join(extract_folder, 'rec_cham_inference_v24'),
|
| 150 |
+
extract_folder
|
| 151 |
+
]
|
| 152 |
+
for cand in infer_candidates:
|
| 153 |
+
if os.path.exists(os.path.join(cand, 'inference.pdmodel')) or os.path.exists(os.path.join(cand, 'model.pdmodel')):
|
| 154 |
+
target_studio_dir = 'ocr-studio/data/output/rec_cham_inference_v24'
|
| 155 |
+
os.makedirs(target_studio_dir, exist_ok=True)
|
| 156 |
+
import shutil
|
| 157 |
+
for item in os.listdir(cand):
|
| 158 |
+
s_item = os.path.join(cand, item)
|
| 159 |
+
d_item = os.path.join(target_studio_dir, item)
|
| 160 |
+
if os.path.isfile(s_item):
|
| 161 |
+
shutil.copy2(s_item, d_item)
|
| 162 |
+
print(f"🚀 Đã đồng bộ mô hình inference V24 vào Studio: {target_studio_dir}")
|
| 163 |
+
break
|
| 164 |
+
return True
|
| 165 |
+
except Exception as e:
|
| 166 |
+
print(f"❌ Lỗi giải nén: {e}")
|
| 167 |
+
return False
|
| 168 |
+
|
| 169 |
+
if __name__ == '__main__':
|
| 170 |
+
run_watchdog()
|
ocr-training/scripts/launch_h100_full_pipeline.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
End-to-End Automated Pipeline for Cham-DBNet on Lightning AI (H100).
|
| 5 |
+
1. Data Gen (2,000 pages ~40,000 lines on CPU)
|
| 6 |
+
2. Switch to H100 GPU
|
| 7 |
+
3. Install paddlepaddle-gpu & launch distributed training
|
| 8 |
+
4. Stream and monitor training progress
|
| 9 |
+
5. Export & package inference model
|
| 10 |
+
6. Download locally and stop Studio
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import time
|
| 16 |
+
from lightning_sdk import Studio, Machine
|
| 17 |
+
|
| 18 |
+
if sys.stdout and sys.stdout.encoding != 'utf-8':
|
| 19 |
+
try:
|
| 20 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 21 |
+
except AttributeError:
|
| 22 |
+
pass
|
| 23 |
+
|
| 24 |
+
def log(msg):
|
| 25 |
+
timestamp = time.strftime("%H:%M:%S")
|
| 26 |
+
print(f"[{timestamp}] {msg}", flush=True)
|
| 27 |
+
|
| 28 |
+
def main():
|
| 29 |
+
log("=================================================================")
|
| 30 |
+
log("🚀 KHỞI CHẠY PIPELINE HUẤN LUYỆN CHAM-DBNET TRÊN LIGHTNING AI H100")
|
| 31 |
+
log("=================================================================")
|
| 32 |
+
|
| 33 |
+
studio = Studio(name="cham-det-h100", teamspace="phucsd", org="phucsd-org")
|
| 34 |
+
log(f"Studio Status: {studio.status} | Machine: {studio.machine}")
|
| 35 |
+
|
| 36 |
+
# Step 1: Check existing dataset
|
| 37 |
+
dataset_exists = studio.run("test -f Cham-OCR/ocr-training/PaddleOCR/data/detector_v2/det_train_label.txt && echo EXISTS || echo NO").strip()
|
| 38 |
+
if "EXISTS" in dataset_exists:
|
| 39 |
+
log("✅ [BƯỚC 1/5] Bộ dữ liệu 2,000 trang (det_train_label.txt) đã có sẵn trên đĩa, bỏ qua bước sinh!")
|
| 40 |
+
else:
|
| 41 |
+
log("\n⚙️ [BƯỚC 1/5] Sinh 2,000 trang dữ liệu đặc trị dòng hẹp (50% gap 3-10px)...")
|
| 42 |
+
studio.run(
|
| 43 |
+
"pkill -9 -f generate_detector_data_v2.py || true",
|
| 44 |
+
"rm -rf Cham-OCR/ocr-training/PaddleOCR/data/detector_v2"
|
| 45 |
+
)
|
| 46 |
+
gen_cmd = "cd Cham-OCR/ocr-training && python3 scripts/generate_detector_data_v2.py --num_train 2000 --num_val 200 --output_dir PaddleOCR/data/detector_v2 --workers 4"
|
| 47 |
+
gen_out = studio.run(gen_cmd)
|
| 48 |
+
for line in gen_out.strip().splitlines()[-4:]:
|
| 49 |
+
log(f" {line}")
|
| 50 |
+
log("✅ Bộ dữ liệu 2,000 trang đã sinh và tạo nhãn SimpleDataSet thành công!")
|
| 51 |
+
|
| 52 |
+
# Step 2: Switch to NVIDIA H100
|
| 53 |
+
log("\n🔥 [BƯỚC 2/5] Nâng cấp phần cứng sang NVIDIA H100 (80GB VRAM SXM5 - lit-h100-80gb-1)...")
|
| 54 |
+
studio.switch_machine("lit-h100-80gb-1")
|
| 55 |
+
log(f"✅ Đã chuyển sang H100! Machine: {studio.machine}")
|
| 56 |
+
|
| 57 |
+
# Step 3: Install PaddlePaddle GPU & Verify
|
| 58 |
+
log("\n📦 [BƯỚC 3/5] Cài đặt PaddlePaddle GPU bản CUDA 11.8/12.x tối ưu cho Hopper H100...")
|
| 59 |
+
studio.run("pip install -q paddlepaddle-gpu -i https://www.paddlepaddle.org.cn/packages/stable/cu118/")
|
| 60 |
+
gpu_check = studio.run("nvidia-smi --query-gpu=name,memory.total --format=csv,noheader")
|
| 61 |
+
log(f"🔥 GPU H100 đã sẵn sàng: {gpu_check.strip()}")
|
| 62 |
+
|
| 63 |
+
# Step 4: Launch Training & Monitor
|
| 64 |
+
log("\n🚀 [BƯỚC 4/5] Khởi chạy huấn luyện Cham-DBNet (batch 64, 150 epochs)...")
|
| 65 |
+
train_cmd = "cd Cham-OCR/ocr-training/PaddleOCR && nohup python3 -m paddle.distributed.launch --gpus '0' tools/train.py -c configs/det/ch_PP-OCRv4_det_h100.yml > train.log 2>&1 &"
|
| 66 |
+
studio.run(train_cmd)
|
| 67 |
+
log("✅ Huấn luyện đã kích hoạt chạy ngầm (background) an toàn trên H100!")
|
| 68 |
+
|
| 69 |
+
log("\n📊 Bắt đầu theo dõi chỉ số loss và epoch liên tục...")
|
| 70 |
+
prev_log = ""
|
| 71 |
+
start_train_time = time.time()
|
| 72 |
+
|
| 73 |
+
while True:
|
| 74 |
+
time.sleep(15)
|
| 75 |
+
# Check running process
|
| 76 |
+
ps_out = studio.run("ps aux | grep tools/train.py | grep -v grep | wc -l").strip()
|
| 77 |
+
is_running = int(ps_out or "0") > 0
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
log_tail = studio.run("cd Cham-OCR/ocr-training/PaddleOCR && tail -n 6 train.log").strip()
|
| 81 |
+
if log_tail and log_tail != prev_log:
|
| 82 |
+
prev_log = log_tail
|
| 83 |
+
log("--- TIẾN ĐỘ HUẤN LUYỆN MỚI NHẤT ---")
|
| 84 |
+
for l in log_tail.splitlines()[-4:]:
|
| 85 |
+
log(f" {l}")
|
| 86 |
+
except Exception as e:
|
| 87 |
+
log(f"Lưu ý: {e}")
|
| 88 |
+
|
| 89 |
+
if not is_running:
|
| 90 |
+
elapsed = (time.time() - start_train_time) / 60
|
| 91 |
+
log(f"\n🎉 Tiến trình huấn luyện hoàn tất sau {elapsed:.1f} phút!")
|
| 92 |
+
break
|
| 93 |
+
|
| 94 |
+
# Step 5: Export, Zip, Download, and Stop
|
| 95 |
+
log("\n📦 [BƯỚC 5/5] Xuất mô hình Inference nhẹ ~4.5MB & đóng gói ZIP...")
|
| 96 |
+
export_cmd = """cd Cham-OCR/ocr-training/PaddleOCR && \
|
| 97 |
+
BEST_MODEL="output/ch_PP-OCRv4_det_cham_h100/best_accuracy" && \
|
| 98 |
+
if [ ! -f "${BEST_MODEL}.pdparams" ]; then BEST_MODEL="output/ch_PP-OCRv4_det_cham_h100/latest"; fi && \
|
| 99 |
+
python3 tools/export_model.py -c configs/det/ch_PP-OCRv4_det_h100.yml -o Global.pretrained_model=${BEST_MODEL} Global.save_inference_dir=../output/ch_PP-OCRv4_det_cham_infer && \
|
| 100 |
+
cd ../output && zip -r ../cham_dbnet_v1_infer.zip ch_PP-OCRv4_det_cham_infer/"""
|
| 101 |
+
studio.run(export_cmd)
|
| 102 |
+
log("✅ Đã xuất mô hình và đóng gói cham_dbnet_v1_infer.zip th��nh công!")
|
| 103 |
+
|
| 104 |
+
# Download to local
|
| 105 |
+
local_zip = os.path.join(os.getcwd(), "ocr-training", "cham_dbnet_v1_infer.zip")
|
| 106 |
+
log(f"⬇️ Đang tải tệp mô hình về máy local: {local_zip}...")
|
| 107 |
+
# Read remote zip in base64 and write locally to ensure 100% reliability
|
| 108 |
+
b64_zip = studio.run("cd Cham-OCR/ocr-training && python3 -c \"import base64, pathlib; print(base64.b64encode(pathlib.Path('cham_dbnet_v1_infer.zip').read_bytes()).decode('ascii'))\"").strip()
|
| 109 |
+
import base64
|
| 110 |
+
with open(local_zip, "wb") as f:
|
| 111 |
+
f.write(base64.b64decode(b64_zip))
|
| 112 |
+
log(f"🎉 ĐÃ TẢI VỀ THÀNH CÔNG: {local_zip} ({os.path.getsize(local_zip)/(1024*1024):.2f} MB)")
|
| 113 |
+
|
| 114 |
+
# STOP STUDIO IMMEDIATELY
|
| 115 |
+
log("\n🛑 Tự động dừng Studio để bảo vệ số dư credit...")
|
| 116 |
+
studio.stop()
|
| 117 |
+
log(f"✅ Studio đã DỪNG (Status: {studio.status})! 0 credit bị tiêu tốn thêm.")
|
| 118 |
+
log("=================================================================")
|
| 119 |
+
log("🎊 HOÀN THÀNH TOÀN DIỆN!")
|
| 120 |
+
log("=================================================================")
|
| 121 |
+
|
| 122 |
+
if __name__ == "__main__":
|
| 123 |
+
main()
|
ocr-training/scripts/launch_l40s_pipeline.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
End-to-End Automated Pipeline for Cham-DBNet on Lightning AI (NVIDIA L40S 48GB).
|
| 5 |
+
- Machine: g6e.4xlarge (NVIDIA L40S 48GB Ada Lovelace - 3.54 cr/h)
|
| 6 |
+
- 2,000 training pages (det_train_label.txt) + 200 validation pages already prepared on disk.
|
| 7 |
+
- 150 epochs, DBNet PP-OCRv4 Mobile, batch size 64.
|
| 8 |
+
- Automatic monitoring, live loss streaming, model export, zip download, and guaranteed auto-stop.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
import time
|
| 14 |
+
import base64
|
| 15 |
+
from lightning_sdk import Studio, Machine
|
| 16 |
+
|
| 17 |
+
if sys.stdout and sys.stdout.encoding != 'utf-8':
|
| 18 |
+
try:
|
| 19 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 20 |
+
except AttributeError:
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
def log(msg):
|
| 24 |
+
timestamp = time.strftime("%H:%M:%S")
|
| 25 |
+
print(f"[{timestamp}] {msg}", flush=True)
|
| 26 |
+
|
| 27 |
+
def main():
|
| 28 |
+
log("=================================================================")
|
| 29 |
+
log("🚀 KHỞI CHẠY PIPELINE HUẤN LUYỆN CHAM-DBNET TRÊN NVIDIA L40S 48GB")
|
| 30 |
+
log("=================================================================")
|
| 31 |
+
|
| 32 |
+
studio = Studio(name="cham-det-h100", teamspace="phucsd", org="phucsd-org")
|
| 33 |
+
log(f"Studio hiện tại: Status={studio.status} | Machine={studio.machine}")
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
target_machine = Machine.L40S
|
| 37 |
+
log(f"\n🔥 [BƯỚC 1/5] Khởi động Studio với GPU NVIDIA L40S 48GB (Machine.L40S - 3.54 cr/h)...")
|
| 38 |
+
if str(studio.status).lower() != "running":
|
| 39 |
+
studio.start(machine=target_machine)
|
| 40 |
+
else:
|
| 41 |
+
if str(studio.machine) != str(target_machine):
|
| 42 |
+
log(f"Đang chuyển phần cứng từ {studio.machine} sang {target_machine}...")
|
| 43 |
+
studio.switch_machine(target_machine)
|
| 44 |
+
|
| 45 |
+
log(f"✅ Studio đang chạy! Machine: {studio.machine} | Status: {studio.status}")
|
| 46 |
+
|
| 47 |
+
log("\n🔍 [BƯỚC 2/5] Kiểm tra GPU và môi trường CUDA...")
|
| 48 |
+
gpu_info = studio.run("nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader").strip()
|
| 49 |
+
log(f"🔥 GPU phát hiện: {gpu_info}")
|
| 50 |
+
|
| 51 |
+
log("📦 Cài đặt/Kiểm tra PaddlePaddle GPU tương thích CUDA & VisualDL & NumPy...")
|
| 52 |
+
paddle_check = studio.run("python3 -c \"import paddle; print('PADDLE_READY', paddle.__version__, paddle.device.cuda.device_count())\" || true").strip()
|
| 53 |
+
if "PADDLE_READY" not in paddle_check or " 0" in paddle_check:
|
| 54 |
+
log("Đang cài đặt paddlepaddle-gpu cu118...")
|
| 55 |
+
studio.run("pip install -q paddlepaddle-gpu -i https://www.paddlepaddle.org.cn/packages/stable/cu118/")
|
| 56 |
+
paddle_check = studio.run("python3 -c \"import paddle; print('PADDLE_READY', paddle.__version__, paddle.device.cuda.device_count())\"").strip()
|
| 57 |
+
log(f"✅ PaddlePaddle Status: {paddle_check}")
|
| 58 |
+
|
| 59 |
+
log("📦 Cập nhật OpenCV, SciPy, RapidFuzz, VisualDL tương thích hoàn toàn NumPy 2.x...")
|
| 60 |
+
studio.run("python3 -m pip install -q --upgrade 'opencv-python>=4.10' 'opencv-python-headless>=4.10' 'scipy>=1.13' rapidfuzz visualdl pyclipper shapely lmdb attrdict")
|
| 61 |
+
|
| 62 |
+
log("🔧 Áp dụng Monkeypatch NumPy 2.x toàn diện qua sitecustomize và tools/...")
|
| 63 |
+
sitecust_cmd = """python3 -c "
|
| 64 |
+
import site, pathlib
|
| 65 |
+
patch = '''import numpy as np
|
| 66 |
+
if not hasattr(np, 'sctypes'): np.sctypes = {'int': [np.int8, np.int16, np.int32, np.int64], 'uint': [np.uint8, np.uint16, np.uint32, np.uint64], 'float': [np.float16, np.float32, np.float64], 'complex': [np.complex64, np.complex128], 'others': [bool, object, bytes, str]}
|
| 67 |
+
if not hasattr(np, 'bool'): np.bool = bool
|
| 68 |
+
if not hasattr(np, 'int'): np.int = int
|
| 69 |
+
if not hasattr(np, 'float'): np.float = float
|
| 70 |
+
if not hasattr(np, 'typeDict'): np.typeDict = {}
|
| 71 |
+
if not hasattr(np, 'long'): np.long = int
|
| 72 |
+
if not hasattr(np, 'ulong'): np.ulong = int
|
| 73 |
+
if not hasattr(np, 'unicode_'): np.unicode_ = str
|
| 74 |
+
if not hasattr(np, 'string_'): np.string_ = bytes
|
| 75 |
+
if not hasattr(np, 'object_'): np.object_ = object
|
| 76 |
+
if not hasattr(np, 'complex_'): np.complex_ = complex
|
| 77 |
+
'''
|
| 78 |
+
sp_list = site.getsitepackages()
|
| 79 |
+
for sp in sp_list:
|
| 80 |
+
try:
|
| 81 |
+
(pathlib.Path(sp) / 'sitecustomize.py').write_text(patch, encoding='utf-8')
|
| 82 |
+
print(f'Wrote sitecustomize.py to {sp}')
|
| 83 |
+
except Exception as e:
|
| 84 |
+
pass
|
| 85 |
+
" """
|
| 86 |
+
sitecust_out = studio.run(sitecust_cmd).strip()
|
| 87 |
+
if sitecust_out:
|
| 88 |
+
log(f" {sitecust_out}")
|
| 89 |
+
|
| 90 |
+
studio.run("cd Cham-OCR/ocr-training/PaddleOCR && git checkout tools/train.py tools/export_model.py 2>/dev/null || true")
|
| 91 |
+
patch_cmd = """python3 -c "
|
| 92 |
+
import pathlib
|
| 93 |
+
for fn in ['Cham-OCR/ocr-training/PaddleOCR/tools/train.py', 'Cham-OCR/ocr-training/PaddleOCR/tools/export_model.py']:
|
| 94 |
+
p = pathlib.Path(fn)
|
| 95 |
+
if p.exists():
|
| 96 |
+
txt = p.read_text(encoding='utf-8')
|
| 97 |
+
patch = '''import numpy as np
|
| 98 |
+
if not hasattr(np, 'sctypes'): np.sctypes = {'int': [np.int8, np.int16, np.int32, np.int64], 'uint': [np.uint8, np.uint16, np.uint32, np.uint64], 'float': [np.float16, np.float32, np.float64], 'complex': [np.complex64, np.complex128], 'others': [bool, object, bytes, str]}
|
| 99 |
+
if not hasattr(np, 'bool'): np.bool = bool
|
| 100 |
+
if not hasattr(np, 'int'): np.int = int
|
| 101 |
+
if not hasattr(np, 'float'): np.float = float
|
| 102 |
+
if not hasattr(np, 'typeDict'): np.typeDict = {}
|
| 103 |
+
if not hasattr(np, 'long'): np.long = int
|
| 104 |
+
if not hasattr(np, 'ulong'): np.ulong = int
|
| 105 |
+
if not hasattr(np, 'unicode_'): np.unicode_ = str
|
| 106 |
+
if not hasattr(np, 'string_'): np.string_ = bytes
|
| 107 |
+
if not hasattr(np, 'object_'): np.object_ = object
|
| 108 |
+
if not hasattr(np, 'complex_'): np.complex_ = complex
|
| 109 |
+
'''
|
| 110 |
+
if 'np.sctypes' not in txt:
|
| 111 |
+
lines = txt.splitlines(keepends=True)
|
| 112 |
+
insert_idx = 0
|
| 113 |
+
for idx, line in enumerate(lines):
|
| 114 |
+
if line.strip().startswith('from __future__'):
|
| 115 |
+
insert_idx = idx + 1
|
| 116 |
+
lines.insert(insert_idx, patch + '\\n')
|
| 117 |
+
p.write_text(''.join(lines), encoding='utf-8')
|
| 118 |
+
print(f'Đã patch toàn diện sau __future__: {fn}')
|
| 119 |
+
" """
|
| 120 |
+
patch_out = studio.run(patch_cmd).strip()
|
| 121 |
+
if patch_out:
|
| 122 |
+
log(f" {patch_out}")
|
| 123 |
+
|
| 124 |
+
# Patch random_crop_data.py để tương thích hoàn toàn NumPy 2.x
|
| 125 |
+
crop_patch_cmd = """python3 -c "
|
| 126 |
+
import pathlib
|
| 127 |
+
p = pathlib.Path('Cham-OCR/ocr-training/PaddleOCR/ppocr/data/imaug/random_crop_data.py')
|
| 128 |
+
if p.exists():
|
| 129 |
+
txt = p.read_text(encoding='utf-8')
|
| 130 |
+
if 'size=1' in txt:
|
| 131 |
+
txt = txt.replace('xx = int(np.random.choice(axis, size=1))', 'xx = int(np.random.choice(axis))')
|
| 132 |
+
p.write_text(txt, encoding='utf-8')
|
| 133 |
+
print('Đã vá thành công random_crop_data.py (loại bỏ size=1 cho NumPy 2.x)')
|
| 134 |
+
else:
|
| 135 |
+
print('random_crop_data.py đã được vá')
|
| 136 |
+
" """
|
| 137 |
+
crop_patch_out = studio.run(crop_patch_cmd).strip()
|
| 138 |
+
if crop_patch_out:
|
| 139 |
+
log(f" {crop_patch_out}")
|
| 140 |
+
|
| 141 |
+
log("🔍 Kiểm tra tính tương thích của toàn bộ thư viện...")
|
| 142 |
+
verify_cmd = """python3 -c "
|
| 143 |
+
import numpy as np
|
| 144 |
+
if not hasattr(np, 'sctypes'): np.sctypes = {'int': [np.int8, np.int16, np.int32, np.int64], 'uint': [np.uint8, np.uint16, np.uint32, np.uint64], 'float': [np.float16, np.float32, np.float64], 'complex': [np.complex64, np.complex128], 'others': [bool, object, bytes, str]}
|
| 145 |
+
if not hasattr(np, 'bool'): np.bool = bool
|
| 146 |
+
if not hasattr(np, 'int'): np.int = int
|
| 147 |
+
if not hasattr(np, 'float'): np.float = float
|
| 148 |
+
if not hasattr(np, 'typeDict'): np.typeDict = {}
|
| 149 |
+
if not hasattr(np, 'long'): np.long = int
|
| 150 |
+
if not hasattr(np, 'ulong'): np.ulong = int
|
| 151 |
+
import cv2, imgaug, paddle
|
| 152 |
+
print('VERIFY_SUCCESS: numpy', np.__version__, 'cv2', cv2.__version__, 'paddle', paddle.__version__)
|
| 153 |
+
" """
|
| 154 |
+
verify_check = studio.run(verify_cmd).strip()
|
| 155 |
+
log(f" {verify_check}")
|
| 156 |
+
|
| 157 |
+
log("\n📂 [BƯỚC 3/5] Xác minh dữ liệu huấn luyện detector_v2...")
|
| 158 |
+
data_stat = studio.run("wc -l Cham-OCR/ocr-training/PaddleOCR/data/detector_v2/det_train_label.txt Cham-OCR/ocr-training/PaddleOCR/data/detector_v2/det_val_label.txt 2>/dev/null || echo NOT_FOUND").strip()
|
| 159 |
+
log(f"Số dòng dữ liệu:\n{data_stat}")
|
| 160 |
+
if "NOT_FOUND" in data_stat:
|
| 161 |
+
raise RuntimeError("Không tìm thấy bộ dữ liệu det_train_label.txt trên đĩa Studio!")
|
| 162 |
+
|
| 163 |
+
sample_img_check = studio.run("python3 -c \"import pathlib, json; p = pathlib.Path('Cham-OCR/ocr-training/PaddleOCR/data/detector_v2/det_train_label.txt'); line = p.read_text(encoding='utf-8').splitlines()[0]; img_rel = line.split('\\t')[0]; img_path = pathlib.Path('Cham-OCR/ocr-training/PaddleOCR/data/detector_v2') / img_rel; print('IMG_SAMPLE_CHECK:', img_rel, img_path.exists())\"").strip()
|
| 164 |
+
log(f" {sample_img_check}")
|
| 165 |
+
|
| 166 |
+
# Tạo thư mục output nếu chưa có
|
| 167 |
+
studio.run("mkdir -p Cham-OCR/ocr-training/output")
|
| 168 |
+
|
| 169 |
+
log("Đồng bộ tệp cấu hình ch_PP-OCRv4_det_h100.yml từ local vào PaddleOCR/configs/det/...")
|
| 170 |
+
local_cfg_path = os.path.join(os.getcwd(), "ocr-training", "configs", "det", "ch_PP-OCRv4_det_h100.yml")
|
| 171 |
+
with open(local_cfg_path, "r", encoding="utf-8") as f:
|
| 172 |
+
cfg_content = f.read()
|
| 173 |
+
|
| 174 |
+
sync_cmd = f"""python3 -c "
|
| 175 |
+
import pathlib
|
| 176 |
+
content = {repr(cfg_content)}
|
| 177 |
+
pathlib.Path('Cham-OCR/ocr-training/PaddleOCR/configs/det/ch_PP-OCRv4_det_h100.yml').write_text(content, encoding='utf-8')
|
| 178 |
+
pathlib.Path('Cham-OCR/ocr-training/configs/det/ch_PP-OCRv4_det_h100.yml').write_text(content, encoding='utf-8')
|
| 179 |
+
print('CONFIG_SYNCED_SUCCESSFULLY')
|
| 180 |
+
" """
|
| 181 |
+
sync_res = studio.run(sync_cmd).strip()
|
| 182 |
+
log(f" {sync_res}")
|
| 183 |
+
|
| 184 |
+
log("\n🚀 [BƯỚC 4/5] Khởi chạy huấn luyện Cham-DBNet (150 epochs, batch 64)...")
|
| 185 |
+
studio.run("pkill -9 -f tools/train.py || true")
|
| 186 |
+
studio.run("cd Cham-OCR/ocr-training/PaddleOCR && rm -f train.log && rm -rf output/ch_PP-OCRv4_det_cham_h100")
|
| 187 |
+
time.sleep(2)
|
| 188 |
+
|
| 189 |
+
launch_cmd = (
|
| 190 |
+
"cd Cham-OCR/ocr-training/PaddleOCR && "
|
| 191 |
+
"nohup python3 -m paddle.distributed.launch --gpus '0' tools/train.py -c configs/det/ch_PP-OCRv4_det_h100.yml > train.log 2>&1 &"
|
| 192 |
+
)
|
| 193 |
+
studio.run(launch_cmd)
|
| 194 |
+
log("✅ Tiến trình huấn luyện đã kích hoạt chạy nền! Đang bắt đầu theo dõi tiến độ...")
|
| 195 |
+
|
| 196 |
+
start_time = time.time()
|
| 197 |
+
prev_log = ""
|
| 198 |
+
|
| 199 |
+
while True:
|
| 200 |
+
time.sleep(15)
|
| 201 |
+
ps_cnt = studio.run("ps aux | grep tools/train.py | grep -v grep | wc -l").strip()
|
| 202 |
+
is_alive = int(ps_cnt or "0") > 0
|
| 203 |
+
|
| 204 |
+
try:
|
| 205 |
+
latest_lines = studio.run("cd Cham-OCR/ocr-training/PaddleOCR && tail -n 8 train.log 2>/dev/null").strip()
|
| 206 |
+
if latest_lines and latest_lines != prev_log:
|
| 207 |
+
prev_log = latest_lines
|
| 208 |
+
elapsed_min = (time.time() - start_time) / 60
|
| 209 |
+
log(f"⏱️ [{elapsed_min:.1f}m] Tiến độ log:")
|
| 210 |
+
matched = False
|
| 211 |
+
for l in latest_lines.splitlines():
|
| 212 |
+
if any(k in l for k in ["epoch:", "loss:", "hmean:", "save checkpoint", "Error", "Traceback", "MainIndicator", "epoch: [", "step:"]):
|
| 213 |
+
log(f" {l}")
|
| 214 |
+
matched = True
|
| 215 |
+
if not matched and latest_lines.strip():
|
| 216 |
+
log(f" {latest_lines.splitlines()[-1]}")
|
| 217 |
+
except Exception as e:
|
| 218 |
+
log(f"Log fetch notice: {e}")
|
| 219 |
+
|
| 220 |
+
if not is_alive:
|
| 221 |
+
final_log = studio.run("cd Cham-OCR/ocr-training/PaddleOCR && tail -n 40 train.log 2>/dev/null").strip()
|
| 222 |
+
log(f"--- 40 DÒNG LOG CUỐI CÙNG ---\n{final_log}\n-----------------------------")
|
| 223 |
+
|
| 224 |
+
# Kiểm tra xem có lỗi ngoại lệ khiến tiến trình dừng đột ngột không
|
| 225 |
+
if "Traceback (most recent call last)" in final_log or "Exit code 1" in final_log or "SystemError:" in final_log:
|
| 226 |
+
raise RuntimeError("Tiến trình huấn luyện gặp lỗi ngoại lệ dừng đột ngột!")
|
| 227 |
+
|
| 228 |
+
chk_check = studio.run("test -f Cham-OCR/ocr-training/PaddleOCR/output/ch_PP-OCRv4_det_cham_h100/latest.pdparams && echo CHK_FOUND || echo NO_CHK").strip()
|
| 229 |
+
if "CHK_FOUND" not in chk_check:
|
| 230 |
+
raise RuntimeError("Huấn luyện dừng bất thường trước khi lưu được checkpoint!")
|
| 231 |
+
|
| 232 |
+
log("\n🎉 Tiến trình huấn luyện đã kết thúc thành công!")
|
| 233 |
+
break
|
| 234 |
+
|
| 235 |
+
log("\n📦 [BƯỚC 5/5] Xuất mô hình Inference & Tải về máy local...")
|
| 236 |
+
export_script = """
|
| 237 |
+
cd Cham-OCR/ocr-training/PaddleOCR
|
| 238 |
+
BEST_MODEL="output/ch_PP-OCRv4_det_cham_h100/best_accuracy"
|
| 239 |
+
if [ ! -f "${BEST_MODEL}.pdparams" ]; then
|
| 240 |
+
BEST_MODEL="output/ch_PP-OCRv4_det_cham_h100/latest"
|
| 241 |
+
fi
|
| 242 |
+
echo "Using checkpoint: $BEST_MODEL"
|
| 243 |
+
mkdir -p ../output
|
| 244 |
+
python3 tools/export_model.py \
|
| 245 |
+
-c configs/det/ch_PP-OCRv4_det_h100.yml \
|
| 246 |
+
-o Global.pretrained_model=${BEST_MODEL} \
|
| 247 |
+
Global.save_inference_dir=../output/ch_PP-OCRv4_det_cham_infer
|
| 248 |
+
|
| 249 |
+
cd ../output
|
| 250 |
+
python3 -c "import shutil; shutil.make_archive('../cham_dbnet_v1_infer', 'zip', '.', 'ch_PP-OCRv4_det_cham_infer')"
|
| 251 |
+
ls -lh ../cham_dbnet_v1_infer.zip
|
| 252 |
+
"""
|
| 253 |
+
export_out = studio.run(export_script).strip()
|
| 254 |
+
log(f"Export log:\n{export_out}")
|
| 255 |
+
|
| 256 |
+
log("⬇️ Đang tải tệp cham_dbnet_v1_infer.zip về thư mục ocr-training/ ...")
|
| 257 |
+
local_dest = os.path.join(os.getcwd(), "ocr-training", "cham_dbnet_v1_infer.zip")
|
| 258 |
+
downloaded = False
|
| 259 |
+
try:
|
| 260 |
+
studio.download_file("Cham-OCR/ocr-training/cham_dbnet_v1_infer.zip", local_dest)
|
| 261 |
+
if os.path.exists(local_dest) and os.path.getsize(local_dest) > 1000:
|
| 262 |
+
downloaded = True
|
| 263 |
+
log(" Đã tải thành công qua native studio.download_file!")
|
| 264 |
+
except Exception as dl_err:
|
| 265 |
+
log(f" Native download gặp lỗi ({dl_err}), chuyển sang Base64 fallback...")
|
| 266 |
+
|
| 267 |
+
if not downloaded:
|
| 268 |
+
remote_b64 = studio.run(
|
| 269 |
+
"python3 -c \"import base64, pathlib; p = pathlib.Path('Cham-OCR/ocr-training/cham_dbnet_v1_infer.zip'); print(base64.b64encode(p.read_bytes()).decode('ascii'))\""
|
| 270 |
+
).strip()
|
| 271 |
+
with open(local_dest, "wb") as f:
|
| 272 |
+
f.write(base64.b64decode(remote_b64))
|
| 273 |
+
|
| 274 |
+
local_size_mb = os.path.getsize(local_dest) / (1024 * 1024)
|
| 275 |
+
log(f"🎉 ĐÃ TẢI THÀNH CÔNG: {local_dest} ({local_size_mb:.2f} MB)")
|
| 276 |
+
|
| 277 |
+
# Tự động giải nén model vào ocr-training/cham_dbnet_v1_infer
|
| 278 |
+
import zipfile
|
| 279 |
+
infer_extract_dir = os.path.join(os.getcwd(), "ocr-training", "cham_dbnet_v1_infer")
|
| 280 |
+
os.makedirs(infer_extract_dir, exist_ok=True)
|
| 281 |
+
with zipfile.ZipFile(local_dest, "r") as zf:
|
| 282 |
+
zf.extractall(infer_extract_dir)
|
| 283 |
+
log(f"📂 Đã giải nén mô hình suy luận vào: {infer_extract_dir}")
|
| 284 |
+
|
| 285 |
+
except Exception as exc:
|
| 286 |
+
log(f"❌ XẢY RA LỖI: {exc}")
|
| 287 |
+
raise exc
|
| 288 |
+
|
| 289 |
+
finally:
|
| 290 |
+
log("\n🛑 [AN TOÀN] Đang dừng Studio để bảo vệ số dư tài khoản...")
|
| 291 |
+
try:
|
| 292 |
+
if str(studio.status).lower() == "running":
|
| 293 |
+
studio.stop()
|
| 294 |
+
log(f"✅ Studio đã DỪNG (Status: {studio.status})! 0 credit bị tiêu tốn thêm.")
|
| 295 |
+
else:
|
| 296 |
+
log(f"✅ Studio hiện tại đã ở trạng thái {studio.status}, an toàn.")
|
| 297 |
+
except Exception as stop_err:
|
| 298 |
+
log(f"Cảnh báo khi dừng Studio: {stop_err}")
|
| 299 |
+
|
| 300 |
+
log("\n🎊 TOÀN BỘ QUY TRÌNH ĐÃ HOÀN TẤT THÀNH CÔNG RỰC RỠ!")
|
| 301 |
+
|
| 302 |
+
if __name__ == "__main__":
|
| 303 |
+
main()
|
ocr-training/scripts/prepare_v24_notebook.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 6 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 7 |
+
|
| 8 |
+
def read_file(path):
|
| 9 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 10 |
+
return f.read()
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
print("📦 Bắt đầu đóng gói Kaggle Notebook huấn luyện V24...")
|
| 14 |
+
|
| 15 |
+
# 1. Đọc mã nguồn các script cục bộ
|
| 16 |
+
dict_extension_code = read_file('ocr-training/scripts/dict_extension_v24.py')
|
| 17 |
+
weight_surgery_code = read_file('ocr-training/scripts/weight_surgery_v24.py')
|
| 18 |
+
generate_data_v24_code = read_file('ocr-training/scripts/generate_data_v24.py')
|
| 19 |
+
config_v24_code = read_file('ocr-training/configs/rec_cham_v24.yml')
|
| 20 |
+
|
| 21 |
+
kaggle_dict_extension = dict_extension_code
|
| 22 |
+
kaggle_weight_surgery = weight_surgery_code
|
| 23 |
+
|
| 24 |
+
cells = []
|
| 25 |
+
|
| 26 |
+
def add_md(text):
|
| 27 |
+
cells.append({
|
| 28 |
+
"cell_type": "markdown",
|
| 29 |
+
"metadata": {},
|
| 30 |
+
"source": [line + '\n' for line in text.splitlines()]
|
| 31 |
+
})
|
| 32 |
+
|
| 33 |
+
def add_code(code_str):
|
| 34 |
+
cells.append({
|
| 35 |
+
"cell_type": "code",
|
| 36 |
+
"execution_count": None,
|
| 37 |
+
"metadata": {},
|
| 38 |
+
"outputs": [],
|
| 39 |
+
"source": [line + '\n' for line in code_str.splitlines()]
|
| 40 |
+
})
|
| 41 |
+
|
| 42 |
+
# Tiêu đề
|
| 43 |
+
add_md("""# Huấn Luyện Mô Hình Nhận Diện Chữ Chăm PP-OCRv4 V24 (Dual GPU T4x2)
|
| 44 |
+
Mô hình giải quyết triệt để:
|
| 45 |
+
- Số thứ tự khổ thơ Chăm 1-99 (`꩑꩞` - `꩙꩙꩞`)
|
| 46 |
+
- Dấu kết thúc câu kép Double Danda (`꩝꩝`)
|
| 47 |
+
- Dấu phụ đối kháng `ꨲ` (Au, U+AA32) vs `ꨶ` (O, U+AA36)
|
| 48 |
+
- Tổ hợp 3 tầng dấu phụ và bảo toàn nguyên âm trước `ꨯ`, `ꨯꨱ`
|
| 49 |
+
- Khả năng chống chịu Mờ rung (Motion Blur) và Nhiễu giấy cổ""")
|
| 50 |
+
|
| 51 |
+
# Phase 1: Environment & GPU Check
|
| 52 |
+
add_md("## Phase 1: Cài đặt thư viện & Kiểm tra GPU T4x2")
|
| 53 |
+
add_code("""# 1. Cài đặt PaddlePaddle GPU và các thư viện phụ trợ
|
| 54 |
+
!pip install --quiet "paddlepaddle-gpu>=2.6.0" "albumentations>=1.4.0" fonttools pyyaml opencv-python Pillow
|
| 55 |
+
|
| 56 |
+
# 2. Kiểm tra môi trường GPU
|
| 57 |
+
import os
|
| 58 |
+
import sys
|
| 59 |
+
import paddle
|
| 60 |
+
|
| 61 |
+
print("="*60)
|
| 62 |
+
print("PADDLE ENVIRONMENT STATUS:")
|
| 63 |
+
print("Paddle Version:", paddle.__version__)
|
| 64 |
+
print("Compiled with CUDA:", paddle.is_compiled_with_cuda())
|
| 65 |
+
gpu_count = paddle.device.cuda.device_count()
|
| 66 |
+
print(f"Detected GPU Count: {gpu_count}")
|
| 67 |
+
print("="*60)
|
| 68 |
+
|
| 69 |
+
if gpu_count < 1:
|
| 70 |
+
print("⚠️ CẢNH BÁO: Không tìm thấy GPU! Vui lòng chọn Accelerator = GPU T4 x2 trong Settings.")
|
| 71 |
+
else:
|
| 72 |
+
for i in range(gpu_count):
|
| 73 |
+
print(f" - GPU {i}: {paddle.device.cuda.get_device_name(i)}")
|
| 74 |
+
""")
|
| 75 |
+
|
| 76 |
+
# Phase 2: Setup Workspace & PaddleOCR
|
| 77 |
+
add_md("## Phase 2: Khởi tạo thư mục làm việc & Clone PaddleOCR")
|
| 78 |
+
add_code("""import os
|
| 79 |
+
import shutil
|
| 80 |
+
|
| 81 |
+
os.makedirs('/kaggle/working/data/fonts', exist_ok=True)
|
| 82 |
+
os.makedirs('/kaggle/working/data/output_v24_temp', exist_ok=True)
|
| 83 |
+
os.makedirs('/kaggle/working/configs', exist_ok=True)
|
| 84 |
+
os.makedirs('/kaggle/working/scripts', exist_ok=True)
|
| 85 |
+
os.makedirs('/kaggle/working/output/rec_cham_v24', exist_ok=True)
|
| 86 |
+
os.makedirs('/kaggle/working/output/v24_training', exist_ok=True)
|
| 87 |
+
|
| 88 |
+
# Tự động phát hiện đường dẫn Assets Input trên Kaggle
|
| 89 |
+
candidate_asset_dirs = [
|
| 90 |
+
"/kaggle/input/cham-ocr-v5-assets",
|
| 91 |
+
"/kaggle/input/datasets/gustavnguyen/cham-ocr-v5-assets"
|
| 92 |
+
]
|
| 93 |
+
ASSETS_DIR = None
|
| 94 |
+
for d in candidate_asset_dirs:
|
| 95 |
+
if os.path.exists(d):
|
| 96 |
+
ASSETS_DIR = d
|
| 97 |
+
break
|
| 98 |
+
|
| 99 |
+
if not ASSETS_DIR and os.path.exists('/kaggle/input'):
|
| 100 |
+
for root, dirs, files in os.walk('/kaggle/input'):
|
| 101 |
+
if 'v23_checkpoint' in dirs:
|
| 102 |
+
ASSETS_DIR = root
|
| 103 |
+
break
|
| 104 |
+
|
| 105 |
+
if not ASSETS_DIR:
|
| 106 |
+
raise FileNotFoundError(f"❌ LỖI: Không tìm thấy thư mục Assets trong /kaggle/input")
|
| 107 |
+
|
| 108 |
+
print(f"✅ Đã tìm thấy Assets Input tại: {ASSETS_DIR}")
|
| 109 |
+
|
| 110 |
+
# Copy fonts
|
| 111 |
+
fonts_src = os.path.join(ASSETS_DIR, "fonts", "fonts")
|
| 112 |
+
if not os.path.exists(fonts_src):
|
| 113 |
+
fonts_src = os.path.join(ASSETS_DIR, "fonts")
|
| 114 |
+
|
| 115 |
+
for f in os.listdir(fonts_src):
|
| 116 |
+
if f.endswith(('.ttf', '.otf')):
|
| 117 |
+
shutil.copy2(os.path.join(fonts_src, f), os.path.join('/kaggle/working/data/fonts', f))
|
| 118 |
+
|
| 119 |
+
print("✅ Đã sao chép fonts:", os.listdir('/kaggle/working/data/fonts'))
|
| 120 |
+
|
| 121 |
+
# Clone PaddleOCR nếu chưa có
|
| 122 |
+
if not os.path.exists('/kaggle/working/PaddleOCR'):
|
| 123 |
+
print("⏳ Đang clone PaddleOCR...")
|
| 124 |
+
!git clone --depth 1 https://github.com/PaddlePaddle/PaddleOCR.git /kaggle/working/PaddleOCR
|
| 125 |
+
!pip install --quiet -r /kaggle/working/PaddleOCR/requirements.txt
|
| 126 |
+
else:
|
| 127 |
+
print("✅ PaddleOCR đã tồn tại sẵn.")
|
| 128 |
+
""")
|
| 129 |
+
|
| 130 |
+
# Phase 3: Ghi mã nguồn các script
|
| 131 |
+
add_md("## Phase 3: Nạp kịch bản Sinh dữ liệu, Mở rộng từ điển & Phẫu thuật trọng số")
|
| 132 |
+
add_code(f"%%writefile /kaggle/working/scripts/dict_extension_v24.py\n{kaggle_dict_extension}")
|
| 133 |
+
add_code(f"%%writefile /kaggle/working/scripts/weight_surgery_v24.py\n{kaggle_weight_surgery}")
|
| 134 |
+
add_code(f"%%writefile /kaggle/working/scripts/generate_data_v24.py\n{generate_data_v24_code}")
|
| 135 |
+
add_code(f"%%writefile /kaggle/working/configs/rec_cham_v24.yml\n{config_v24_code}")
|
| 136 |
+
|
| 137 |
+
# Phase 4: Chạy chuẩn bị dữ liệu và phẫu thuật trọng số
|
| 138 |
+
add_md("## Phase 4: Mở rộng từ điển, Phẫu thuật trọng số & Sinh 50K dữ liệu V24 đặc trị")
|
| 139 |
+
add_code("""import os, sys, time
|
| 140 |
+
%cd /kaggle/working
|
| 141 |
+
|
| 142 |
+
print("=== BƯỚC 1: MỞ RỘNG TỪ ĐIỂN V24 ===")
|
| 143 |
+
!python3 scripts/dict_extension_v24.py
|
| 144 |
+
|
| 145 |
+
print("\\n=== BƯỚC 2: PHẪU THUẬT TRỌNG SỐ V24 ===")
|
| 146 |
+
!python3 scripts/weight_surgery_v24.py
|
| 147 |
+
|
| 148 |
+
print("\\n=== BƯỚC 3: SINH DỮ LIỆU TỔNG HỢP V24 ĐẶC TRỊ (45K TRAIN, 4.5K VAL) ===")
|
| 149 |
+
t_start = time.time()
|
| 150 |
+
# Chạy với 4 vCPU
|
| 151 |
+
!python3 scripts/generate_data_v24.py --output-dir /kaggle/working/data/cham_synthetic_v24 --fonts-dir /kaggle/working/data/fonts --num-train 45000 --num-val 4500 --num-workers 4
|
| 152 |
+
print(f"🎉 Hoàn thành toàn bộ dữ liệu trong {(time.time()-t_start)/60:.2f} phút!")
|
| 153 |
+
""")
|
| 154 |
+
|
| 155 |
+
# Phase 5: Khởi chạy huấn luyện song song phân tán trên GPU T4x2
|
| 156 |
+
add_md("## Phase 5: Huấn luyện phân tán song song trên Dual GPU Tesla T4x2 (20 Epochs)")
|
| 157 |
+
add_code("""import os
|
| 158 |
+
import paddle
|
| 159 |
+
|
| 160 |
+
gpu_count = paddle.device.cuda.device_count()
|
| 161 |
+
print(f"🔥 Khởi chạy huấn luyện với {gpu_count} GPU...")
|
| 162 |
+
|
| 163 |
+
os.environ["PYTHONUNBUFFERED"] = "1"
|
| 164 |
+
os.environ["FLAGS_fraction_of_gpu_memory_to_use"] = "0.90"
|
| 165 |
+
|
| 166 |
+
%cd /kaggle/working/PaddleOCR
|
| 167 |
+
|
| 168 |
+
if gpu_count > 1:
|
| 169 |
+
print("🚀 Sử dụng lệnh phân tán song song đa GPU (paddle.distributed.launch --gpus '0,1'):")
|
| 170 |
+
!python3 -m paddle.distributed.launch --gpus '0,1' tools/train.py -c /kaggle/working/configs/rec_cham_v24.yml
|
| 171 |
+
else:
|
| 172 |
+
print("⚡ Huấn luyện trên Single GPU:")
|
| 173 |
+
!python3 tools/train.py -c /kaggle/working/configs/rec_cham_v24.yml
|
| 174 |
+
""")
|
| 175 |
+
|
| 176 |
+
# Phase 6: Xuất mô hình Inference & Đóng gói ZIP
|
| 177 |
+
add_md("## Phase 6: Xuất mô hình Inference & Đóng gói rec_cham_v24_best.zip")
|
| 178 |
+
add_code("""import os
|
| 179 |
+
import shutil
|
| 180 |
+
import zipfile
|
| 181 |
+
|
| 182 |
+
%cd /kaggle/working/PaddleOCR
|
| 183 |
+
|
| 184 |
+
best_ckpt = "/kaggle/working/output/rec_cham_v24/best_accuracy"
|
| 185 |
+
latest_ckpt = "/kaggle/working/output/rec_cham_v24/latest"
|
| 186 |
+
|
| 187 |
+
target_ckpt = best_ckpt if os.path.exists(best_ckpt + ".pdparams") else latest_ckpt
|
| 188 |
+
print(f"🎯 Sử dụng checkpoint: {target_ckpt}")
|
| 189 |
+
|
| 190 |
+
infer_dir = "/kaggle/working/output/rec_cham_inference_v24"
|
| 191 |
+
os.makedirs(infer_dir, exist_ok=True)
|
| 192 |
+
|
| 193 |
+
# Xuất mô hình inference
|
| 194 |
+
!python3 tools/export_model.py -c /kaggle/working/configs/rec_cham_v24.yml -o Global.pretrained_model={target_ckpt} Global.save_inference_dir={infer_dir} Global.export_with_pir=False
|
| 195 |
+
|
| 196 |
+
# Copy từ điển vào thư mục inference
|
| 197 |
+
shutil.copy2('/kaggle/working/data/cham_dict_v24.txt', os.path.join(infer_dir, 'cham_dict_v24.txt'))
|
| 198 |
+
|
| 199 |
+
print("✅ Thư mục Inference:", os.listdir(infer_dir))
|
| 200 |
+
|
| 201 |
+
# Đóng gói toàn bộ kết quả thành file zip trong /kaggle/working
|
| 202 |
+
zip_path = "/kaggle/working/rec_cham_v24_best.zip"
|
| 203 |
+
print(f"📦 Đang nén kết quả vào {zip_path}...")
|
| 204 |
+
|
| 205 |
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
| 206 |
+
# Nén inference model
|
| 207 |
+
for root, dirs, files in os.walk(infer_dir):
|
| 208 |
+
for f in files:
|
| 209 |
+
full_p = os.path.join(root, f)
|
| 210 |
+
rel_p = os.path.relpath(full_p, "/kaggle/working")
|
| 211 |
+
zipf.write(full_p, rel_p)
|
| 212 |
+
|
| 213 |
+
# Nén best accuracy pdparams
|
| 214 |
+
for ext in [".pdparams", ".pdopt", ".states"]:
|
| 215 |
+
p = target_ckpt + ext
|
| 216 |
+
if os.path.exists(p):
|
| 217 |
+
zipf.write(p, os.path.join("best_checkpoint", os.path.basename(p)))
|
| 218 |
+
|
| 219 |
+
# Nén từ điển
|
| 220 |
+
zipf.write('/kaggle/working/data/cham_dict_v24.txt', 'cham_dict_v24.txt')
|
| 221 |
+
|
| 222 |
+
print(f"🎉 HOÀN TẤT TRỌN GÓI! File zip sẵn sàng ({os.path.getsize(zip_path) / (1024*1024):.2f} MB): {zip_path}")
|
| 223 |
+
""")
|
| 224 |
+
|
| 225 |
+
# 5. Lưu Notebook JSON
|
| 226 |
+
notebook_dict = {
|
| 227 |
+
"cells": cells,
|
| 228 |
+
"metadata": {
|
| 229 |
+
"kernelspec": {
|
| 230 |
+
"display_name": "Python 3",
|
| 231 |
+
"language": "python",
|
| 232 |
+
"name": "python3"
|
| 233 |
+
},
|
| 234 |
+
"language_info": {
|
| 235 |
+
"name": "python",
|
| 236 |
+
"version": "3.10.12"
|
| 237 |
+
}
|
| 238 |
+
},
|
| 239 |
+
"nbformat": 4,
|
| 240 |
+
"nbformat_minor": 4
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
submission_dir = 'ocr-training/output/kaggle_v24_submission'
|
| 244 |
+
os.makedirs(submission_dir, exist_ok=True)
|
| 245 |
+
|
| 246 |
+
nb_path = os.path.join(submission_dir, 'paddleocr_cham_v24_train.ipynb')
|
| 247 |
+
with open(nb_path, 'w', encoding='utf-8') as f:
|
| 248 |
+
json.dump(notebook_dict, f, indent=1, ensure_ascii=False)
|
| 249 |
+
|
| 250 |
+
meta_path = os.path.join(submission_dir, 'kernel-metadata.json')
|
| 251 |
+
metadata = {
|
| 252 |
+
"id": "gustavnguyen/paddleocr-cham-finetune",
|
| 253 |
+
"title": "paddleocr-cham-finetune",
|
| 254 |
+
"code_file": "paddleocr_cham_v24_train.ipynb",
|
| 255 |
+
"language": "python",
|
| 256 |
+
"kernel_type": "notebook",
|
| 257 |
+
"is_private": "true",
|
| 258 |
+
"enable_gpu": "true",
|
| 259 |
+
"enable_tpu": "false",
|
| 260 |
+
"enable_internet": "true",
|
| 261 |
+
"dataset_sources": [
|
| 262 |
+
"gustavnguyen/cham-ocr-v5-assets"
|
| 263 |
+
],
|
| 264 |
+
"competition_sources": [],
|
| 265 |
+
"kernel_sources": [],
|
| 266 |
+
"model_sources": []
|
| 267 |
+
}
|
| 268 |
+
with open(meta_path, 'w', encoding='utf-8') as f:
|
| 269 |
+
json.dump(metadata, f, indent=2)
|
| 270 |
+
|
| 271 |
+
print(f"✅ Đã tạo thành công Notebook tại: {nb_path}")
|
| 272 |
+
print(f"✅ Đã tạo kernel-metadata tại: {meta_path}")
|
| 273 |
+
return nb_path, meta_path
|
| 274 |
+
|
| 275 |
+
if __name__ == '__main__':
|
| 276 |
+
main()
|
ocr-training/scripts/run_l4_interactive.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
End-to-End Optimized Pipeline on NVIDIA L4 (1.68 cr/h - g6.4xlarge).
|
| 5 |
+
- Cost: 1.68 cr/h (53% cheaper than AWS L40S, cheaper than Nebius 2.14 cr/h)
|
| 6 |
+
- In-Place Interactive Pre-Flight Verification:
|
| 7 |
+
1. Train DataLoader (2 batches)
|
| 8 |
+
2. Eval DataLoader (5 batches, batch_size=1)
|
| 9 |
+
3. Forward pass + DBPostProcess + DetMetric
|
| 10 |
+
- Only launches 150 epochs training after 100% Pre-flight test passed.
|
| 11 |
+
- Model export + local download + guaranteed auto-stop when complete.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import time
|
| 17 |
+
import base64
|
| 18 |
+
import zipfile
|
| 19 |
+
from lightning_sdk import Studio, Machine
|
| 20 |
+
|
| 21 |
+
if sys.stdout and sys.stdout.encoding != 'utf-8':
|
| 22 |
+
try:
|
| 23 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 24 |
+
except AttributeError:
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
def log(msg):
|
| 28 |
+
timestamp = time.strftime("%H:%M:%S")
|
| 29 |
+
print(f"[{timestamp}] {msg}", flush=True)
|
| 30 |
+
|
| 31 |
+
def main():
|
| 32 |
+
log("=================================================================")
|
| 33 |
+
log("🚀 OPTION A: KHỞI CHẠY PIPELINE TRÊN NVIDIA L4 (1.68 cr/h - 24GB)")
|
| 34 |
+
log("=================================================================")
|
| 35 |
+
|
| 36 |
+
studio = Studio(name="cham-det-h100", teamspace="phucsd", org="phucsd-org")
|
| 37 |
+
log(f"Studio hiện tại: Status={studio.status} | Machine={studio.machine}")
|
| 38 |
+
|
| 39 |
+
target_machine = Machine.L4
|
| 40 |
+
log(f"\n🔥 [BƯỚC 1/6] Khởi động Studio với NVIDIA L4 (Machine.L4 - 1.68 cr/h)...")
|
| 41 |
+
if str(studio.status).lower() != "running":
|
| 42 |
+
studio.start(machine=target_machine)
|
| 43 |
+
else:
|
| 44 |
+
if str(studio.machine) != str(target_machine):
|
| 45 |
+
log(f"Chuyển máy từ {studio.machine} sang {target_machine}...")
|
| 46 |
+
studio.switch_machine(target_machine)
|
| 47 |
+
|
| 48 |
+
log(f"✅ Studio đang chạy! Machine: {studio.machine} | Status: {studio.status}")
|
| 49 |
+
|
| 50 |
+
# Giai đoạn Pre-flight: GIỮ MÁY MỞ nếu có lỗi để sửa ngay tại chỗ
|
| 51 |
+
log("\n🔍 [BƯỚC 2/6] Kiểm tra phần cứng GPU L4...")
|
| 52 |
+
gpu_info = studio.run("nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader").strip()
|
| 53 |
+
log(f"🔥 GPU: {gpu_info}")
|
| 54 |
+
|
| 55 |
+
log("📦 Kiểm tra PaddlePaddle GPU 3.3.1...")
|
| 56 |
+
paddle_check = studio.run("python3 -c \"import paddle; print('PADDLE_READY', paddle.__version__, paddle.device.cuda.device_count())\" || true").strip()
|
| 57 |
+
if "PADDLE_READY" not in paddle_check or " 0" in paddle_check:
|
| 58 |
+
log("Cài đặt paddlepaddle-gpu cu118...")
|
| 59 |
+
studio.run("pip install -q paddlepaddle-gpu -i https://www.paddlepaddle.org.cn/packages/stable/cu118/")
|
| 60 |
+
paddle_check = studio.run("python3 -c \"import paddle; print('PADDLE_READY', paddle.__version__, paddle.device.cuda.device_count())\"").strip()
|
| 61 |
+
log(f"✅ PaddlePaddle: {paddle_check}")
|
| 62 |
+
|
| 63 |
+
log("📦 Cập nhật gói phụ thuộc tương thích NumPy 2.x...")
|
| 64 |
+
studio.run("python3 -m pip install -q --upgrade 'opencv-python>=4.10' 'opencv-python-headless>=4.10' 'scipy>=1.13' rapidfuzz visualdl pyclipper shapely lmdb attrdict")
|
| 65 |
+
|
| 66 |
+
log("🔧 Thiết lập sitecustomize.py monkeypatch...")
|
| 67 |
+
sitecust_cmd = """python3 -c "
|
| 68 |
+
import site, pathlib
|
| 69 |
+
patch = '''import numpy as np
|
| 70 |
+
if not hasattr(np, 'sctypes'): np.sctypes = {'int': [np.int8, np.int16, np.int32, np.int64], 'uint': [np.uint8, np.uint16, np.uint32, np.uint64], 'float': [np.float16, np.float32, np.float64], 'complex': [np.complex64, np.complex128], 'others': [bool, object, bytes, str]}
|
| 71 |
+
if not hasattr(np, 'bool'): np.bool = bool
|
| 72 |
+
if not hasattr(np, 'int'): np.int = int
|
| 73 |
+
if not hasattr(np, 'float'): np.float = float
|
| 74 |
+
if not hasattr(np, 'typeDict'): np.typeDict = {}
|
| 75 |
+
if not hasattr(np, 'long'): np.long = int
|
| 76 |
+
if not hasattr(np, 'ulong'): np.ulong = int
|
| 77 |
+
if not hasattr(np, 'unicode_'): np.unicode_ = str
|
| 78 |
+
if not hasattr(np, 'string_'): np.string_ = bytes
|
| 79 |
+
if not hasattr(np, 'object_'): np.object_ = object
|
| 80 |
+
if not hasattr(np, 'complex_'): np.complex_ = complex
|
| 81 |
+
'''
|
| 82 |
+
sp_list = site.getsitepackages()
|
| 83 |
+
for sp in sp_list:
|
| 84 |
+
try:
|
| 85 |
+
(pathlib.Path(sp) / 'sitecustomize.py').write_text(patch, encoding='utf-8')
|
| 86 |
+
print(f'Wrote sitecustomize.py to {sp}')
|
| 87 |
+
except Exception as e:
|
| 88 |
+
pass
|
| 89 |
+
" """
|
| 90 |
+
studio.run(sitecust_cmd)
|
| 91 |
+
|
| 92 |
+
log("🔧 Vá random_crop_data.py loại bỏ size=1 cho NumPy 2.x...")
|
| 93 |
+
crop_patch_cmd = """python3 -c "
|
| 94 |
+
import pathlib
|
| 95 |
+
p = pathlib.Path('Cham-OCR/ocr-training/PaddleOCR/ppocr/data/imaug/random_crop_data.py')
|
| 96 |
+
if p.exists():
|
| 97 |
+
txt = p.read_text(encoding='utf-8')
|
| 98 |
+
if 'size=1' in txt:
|
| 99 |
+
txt = txt.replace('xx = int(np.random.choice(axis, size=1))', 'xx = int(np.random.choice(axis))')
|
| 100 |
+
p.write_text(txt, encoding='utf-8')
|
| 101 |
+
print('Đã vá random_crop_data.py')
|
| 102 |
+
else:
|
| 103 |
+
print('random_crop_data.py đã được vá trước đó')
|
| 104 |
+
" """
|
| 105 |
+
log(f" {studio.run(crop_patch_cmd).strip()}")
|
| 106 |
+
|
| 107 |
+
log("\n📄 [BƯỚC 3/6] Đồng bộ cấu hình ch_PP-OCRv4_det_h100.yml trực tiếp từ máy local...")
|
| 108 |
+
local_cfg_path = os.path.join(os.getcwd(), "ocr-training", "configs", "det", "ch_PP-OCRv4_det_h100.yml")
|
| 109 |
+
with open(local_cfg_path, "rb") as f:
|
| 110 |
+
b64_cfg = base64.b64encode(f.read()).decode('ascii')
|
| 111 |
+
studio.run(f"echo '{b64_cfg}' | base64 -d > Cham-OCR/ocr-training/PaddleOCR/configs/det/ch_PP-OCRv4_det_h100.yml")
|
| 112 |
+
studio.run(f"cp Cham-OCR/ocr-training/PaddleOCR/configs/det/ch_PP-OCRv4_det_h100.yml Cham-OCR/ocr-training/configs/det/ch_PP-OCRv4_det_h100.yml")
|
| 113 |
+
log(" CONFIG_SYNCED_SUCCESSFULLY via base64 pipe")
|
| 114 |
+
|
| 115 |
+
log("\n🧪 [BƯỚC 4/6] Chạy kiểm thử tại chỗ PRE-FLIGHT TEST (Train + Eval + Metric)...")
|
| 116 |
+
preflight_script = """cd Cham-OCR/ocr-training/PaddleOCR && python3 -c "
|
| 117 |
+
import logging
|
| 118 |
+
logger = logging.getLogger('preflight')
|
| 119 |
+
import yaml, paddle, time
|
| 120 |
+
from ppocr.data import build_dataloader
|
| 121 |
+
from ppocr.modeling.architectures import build_model
|
| 122 |
+
from ppocr.postprocess import build_post_process
|
| 123 |
+
from ppocr.metrics import build_metric
|
| 124 |
+
|
| 125 |
+
with open('configs/det/ch_PP-OCRv4_det_h100.yml', 'r') as f:
|
| 126 |
+
config = yaml.safe_load(f)
|
| 127 |
+
|
| 128 |
+
print('1. Kiểm tra Train DataLoader (batch size 32)...')
|
| 129 |
+
train_loader = build_dataloader(config, 'Train', paddle.device.get_device(), logger)
|
| 130 |
+
sample_train_batch = None
|
| 131 |
+
for idx, batch in enumerate(train_loader):
|
| 132 |
+
print(f' Train Batch {idx}: image shape = {batch[0].shape}')
|
| 133 |
+
sample_train_batch = batch
|
| 134 |
+
if idx >= 1:
|
| 135 |
+
break
|
| 136 |
+
|
| 137 |
+
print('2. Kiểm tra Eval DataLoader (batch size 1, dynamic shapes)...')
|
| 138 |
+
eval_loader = build_dataloader(config, 'Eval', paddle.device.get_device(), logger)
|
| 139 |
+
eval_batches = []
|
| 140 |
+
for idx, batch in enumerate(eval_loader):
|
| 141 |
+
print(f' Eval Batch {idx}: image shape = {batch[0].shape}, polys count = {len(batch[2][0])}')
|
| 142 |
+
eval_batches.append(batch)
|
| 143 |
+
if idx >= 4:
|
| 144 |
+
break
|
| 145 |
+
|
| 146 |
+
print('3. Kiểm tra Model forward pass + PostProcess + Metric...')
|
| 147 |
+
model = build_model(config['Architecture'])
|
| 148 |
+
model.eval()
|
| 149 |
+
post_process_class = build_post_process(config['PostProcess'])
|
| 150 |
+
eval_class = build_metric(config['Metric'])
|
| 151 |
+
|
| 152 |
+
with paddle.no_grad():
|
| 153 |
+
for batch in eval_batches:
|
| 154 |
+
images = batch[0]
|
| 155 |
+
preds = model(images)
|
| 156 |
+
post_res = post_process_class(preds, batch[1])
|
| 157 |
+
eval_class(post_res, batch)
|
| 158 |
+
|
| 159 |
+
metric = eval_class.get_metric()
|
| 160 |
+
print(f' Evaluation metric calculated successfully: {metric}')
|
| 161 |
+
|
| 162 |
+
print('=== PRE_FLIGHT_PASSED_100% ===')
|
| 163 |
+
" """
|
| 164 |
+
preflight_out = studio.run(preflight_script).strip()
|
| 165 |
+
log(f"Kết quả Pre-flight Test:\n{preflight_out}")
|
| 166 |
+
|
| 167 |
+
if "=== PRE_FLIGHT_PASSED_100% ===" not in preflight_out:
|
| 168 |
+
log("❌ Pre-flight test chưa hoàn thành trọn vẹn! Máy vẫn được GIỮ MỞ để kiểm tra.")
|
| 169 |
+
return
|
| 170 |
+
|
| 171 |
+
log("\n🎉 PRE-FLIGHT TEST ĐÃ ĐẠT 100%! BẮT ĐẦU CHẠY HUẤN LUYỆN CHÍNH THỨC 150 EPOCHS...")
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
log("\n🚀 [BƯỚC 5/6] Khởi chạy huấn luyện Cham-DBNet (150 epochs, batch 32, single-GPU L4)...")
|
| 175 |
+
studio.run("pkill -9 -f train.py 2>/dev/null || true")
|
| 176 |
+
studio.run("pkill -9 -f python 2>/dev/null || true")
|
| 177 |
+
time.sleep(2)
|
| 178 |
+
studio.run("cd Cham-OCR/ocr-training/PaddleOCR && rm -f train.log && rm -rf log output/ch_PP-OCRv4_det_cham_h100")
|
| 179 |
+
studio.run("mkdir -p Cham-OCR/ocr-training/output")
|
| 180 |
+
time.sleep(1)
|
| 181 |
+
|
| 182 |
+
vram_check = studio.run("nvidia-smi --query-gpu=memory.used,memory.total --format=csv,noheader").strip()
|
| 183 |
+
log(f" VRAM trước khi khởi chạy: {vram_check}")
|
| 184 |
+
|
| 185 |
+
launch_cmd = (
|
| 186 |
+
"cd Cham-OCR/ocr-training/PaddleOCR && "
|
| 187 |
+
"nohup python3 -u tools/train.py -c configs/det/ch_PP-OCRv4_det_h100.yml > train.log 2>&1 &"
|
| 188 |
+
)
|
| 189 |
+
studio.run(launch_cmd)
|
| 190 |
+
time.sleep(5)
|
| 191 |
+
log("✅ Tiến trình huấn luyện đã kích hoạt chạy nền! Đang bắt đầu theo dõi tiến độ...")
|
| 192 |
+
|
| 193 |
+
start_time = time.time()
|
| 194 |
+
prev_log = ""
|
| 195 |
+
|
| 196 |
+
while True:
|
| 197 |
+
time.sleep(20)
|
| 198 |
+
ps_cnt = studio.run("ps aux | grep '[t]ools/train.py' | wc -l").strip()
|
| 199 |
+
is_alive = int(ps_cnt or "0") > 0
|
| 200 |
+
|
| 201 |
+
try:
|
| 202 |
+
latest_lines = studio.run(
|
| 203 |
+
"cd Cham-OCR/ocr-training/PaddleOCR && tail -n 15 train.log"
|
| 204 |
+
).strip()
|
| 205 |
+
if latest_lines and latest_lines != prev_log:
|
| 206 |
+
prev_log = latest_lines
|
| 207 |
+
elapsed_min = (time.time() - start_time) / 60
|
| 208 |
+
log(f"⏱️ [{elapsed_min:.1f}m] Tiến độ log:")
|
| 209 |
+
matched = False
|
| 210 |
+
for l in latest_lines.splitlines():
|
| 211 |
+
if any(k in l for k in ["epoch:", "loss:", "hmean:", "save checkpoint", "Error", "Traceback", "MainIndicator", "step:", "eval model:", "PPLCNet", "download"]):
|
| 212 |
+
log(f" {l}")
|
| 213 |
+
matched = True
|
| 214 |
+
if not matched and latest_lines.strip():
|
| 215 |
+
log(f" {latest_lines.splitlines()[-1]}")
|
| 216 |
+
except Exception as e:
|
| 217 |
+
log(f"Log notice: {e}")
|
| 218 |
+
|
| 219 |
+
if not is_alive:
|
| 220 |
+
final_log = studio.run(
|
| 221 |
+
"cd Cham-OCR/ocr-training/PaddleOCR && tail -n 50 train.log"
|
| 222 |
+
).strip()
|
| 223 |
+
log(f"--- 50 DÒNG LOG CUỐI CÙNG ---\n{final_log}\n-----------------------------")
|
| 224 |
+
|
| 225 |
+
if "Traceback (most recent call last)" in final_log or "Exit code 1" in final_log or "SystemError:" in final_log or "FatalError:" in final_log:
|
| 226 |
+
raise RuntimeError("Tiến trình huấn luyện gặp lỗi ngoại lệ dừng đột ngột!")
|
| 227 |
+
|
| 228 |
+
chk_check = studio.run("test -f Cham-OCR/ocr-training/PaddleOCR/output/ch_PP-OCRv4_det_cham_h100/latest.pdparams && echo CHK_FOUND || echo NO_CHK").strip()
|
| 229 |
+
if "CHK_FOUND" not in chk_check:
|
| 230 |
+
raise RuntimeError("Huấn luyện dừng trước khi lưu checkpoint!")
|
| 231 |
+
|
| 232 |
+
log("\n🎉 TIẾN TRÌNH HUẤN LUYỆN 150 EPOCHS ĐÃ KẾT THÚC HOÀN TOÀN THÀNH CÔNG!")
|
| 233 |
+
break
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
log("\n📦 [BƯỚC 6/6] Xuất mô hình Inference & Tải về máy local...")
|
| 237 |
+
export_script = """
|
| 238 |
+
cd Cham-OCR/ocr-training/PaddleOCR
|
| 239 |
+
BEST_MODEL="output/ch_PP-OCRv4_det_cham_h100/best_accuracy"
|
| 240 |
+
if [ ! -f "${BEST_MODEL}.pdparams" ]; then
|
| 241 |
+
BEST_MODEL="output/ch_PP-OCRv4_det_cham_h100/latest"
|
| 242 |
+
fi
|
| 243 |
+
echo "Using checkpoint: $BEST_MODEL"
|
| 244 |
+
mkdir -p ../output
|
| 245 |
+
python3 tools/export_model.py \
|
| 246 |
+
-c configs/det/ch_PP-OCRv4_det_h100.yml \
|
| 247 |
+
-o Global.pretrained_model=${BEST_MODEL} \
|
| 248 |
+
Global.save_inference_dir=../output/ch_PP-OCRv4_det_cham_infer
|
| 249 |
+
|
| 250 |
+
cd ../output
|
| 251 |
+
rm -f ../cham_dbnet_v1_infer.zip
|
| 252 |
+
python3 -c "import shutil; shutil.make_archive('../cham_dbnet_v1_infer', 'zip', '.', 'ch_PP-OCRv4_det_cham_infer')"
|
| 253 |
+
ls -lh ../cham_dbnet_v1_infer.zip
|
| 254 |
+
"""
|
| 255 |
+
export_out = studio.run(export_script).strip()
|
| 256 |
+
log(f"Export log:\n{export_out}")
|
| 257 |
+
|
| 258 |
+
log("⬇️ Đang tải tệp cham_dbnet_v1_infer.zip về thư mục ocr-training/ ...")
|
| 259 |
+
local_dest = os.path.join(os.getcwd(), "ocr-training", "cham_dbnet_v1_infer.zip")
|
| 260 |
+
downloaded = False
|
| 261 |
+
try:
|
| 262 |
+
studio.download_file("Cham-OCR/ocr-training/cham_dbnet_v1_infer.zip", local_dest)
|
| 263 |
+
if os.path.exists(local_dest) and os.path.getsize(local_dest) > 1000:
|
| 264 |
+
downloaded = True
|
| 265 |
+
log(" Đã tải thành công qua native studio.download_file!")
|
| 266 |
+
except Exception as dl_err:
|
| 267 |
+
log(f" Native download gặp lỗi ({dl_err}), chuyển sang Base64 fallback...")
|
| 268 |
+
|
| 269 |
+
if not downloaded:
|
| 270 |
+
remote_b64 = studio.run(
|
| 271 |
+
"python3 -c \"import base64, pathlib; p = pathlib.Path('Cham-OCR/ocr-training/cham_dbnet_v1_infer.zip'); print(base64.b64encode(p.read_bytes()).decode('ascii'))\""
|
| 272 |
+
).strip()
|
| 273 |
+
with open(local_dest, "wb") as f:
|
| 274 |
+
f.write(base64.b64decode(remote_b64))
|
| 275 |
+
|
| 276 |
+
local_size_mb = os.path.getsize(local_dest) / (1024 * 1024)
|
| 277 |
+
log(f"🎉 ĐÃ TẢI THÀNH CÔNG: {local_dest} ({local_size_mb:.2f} MB)")
|
| 278 |
+
|
| 279 |
+
infer_extract_dir = os.path.join(os.getcwd(), "ocr-training", "cham_dbnet_v1_infer")
|
| 280 |
+
os.makedirs(infer_extract_dir, exist_ok=True)
|
| 281 |
+
with zipfile.ZipFile(local_dest, "r") as zf:
|
| 282 |
+
zf.extractall(infer_extract_dir)
|
| 283 |
+
log(f"📂 Đã giải nén mô hình suy luận vào: {infer_extract_dir}")
|
| 284 |
+
|
| 285 |
+
except Exception as exc:
|
| 286 |
+
log(f"❌ XẢY RA LỖI: {exc}")
|
| 287 |
+
raise exc
|
| 288 |
+
|
| 289 |
+
finally:
|
| 290 |
+
log("\n🛑 [AN TOÀN] Đang dừng Studio để bảo vệ số dư tài khoản...")
|
| 291 |
+
try:
|
| 292 |
+
if str(studio.status).lower() == "running":
|
| 293 |
+
studio.stop()
|
| 294 |
+
log(f"✅ Studio đã DỪNG (Status: {studio.status})! 0 credit bị tiêu tốn thêm.")
|
| 295 |
+
else:
|
| 296 |
+
log(f"✅ Studio hiện tại đã ở trạng thái {studio.status}, an toàn.")
|
| 297 |
+
except Exception as stop_err:
|
| 298 |
+
log(f"Cảnh báo khi dừng Studio: {stop_err}")
|
| 299 |
+
|
| 300 |
+
log("\n🎊 TOÀN BỘ QUY TRÌNH ĐÃ HOÀN TẤT THÀNH CÔNG RỰC RỠ!")
|
| 301 |
+
|
| 302 |
+
if __name__ == "__main__":
|
| 303 |
+
main()
|
ocr-training/scripts/run_lightning_remote.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Automated Remote Orchestrator for Cham-DBNet on Lightning AI Studio.
|
| 5 |
+
Manages:
|
| 6 |
+
1. Phase 1 (CPU): Git clone, dependencies install, dataset generation (12,000 pages, 50% tight lines).
|
| 7 |
+
2. Phase 2 (H100 Switch): Switch machine to NVIDIA H100 (80GB VRAM SXM5).
|
| 8 |
+
3. Phase 3 (Training & Monitoring): Launch PaddleOCR DBNet distributed training, stream log metrics.
|
| 9 |
+
4. Phase 4 (Export & Download): Export lightweight inference model, package ZIP, download locally.
|
| 10 |
+
5. Phase 5 (Auto Stop): Stop Studio immediately to prevent credit consumption.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import time
|
| 16 |
+
from lightning_sdk import Studio, Machine
|
| 17 |
+
|
| 18 |
+
if sys.stdout and sys.stdout.encoding != 'utf-8':
|
| 19 |
+
try:
|
| 20 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 21 |
+
except AttributeError:
|
| 22 |
+
pass
|
| 23 |
+
|
| 24 |
+
def log(msg):
|
| 25 |
+
timestamp = time.strftime("%H:%M:%S")
|
| 26 |
+
print(f"[{timestamp}] {msg}", flush=True)
|
| 27 |
+
|
| 28 |
+
def main():
|
| 29 |
+
log("=================================================================")
|
| 30 |
+
log("🚀 KHỞI ĐỘNG REMOTE ORCHESTRATOR TRÊN LIGHTNING AI")
|
| 31 |
+
log("=================================================================")
|
| 32 |
+
|
| 33 |
+
studio = Studio(name="cham-det-h100", teamspace="phucsd", org="phucsd-org")
|
| 34 |
+
log(f"Trạng thái Studio hiện tại: {studio.status} (Machine: {studio.machine})")
|
| 35 |
+
|
| 36 |
+
# PHASE 1: CPU SETUP & DATASET GENERATION
|
| 37 |
+
log("\n📦 --- PHASE 1: THIẾT LẬP MÔI TRƯỜNG & SINH DỮ LIỆU TRÊN CPU (0 CREDIT) ---")
|
| 38 |
+
|
| 39 |
+
setup_cmds = [
|
| 40 |
+
# 1. Clone or Pull Cham-OCR
|
| 41 |
+
"if [ ! -d 'Cham-OCR' ]; then git clone https://github.com/phucsd1/Cham-OCR.git; else cd Cham-OCR && git pull origin main && cd ..; fi",
|
| 42 |
+
# 2. Clone PaddleOCR inside Cham-OCR/ocr-training if not exists
|
| 43 |
+
"cd /teamspace/studios/this_studio/Cham-OCR/ocr-training && if [ ! -d 'PaddleOCR' ]; then git clone -b release/2.7 https://github.com/PaddlePaddle/PaddleOCR.git; fi",
|
| 44 |
+
# 3. Download fonts
|
| 45 |
+
"cd /teamspace/studios/this_studio/Cham-OCR/ocr-training && mkdir -p data/fonts && wget -q -nc -O data/fonts/NotoSansCham-Regular.ttf https://raw.githubusercontent.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansCham/NotoSansCham-Regular.ttf && wget -q -nc -O data/fonts/NotoSansCham-Bold.ttf https://raw.githubusercontent.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansCham/NotoSansCham-Bold.ttf",
|
| 46 |
+
# 4. Download pretrained weights
|
| 47 |
+
"cd /teamspace/studios/this_studio/Cham-OCR/ocr-training && mkdir -p PaddleOCR/pretrain_models && wget -q -nc -O PaddleOCR/pretrain_models/PPLCNetV3_x0_75_ocr_det.pdparams https://paddleocr.bj.bcebos.com/pretrained/PPLCNetV3_x0_75_ocr_det.pdparams",
|
| 48 |
+
# 5. Install general pip dependencies
|
| 49 |
+
"pip install -q pyclipper shapely imgaug lmdb attrdict pillow opencv-python pyyaml",
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
for cmd in setup_cmds:
|
| 53 |
+
log(f"Đang thực thi: {cmd[:70]}...")
|
| 54 |
+
out = studio.run(cmd)
|
| 55 |
+
if out.strip():
|
| 56 |
+
log(f" -> {out.strip()[:200]}")
|
| 57 |
+
|
| 58 |
+
log("✅ Thiết lập mã nguồn, phông chữ và pretrained model hoàn tất!")
|
| 59 |
+
|
| 60 |
+
# 6. Generate dataset on CPU
|
| 61 |
+
log("⚙️ Bắt đầu sinh 12,000 trang huấn luyện và 1,200 trang kiểm thử (50% cự ly siêu hẹp 3-10px)...")
|
| 62 |
+
gen_cmd = "cd /teamspace/studios/this_studio/Cham-OCR/ocr-training && python3 scripts/generate_detector_data_v2.py --num_train 12000 --num_val 1200 --output_dir PaddleOCR/data/detector_v2 --workers 8"
|
| 63 |
+
out = studio.run(gen_cmd)
|
| 64 |
+
log("Kết quả sinh dữ liệu:")
|
| 65 |
+
for line in out.strip().splitlines()[-6:]:
|
| 66 |
+
log(f" {line}")
|
| 67 |
+
|
| 68 |
+
# Copy config
|
| 69 |
+
studio.run("cd /teamspace/studios/this_studio/Cham-OCR/ocr-training && mkdir -p PaddleOCR/configs/det && cp configs/det/ch_PP-OCRv4_det_h100.yml PaddleOCR/configs/det/")
|
| 70 |
+
log("✅ Bộ dữ liệu và cấu hình H100 đã sẵn sàng!")
|
| 71 |
+
|
| 72 |
+
# PHASE 2: SWITCH MACHINE TO H100
|
| 73 |
+
log("\n🔥 --- PHASE 2: CHUYỂN ĐỔI PHẦN CỨNG SANG GPU NVIDIA H100 (80GB VRAM) ---")
|
| 74 |
+
log("Đang gửi lệnh switch_machine sang H100...")
|
| 75 |
+
studio.switch_machine(Machine.H100)
|
| 76 |
+
log(f"Trạng thái sau khi chuyển: {studio.status} (Machine: {studio.machine})")
|
| 77 |
+
|
| 78 |
+
# Install paddlepaddle-gpu for Hopper/CUDA
|
| 79 |
+
log("📦 Cài đặt PaddlePaddle GPU bản CUDA 11.8/12.x tối ưu H100...")
|
| 80 |
+
studio.run("pip install -q paddlepaddle-gpu -i https://www.paddlepaddle.org.cn/packages/stable/cu118/")
|
| 81 |
+
gpu_info = studio.run("nvidia-smi --query-gpu=name,memory.total --format=csv,noheader")
|
| 82 |
+
log(f"🔥 GPU đã nhận diện: {gpu_info.strip()}")
|
| 83 |
+
|
| 84 |
+
# PHASE 3: LAUNCH TRAINING WITH BACKGROUND MONITORING
|
| 85 |
+
log("\n🚀 --- PHASE 3: KHỞI CHẠY HUẤN LUYỆN CHAM-DBNET TRÊN H100 ---")
|
| 86 |
+
train_script = """cd /teamspace/studios/this_studio/Cham-OCR/ocr-training/PaddleOCR && nohup python3 -m paddle.distributed.launch --gpus '0' tools/train.py -c configs/det/ch_PP-OCRv4_det_h100.yml > train.log 2>&1 &"""
|
| 87 |
+
studio.run(train_script)
|
| 88 |
+
log("✅ Lệnh huấn luyện đã kích hoạt chạy ngầm (background) an toàn trên H100!")
|
| 89 |
+
|
| 90 |
+
# MONITORING LOOP
|
| 91 |
+
log("📊 Bắt đầu vòng lặp theo dõi liên tục tiến trình huấn luyện...")
|
| 92 |
+
prev_log_tail = ""
|
| 93 |
+
idle_count = 0
|
| 94 |
+
start_time = time.time()
|
| 95 |
+
|
| 96 |
+
while True:
|
| 97 |
+
time.sleep(15)
|
| 98 |
+
# Check if process is still running
|
| 99 |
+
ps_out = studio.run("ps aux | grep tools/train.py | grep -v grep | wc -l")
|
| 100 |
+
is_running = int(ps_out.strip() or "0") > 0
|
| 101 |
+
|
| 102 |
+
# Read log tail
|
| 103 |
+
try:
|
| 104 |
+
log_tail = studio.run("cd /teamspace/studios/this_studio/Cham-OCR/ocr-training/PaddleOCR && tail -n 8 train.log")
|
| 105 |
+
if log_tail.strip() != prev_log_tail:
|
| 106 |
+
prev_log_tail = log_tail.strip()
|
| 107 |
+
log("--- Cập nhật tiến trình mới nhất ---")
|
| 108 |
+
for l in log_tail.strip().splitlines()[-4:]:
|
| 109 |
+
log(f" {l}")
|
| 110 |
+
except Exception as e:
|
| 111 |
+
log(f"Không thể đọc log: {e}")
|
| 112 |
+
|
| 113 |
+
elapsed_mins = (time.time() - start_time) / 60
|
| 114 |
+
if not is_running:
|
| 115 |
+
log(f"\n🎉 Tiến trình huấn luyện đã kết thúc sau {elapsed_mins:.1f} phút!")
|
| 116 |
+
break
|
| 117 |
+
|
| 118 |
+
# PHASE 4: EXPORT MODEL & DOWNLOAD
|
| 119 |
+
log("\n📦 --- PHASE 4: XUẤT MÔ HÌNH INFERENCE & ĐÓNG GÓI ---")
|
| 120 |
+
export_cmd = """cd /teamspace/studios/this_studio/Cham-OCR/ocr-training/PaddleOCR && \
|
| 121 |
+
BEST_MODEL="output/ch_PP-OCRv4_det_cham_h100/best_accuracy" && \
|
| 122 |
+
if [ ! -f "${BEST_MODEL}.pdparams" ]; then BEST_MODEL="output/ch_PP-OCRv4_det_cham_h100/latest"; fi && \
|
| 123 |
+
python3 tools/export_model.py -c configs/det/ch_PP-OCRv4_det_h100.yml -o Global.pretrained_model=${BEST_MODEL} Global.save_inference_dir=../output/ch_PP-OCRv4_det_cham_infer && \
|
| 124 |
+
cd ../output && zip -r ../cham_dbnet_v1_infer.zip ch_PP-OCRv4_det_cham_infer/"""
|
| 125 |
+
|
| 126 |
+
out = studio.run(export_cmd)
|
| 127 |
+
log("Kết quả đóng gói:")
|
| 128 |
+
for l in out.strip().splitlines()[-5:]:
|
| 129 |
+
log(f" {l}")
|
| 130 |
+
|
| 131 |
+
# Download file to local workspace
|
| 132 |
+
local_zip_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "cham_dbnet_v1_infer.zip")
|
| 133 |
+
log(f"⬇️ Đang tải tệp mô hình về máy local: {local_zip_path}...")
|
| 134 |
+
studio.download_file("Cham-OCR/ocr-training/cham_dbnet_v1_infer.zip", local_zip_path)
|
| 135 |
+
log(f"✅ Đã tải thành công tệp mô hình ({os.path.getsize(local_zip_path) / (1024*1024):.2f} MB)!")
|
| 136 |
+
|
| 137 |
+
# PHASE 5: STOP STUDIO TO SAVE CREDITS
|
| 138 |
+
log("\n🛑 --- PHASE 5: TỰ ĐỘNG DỪNG STUDIO ĐỂ BẢO VỆ CREDIT ---")
|
| 139 |
+
studio.stop()
|
| 140 |
+
log("✅ Studio đã được DỪNG (STOPPED) an toàn! Không còn tiêu tốn bất kỳ credit nào.")
|
| 141 |
+
log("=================================================================")
|
| 142 |
+
log("🎊 HOÀN THÀNH TOÀN BỘ QUY TRÌNH HUẤN LUYỆN CHAM-DBNET TRÊN H100!")
|
| 143 |
+
log("=================================================================")
|
| 144 |
+
|
| 145 |
+
if __name__ == '__main__':
|
| 146 |
+
main()
|
ocr-training/scripts/test_live_ocr.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import requests
|
| 3 |
+
import json
|
| 4 |
+
import base64
|
| 5 |
+
|
| 6 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 7 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 8 |
+
|
| 9 |
+
print("=== KIỂM THỬ GIAO DIỆN & API TINH GIẢN HÓA (CHỈ CẦN ẢNH & MÔ HÌNH) ===")
|
| 10 |
+
|
| 11 |
+
# 1. Kiểm tra HTML đã gỡ bỏ hoàn toàn khối cấu hình thủ công
|
| 12 |
+
html_resp = requests.get('https://ocr.cham.asia', timeout=30)
|
| 13 |
+
html_text = html_resp.text
|
| 14 |
+
has_adv = 'advanced-settings-content' in html_text
|
| 15 |
+
has_method_sel = 'method-select' in html_text
|
| 16 |
+
has_thresh = 'threshold-input' in html_text
|
| 17 |
+
print(f"1. Kiểm tra HTML giao diện Web:")
|
| 18 |
+
print(f" - Mã phản hồi HTTP: {html_resp.status_code}")
|
| 19 |
+
print(f" - Còn chứa khối 'Cấu hình nâng cao': {has_adv}")
|
| 20 |
+
print(f" - Còn chứa menu chọn thuật toán: {has_method_sel}")
|
| 21 |
+
print(f" - Còn chứa input ngưỡng nhị phân: {has_thresh}")
|
| 22 |
+
|
| 23 |
+
if not has_adv and not has_method_sel and not has_thresh:
|
| 24 |
+
print(" 👉 Giao diện Web đã được tinh giản hoàn hảo!")
|
| 25 |
+
else:
|
| 26 |
+
print(" ⚠️ Vẫn còn sót thành phần cũ trên giao diện.")
|
| 27 |
+
|
| 28 |
+
# 2. Kiểm tra API POST /ocr với payload tối giản
|
| 29 |
+
with open('ocr-training/output/evidence/page_ocr/page_0.png', 'rb') as f:
|
| 30 |
+
img_b64 = base64.b64encode(f.read()).decode('utf-8')
|
| 31 |
+
|
| 32 |
+
print(f"\n2. Gửi yêu cầu OCR với Payload tối giản (không gửi method/threshold):")
|
| 33 |
+
payload = {
|
| 34 |
+
'image': 'data:image/png;base64,' + img_b64,
|
| 35 |
+
'model': 'v24'
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
resp = requests.post('https://ocr.cham.asia/ocr', json=payload, timeout=60)
|
| 39 |
+
data = resp.json()
|
| 40 |
+
|
| 41 |
+
if 'error' in data:
|
| 42 |
+
print("❌ Lỗi:", data['error'])
|
| 43 |
+
else:
|
| 44 |
+
lines = data.get('lines', [])
|
| 45 |
+
profiler = data.get('profiler', {})
|
| 46 |
+
print(f"✅ Thành công 100%! Phát hiện {len(lines)} dòng chữ:")
|
| 47 |
+
print(f" - Thuật toán phân đoạn tự động kích hoạt: {profiler.get('method')}")
|
| 48 |
+
print(f" - Tổng thời gian xử lý: {profiler.get('backend_total_sec')}s")
|
| 49 |
+
for idx, l in enumerate(lines):
|
| 50 |
+
print(f" [Dòng {idx+1}] Độ tin cậy: {l['confidence']:.4f} | BBox: {l['bbox']} | Text: {l['text']}")
|
ocr-training/scripts/train_and_export_h100.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
One-Click Automated Training & Inference Export Pipeline for Cham-DBNet on Lightning AI (H100).
|
| 5 |
+
Executes:
|
| 6 |
+
1. Environment & CUDA hardware check (Hopper H100 / sm_90 verification)
|
| 7 |
+
2. Dataset presence check (generates if missing)
|
| 8 |
+
3. Pretrained weight verification
|
| 9 |
+
4. Distributed/Single GPU training with PaddleOCR DBNet
|
| 10 |
+
5. Model export to lightweight inference model (inference.pdmodel / inference.pdiparams)
|
| 11 |
+
6. Automatic ZIP packaging for 1-click download.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import shutil
|
| 17 |
+
import zipfile
|
| 18 |
+
import subprocess
|
| 19 |
+
|
| 20 |
+
# Force UTF-8 stdout
|
| 21 |
+
if sys.stdout and sys.stdout.encoding != 'utf-8':
|
| 22 |
+
try:
|
| 23 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 24 |
+
except AttributeError:
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
def check_environment():
|
| 28 |
+
print("=" * 65)
|
| 29 |
+
print("🔍 1. KIỂM TRA MÔI TRƯỜNG & GPU PHẦN CỨNG (LIGHTNING AI)")
|
| 30 |
+
print("=" * 65)
|
| 31 |
+
try:
|
| 32 |
+
import paddle
|
| 33 |
+
print(f" - PaddlePaddle version: {paddle.__version__}")
|
| 34 |
+
print(f" - CUDA compiled: {paddle.is_compiled_with_cuda()}")
|
| 35 |
+
if paddle.is_compiled_with_cuda():
|
| 36 |
+
count = paddle.device.cuda.device_count()
|
| 37 |
+
print(f" - Số lượng GPU khả dụng: {count}")
|
| 38 |
+
for i in range(count):
|
| 39 |
+
name = paddle.device.cuda.get_device_name(i)
|
| 40 |
+
print(f" + GPU {i}: {name}")
|
| 41 |
+
else:
|
| 42 |
+
print(" ⚠️ CẢNH BÁO: PaddlePaddle chưa kích hoạt CUDA GPU!")
|
| 43 |
+
except ImportError:
|
| 44 |
+
print(" ❌ LỖI: Chưa cài đặt PaddlePaddle! Chạy: pip install paddlepaddle-gpu")
|
| 45 |
+
sys.exit(1)
|
| 46 |
+
|
| 47 |
+
def run_command(cmd, desc="Running"):
|
| 48 |
+
print(f"\n🚀 [{desc}] Lệnh: {cmd}")
|
| 49 |
+
ret = subprocess.run(cmd, shell=True)
|
| 50 |
+
if ret.returncode != 0:
|
| 51 |
+
print(f"❌ Lỗi khi thực hiện: {desc} (Mã lỗi: {ret.returncode})")
|
| 52 |
+
sys.exit(ret.returncode)
|
| 53 |
+
print(f"✅ Hoàn tất: {desc}")
|
| 54 |
+
|
| 55 |
+
def main():
|
| 56 |
+
check_environment()
|
| 57 |
+
|
| 58 |
+
workspace_dir = os.path.abspath(os.getcwd())
|
| 59 |
+
paddleocr_dir = os.path.join(workspace_dir, "PaddleOCR")
|
| 60 |
+
if not os.path.exists(paddleocr_dir):
|
| 61 |
+
print("\n📥 Chưa tìm thấy PaddleOCR, đang clone release/2.7...")
|
| 62 |
+
run_command("git clone -b release/2.7 https://github.com/PaddlePaddle/PaddleOCR.git", "Clone PaddleOCR")
|
| 63 |
+
|
| 64 |
+
config_path = os.path.join(workspace_dir, "configs", "det", "ch_PP-OCRv4_det_h100.yml")
|
| 65 |
+
if not os.path.exists(config_path):
|
| 66 |
+
# Check inside PaddleOCR
|
| 67 |
+
config_path = os.path.join(paddleocr_dir, "configs", "det", "ch_PP-OCRv4_det_h100.yml")
|
| 68 |
+
|
| 69 |
+
# Check dataset
|
| 70 |
+
dataset_dir = os.path.join(paddleocr_dir, "data", "detector_v2")
|
| 71 |
+
if not os.path.exists(dataset_dir) or not os.path.exists(os.path.join(dataset_dir, "det_train_label.txt")):
|
| 72 |
+
print("\n📂 Chưa có dữ liệu detector_v2, đang khởi chạy bộ sinh dữ liệu đặc trị dòng hẹp...")
|
| 73 |
+
gen_script = os.path.join(workspace_dir, "scripts", "generate_detector_data_v2.py")
|
| 74 |
+
if not os.path.exists(gen_script):
|
| 75 |
+
gen_script = "scripts/generate_detector_data_v2.py"
|
| 76 |
+
run_command(f"python3 {gen_script} --num_train 12000 --num_val 1200 --output_dir {dataset_dir} --workers 8", "Sinh dữ liệu Detection V2")
|
| 77 |
+
|
| 78 |
+
# Start training
|
| 79 |
+
print("\n" + "=" * 65)
|
| 80 |
+
print("🔥 2. BẮT ĐẦU HUẤN LUYỆN CHAM-DBNET TRÊN GPU H100")
|
| 81 |
+
print("=" * 65)
|
| 82 |
+
|
| 83 |
+
import paddle
|
| 84 |
+
gpu_count = paddle.device.cuda.device_count() if paddle.is_compiled_with_cuda() else 1
|
| 85 |
+
gpu_list = ",".join([str(i) for i in range(gpu_count)])
|
| 86 |
+
|
| 87 |
+
os.chdir(paddleocr_dir)
|
| 88 |
+
train_cmd = f"python3 -m paddle.distributed.launch --gpus '{gpu_list}' tools/train.py -c {config_path}"
|
| 89 |
+
run_command(train_cmd, "Huấn luyện PaddleOCR DBNet")
|
| 90 |
+
|
| 91 |
+
# Export Model
|
| 92 |
+
print("\n" + "=" * 65)
|
| 93 |
+
print("📦 3. XUẤT MÔ HÌNH INFERENCE (INFERENCE MODEL EXPORT)")
|
| 94 |
+
print("=" * 65)
|
| 95 |
+
|
| 96 |
+
output_dir = os.path.join(paddleocr_dir, "output", "ch_PP-OCRv4_det_cham_h100")
|
| 97 |
+
best_model = os.path.join(output_dir, "best_accuracy")
|
| 98 |
+
if not os.path.exists(best_model + ".pdparams"):
|
| 99 |
+
best_model = os.path.join(output_dir, "latest")
|
| 100 |
+
|
| 101 |
+
export_infer_dir = os.path.join(workspace_dir, "output", "ch_PP-OCRv4_det_cham_infer")
|
| 102 |
+
os.makedirs(export_infer_dir, exist_ok=True)
|
| 103 |
+
|
| 104 |
+
export_cmd = f"python3 tools/export_model.py -c {config_path} -o Global.pretrained_model={best_model} Global.save_inference_dir={export_infer_dir}"
|
| 105 |
+
run_command(export_cmd, "Xuất mô hình Inference DBNet")
|
| 106 |
+
|
| 107 |
+
# Package into ZIP
|
| 108 |
+
print("\n" + "=" * 65)
|
| 109 |
+
print("🎁 4. ĐÓNG GÓI TỆP NÉN SẴN SÀNG TẢI VỀ (ZIP PACKAGING)")
|
| 110 |
+
print("=" * 65)
|
| 111 |
+
|
| 112 |
+
zip_path = os.path.join(workspace_dir, "cham_dbnet_v1_infer.zip")
|
| 113 |
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
| 114 |
+
for root, _, files in os.walk(export_infer_dir):
|
| 115 |
+
for file in files:
|
| 116 |
+
full_path = os.path.join(root, file)
|
| 117 |
+
rel_path = os.path.relpath(full_path, os.path.dirname(export_infer_dir))
|
| 118 |
+
zipf.write(full_path, rel_path)
|
| 119 |
+
|
| 120 |
+
print(f"🎉 HOÀN THÀNH TOÀN DIỆN!")
|
| 121 |
+
print(f" - Mô hình Inference tại: {export_infer_dir}")
|
| 122 |
+
print(f" - Tệp nén sẵn sàng tải về: {zip_path} ({os.path.getsize(zip_path) / (1024*1024):.2f} MB)")
|
| 123 |
+
print(f"👉 Hãy tải tệp zip này về và giải nén vào ocr-studio/data/output/ (thư mục ch_PP-OCRv4_det_cham_infer) để cập nhật hệ thống.")
|
| 124 |
+
|
| 125 |
+
if __name__ == '__main__':
|
| 126 |
+
main()
|
ocr-training/scripts/weight_surgery_v24.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import paddle
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
|
| 8 |
+
sys.stdout.reconfigure(encoding='utf-8')
|
| 9 |
+
|
| 10 |
+
def perform_weight_surgery_v24(old_ckpt_path='ocr-training/data/cham-ocr-v5-assets/v23_checkpoint/iter_epoch_200.pdparams',
|
| 11 |
+
new_ckpt_dir='ocr-training/data/output_v24_temp/rec_cham_best_model',
|
| 12 |
+
report_path='ocr-training/output/v24_training/head_extension_report.json',
|
| 13 |
+
old_dict_path='ocr-training/data/cham_dict_v23.txt',
|
| 14 |
+
new_dict_path='ocr-training/data/cham_dict_v24.txt'):
|
| 15 |
+
|
| 16 |
+
os.makedirs(new_ckpt_dir, exist_ok=True)
|
| 17 |
+
os.makedirs(os.path.dirname(os.path.abspath(report_path)), exist_ok=True)
|
| 18 |
+
new_ckpt_path = os.path.join(new_ckpt_dir, 'iter_epoch_200.pdparams')
|
| 19 |
+
|
| 20 |
+
# 1. Đếm số ký tự từ điển
|
| 21 |
+
with open(old_dict_path, 'r', encoding='utf-8') as f:
|
| 22 |
+
old_chars = [l for l in f.read().splitlines() if l]
|
| 23 |
+
with open(new_dict_path, 'r', encoding='utf-8') as f:
|
| 24 |
+
new_chars = [l for l in f.read().splitlines() if l]
|
| 25 |
+
|
| 26 |
+
num_old = len(old_chars) # 82
|
| 27 |
+
num_new = len(new_chars) # 103
|
| 28 |
+
|
| 29 |
+
# CTC: blank (0) + characters (1..num) + space (num+1)
|
| 30 |
+
old_ctc_classes = num_old + 2 # 84
|
| 31 |
+
new_ctc_classes = num_new + 2 # 105
|
| 32 |
+
|
| 33 |
+
# GTC: 6 special tokens + characters
|
| 34 |
+
old_gtc_vocab = num_old + 6 # 88
|
| 35 |
+
new_gtc_vocab = num_new + 6 # 109
|
| 36 |
+
|
| 37 |
+
print(f"🔬 Khởi chạy Phẫu thuật Trọng số (Weight Surgery) V24:")
|
| 38 |
+
print(f" - Từ điển: {num_old} -> {num_new} ký tự")
|
| 39 |
+
print(f" - CTC Classes: {old_ctc_classes} -> {new_ctc_classes}")
|
| 40 |
+
print(f" - GTC Vocab: {old_gtc_vocab} -> {new_gtc_vocab}")
|
| 41 |
+
|
| 42 |
+
old_state = paddle.load(old_ckpt_path)
|
| 43 |
+
new_state = {}
|
| 44 |
+
|
| 45 |
+
for k, v in old_state.items():
|
| 46 |
+
if k == 'head.ctc_head.fc.weight':
|
| 47 |
+
in_features, old_c = v.shape
|
| 48 |
+
assert old_c == old_ctc_classes, f"Mismatch old CTC shape: {v.shape}"
|
| 49 |
+
|
| 50 |
+
std = v.std().item()
|
| 51 |
+
mean = v.mean().item()
|
| 52 |
+
new_v_np = np.random.normal(mean, std, (in_features, new_ctc_classes)).astype('float32')
|
| 53 |
+
|
| 54 |
+
# Bảo tồn trọng số cũ:
|
| 55 |
+
# idx 0: CTC blank
|
| 56 |
+
# idx 1..num_old: các ký tự cũ
|
| 57 |
+
new_v_np[:, 0:num_old+1] = v.numpy()[:, 0:num_old+1]
|
| 58 |
+
# idx space cũ chuyển sang idx space mới
|
| 59 |
+
new_v_np[:, new_ctc_classes - 1] = v.numpy()[:, old_ctc_classes - 1]
|
| 60 |
+
|
| 61 |
+
new_state[k] = paddle.to_tensor(new_v_np)
|
| 62 |
+
print(f" ✅ {k}: {v.shape} -> {new_state[k].shape}")
|
| 63 |
+
|
| 64 |
+
elif k == 'head.ctc_head.fc.bias':
|
| 65 |
+
assert v.shape[0] == old_ctc_classes
|
| 66 |
+
new_v_np = np.zeros(new_ctc_classes, dtype='float32')
|
| 67 |
+
new_v_np[0:num_old+1] = v.numpy()[0:num_old+1]
|
| 68 |
+
new_v_np[new_ctc_classes - 1] = v.numpy()[old_ctc_classes - 1]
|
| 69 |
+
|
| 70 |
+
new_state[k] = paddle.to_tensor(new_v_np)
|
| 71 |
+
print(f" ✅ {k}: {v.shape} -> {new_state[k].shape}")
|
| 72 |
+
|
| 73 |
+
elif k == 'head.gtc_head.embedding.embedding.weight':
|
| 74 |
+
old_v, embed_dim = v.shape
|
| 75 |
+
assert old_v == old_gtc_vocab
|
| 76 |
+
std = v.std().item()
|
| 77 |
+
mean = v.mean().item()
|
| 78 |
+
new_v_np = np.random.normal(mean, std, (new_gtc_vocab, embed_dim)).astype('float32')
|
| 79 |
+
|
| 80 |
+
# idx 0..num_old+3: blank, unk, bos, eos + old characters
|
| 81 |
+
# Trong GTC: 4 tokens đầu + num_old ký tự = num_old + 4
|
| 82 |
+
split_point = num_old + 4
|
| 83 |
+
new_v_np[0:split_point, :] = v.numpy()[0:split_point, :]
|
| 84 |
+
|
| 85 |
+
# 2 tokens cuối: space và pad
|
| 86 |
+
new_v_np[new_gtc_vocab - 2:new_gtc_vocab, :] = v.numpy()[old_gtc_vocab - 2:old_gtc_vocab, :]
|
| 87 |
+
|
| 88 |
+
new_state[k] = paddle.to_tensor(new_v_np)
|
| 89 |
+
print(f" ✅ {k}: {v.shape} -> {new_state[k].shape}")
|
| 90 |
+
|
| 91 |
+
elif k == 'head.gtc_head.tgt_word_prj.weight':
|
| 92 |
+
embed_dim, old_v = v.shape
|
| 93 |
+
assert old_v == old_gtc_vocab
|
| 94 |
+
std = v.std().item()
|
| 95 |
+
mean = v.mean().item()
|
| 96 |
+
new_v_np = np.random.normal(mean, std, (embed_dim, new_gtc_vocab)).astype('float32')
|
| 97 |
+
|
| 98 |
+
split_point = num_old + 4
|
| 99 |
+
new_v_np[:, 0:split_point] = v.numpy()[:, 0:split_point]
|
| 100 |
+
new_v_np[:, new_gtc_vocab - 2:new_gtc_vocab] = v.numpy()[:, old_gtc_vocab - 2:old_gtc_vocab]
|
| 101 |
+
|
| 102 |
+
new_state[k] = paddle.to_tensor(new_v_np)
|
| 103 |
+
print(f" ✅ {k}: {v.shape} -> {new_state[k].shape}")
|
| 104 |
+
|
| 105 |
+
else:
|
| 106 |
+
new_state[k] = v
|
| 107 |
+
|
| 108 |
+
# Lưu trọng số mở rộng mới
|
| 109 |
+
paddle.save(new_state, new_ckpt_path)
|
| 110 |
+
|
| 111 |
+
report = {
|
| 112 |
+
"old_num_classes": old_ctc_classes,
|
| 113 |
+
"new_num_classes": new_ctc_classes,
|
| 114 |
+
"old_gtc_vocab": old_gtc_vocab,
|
| 115 |
+
"new_gtc_vocab": new_gtc_vocab,
|
| 116 |
+
"old_chars_preserved": True,
|
| 117 |
+
"new_chars_initialized": True,
|
| 118 |
+
"new_ckpt_path": new_ckpt_path
|
| 119 |
+
}
|
| 120 |
+
with open(report_path, 'w', encoding='utf-8') as f_rep:
|
| 121 |
+
json.dump(report, f_rep, indent=2)
|
| 122 |
+
|
| 123 |
+
print(f"🎉 Phẫu thuật thành công! Đã lưu checkpoint nền V24 tại: {new_ckpt_path}")
|
| 124 |
+
return new_ckpt_path, report
|
| 125 |
+
|
| 126 |
+
def find_assets_dir():
|
| 127 |
+
candidates = [
|
| 128 |
+
"ocr-training/data/cham-ocr-v5-assets",
|
| 129 |
+
"/kaggle/input/cham-ocr-v5-assets",
|
| 130 |
+
"/kaggle/input/datasets/gustavnguyen/cham-ocr-v5-assets"
|
| 131 |
+
]
|
| 132 |
+
for c in candidates:
|
| 133 |
+
if os.path.exists(c):
|
| 134 |
+
return c
|
| 135 |
+
if os.path.exists('/kaggle/input'):
|
| 136 |
+
for root, dirs, files in os.walk('/kaggle/input'):
|
| 137 |
+
if 'v23_checkpoint' in dirs:
|
| 138 |
+
return root
|
| 139 |
+
return "ocr-training/data/cham-ocr-v5-assets"
|
| 140 |
+
|
| 141 |
+
def main():
|
| 142 |
+
import argparse
|
| 143 |
+
parser = argparse.ArgumentParser()
|
| 144 |
+
parser.add_argument('--assets-dir', type=str, default=None)
|
| 145 |
+
parser.add_argument('--old-ckpt', type=str, default=None)
|
| 146 |
+
parser.add_argument('--new-ckpt-dir', type=str, default=None)
|
| 147 |
+
args = parser.parse_args()
|
| 148 |
+
|
| 149 |
+
assets_dir = args.assets_dir or find_assets_dir()
|
| 150 |
+
old_ckpt = args.old_ckpt or os.path.join(assets_dir, 'v23_checkpoint', 'iter_epoch_200.pdparams')
|
| 151 |
+
new_ckpt_dir = args.new_ckpt_dir or ('/kaggle/working/data/output_v24_temp/rec_cham_best_model' if os.path.exists('/kaggle') else 'ocr-training/data/output_v24_temp/rec_cham_best_model')
|
| 152 |
+
report_path = '/kaggle/working/output/v24_training/head_extension_report.json' if os.path.exists('/kaggle') else 'ocr-training/output/v24_training/head_extension_report.json'
|
| 153 |
+
old_dict = os.path.join(assets_dir, 'v23_checkpoint', 'cham_dict_v23.txt')
|
| 154 |
+
if not os.path.exists(old_dict):
|
| 155 |
+
old_dict = 'ocr-training/data/cham_dict_v23.txt'
|
| 156 |
+
new_dict = '/kaggle/working/data/cham_dict_v24.txt' if os.path.exists('/kaggle') else 'ocr-training/data/cham_dict_v24.txt'
|
| 157 |
+
|
| 158 |
+
perform_weight_surgery_v24(
|
| 159 |
+
old_ckpt_path=old_ckpt,
|
| 160 |
+
new_ckpt_dir=new_ckpt_dir,
|
| 161 |
+
report_path=report_path,
|
| 162 |
+
old_dict_path=old_dict,
|
| 163 |
+
new_dict_path=new_dict
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
if __name__ == '__main__':
|
| 167 |
+
main()
|