File size: 22,529 Bytes
efd2c7f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Master Synthetic Dataset Generator for Cham OCR Model V25.
Optimized for 32-core Data Prep CPU Machine on Lightning AI Studios.

Produces 140,000 Train + 10,000 Val (150,000 total) line images across 5 core pillars:
1. Standard & Authentic Cham Literature (Po Klong Garai, 57-stanza poem, administration)
2. Extreme Anti-Motion Blur & Defocus Augmentation (Kernels 7x7 to 13x13, downsampling)
3. Intra-line Bilingual Code-Switching (Cham phrase + Vietnamese diacritics + Latin)
4. Full Stanza Digits (1-99: ꩑꩞..꩙꩙꩞) & Boundary Punctuation (꩞, :, –, ?, !)
5. Minimal Pairs Hard-Examples (Vowel Sign UE ꨲ U+AA32 vs Medial WA ꨶ U+AA36, ꨯꨮꨩ vs ꨯꨱ, ꩝꩝ variable gap 2-8px)
"""

import os
import sys
import math
import random
import time
import json
import unicodedata
import argparse
import cv2
import numpy as np
from multiprocessing import Pool, cpu_count
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance

# UTF-8 stdout encoding
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
    sys.stdout.reconfigure(encoding='utf-8')
if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
    sys.stderr.reconfigure(encoding='utf-8')

# NumPy 2.x compatibility monkeypatch
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]
    }
if not hasattr(np, 'bool'): np.bool = bool
if not hasattr(np, 'int'): np.int = int
if not hasattr(np, 'float'): np.float = float
if not hasattr(np, 'typeDict'): np.typeDict = {}

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
TRAINING_DIR = os.path.dirname(SCRIPT_DIR)
PROJECT_ROOT = os.path.dirname(TRAINING_DIR)

# Fonts
FONT_CHAM_REG = os.path.join(TRAINING_DIR, "data", "fonts", "NotoSansCham-Regular.ttf")
FONT_CHAM_BOLD = os.path.join(TRAINING_DIR, "data", "fonts", "NotoSansCham-Bold.ttf")

# ==============================================================================
# 1. Ký Tự & Ngữ Liệu Tiếng Chăm & Song Ngữ V25
# ==============================================================================
CHAM_CONSONANTS = list("ꨆꨇꨈꨉꨊꨋꨌꨍꨎꨏꨐꨑꨒꨓꨔꨕꨖꨗꨘꨙꨚꨛꨜꨝꨞꨟꨠꨡꨢꨣꨤꨥꨦꨧꨨꨀꨁꨂꨃꨄꨅ")
CHAM_DIGITS_LIST = list("꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙")

def to_cham_num(n):
    """Chuyển số nguyên n thành chuỗi chữ số Chăm"""
    return "".join(CHAM_DIGITS_LIST[int(d)] for d in str(n))

# Ngữ liệu từ bài thơ 57 khổ và truyền thuyết Po Klong Garai
CHAM_VOCAB_POOL = [
    "ꨛꨯꨮ", "ꨆꨵꨯꨱꩃ", "ꨈꨣꩈ", "ꨧꨯꨱꩃ", "ꨚꩆ", "ꨆꩀ", "ꨨꨤꨩ", "ꨟꨐꨪꩌ", "ꨕꨶꨩ", "ꨝꨯꨱꩍ",
    "ꨝꨰ", "ꨙꨮ", "ꨎꨤꨭꩀ", "ꨟꨐꨭꩌ", "ꨀꨳꨩ", "ꨝꨵꨯꨱꩍ", "ꨝꨩ", "ꨧꨩ", "ꨝꨵꩍ", "ꨀꨇꩆ",
    "ꨗꨯꨱ", "ꨝꨣꨭꨥ", "ꨟꩃ", "ꨓꨮꩊ", "ꨊꨯꨱꩀ", "ꨣꨈꨵꨰ", "ꨚꨢꨯꨱꩌ", "ꨘꩆ", "ꨤꩄ", "ꨆꨳꨮꩃ",
    "ꨧꨤꨪꩍ", "ꨕꨨꨵꩀ", "ꨨꨕꨯꩌ", "ꨀꨣꩀ", "ꨗꨫ", "ꨢꨭꩅ", "ꨚꨰꩀ", "ꨝꨪ", "ꨝꩀ", "ꨆꨮꨭ",
    "ꨝꨴꨬ", "ꨆꨩ", "ꨨꨲꨩ", "ꨡꨯꩍ", "ꨀꨦꨪꩅ", "ꨟꩀ", "ꨟꨰ", "ꨝꨭꩍ", "ꨕꨫ", "ꨕꨤꩌ",
    "ꨅꨩ", "ꨨꨕꨳꨮꩇ", "ꨀꨗꩀ", "ꨓꨌꨯꨱꨥ", "ꨙꩃ", "ꨀꨝꨪꩍ", "ꨤꨝꨭꩀ", "ꨎꨥ", "ꨥꨮꩀ", "ꨢꨮ",
    "ꨐꨭꨩ", "ꨓꨴꨩ", "ꨚꨤꨬ", "ꨂꩀ", "ꨀꨗꨯꨱꩃ", "ꨧꨝꨭꩃ", "ꨈꨮꩇ", "ꨀꨧꨪꩅ", "ꨢꨮꨭ", "ꨕꨘꩀ",
    "ꨎꨮ", "ꨚꨶꨮꩄ", "ꨚꨆꨴꨲꨩ", "ꨟꨪꩆ", "ꨙꨩ", "ꨝꨳꩀ", "ꨚꨗꨶꨮꩄ", "ꨔꨮꨭ", "ꨕꨯꨱꩀ", "ꨓꨳꩆ",
    "ꨀꨶꨮꩆ", "ꨓꨝꨶꨮꩆ", "ꨚꨎꨳꩀ", "ꨆꨵꨮꨭ", "ꨀꨮꩈ", "ꨧꨝꩀ", "ꨈꨗꨴꨮꩍ", "ꨚꩀ", "ꨨꨚꨭꩍ", "ꨤꨪꨎꩃ",
    "ꨝꨵꨬ", "ꨣꨬ", "ꨈꨴꨮꩇ", "ꨧꩃ", "ꨠꨎꨰꩀ", "ꨚꨙꨳꩀ", "ꨤꨯꨩ", "ꨤꨮꩍ", "ꨚꨕꨬ", "ꨈꨵꨰꩍ",
    "ꨀꨤꨩ", "ꨨꨤꨶꨬ", "ꨆꨭꨢꨮꨭ", "ꨤꨪꨔꨬ", "ꨨꨤꨬ", "ꨇꨪꩀ", "ꨙꨪꩍ", "ꨥꩉ", "ꨕꨴꨬ", "ꨁꨗꨩ",
    "ꨈꨭꨣꩈ", "ꨤꨳꩍ", "ꨣꨭꩇ", "ꨤꨶꩀ", "ꨓꨟꨩ", "ꨓꨗꩍ", "ꨣꨪꨢꨩ", "ꨖꨪꩅ", "ꨀꨧꩉ", "ꨚꨓꨯꨱ",
    "ꨇꨪꩆ", "ꨨꨶꩀ", "ꨃꨥ", "ꨧꨣꨥꨩ", "ꨚꨙꩉ", "ꨢꩍ", "ꨄꨰ", "ꨆꨯꨱꩍ", "ꨚꨓꨬ", "ꨓꨭꩍ",
    "ꨓꨌꨬ", "ꨌꨪꩍ", "ꨈꩆ", "ꨆꨴꨲꩍ", "ꨓꨟꨯꨱꩃ", "ꨨꨭꨩ", "ꨗꩌ", "ꨈꩍ", "ꨆꨕꨯꨱꩍ", "ꨕꨨꨵꨮꨭ",
    "ꨌꨙꨳꩀ", "ꨓꨆꨶꨯꩈ", "ꨀꨥꨰꩅ", "ꨗꨆꨓꨯꨮꨩ", "ꨀꨆꨢꨯꨮꩅ", "ꨓꨆꨴꨲꨩ", "ꨚꨟꨆꨴꨲꨩ", "ꨣꨳꨪꩌ", "ꨨꨈꨴꨮꩌ"
]

# Minimal pairs cho Vowel Sign UE ꨲ (U+AA32) vs Consonant Sign Medial WA ꨶ (U+AA36)
MINIMAL_PAIRS_UE_WA = [
    ("ꨀꨲꩆ", "ꨀꨶꩆ"),
    ("ꨓꨆꨴꨲꨩ", "ꨓꨆꨴꨶꨩ"),
    ("ꨚꨴꨲꨩ", "ꨚꨴꨶꨩ"),
    ("ꨆꨲꩆ", "ꨆꨶꩆ"),
    ("ꨟꨲꩆ", "ꨟꨶꩆ"),
    ("ꨣꨲꨩ", "ꨣꨶꨩ"),
    ("ꨚꨟꨆꨴꨲꨩ", "ꨚꨟꨆꨴꨶꨩ"),
    ("ꨚꨲ", "ꨚꨶ"),
    ("ꨆꨲ", "ꨆꨶ"),
    ("ꨀꨲ", "ꨀꨶ")
]

# Cụm dấu 3 tầng & tổ hợp nguyên âm phức tạp
COMPLEX_CLUSTERS = [
    "ꨣꨳꨪꩌ", "ꨓꨳꨪꩌ", "ꨚꨳꨪꩌ", "ꨕꨨꨵꩀ", "ꨝꨪꨗꨳꨪ", 
    "ꨨꨕꨳꨮꩇ", "ꨞꨯꨚꨓꨪꩍ", "ꨧꨪꩆꨝꨳꨪ", "ꨆꨵꨯꨱꩃ", "ꨚꨗꨴꨯꨱꩃ",
    "ꨗꨆꨓꨯꨮꨩ", "ꨚꨓꨯꨮꨩ", "ꨤꨯꨮꨩ", "ꨨꨝꨳꨯꨮꩆ", "ꨨꨈꨴꨮꩌ", "ꨣꨪꨈꨮꩌ"
]

# Mẫu song ngữ nội dòng (Cham phrase + Viet translation/commentary)
BILINGUAL_TEMPLATES = [
    ("ꨛꨯꨮ ꨆꨵꨯꨱꩃ ꨈꨣꩈ", "Vua Pô Klông Gia-rai"),
    ("ꨛꨯꨮ ꨚꩆ", "Pô Păt - Po Pat"),
    ("ꨚꨰꩀ ꨨꨤꨩ", "đắp bờ đập ngăn nước"),
    ("ꨕꨶꨩ ꨝꨯꨱꩍ ꨝꨰ", "dẫn nguồn nước vào ruộng"),
    ("ꨞꩇ ꨂꨣꩃ ꨌꩌ", "xứ sở người Chăm Panduranga"),
    ("yutꨢꨮ", "bờ đập nước sông lớn"),
    ("ꨁꨗꨩ ꨈꨭꨣꩈ", "Bà Thầy truyền dạy giáo lý"),
    ("ꨤꨪꨔꨬ ꨓꨝꨳꩀ", "cơm nắm gói trong lá chuối"),
    ("ꨝꨣꨭꨥ ꨟꩃ ꨣꨈꨵꨰ", "sau cuộc thi tài kết thúc"),
    ("ꨕꨨꨵꨀ ꨨꨕꨯꩌ", "nước chảy quanh năm trên đồng"),
    ("ꨎꨤꨭꩀ ꨟꨐꨭꩌ", "mở rộng hệ thống mương máng"),
    ("ꨣꨈꨵꨰ ꨚꨤꨬ ꨂꩀ", "thi tài đắp đập ngăn sông"),
    ("ꨓꩀ ꨕꨫ ꨚꨰꩀ ꨨꨤꨩ", "đến lúc đắp bờ đập nước"),
    ("ꨝꨰ ꨘꩆ ꨀꨦꨪꩅ", "ruộng lúa tốt tươi màu mỡ"),
    ("ꨣꨈꨵꨰ ꨧꨝꨭꩃ", "thi tài đắp đập đá lớn")
]

# ==============================================================================
# 2. Bộ Sinh Nội Dung Văn Bản (V25 Text Sampler)
# ==============================================================================
def sample_v25_text(pillar_type):
    """
    Sinh dòng văn bản theo từng trụ cột kỹ thuật:
    - pillar 1: Cham chuẩn (chuẩn bị cho Anti-Motion Blur)
    - pillar 2: Song ngữ nội dòng (Cham + Viet + Latin)
    - pillar 3: Số khổ thơ 1-99 & Dấu ngắt câu (꩑꩞..꩔꩓꩞, :, –, ?)
    - pillar 4: Cặp đối kháng Minimal Pairs (ꨲ vs ꨶ, ꩝꩝ variable gap, ꨯꨮꨩ)
    """
    if pillar_type == "bilingual":
        # Trụ cột 2: Song ngữ nội dòng
        pair = random.choice(BILINGUAL_TEMPLATES)
        style = random.choice(["bracket", "colon", "hyphen", "en_dash"])
        if style == "bracket":
            return f"{pair[0]} ({pair[1]})"
        elif style == "colon":
            return f"{pair[0]}: {pair[1]}"
        elif style == "en_dash":
            return f"{pair[0]} – {pair[1]}"
        else:
            return f"{pair[0]} - {pair[1]}"

    elif pillar_type == "stanza_punct":
        # Trụ cột 3: Số khổ 1-99 & Dấu ngắt câu
        num_val = random.randint(1, 99)
        cham_num_str = to_cham_num(num_val)
        sep = random.choice(["꩞ ", "꩝ "])
        words = random.sample(CHAM_VOCAB_POOL, random.randint(2, 4))
        content = " ".join(words)
        end_mark = random.choice(["꩞", "꩝꩝", "꩝", ":", "?", "!"])
        return f"{cham_num_str}{sep}{content}{end_mark}"

    elif pillar_type == "minimal_pairs":
        # Trụ cột 4: Cặp đối kháng
        sub = random.choice(["ue_wa", "double_danda", "complex_clusters"])
        if sub == "ue_wa":
            pair = random.choice(MINIMAL_PAIRS_UE_WA)
            target = pair[0] if random.random() < 0.5 else pair[1]
            surrounding = random.sample(CHAM_VOCAB_POOL, random.randint(2, 3))
            return f"{target} " + " ".join(surrounding) + random.choice(["꩝", "꩝꩝", "꩞", ""])
        elif sub == "double_danda":
            words = random.sample(CHAM_VOCAB_POOL, random.randint(3, 5))
            return " ".join(words) + "꩝꩝"
        else:
            c = random.choice(COMPLEX_CLUSTERS)
            words = random.sample(CHAM_VOCAB_POOL, random.randint(2, 3))
            return f"{c} " + " ".join(words) + random.choice(["꩝", "꩞", ""])

    else:
        # Chuẩn (sẽ áp dụng Motion Blur nếu thuộc pillar anti_blur)
        words = random.sample(CHAM_VOCAB_POOL, random.randint(3, 6))
        text = " ".join(words)
        if random.random() < 0.5:
            text += random.choice(["꩝", "꩝꩝", "꩞", ""])
        return text

# ==============================================================================
# 3. Kỹ Thuật Render Ảnh & Tinh Chỉnh Nét Chữ
# ==============================================================================
def render_line_v25(text, is_bold=False, font_size=26, pad_left=12, pad_right=12, danda_gap=3):
    """
    Render dòng chữ Chăm / Song ngữ.
    Hỗ trợ biến thiên khoảng cách Double Danda '꩝꩝' từ 2px đến 8px.
    """
    font_path = FONT_CHAM_BOLD if is_bold else FONT_CHAM_REG
    font = ImageFont.truetype(font_path, font_size)

    # Nếu có ꩝꩝ ở cuối và cần biến thiên khoảng cách
    if text.endswith("꩝꩝") and danda_gap != 3:
        base_text = text[:-2]
        bbox_base = font.getbbox(base_text) if base_text else (0, 0, 0, font_size)
        w_base = (bbox_base[2] - bbox_base[0]) if base_text else 0
        
        bbox_danda = font.getbbox("꩝")
        w_danda = bbox_danda[2] - bbox_danda[0]
        h_line = max(bbox_base[3] - bbox_base[1], bbox_danda[3] - bbox_danda[1]) + 16
        w_total = pad_left + w_base + (6 if base_text else 0) + w_danda + danda_gap + w_danda + pad_right

        img = Image.new("RGB", (max(w_total, 60), max(h_line, 48)), (255, 255, 255))
        draw = ImageDraw.Draw(img)

        cur_x = pad_left
        y = 8
        if base_text:
            draw.text((cur_x, y), base_text, fill=(20, 20, 25), font=font)
            cur_x += w_base + 6
        draw.text((cur_x, y), "꩝", fill=(20, 20, 25), font=font)
        cur_x += w_danda + danda_gap
        draw.text((cur_x, y), "꩝", fill=(20, 20, 25), font=font)
        return img

    # Render thông thường
    bbox = font.getbbox(text)
    tw = bbox[2] - bbox[0]
    th = bbox[3] - bbox[1]
    w = tw + pad_left + pad_right
    h = max(th + 16, 48)

    img = Image.new("RGB", (max(w, 60), h), (255, 255, 255))
    draw = ImageDraw.Draw(img)
    x = pad_left - bbox[0]
    y = 8 - bbox[1]
    draw.text((x, y), text, fill=(20, 20, 25), font=font)
    return img

# ==============================================================================
# 4. Pipeline Biến Dạng Thực Địa (Augmentations) - Đặc Trị Motion Blur
# ==============================================================================
def apply_heavy_motion_blur(img_np, ksize=9, angle=45.0):
    """Tạo ma trận directional motion blur kernel mô phỏng rung tay máy ảnh di động."""
    kernel = np.zeros((ksize, ksize), dtype=np.float32)
    rad = math.radians(angle)
    dx = math.cos(rad)
    dy = math.sin(rad)
    cx, cy = ksize // 2, ksize // 2
    for r in range(-ksize // 2, ksize // 2 + 1):
        kx = int(round(cx + r * dx))
        ky = int(round(cy + r * dy))
        if 0 <= kx < ksize and 0 <= ky < ksize:
            kernel[ky, kx] = 1.0
    if kernel.sum() > 0:
        kernel /= kernel.sum()
    else:
        kernel[cx, cy] = 1.0
    return cv2.filter2D(img_np, -1, kernel)

def augment_image_v25(img_pil, is_motion_blur_pillar=False, bake_blur=False):
    """
    Áp dụng các phép biến dạng hình ảnh thực tế:
    - Nếu bake_blur=False (MẶC ĐỊNH CHO V25): Không nướng (bake) motion blur nặng vào ảnh đĩa.
      Mọi thao tác làm mờ được thực hiện On-the-fly trong DataLoader qua RecAug để tránh double-blur.
    - Giữ biến dạng nhẹ: nhiễu hạt giấy cổ, độ tương phản và chuẩn hóa chiều cao 48px.
    """
    img_bgr = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
    h, w = img_bgr.shape[:2]

    if bake_blur and is_motion_blur_pillar:
        # Chỉ áp dụng nếu người dùng cố ý bật bake_blur
        ksize = random.choice([7, 9, 11, 13])
        angle = random.uniform(0.0, 180.0)
        img_bgr = apply_heavy_motion_blur(img_bgr, ksize=ksize, angle=angle)
        if random.random() < 0.4:
            scale = random.uniform(0.5, 0.75)
            small = cv2.resize(img_bgr, (max(int(w * scale), 20), max(int(h * scale), 16)), interpolation=cv2.INTER_LINEAR)
            img_bgr = cv2.resize(small, (w, h), interpolation=cv2.INTER_CUBIC)

    # Thêm nhiễu nền giấy cổ nhẹ
    if random.random() < 0.6:
        noise = np.random.normal(0, random.uniform(1.0, 3.5), (h, w, 3)).astype(np.float32)
        img_bgr = np.clip(img_bgr.astype(np.float32) + noise, 0, 255).astype(np.uint8)

    # Thay đổi độ tương phản và độ sáng
    if random.random() < 0.5:
        alpha = random.uniform(0.85, 1.15)
        beta = random.uniform(-10, 10)
        img_bgr = np.clip(img_bgr.astype(np.float32) * alpha + beta, 0, 255).astype(np.uint8)

    # Đảm bảo resize về chuẩn height 48px cho V25
    target_h = 48
    scale = target_h / float(h)
    new_w = max(int(round(w * scale)), 32)
    img_resized = cv2.resize(img_bgr, (new_w, target_h), interpolation=cv2.INTER_CUBIC)
    return img_resized

# ==============================================================================
# 5. Hàm Worker Đa Tiến Trình (Multiprocessing Task)
# ==============================================================================
def generate_sample_worker(args):
    """Worker tạo một mẫu ảnh và nhãn."""
    sample_idx, pillar_type, out_img_path, bake_blur = args

    # 1. Sinh văn bản và chuẩn hóa tuyệt đối Unicode NFC
    raw_text = sample_v25_text(pillar_type)
    text = unicodedata.normalize('NFC', raw_text)

    # 2. Render ảnh
    is_bold = (random.random() < 0.35)
    pad_l = random.randint(2, 14)
    pad_r = random.randint(2, 14)
    danda_gap = random.randint(2, 8) if "꩝꩝" in text else 3
    
    img_pil = render_line_v25(text, is_bold=is_bold, pad_left=pad_l, pad_right=pad_r, danda_gap=danda_gap)

    # 3. Biến dạng (Augmentation sạch cho lưu trữ đĩa)
    is_blur_pillar = (pillar_type == "anti_blur")
    img_final = augment_image_v25(img_pil, is_motion_blur_pillar=is_blur_pillar, bake_blur=bake_blur)

    # 4. Ghi file ảnh
    cv2.imwrite(out_img_path, img_final)

    # 5. Trả về tên file tương đối và nhãn
    rel_path = os.path.basename(os.path.dirname(out_img_path)) + "/" + os.path.basename(out_img_path)
    return f"{rel_path}\t{text}\n"

# ==============================================================================
# 6. Điều Phối Huấn Luyện Toàn Cục (Master Generator Loop)
# ==============================================================================
def main():
    default_manifest = os.path.join(TRAINING_DIR, "configs", "v25_dataset_manifest.json")
    parser = argparse.ArgumentParser(description="Generate Cham OCR V25 Synthetic Dataset (Manifest-Aligned)")
    parser.add_argument("--manifest", type=str, default=default_manifest, help="Đường dẫn file manifest JSON chuẩn")
    parser.add_argument("--output_dir", type=str, default="./data/cham_synthetic_v25", help="Thư mục xuất dữ liệu")
    parser.add_argument("--num_train", type=int, default=None, help="Số mẫu Train (mặc định lấy từ manifest: 140,000)")
    parser.add_argument("--num_val", type=int, default=None, help="Số mẫu Val (mặc định lấy từ manifest: 10,000)")
    parser.add_argument("--bake_blur", action="store_true", default=False, help="Bake mờ nặng vào ảnh đĩa (mặc định False: on-the-fly)")
    parser.add_argument("--workers", type=int, default=None, help="Số tiến trình CPU song song (mặc định cpu_count)")
    args = parser.parse_args()

    # Nạp cấu hình từ manifest nếu có
    num_train = args.num_train
    num_val = args.num_val
    pillars = ["standard", "anti_blur", "bilingual", "stanza_punct", "minimal_pairs"]
    weights = [0.433, 0.200, 0.167, 0.120, 0.080] # Tương ứng 65k, 30k, 25k, 18k, 12k

    if os.path.exists(args.manifest):
        try:
            with open(args.manifest, "r", encoding="utf-8") as f:
                manifest_data = json.load(f)
                dist = manifest_data.get("data_distribution", {})
                if num_train is None:
                    num_train = dist.get("train_samples", 140000)
                if num_val is None:
                    num_val = dist.get("val_samples", 10000)
                print(f"📖 Đã nạp cấu hình phân bổ dữ liệu từ Manifest: {args.manifest}")
        except Exception as e:
            print(f"⚠️ Không đọc được manifest ({e}), dùng thông số mặc định.")

    if num_train is None: num_train = 140000
    if num_val is None: num_val = 10000

    num_workers = args.workers or cpu_count()
    print("=" * 75)
    print("🚀 MASTER SYNTHETIC DATASET GENERATOR V25 (MANIFEST SYNCHRONIZED)")
    print(f"   • Số CPU Workers song song : {num_workers} tiến trình")
    print(f"   • Quy mô mẫu Huấn luyện     : {num_train:,} ảnh")
    print(f"   • Quy mô mẫu Kiểm thử       : {num_val:,} ảnh")
    print(f"   • Tổng quy mô dữ liệu       : {num_train + num_val:,} ảnh")
    print(f"   • Chính sách Mờ Động Học   : {'Bake tĩnh trên đĩa' if args.bake_blur else 'On-The-Fly trong DataLoader (Khuyến nghị)'}")
    print(f"   • Thư mục đầu ra           : {args.output_dir}")
    print("=" * 75)

    train_img_dir = os.path.join(args.output_dir, "train_images")
    val_img_dir = os.path.join(args.output_dir, "val_images")
    os.makedirs(train_img_dir, exist_ok=True)
    os.makedirs(val_img_dir, exist_ok=True)

    # Chuẩn bị danh sách tham số cho Train
    print(f"📦 Chuẩn bị danh mục tác vụ cho {num_train:,} mẫu Train...")
    train_tasks = []
    for i in range(num_train):
        p_type = random.choices(pillars, weights=weights)[0]
        img_name = f"train_{i+1:07d}.png"
        out_path = os.path.join(train_img_dir, img_name)
        train_tasks.append((i, p_type, out_path, args.bake_blur))

    # Chuẩn bị danh sách tham số cho Val
    print(f"📦 Chuẩn bị danh mục tác vụ cho {num_val:,} mẫu Val...")
    val_tasks = []
    for i in range(num_val):
        p_type = random.choices(pillars, weights=weights)[0]
        img_name = f"val_{i+1:06d}.png"
        out_path = os.path.join(val_img_dir, img_name)
        val_tasks.append((i, p_type, out_path, args.bake_blur))

    t_start = time.time()

    # 1. Sinh tập Train với đa tiến trình
    print(f"\n⚡ Đang sinh tập Train ({num_train:,} ảnh) trên {num_workers} CPU cores...")
    train_label_path = os.path.join(args.output_dir, "train_label.txt")
    with open(train_label_path, "w", encoding="utf-8") as f_train:
        with Pool(processes=num_workers) as pool:
            chunksize = 250
            completed = 0
            for label_line in pool.imap_unordered(generate_sample_worker, train_tasks, chunksize=chunksize):
                f_train.write(label_line)
                completed += 1
                if completed % 25000 == 0 or completed == num_train:
                    elapsed = time.time() - t_start
                    fps = completed / max(elapsed, 1e-5)
                    print(f"   [Train Progress] {completed:,} / {num_train:,} ảnh ({completed/num_train*100:.1f}%) - Tốc độ: {fps:.1f} ảnh/s")

    t_train_done = time.time()
    print(f"✅ Hoàn thành tập Train trong {t_train_done - t_start:.2f}s (~{(t_train_done - t_start)/60:.1f} phút)")

    # 2. Sinh tập Val với đa tiến trình
    if num_val > 0:
        print(f"\n⚡ Đang sinh tập Val ({num_val:,} ảnh) trên {num_workers} CPU cores...")
        val_label_path = os.path.join(args.output_dir, "val_label.txt")
        with open(val_label_path, "w", encoding="utf-8") as f_val:
            with Pool(processes=num_workers) as pool:
                chunksize = 250
                completed = 0
                for label_line in pool.imap_unordered(generate_sample_worker, val_tasks, chunksize=chunksize):
                    f_val.write(label_line)
                    completed += 1
                    if completed % 5000 == 0 or completed == num_val:
                        print(f"   [Val Progress] {completed:,} / {num_val:,} ảnh ({completed/num_val*100:.1f}%)")
    else:
        val_label_path = os.path.join(args.output_dir, "val_label.txt")
        print(f"\n❄️  Bỏ qua sinh tập Val (num_val = 0). Giữ nguyên tập Val đóng băng tại: {val_label_path}")

    total_time = time.time() - t_start
    print("=" * 75)
    print(f"🎉 HOÀN TẤT SINH TOÀN BỘ BỘ DỮ LIỆU V25 ({num_train + num_val:,} ẢNH)!")
    print(f"   • Tổng thời gian thực thi: {total_time:.2f}s (~{total_time/60:.2f} phút)")
    print(f"   • Tệp nhãn Train          : {train_label_path}")
    print(f"   • Tệp nhãn Val            : {val_label_path}")
    print("=" * 75)

if __name__ == "__main__":
    main()